UNPKG

oauth-fetch

Version:

A lightweight HTTP client built on top of the native fetch API, designed to simplify making requests to both public and OAuth-protected resources (Bearer and DPoP).

603 lines (593 loc) 22.4 kB
/** * HTTP content type constants, representing common content types used in requests and responses. */ declare const HTTP_CONTENT_TYPE: { readonly JSON: "json"; readonly TEXT: "text"; readonly FORM_DATA: "formData"; readonly FORM_URL_ENCODED: "formUrlEncoded"; }; /** * Constants for HTTP methods (GET, POST, PATCH, PUT, DELETE). */ declare const HTTP_METHOD: { readonly GET: "GET"; readonly POST: "POST"; readonly PATCH: "PATCH"; readonly PUT: "PUT"; readonly DELETE: "DELETE"; }; /** * Supported OAuth token types, including case-insensitive variations for each type. */ declare const SUPPORTED_TOKEN_TYPES: { readonly BEARER: "Bearer"; readonly DPOP: "DPoP"; }; /** * Supported cryptographic algorithms for DPoP, along with their valid parameters. * - ECDSA: P-256, P-384, P-521 curves * - RSA-PSS: 2048, 3072, 4096 bit modulus lengths * - EdDSA: Ed25519 curve */ declare const DPOP_SUPPORTED_ALGORITHMS: { readonly ECDSA: readonly ["P-256", "P-384", "P-521"]; readonly "RSA-PSS": readonly ["2048", "3072", "4096"]; readonly EdDSA: readonly ["Ed25519"]; }; type TokenProviderTokenType = (typeof SUPPORTED_TOKEN_TYPES)[keyof typeof SUPPORTED_TOKEN_TYPES]; /** * Represents the response from a token acquisition operation. * Contains the access token and its type for use in API requests. */ type TokenProviderGetTokenResponse = { /** * The OAuth access token value used for API authorization. * This token should be included in the Authorization header of API requests. */ access_token: string; /** * The type of token returned, which determines how it should be used in requests. * Common types include "Bearer" and "DPoP" (Demonstrating Proof-of-Possession). */ token_type: TokenProviderTokenType; }; /** * Abstract class representing a Token Provider responsible for managing the * lifecycle of access tokens. * * This class defines the contract for acquiring, refreshing, and caching tokens. * By extending this class, you can implement custom strategies for interacting with identity * providers such as Auth0, Clerk, WorkOS, or any other OAuth-compliant service. * * It also provides a mechanism to override the configuration for token * acquisition per request, enabling granular control over authorization * parameters. * * @example * ```typescript * import { AbstractTokenProvider, type TokenProviderGetTokenResponse } from "oauth-fetch"; * import { Auth0Client, GetTokenSilentlyOptions } from "@auth0/auth0-spa-js"; * * export class Auth0TokenProvider extends AbstractTokenProvider<GetTokenSilentlyOptions> { * private auth0: Auth0Client; * * constructor(auth0: Auth0Client, config?: GetTokenSilentlyOptions) { * super(config); * this.auth0 = auth0; * } * * async getToken(): Promise<TokenProviderGetTokenResponse> { * try { * const accessToken = await this.auth0.getTokenSilently(this.config); * return { * access_token: accessToken, * token_type: "Bearer", * }; * } catch { * throw new Error("Failed to retrieve access token."); * } * } * } * ``` */ declare abstract class AbstractTokenProvider<TokenProviderGetTokenConfig = unknown> { /** * Allows any property to be dynamically attached to the instance. */ [key: string]: unknown; /** * Configuration for the token provider's `getToken()` method. * This can include options for token acquisition, caching, and other settings * that the provider supports. */ protected config?: TokenProviderGetTokenConfig; /** * Initializes a new instance of the `AbstractTokenProvider`. */ constructor(config?: TokenProviderGetTokenConfig); /** * Retrieves a valid OAuth access token for API requests. * * This method should be responsible for the entire token lifecycle management: * - Returning cached tokens if they're still valid * - Refreshing expired tokens automatically when possible * - Handling token acquisition when no valid token is available * - Implementing appropriate error handling for token-related failures * * Implementations should be designed to minimize overhead by efficiently * caching tokens and only performing network requests when necessary. * * @throws {TokenProviderError} If `access_token` is not returned * @throws {TokenProviderError} If `token_type` is not returned * @throws {TokenProviderError} If `token_type` is not supported */ abstract getToken(): Promise<TokenProviderGetTokenResponse>; /** * Creates a new instance of the token provider with overridden `getToken` config. * * This method allows you to generate a modified instance of the token provider * with specific configuration changes. Only the matching properties are overridden, * while the rest of the configuration remains unchanged. This is ideal for fine-tuning * authorization scopes or parameters on a per-request basis without affecting the * original instance. * * @example * ```typescript * const newTokenProvider = tokenProvider.withConfigOverrides({ * authorizationParams: { * scope: "write:profile", * }, * }); * ``` */ withConfigOverrides(overrides: Partial<TokenProviderGetTokenConfig>): this; } /** * Type representing the available HTTP content types defined in the `HTTP_CONTENT_TYPE` constant. * It includes values like 'json', 'text', 'formData', and 'formUrlEncoded'. */ type HttpContentType = (typeof HTTP_CONTENT_TYPE)[keyof typeof HTTP_CONTENT_TYPE]; /** * Type representing the available HTTP methods defined in the `HTTP_METHOD` constant. * It includes methods like 'GET', 'POST', 'PATCH', 'PUT', and 'DELETE'. */ type HttpMethod = (typeof HTTP_METHOD)[keyof typeof HTTP_METHOD]; /** * Cryptographic key pair used for generating and verifying DPoP token proof-of-possession. */ type DPoPKeyPair = { /** The private key used for signing DPoP proofs */ privateKey: CryptoKey; /** The public key used for generating the JWK thumbprint and verifying DPoP proofs */ publicKey: CryptoKey; }; /** * Enum-like type representing the supported cryptographic algorithms for DPoP proof generation. * This type is derived from the keys of `DPOP_SUPPORTED_ALGORITHMS`. */ type DPoPSupportedAlgorithms = keyof typeof DPOP_SUPPORTED_ALGORITHMS; /** * Supported curves (for ECDSA/EdDSA) or modulus lengths (for RSA) that are valid for the specified algorithm. * This type is based on the values associated with each algorithm in `DPOP_SUPPORTED_ALGORITHMS`. */ type DPoPSupportedCurveOrModulus = (typeof DPOP_SUPPORTED_ALGORITHMS)[DPoPSupportedAlgorithms][number]; /** * Configuration options for generating a DPoP key pair. */ type DPoPKeyGenConfig = { /** * The cryptographic algorithm to use for generating the DPoP key pair. * @default "ECDSA" */ algorithm?: DPoPSupportedAlgorithms; /** * The curve (for ECDSA/EdDSA) or modulus length (for RSA) to use. * @default "P-256" for ECDSA */ curveOrModulus?: DPoPSupportedCurveOrModulus; }; /** * Configuration parameters required to generate a DPoP proof. */ type DPoPGenerateProofConfig = { /** The target URL for the HTTP request to which the proof is bound */ url: URL; /** The HTTP method (e.g., GET, POST, PUT) used for the request */ method: HttpMethod; /** The DPoP key pair used for signing the proof */ dpopKeyPair: DPoPKeyPair; /** * An optional nonce provided by the server for replay protection. * If a DPoP-Nonce header was returned in a previous response, include it here. */ nonce?: string; /** * An optional access token to include with the proof. * This is used when the proof is tied to an access token in the Authorization: DPoP header. */ accessToken?: string; }; /** * Configuration for public (non-authenticated) resources */ type OAuthFetchPublicResourceConfig = { /** Base URL for API requests (e.g., 'https://api.example.com') */ baseUrl: string; /** * Content type for requests (defaults to JSON if not specified) * @default "json" */ contentType?: HttpContentType; /** * Custom fetch implementation * * @example * // Example using a custom fetch implementation * const client = new OAuthFetch({ * baseUrl: 'https://api.example.com', * isProtected: false, * customFetch: async (url, options) => { * // Custom fetch logic * } * }); */ customFetch?: typeof fetch; /** Whether the API requires authentication (defaults to true) * @default true */ isProtected: false; }; /** * Configuration for protected (authenticated) resources */ type OAuthFetchPrivateResourceConfig = { /** Base URL for API requests (e.g., 'https://api.example.com') */ baseUrl: string; /** * Content type for requests (defaults to JSON if not specified) * @default "json" */ contentType?: HttpContentType; /** * Custom fetch implementation * * @example * // Example using a custom fetch implementation * const client = new OAuthFetch({ * baseUrl: 'https://api.example.com', * isProtected: false, * customFetch: async (url, options) => { * // Custom fetch logic * } * }); */ customFetch?: typeof fetch; /** Whether the API requires authentication (defaults to true) * @default true */ isProtected?: true; /** Provider responsible for fetching OAuth tokens */ tokenProvider: AbstractTokenProvider; /** Required for DPoP authentication flow */ dpopKeyPair?: DPoPKeyPair; }; /** * Extended request options that support authentication overrides and all standard fetch options */ type RequestOptions = Omit<RequestInit, "method" | "body"> & { /** Override the default protection setting for this specific request */ isProtected?: boolean; /** Override the default provider responsible for fetching OAuth tokens */ tokenProvider?: AbstractTokenProvider; }; type RequestBody = Record<string, unknown> | string; /** * OAuth-compatible HTTP client that supports Bearer and DPoP tokens for secure API requests. * * This client can be configured globally for an API with default authentication settings, * but also supports per-request overrides for accessing resources with different token * scopes, audiences, or authentication requirements. * * All HTTP methods accept standard fetch options (cache, mode, credentials, signal, etc.) * in addition to the custom authentication options. * * @example * // Public API client * const publicClient = new OAuthFetch({ * baseUrl: 'https://api.example.com', * isProtected: false * }); * * // Protected API with Bearer tokens * const bearerClient = new OAuthFetch({ * baseUrl: 'https://api.example.com', * tokenProvider: new MyTokenProvider() * }); * * // Protected API with DPoP tokens * const dpopClient = new OAuthFetch({ * baseUrl: 'https://api.example.com', * tokenProvider: new MyDPoPTokenProvider(), * dpopKeyPair: await createDPoPKeyPair() * }); */ declare class OAuthFetch { #private; /** * Initializes a new `OAuthFetch` instance with the provided configuration. * * @throws {ConfigurationError} If `isProtected` is `true` and `tokenProvider` is not provided */ constructor(config: OAuthFetchPrivateResourceConfig | OAuthFetchPublicResourceConfig); /** * Makes an HTTP GET request. * * @throws {ApiResponseError} If API responds with a non-successful status code * @throws {ConfigurationError} If `isProtected` is `true` and `tokenProvider` is missing * @throws {ConfigurationError} If token provider returns a DPoP token type, and `dpopKeyPair` is missing */ get(endpoint: string, options?: RequestOptions): Promise<unknown>; /** * Makes an HTTP DELETE request. * * @throws {ApiResponseError} If API responds with a non-successful status code * @throws {ConfigurationError} If `isProtected` is `true` and `tokenProvider` is missing * @throws {ConfigurationError} If token provider returns a DPoP token type, and `dpopKeyPair` is missing */ delete(endpoint: string, body?: RequestBody, options?: RequestOptions): Promise<unknown>; /** * Makes an HTTP POST request. * * @throws {ApiResponseError} If API responds with a non-successful status code * @throws {ConfigurationError} If `isProtected` is `true` and `tokenProvider` is missing * @throws {ConfigurationError} If token provider returns a DPoP token type, and `dpopKeyPair` is missing */ post(endpoint: string, body?: RequestBody, options?: RequestOptions): Promise<unknown>; /** * Makes an HTTP PATCH request. * * @throws {ApiResponseError} If API responds with a non-successful status code * @throws {ConfigurationError} If `isProtected` is `true` and `tokenProvider` is missing * @throws {ConfigurationError} If token provider returns a DPoP token type, and `dpopKeyPair` is missing */ patch(endpoint: string, body?: RequestBody, options?: RequestOptions): Promise<unknown>; /** * Makes an HTTP PUT request. * * @throws {ApiResponseError} If API responds with a non-successful status code * @throws {ConfigurationError} If `isProtected` is `true` and `tokenProvider` is missing * @throws {ConfigurationError} If token provider returns a DPoP token type, and `dpopKeyPair` is missing */ put(endpoint: string, body?: RequestBody, options?: RequestOptions): Promise<unknown>; } /** * Utility class for DPoP (Demonstrating Proof-of-Possession) operations. * * DPoP is a security mechanism used in OAuth 2.0 that cryptographically binds access tokens * to a specific client by requiring the client to prove possession of a private key. * This prevents token theft and misuse, as the stolen token cannot be used without the * corresponding private key. * * This class provides methods to: * - Generate DPoP key pairs for signing proofs * - Calculate JWK thumbprints for authorization requests * - Generate DPoP proofs for HTTP requests * * @example * ```typescript * // Basic usage flow: * * // 1. Generate a DPoP key pair (once per client session) * const keyPair = await DPoPUtils.generateKeyPair(); * * // 2. For authorization requests, calculate JWK thumbprint * const jkt = await DPoPUtils.calculateJwkThumbprint(keyPair.publicKey); * * // 3. Generate a DPoP proof for an API request * const proof = await DPoPUtils.generateProof({ * url: new URL("https://api.example.com/resources"), * method: "GET", * dpopKeyPair: keyPair, * accessToken: "eyJhbGciOiJSUzI1NiIsI..." // Optional * }); * * // 4. Use the proof in an HTTP request * fetch("https://api.example.com/resources", { * headers: { * "DPoP": proof, * "Authorization": `DPoP ${accessToken}` * } * }); * ``` */ declare class DPoPUtils { #private; private constructor(); /** * Calculates the DPoP JWK Thumbprint (dpop_jkt) for a public key. * * The JWK Thumbprint is a base64url-encoded SHA-256 hash of the JSON representation * of the JWK, with specific formatting requirements as per RFC 7638. * This can be used to bind an authorization code to a DPoP key pair by including * it as the `dpop_jkt` parameter in authorization requests. * * @throws {ConfigurationError} If the public key uses an unsupported algorithm. * * @example * ```typescript * // Generate a DPoP key pair * const keyPair = await DPoPUtils.generateKeyPair(); * * // Calculate the JWK thumbprint (dpop_jkt) * const jkt = await DPoPUtils.calculateJwkThumbprint(keyPair.publicKey); * * // Use the JWK thumbprint in an authorization request * const authUrl = new URL("https://auth.example.com/oauth/authorize"); * authUrl.searchParams.set("client_id", "client123"); * authUrl.searchParams.set("response_type", "code"); * authUrl.searchParams.set("redirect_uri", "https://app.example.com/callback"); * authUrl.searchParams.set("dpop_jkt", jkt); * ``` */ static calculateJwkThumbprint(publicKey: CryptoKey): Promise<string>; /** * Generates a new DPoP key pair using the specified cryptographic algorithm and parameters. * * This method creates a cryptographic key pair that can be used for signing DPoP proofs. * The generated keys are non-extractable and can only be used for signing and verification. * Typically, you should generate a key pair once per client session and reuse it * for all DPoP proofs in that session. * * @throws {ConfigurationError} If the requested algorithm/curve combination is not supported * * @example * ```typescript * // Generate a key pair using default parameters (ECDSA with P-256 curve) * const defaultKeyPair = await DPoPUtils.generateKeyPair(); * * // Generate an ECDSA key pair with the P-384 curve * const ecdsaKeyPair = await DPoPUtils.generateKeyPair({ * algorithm: "ECDSA", * curveOrModulus: "P-384" * }); * * // Generate an RSA key pair with 2048-bit modulus * const rsaKeyPair = await DPoPUtils.generateKeyPair({ * algorithm: "RSA-PSS", * curveOrModulus: "2048" * }); * * // Generate an EdDSA key pair with Ed25519 curve * const eddsaKeyPair = await DPoPUtils.generateKeyPair({ * algorithm: "EdDSA", * curveOrModulus: "Ed25519" * }); * ``` */ static generateKeyPair({ algorithm, curveOrModulus, }?: DPoPKeyGenConfig): Promise<DPoPKeyPair>; /** * Generates a DPoP proof JWT for a specific HTTP request. * * The proof is a JWT that binds the request to the DPoP key pair and optionally to an access token. * It proves possession of the private key corresponding to the public key included in the JWT header. * Each proof has a unique JTI (JWT ID) and a timestamp to prevent replay attacks. * * @throws {ConfigurationError} If the DPoP key pair is not properly initialized * @throws {ConfigurationError} If the private key's extractable value is `true` * @throws {ConfigurationError} If the private key does not have the `sign` usage permission. * @throws {ConfigurationError} If the public key uses an unsupported algorithm. * * @example * ```typescript * // Generate a DPoP proof for a request without an access token * const basicProof = await DPoPUtils.generateProof({ * url: new URL("https://auth.example.com/oauth/token"), * method: "POST", * dpopKeyPair: keyPair * }); * * // Use the proof in a request header * fetch("https://auth.example.com/oauth/token", { * method: "POST", * headers: { * "DPoP": basicProof * } * }); * * // Generate a DPoP proof bound to an access token * const tokenProof = await DPoPUtils.generateProof({ * url: new URL("https://api.example.com/protected"), * method: "GET", * dpopKeyPair: keyPair, * accessToken: "eyJhbGciOiJSUzI1NiIsInR5cCI6Ikp...", * nonce: "8IBTHwOdqNK..." // From previous DPoP-Nonce header * }); * * // Use the proof with the access token to consume a protected API * fetch("https://api.example.com/protected", { * method: "GET", * headers: { * "DPoP": tokenProof, * "Authorization": `DPoP ${accessToken}` * } * }); * ``` */ static generateProof({ url, method, dpopKeyPair, nonce, accessToken, }: DPoPGenerateProofConfig): Promise<string>; } declare const ERR_DESCRIPTION: { TOKEN_PROVIDER: { REQUIRED: string; MISSING_ACCESS_TOKEN: string; MISSING_TOKEN_TYPE: string; UNSUPPORTED_TOKEN_TYPE: string; }; RESPONSE: { BODY_PARSING_ERROR: string; NON_SUCCESSFUL: (url: URL, method: RequestInit["method"], response: Response) => string; }; DPOP: { REQUIRED: string; INVALID_INSTANCE: string; PRIVATE_KEY_NON_EXPORTABLE: string; PRIVATE_KEY_SIGN_USAGE: string; }; CRYPTO: { UNSUPPORTED_PUBLIC_KEY_TYPE: string; UNSUPPORTED_ALGORITHM: string; UNSUPPORTED_ALGORITHM_CONFIGURATION: string; UNSUPPORTED_RSA_HASH_ALGORITHM: string; INVALID_RSA_MODULUS_LENGTH: string; UNSUPPORTED_ECDSA_CURVE: string; UNSUPPORTED_RSA_PSS_HASH_ALGORITHM: string; }; }; /** * @internal * Base error class. */ declare class BaseError extends Error { /** * @internal */ constructor(message: string); } /** * Error thrown when the configuration is invalid. * * @group Errors */ declare class ConfigurationError extends BaseError { /** * @internal */ constructor(message: string); } /** * Error thrown when there's an issue with the token provider. * * @group Errors */ declare class TokenProviderError extends BaseError { /** * @internal */ constructor(message: string); } /** * Error thrown when non-successful API response is returned. * * @group Errors */ declare class ApiResponseError extends BaseError { readonly response: Response; readonly status: number; readonly statusText: string; readonly body?: unknown; /** * @internal */ constructor(message: string, response: Response, body?: unknown); } export { AbstractTokenProvider, ApiResponseError, BaseError, ConfigurationError, DPOP_SUPPORTED_ALGORITHMS, type DPoPGenerateProofConfig, type DPoPKeyGenConfig, type DPoPKeyPair, type DPoPSupportedAlgorithms, type DPoPSupportedCurveOrModulus, DPoPUtils, ERR_DESCRIPTION, HTTP_CONTENT_TYPE, HTTP_METHOD, type HttpContentType, type HttpMethod, OAuthFetch, type OAuthFetchPrivateResourceConfig, type OAuthFetchPublicResourceConfig, type RequestBody, type RequestOptions, SUPPORTED_TOKEN_TYPES, TokenProviderError, type TokenProviderGetTokenResponse, type TokenProviderTokenType };