CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-openid-client

OAuth 2.0 and OpenID Connect client library for JavaScript runtimes with comprehensive authentication flows and security features.

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

advanced-security.mddocs/

Advanced Security Features

FAPI compliance, DPoP, JARM, JAR, PAR, response encryption, and other advanced security mechanisms for high-security OAuth 2.0 and OpenID Connect implementations.

Capabilities

DPoP (Demonstrating Proof-of-Possession)

Implement sender-constrained access tokens using DPoP for enhanced security.

/**
 * Generate DPoP key pair
 * @param alg - JWS algorithm (default: 'ES256')
 * @param options - Key generation options
 * @returns Promise resolving to cryptographic key pair
 */
function randomDPoPKeyPair(
  alg?: string,
  options?: GenerateKeyPairOptions
): Promise<CryptoKeyPair>;

/**
 * Create DPoP handle for sender-constrained tokens
 * @param config - Configuration instance
 * @param keyPair - DPoP key pair for signing proofs
 * @param options - JWT modification options
 * @returns DPoP handle for use with requests
 */
function getDPoPHandle(
  config: Configuration,
  keyPair: CryptoKeyPair,
  options?: ModifyAssertionOptions
): DPoPHandle;

Usage Examples:

import * as client from "openid-client";

// Generate DPoP key pair
const dpopKeyPair = await client.randomDPoPKeyPair("ES256");

// Create DPoP handle
const dpopHandle = client.getDPoPHandle(config, dpopKeyPair);

// Use DPoP with authorization code grant
const tokens = await client.authorizationCodeGrant(
  config,
  currentUrl,
  { pkceCodeVerifier: codeVerifier },
  undefined,
  { DPoP: dpopHandle }
);

// Check if access token is sender-constrained
if (tokens.token_type === "dpop") {
  console.log("Access token is sender-constrained with DPoP");
}

// Use DPoP handle with protected resource requests
const response = await client.fetchProtectedResource(
  config,
  tokens.access_token,
  new URL("https://api.example.com/data"),
  "GET",
  undefined,
  undefined,
  { DPoP: dpopHandle }
);

// DPoP works with all token operations
const newTokens = await client.refreshTokenGrant(
  config,
  tokens.refresh_token,
  undefined,
  { DPoP: dpopHandle }
);

JARM (JWT Authorization Response Mode)

Enable JWT-secured authorization responses for enhanced security.

/**
 * Enable JWT Authorization Response Mode
 * @param config - Configuration instance to modify
 */
function useJwtResponseMode(config: Configuration): void;

Usage Examples:

import * as client from "openid-client";

// Enable JARM during discovery
const config = await client.discovery(
  new URL("https://example.com"),
  "client-id",
  "client-secret",
  undefined,
  {
    execute: [client.useJwtResponseMode]
  }
);

// Or enable on existing configuration
client.useJwtResponseMode(config);

// Authorization responses will now be JWT-signed
const authUrl = client.buildAuthorizationUrl(config, {
  redirect_uri: "https://example.com/callback",
  scope: "openid profile",
  response_mode: "jwt", // This is set automatically
  code_challenge: codeChallenge,
  code_challenge_method: "S256"
});

// Handle JWT response (library validates signature automatically)
const tokens = await client.authorizationCodeGrant(
  config,
  currentUrl, // Contains JWT response
  { pkceCodeVerifier: codeVerifier }
);

OpenID Connect Hybrid Flow

Enable code id_token response type for hybrid flow with ID Token validation.

/**
 * Enable OpenID Connect hybrid flow (code id_token response type)
 * @param config - Configuration instance to modify
 */
function useCodeIdTokenResponseType(config: Configuration): void;

Usage Examples:

import * as client from "openid-client";

// Enable hybrid flow
const config = await client.discovery(
  new URL("https://example.com"),
  "client-id",
  "client-secret",
  undefined,
  {
    execute: [client.useCodeIdTokenResponseType]
  }
);

// Build authorization URL (response_type=code id_token is set automatically)
const authUrl = client.buildAuthorizationUrl(config, {
  redirect_uri: "https://example.com/callback",
  scope: "openid profile email",
  nonce: client.randomNonce(),
  state: client.randomState(),
  code_challenge: codeChallenge,
  code_challenge_method: "S256"
});

// Handle hybrid flow response
const tokens = await client.authorizationCodeGrant(
  config,
  currentUrl, // Contains both code and id_token
  {
    pkceCodeVerifier: codeVerifier,
    expectedNonce: expectedNonce,
    expectedState: expectedState
  }
);

// Access both authorization response ID Token and token endpoint tokens
console.log("Auth response ID Token:", tokens.claims());
console.log("Token endpoint access token:", tokens.access_token);

OpenID Connect Implicit Flow

Enable ID Token-only implicit flow for specific use cases.

/**
 * Enable OpenID Connect implicit flow (id_token response type)
 * @param config - Configuration instance to modify
 */
function useIdTokenResponseType(config: Configuration): void;

Usage Examples:

import * as client from "openid-client";

// Enable implicit flow (typically for public clients)
const config = await client.discovery(
  new URL("https://example.com"),
  "public-client-id",
  undefined,
  client.None(), // Public client authentication
  {
    execute: [client.useIdTokenResponseType]
  }
);

// Build authorization URL for implicit flow
const authUrl = client.buildAuthorizationUrl(config, {
  redirect_uri: "https://spa.example.com/callback",
  scope: "openid profile email",
  nonce: client.randomNonce(),
  state: client.randomState()
  // response_type=id_token is set automatically
});

// Handle implicit flow response (use implicitAuthentication, not authorizationCodeGrant)
const idTokenClaims = await client.implicitAuthentication(
  config,
  new URL(location.href), // Browser location with hash fragment
  expectedNonce,
  {
    expectedState: expectedState,
    maxAge: 3600
  }
);

console.log("User authenticated:", idTokenClaims.sub);
console.log("Email:", idTokenClaims.email);

FAPI 1.0 Advanced Security

Enable FAPI 1.0 Advanced profile with detached signature response checks.

/**
 * Enable FAPI 1.0 Advanced detached signature response checks
 * @param config - Configuration instance to modify
 */
function enableDetachedSignatureResponseChecks(config: Configuration): void;

Usage Examples:

import * as client from "openid-client";

// Enable FAPI 1.0 Advanced profile
const config = await client.discovery(
  new URL("https://fapi-server.example.com"),
  "client-id",
  "client-secret",
  undefined,
  {
    execute: [
      client.useCodeIdTokenResponseType, // Required for FAPI Advanced
      client.enableDetachedSignatureResponseChecks // Enable FAPI validation
    ]
  }
);

// FAPI requires specific security measures
const authUrl = client.buildAuthorizationUrl(config, {
  redirect_uri: "https://example.com/callback",
  scope: "openid accounts",
  nonce: client.randomNonce(),
  state: client.randomState(),
  code_challenge: codeChallenge,
  code_challenge_method: "S256" // PKCE required for FAPI
});

// Handle FAPI response with enhanced validation
const tokens = await client.authorizationCodeGrant(
  config,
  currentUrl,
  {
    pkceCodeVerifier: codeVerifier,
    expectedNonce: expectedNonce,
    expectedState: expectedState
  }
);

Response Signature Validation

Enable JWS signature validation for enhanced security.

/**
 * Enable non-repudiation checks (JWS signature validation)
 * @param config - Configuration instance to modify
 */
function enableNonRepudiationChecks(config: Configuration): void;

Usage Examples:

import * as client from "openid-client";

// Enable signature validation
const config = await client.discovery(
  new URL("https://example.com"),
  "client-id",
  "client-secret",
  undefined,
  {
    execute: [client.enableNonRepudiationChecks]
  }
);

// All JWT responses (UserInfo, Introspection, etc.) will be signature-validated
const userInfo = await client.fetchUserInfo(
  config,
  accessToken,
  expectedSubject
);

// JWT introspection responses are validated
const introspection = await client.tokenIntrospection(config, token);

Response Encryption

Enable JWE decryption for encrypted responses.

/**
 * Enable decryption of encrypted responses
 * @param config - Configuration instance to modify
 * @param contentEncryptionAlgorithms - Allowed content encryption algorithms
 * @param keys - Decryption keys
 */
function enableDecryptingResponses(
  config: Configuration,
  contentEncryptionAlgorithms?: string[],
  ...keys: Array<CryptoKey | DecryptionKey>
): void;

Usage Examples:

import * as client from "openid-client";

// Generate or import decryption key
const keyPair = await crypto.subtle.generateKey(
  { name: "ECDH", namedCurve: "P-256" },
  true,
  ["deriveKey"]
);

// Enable response decryption
client.enableDecryptingResponses(
  config,
  ["A256GCM", "A128CBC-HS256"], // Allowed content encryption algorithms
  keyPair.privateKey
);

// Or with DecryptionKey interface for more control
const decryptionKey: client.DecryptionKey = {
  key: keyPair.privateKey,
  alg: "ECDH-ES+A256KW", // Key management algorithm
  kid: "my-decryption-key-id"
};

client.enableDecryptingResponses(
  config,
  ["A256GCM"],
  decryptionKey
);

// Encrypted responses will be automatically decrypted
const userInfo = await client.fetchUserInfo(config, accessToken, expectedSubject);
const introspection = await client.tokenIntrospection(config, token);

JWKS Caching for Stateless Environments

Manage JWKS cache for cloud functions and stateless environments.

/**
 * Export JWKS cache for external storage
 * @param config - Configuration instance
 * @returns Exported JWKS cache or undefined
 */
function getJwksCache(config: Configuration): ExportedJWKSCache | undefined;

/**
 * Import JWKS cache from external storage
 * @param config - Configuration instance
 * @param jwksCache - Previously exported JWKS cache
 */
function setJwksCache(config: Configuration, jwksCache: ExportedJWKSCache): void;

Usage Examples:

import * as client from "openid-client";

// In a serverless function
export async function handler(event: any) {
  const config = await client.discovery(
    new URL("https://example.com"),
    "client-id",
    "client-secret"
  );
  
  // Load JWKS cache from external storage (Redis, DynamoDB, etc.)
  const savedCache = await loadJwksCacheFromStorage();
  if (savedCache) {
    client.setJwksCache(config, savedCache);
  }
  
  // Perform operations that may use JWKS
  const userInfo = await client.fetchUserInfo(config, accessToken, expectedSubject);
  
  // Save updated JWKS cache
  const currentCache = client.getJwksCache(config);
  if (currentCache) {
    await saveJwksCacheToStorage(currentCache);
  }
  
  return { statusCode: 200, body: JSON.stringify(userInfo) };
}

// Cache storage implementation example
async function loadJwksCacheFromStorage(): Promise<client.ExportedJWKSCache | null> {
  try {
    const cached = await redis.get("jwks-cache");
    return cached ? JSON.parse(cached) : null;
  } catch {
    return null;
  }
}

async function saveJwksCacheToStorage(cache: client.ExportedJWKSCache): Promise<void> {
  try {
    await redis.setex("jwks-cache", 3600, JSON.stringify(cache)); // 1 hour TTL
  } catch (error) {
    console.warn("Failed to save JWKS cache:", error);
  }
}

Custom Security Configuration

Override default security settings for development or specific requirements.

/**
 * Allow insecure HTTP requests (development only)
 * @param config - Configuration instance to modify
 */
function allowInsecureRequests(config: Configuration): void;

Usage Examples:

import * as client from "openid-client";

// For development/testing only - allow HTTP
const config = await client.discovery(
  new URL("http://localhost:8080"), // HTTP issuer
  "dev-client-id",
  "dev-client-secret",
  undefined,
  {
    execute: [client.allowInsecureRequests]
  }
);

// Or enable on existing configuration
client.allowInsecureRequests(config);

Error Types

Re-exported error classes from oauth4webapi for comprehensive error handling:

/**
 * Authorization endpoint error response
 */
class AuthorizationResponseError extends Error {
  error: string;
  error_description?: string;
  error_uri?: string;
  state?: string;
}

/**
 * HTTP response body parsing error
 */
class ResponseBodyError extends Error {
  response: Response;
}

/**
 * WWW-Authenticate header parsing error  
 */
class WWWAuthenticateChallengeError extends Error {
  challenges: WWWAuthenticateChallenge[];
}

Advanced Types

interface DecryptionKey {
  /** Decryption private key */
  key: CryptoKey;
  /** JWE Key Management Algorithm identifier */
  alg?: string;
  /** Key ID */
  kid?: string;
}

interface DPoPHandle {
  // DPoP handle implementation (opaque to consumers)
}

interface GenerateKeyPairOptions {
  /** Whether the key should be extractable */
  extractable?: boolean;
}

interface ExportedJWKSCache {
  /** Cached JWKS data */
  jwks: any;
  /** Cache timestamp */
  uat: number;
}

interface WWWAuthenticateChallenge {
  scheme: string;
  parameters: WWWAuthenticateChallengeParameters;
}

interface WWWAuthenticateChallengeParameters {
  [parameter: string]: string;
}

Security Symbols

/**
 * Symbol for JWT modification in assertions
 */
declare const modifyAssertion: unique symbol;

/**
 * Symbol for clock skew adjustment
 */
declare const clockSkew: unique symbol;

/**
 * Symbol for clock tolerance configuration
 */
declare const clockTolerance: unique symbol;

Symbol Usage Examples:

import * as client from "openid-client";

// Clock management for JWT validation
const clientMetadata: client.ClientMetadata = {
  client_id: "client-id",
  client_secret: "client-secret",
  [client.clockSkew]: 30, // Local clock is 30 seconds behind server
  [client.clockTolerance]: 60 // Allow 60 seconds tolerance for JWT validation
};

const config = new client.Configuration(
  serverMetadata,
  "client-id",
  clientMetadata
);

// JWT assertion modification
const privateKeyJwtAuth = client.PrivateKeyJwt(privateKey, {
  [client.modifyAssertion]: (header, payload) => {
    // Modify JWT before signing
    header.kid = "my-key-id";
    payload.jti = crypto.randomUUID();
    payload.iat = Math.floor(Date.now() / 1000) - 30; // 30 seconds ago
  }
});

// Security override symbols (use with caution)
const tokens = await client.authorizationCodeGrant(
  config,
  currentUrl,
  {
    expectedState: client.skipStateCheck, // Skip state validation (not recommended)
    expectedNonce: expectedNonce
  }
);

const userInfo = await client.fetchUserInfo(
  config,
  tokens.access_token,
  client.skipSubjectCheck // Skip subject validation (not recommended)
);

Install with Tessl CLI

npx tessl i tessl/npm-openid-client

docs

advanced-security.md

authorization-flows.md

client-authentication.md

configuration.md

grant-types.md

index.md

passport-integration.md

protected-resources.md

token-management.md

tile.json