OAuth 2.0 and OpenID Connect client library for JavaScript runtimes with comprehensive authentication flows and security features.
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Access protected resources and UserInfo endpoint with proper access token handling, including DPoP support for sender-constrained tokens.
Retrieve user information from the OpenID Connect UserInfo endpoint.
/**
* Fetch UserInfo from authorization server
* @param config - Configuration instance
* @param accessToken - Access token for authorization
* @param expectedSubject - Expected subject claim or skipSubjectCheck
* @param options - Request options (DPoP, etc.)
* @returns Promise resolving to UserInfo response
*/
function fetchUserInfo(
config: Configuration,
accessToken: string,
expectedSubject: string | typeof skipSubjectCheck,
options?: DPoPOptions
): Promise<UserInfoResponse>;Usage Examples:
import * as client from "openid-client";
// Basic UserInfo request
const tokens = await client.authorizationCodeGrant(config, currentUrl, checks);
const userInfo = await client.fetchUserInfo(
config,
tokens.access_token,
tokens.claims()?.sub // expected subject from ID Token
);
console.log("User Info:", userInfo);
console.log("Email:", userInfo.email);
console.log("Name:", userInfo.name);
// Skip subject validation (not recommended)
const userInfo = await client.fetchUserInfo(
config,
tokens.access_token,
client.skipSubjectCheck
);
// With DPoP for sender-constrained tokens
const dpopKeyPair = await client.randomDPoPKeyPair();
const dpopHandle = client.getDPoPHandle(config, dpopKeyPair);
const tokens = await client.authorizationCodeGrant(
config,
currentUrl,
checks,
undefined,
{ DPoP: dpopHandle }
);
// Use same DPoP handle for UserInfo request
const userInfo = await client.fetchUserInfo(
config,
tokens.access_token,
tokens.claims()?.sub,
{ DPoP: dpopHandle }
);Make authenticated requests to any protected resource endpoint.
/**
* Make authenticated request to protected resource
* @param config - Configuration instance
* @param accessToken - Access token for authorization
* @param url - Target URL for the request
* @param method - HTTP method
* @param body - Request body (optional)
* @param headers - Additional headers (optional)
* @param options - Request options (DPoP, etc.)
* @returns Promise resolving to Response object
*/
function fetchProtectedResource(
config: Configuration,
accessToken: string,
url: URL,
method: string,
body?: FetchBody,
headers?: Headers,
options?: DPoPOptions
): Promise<Response>;Usage Examples:
import * as client from "openid-client";
// GET request to API endpoint
const response = await client.fetchProtectedResource(
config,
accessToken,
new URL("https://api.example.com/users/me"),
"GET"
);
if (response.ok) {
const userData = await response.json();
console.log("User data:", userData);
} else {
console.log("Request failed:", response.status, response.statusText);
}
// POST request with JSON body
const postResponse = await client.fetchProtectedResource(
config,
accessToken,
new URL("https://api.example.com/users"),
"POST",
JSON.stringify({
name: "John Doe",
email: "john@example.com"
}),
new Headers({
"Content-Type": "application/json"
})
);
// PUT request with form data
const formData = new URLSearchParams();
formData.append("name", "Jane Doe");
formData.append("department", "Engineering");
const putResponse = await client.fetchProtectedResource(
config,
accessToken,
new URL("https://api.example.com/users/123"),
"PUT",
formData,
new Headers({
"Content-Type": "application/x-www-form-urlencoded"
})
);
// DELETE request
const deleteResponse = await client.fetchProtectedResource(
config,
accessToken,
new URL("https://api.example.com/users/123"),
"DELETE"
);
// With DPoP for sender-constrained access tokens
const response = await client.fetchProtectedResource(
config,
dpopConstrainedToken,
new URL("https://api.example.com/sensitive-data"),
"GET",
undefined,
undefined,
{ DPoP: dpopHandle }
);Create a reusable API client with automatic token handling:
import * as client from "openid-client";
class APIClient {
constructor(
private config: client.Configuration,
private tokenManager: TokenManager
) {}
async get<T = any>(endpoint: string): Promise<T> {
const token = await this.tokenManager.getValidAccessToken();
const response = await client.fetchProtectedResource(
this.config,
token,
new URL(endpoint),
"GET"
);
if (!response.ok) {
throw new Error(`API request failed: ${response.status}`);
}
return response.json();
}
async post<T = any>(endpoint: string, data: any): Promise<T> {
const token = await this.tokenManager.getValidAccessToken();
const response = await client.fetchProtectedResource(
this.config,
token,
new URL(endpoint),
"POST",
JSON.stringify(data),
new Headers({ "Content-Type": "application/json" })
);
if (!response.ok) {
throw new Error(`API request failed: ${response.status}`);
}
return response.json();
}
async put<T = any>(endpoint: string, data: any): Promise<T> {
const token = await this.tokenManager.getValidAccessToken();
const response = await client.fetchProtectedResource(
this.config,
token,
new URL(endpoint),
"PUT",
JSON.stringify(data),
new Headers({ "Content-Type": "application/json" })
);
if (!response.ok) {
throw new Error(`API request failed: ${response.status}`);
}
return response.json();
}
async delete(endpoint: string): Promise<void> {
const token = await this.tokenManager.getValidAccessToken();
const response = await client.fetchProtectedResource(
this.config,
token,
new URL(endpoint),
"DELETE"
);
if (!response.ok) {
throw new Error(`API request failed: ${response.status}`);
}
}
}
// Usage
const apiClient = new APIClient(config, tokenManager);
const user = await apiClient.get<UserProfile>("https://api.example.com/user");
const newPost = await apiClient.post<Post>("https://api.example.com/posts", {
title: "Hello World",
content: "This is my first post"
});Handle common API errors and implement retry logic:
import * as client from "openid-client";
class ProtectedResourceClient {
constructor(
private config: client.Configuration,
private tokenManager: TokenManager
) {}
async request(
url: URL,
method: string,
body?: client.FetchBody,
headers?: Headers,
retries = 1
): Promise<Response> {
for (let attempt = 0; attempt <= retries; attempt++) {
try {
const token = await this.tokenManager.getValidAccessToken();
const response = await client.fetchProtectedResource(
this.config,
token,
url,
method,
body,
headers
);
// Handle token expiration
if (response.status === 401) {
const wwwAuth = response.headers.get("WWW-Authenticate");
if (wwwAuth?.includes("invalid_token") && attempt < retries) {
// Try refreshing token and retry
await this.tokenManager.refreshTokens();
continue;
}
}
// Handle rate limiting
if (response.status === 429 && attempt < retries) {
const retryAfter = response.headers.get("Retry-After");
const delay = retryAfter ? parseInt(retryAfter) * 1000 : 1000;
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
return response;
} catch (error) {
if (attempt === retries) {
throw error;
}
// Exponential backoff for network errors
const delay = Math.pow(2, attempt) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw new Error("Max retries exceeded");
}
}Handle file uploads to protected endpoints:
import * as client from "openid-client";
async function uploadFile(
config: client.Configuration,
accessToken: string,
file: File,
uploadUrl: string
): Promise<any> {
const formData = new FormData();
formData.append("file", file);
formData.append("filename", file.name);
const response = await client.fetchProtectedResource(
config,
accessToken,
new URL(uploadUrl),
"POST",
formData
// Note: Don't set Content-Type header for FormData - browser will set it with boundary
);
if (!response.ok) {
throw new Error(`Upload failed: ${response.status} ${response.statusText}`);
}
return response.json();
}
// Usage
const fileInput = document.getElementById("file") as HTMLInputElement;
const file = fileInput.files?.[0];
if (file) {
try {
const result = await uploadFile(
config,
accessToken,
file,
"https://api.example.com/upload"
);
console.log("Upload successful:", result);
} catch (error) {
console.error("Upload failed:", error);
}
}interface UserInfoResponse {
/** Subject identifier */
sub: string;
/** End-user's full name */
name?: string;
/** Given name(s) or first name(s) */
given_name?: string;
/** Surname(s) or last name(s) */
family_name?: string;
/** Middle name(s) */
middle_name?: string;
/** Casual name */
nickname?: string;
/** Shorthand name */
preferred_username?: string;
/** Profile page URL */
profile?: string;
/** Profile picture URL */
picture?: string;
/** Web page or blog URL */
website?: string;
/** Preferred e-mail address */
email?: string;
/** True if email has been verified */
email_verified?: boolean;
/** Gender */
gender?: string;
/** Birthdate (YYYY-MM-DD format) */
birthdate?: string;
/** Time zone */
zoneinfo?: string;
/** Locale */
locale?: string;
/** Preferred telephone number */
phone_number?: string;
/** True if phone number has been verified */
phone_number_verified?: boolean;
/** Preferred postal address */
address?: UserInfoAddress;
/** Time the information was last updated */
updated_at?: number;
// Additional claims may be present
[key: string]: any;
}
interface UserInfoAddress {
/** Full mailing address */
formatted?: string;
/** Full street address */
street_address?: string;
/** City or locality */
locality?: string;
/** State, province, prefecture, or region */
region?: string;
/** Zip code or postal code */
postal_code?: string;
/** Country name */
country?: string;
}
type FetchBody = ArrayBuffer | null | ReadableStream | string | Uint8Array | undefined | URLSearchParams;/**
* Skip subject validation in UserInfo requests
* WARNING: Use only when you understand the security implications
*/
declare const skipSubjectCheck: unique symbol;For sender-constrained access tokens:
interface DPoPOptions {
/** DPoP handle for proof-of-possession */
DPoP?: DPoPHandle;
}Complete DPoP Example:
import * as client from "openid-client";
// Create DPoP key pair and handle
const dpopKeyPair = await client.randomDPoPKeyPair();
const dpopHandle = client.getDPoPHandle(config, dpopKeyPair);
// Get sender-constrained tokens
const tokens = await client.authorizationCodeGrant(
config,
currentUrl,
checks,
undefined,
{ DPoP: dpopHandle }
);
// Verify token is sender-constrained
if (tokens.token_type === "dpop") {
console.log("Access token is sender-constrained with DPoP");
}
// Use with UserInfo
const userInfo = await client.fetchUserInfo(
config,
tokens.access_token,
tokens.claims()?.sub,
{ DPoP: dpopHandle }
);
// Use with protected resources
const apiResponse = await client.fetchProtectedResource(
config,
tokens.access_token,
new URL("https://api.example.com/data"),
"GET",
undefined,
undefined,
{ DPoP: dpopHandle }
);Install with Tessl CLI
npx tessl i tessl/npm-openid-client