UNPKG

oauth2-mock-server

Version:

Configurable OAuth2/OpenID Connect server for automated testing and development purposes

398 lines (397 loc) 14 kB
import { IncomingMessage, RequestListener, ServerResponse } from "node:http"; import { ServerOptions } from "node:https"; import { AddressInfo } from "node:net"; import { JWK } from "jose"; import { EventEmitter } from "node:events"; //#region src/lib/types-internals.d.ts interface JWKWithKid extends JWK { kid: string; alg: string; [propName: string]: unknown; } declare const supportedPkceAlgorithms: readonly ["plain", "S256"]; //#endregion //#region src/lib/types.d.ts interface TokenRequest { scope?: string; grant_type: string; username?: unknown; client_id?: unknown; code?: string; aud?: string[] | string; code_verifier?: string; assertion?: string; } interface TokenRequestIncomingMessage extends IncomingMessage { body: TokenRequest; } interface Options { host?: string; port: number; cert?: string; key?: string; keys: Record<string, unknown>[]; saveJWK: boolean; issuerUrlTrailingSlash: boolean; } type HttpServerOptions = Pick<ServerOptions, "key"> & Pick<ServerOptions, "cert">; interface MutableRedirectUri { url: URL; } interface MutableToken { header: Header; payload: Payload; } interface Header { kid: string; [key: string]: unknown; } interface Payload { iss: string; iat: number; exp: number; nbf: number; [key: string]: unknown; } interface StatusCodeMutableResponse { statusCode: number; } interface MutableResponse extends StatusCodeMutableResponse { body: Record<string, unknown> | ""; } type ScopesOrTransform = string | string[] | JwtTransform; type JwtTransform = (header: Header, payload: Payload) => void; /** * Events emitted by {@link OAuth2Service} at key points in request processing. * Register handlers via `service.on(Events.Xxx, handler)` for persistent hooks, * or `service.once(Events.Xxx, handler)` to intercept a single request. * Each handler receives a mutable object that can be modified in-place to * customise the server's behaviour — no return value is required. */ declare enum Events { /** * Raised by the `POST /token` endpoint before the JWT is signed. * Allows mutating the token's header and payload — e.g. adding custom claims, * overriding the expiry, or attaching a client ID. * * Handler signature: `(token: MutableToken, req: TokenRequestIncomingMessage) => void` */ BeforeTokenSigning = "beforeTokenSigning", /** * Raised by the `POST /token` endpoint after the access token is built, * immediately before the HTTP response is sent. * Allows mutating the response body and status code — e.g. simulating an * `invalid_grant` error or injecting additional response fields. * * Handler signature: `(tokenEndpointResponse: MutableResponse, req: TokenRequestIncomingMessage) => void` */ BeforeResponse = "beforeResponse", /** * Raised by the `GET /userinfo` endpoint before the response is sent. * Allows mutating the response body and status code — e.g. adding extra * claims or simulating an authorization error. * * Handler signature: `(userInfoResponse: MutableResponse, req: IncomingMessage) => void` */ BeforeUserinfo = "beforeUserinfo", /** * Raised by the `POST /revoke` endpoint before the response is sent. * Allows mutating the response status code only — e.g. simulating a * non-200 revocation result. * * Handler signature: `(revokeResponse: StatusCodeMutableResponse, req: IncomingMessage) => void` */ BeforeRevoke = "beforeRevoke", /** * Raised by the `GET /authorize` endpoint before the authorization code * redirect is performed. * Allows mutating the redirect URL and its query parameters — e.g. injecting * extra parameters into the callback URI. * * Handler signature: `(authorizeRedirectUri: MutableRedirectUri, req: IncomingMessage) => void` */ BeforeAuthorizeRedirect = "beforeAuthorizeRedirect", /** * Raised by the `GET /endsession` endpoint before the post-logout redirect * is performed. * Allows mutating the redirect URL and its query parameters — e.g. appending * extra state to the `post_logout_redirect_uri`. * * Handler signature: `(postLogoutRedirectUri: MutableRedirectUri, req: IncomingMessage) => void` */ BeforePostLogoutRedirect = "beforePostLogoutRedirect", /** * Raised by the `POST /introspect` endpoint before the response is sent. * Allows mutating the response body and status code — e.g. adding token * metadata such as scope, username, and expiry, or simulating an inactive token. * * Handler signature: `(introspectResponse: MutableResponse, req: IncomingMessage) => void` */ BeforeIntrospect = "beforeIntrospect" } interface TokenBuildOptions { /** * The 'kid' of the key that will be used to sign the JWT. * If omitted, the next key in the round - robin will be used. */ kid?: string | undefined; /** * A scope, array of scopes, or JWT transformation callback. */ scopesOrTransform?: ScopesOrTransform | undefined; /** * Time in seconds before the JWT to expire. Default: 3600 seconds. */ expiresIn?: number | undefined; } interface JWK$1 extends JWKWithKid { alg: string; } interface OAuth2Endpoints { wellKnownDocument: string; token: string; jwks: string; authorize: string; userinfo: string; revoke: string; endSession: string; introspect: string; } type OAuth2EndpointsInput = Partial<OAuth2Endpoints>; interface OAuth2Options { endpoints?: OAuth2EndpointsInput; shouldIssuerUrlBeSuffixedWithATralingSlash?: boolean; } type PKCEAlgorithm = (typeof supportedPkceAlgorithms)[number]; interface CodeChallenge { challenge: string; method: PKCEAlgorithm; } declare const supportedHttpMethods: readonly ["GET", "POST", "PUT", "DELETE", "PATCH"]; type HttpMethod = (typeof supportedHttpMethods)[number]; interface AugmentedRequest extends IncomingMessage { body: Record<string, unknown> | unknown[] | undefined; query: Record<string, string | string[] | undefined>; } type RouteHandler = (req: AugmentedRequest, res: ServerResponse) => Promise<void> | void; //#endregion //#region src/lib/http-server.d.ts /** * Provides a restartable wrapper for http.CreateServer(). */ declare class HttpServer { #private; /** * Creates a new instance of HttpServer. * @param requestListener The function that will handle the server's requests. * @param options Optional HttpServerOptions to start the server with https. */ constructor(requestListener: RequestListener, options?: HttpServerOptions); /** * Returns a value indicating whether or not the server is listening for connections. * @returns A boolean value indicating whether the server is listening. */ get listening(): boolean; /** * Returns the bound address, family name and port where the server is listening, * or null if the server has not been started. * @returns The server bound address information. */ address(): AddressInfo; /** * Starts the server. * @param port Port number. If omitted, it will be assigned by the operating system. * @param host Host name. * @returns A promise that resolves when the server has been started. */ start(port?: number, host?: string): Promise<void>; /** * Stops the server. * @returns Resolves when the server has been stopped. */ stop(): Promise<void>; protected buildIssuerUrl(host: string | undefined, port: number): string; } //#endregion //#region src/lib/jwk-store.d.ts /** * Simple JWK store */ declare class JWKStore { #private; /** * Creates a new instance of the keystore. */ constructor(); /** * Generates a new random key and adds it into this keystore. * @param alg The selected algorithm. * @param opts The options. * @param opts.kid The key identifier to use. * @param opts.crv The OKP "crv" to be used for "EdDSA" algorithm. * @returns The promise for the generated key. */ generate(alg: string, opts?: { kid?: string; crv?: string; }): Promise<JWK$1>; /** * Adds a JWK key to this keystore. * @param maybeJwk The JWK key to add. * @returns The promise for the added key. */ add(maybeJwk: Record<string, unknown>): Promise<JWK$1>; /** * Gets a key from the keystore in a round-robin fashion. * If a 'kid' is provided, only keys that match will be taken into account. * @param kid The optional key identifier to match keys against. * @returns The retrieved key. */ get(kid?: string): JWK$1 | undefined; /** * Generates a JSON representation of this keystore, which conforms * to a JWK Set from {I-D.ietf-jose-json-web-key}. * @param [includePrivateFields] `true` if the private fields * of stored keys are to be included. * @returns The JSON representation of this keystore. */ toJSON(includePrivateFields?: boolean): JWK$1[]; } //#endregion //#region src/lib/oauth2-issuer.d.ts /** * Represents an OAuth 2 issuer. */ declare class OAuth2Issuer extends EventEmitter { #private; /** * Gets the issuer URL. * @returns The issuer URL or undefined if not set. */ get url(): string | undefined; /** * Sets the issuer URL, normalizing the value based on the suffixing option defined in the constructor. * When true and the URL doesn't end with a slash, a trailing slash will be added. * When false and the URL ends with a slash, the trailing slash will be removed. * Otherwise, the URL is set as is. */ set url(value: string | undefined); /** * Creates a new instance of HttpServer. * @param shouldIssuerUrlBeSuffixedWithATralingSlash When true, ensures the issuer URL always ends with a trailing slash; * when false, ensures it does not end with a trailing slash; * when undefined, no modification is made to the URL. */ constructor(shouldIssuerUrlBeSuffixedWithATralingSlash?: boolean); /** * Returns the key store. * @returns The key store. */ get keys(): JWKStore; /** * Builds a JWT. * @param opts JWT token building overrides * @returns The produced JWT. * @fires OAuth2Issuer#beforeSigning */ buildToken(opts?: TokenBuildOptions): Promise<string>; } //#endregion //#region src/lib/oauth2-service.d.ts /** * Provides a request handler for an OAuth 2 server. */ declare class OAuth2Service extends EventEmitter { #private; constructor(oauth2Issuer: OAuth2Issuer, endpoints?: OAuth2EndpointsInput); /** * Returns the OAuth2Issuer instance bound to this service. * @returns The OAuth2Issuer instance. */ get issuer(): OAuth2Issuer; /** * Builds a JWT with a key in the keystore. The key will be selected in a round-robin fashion. * @param req The incoming HTTP request. * @param expiresIn Time in seconds for the JWT to expire. Default: 3600 seconds. * @param scopesOrTransform A scope, array of scopes, * or JWT transformation callback. * @returns The produced JWT. * @fires OAuth2Service#beforeTokenSigning */ buildToken(req: TokenRequestIncomingMessage, expiresIn: number, scopesOrTransform: ScopesOrTransform | undefined): Promise<string>; /** * Returns a request handler to be used as a callback for http.createServer(). * @returns The request handler. */ get requestHandler(): RequestListener; /** * Adds a custom route to the service. * @param method The HTTP method for the route. * @param path The path for the route. * @param handler The handler function for the route. */ addRoute(method: HttpMethod, path: string, handler: RouteHandler): void; private addRouteInternal; private registerBuiltInRoutes; private buildRequestHandler; private openidConfigurationHandler; private jwksHandler; private tokenHandler; private authorizeHandler; private userInfoHandler; private revokeHandler; private endSessionHandler; private introspectHandler; } //#endregion //#region src/lib/oauth2-server.d.ts /** * Represents an OAuth2 HTTP server. */ declare class OAuth2Server extends HttpServer { private _service; private _issuer; /** * Creates a new instance of OAuth2Server. * @param key Optional key file path for ssl * @param cert Optional cert file path for ssl * @param oauth2Options Optional additional settings * @returns A new instance of OAuth2Server. */ constructor(key?: string, cert?: string, oauth2Options?: OAuth2Options); /** * Returns the OAuth2Issuer instance used by the server. * @returns The OAuth2Issuer instance. */ get issuer(): OAuth2Issuer; /** * Returns the OAuth2Service instance used by the server. * @returns The OAuth2Service instance. */ get service(): OAuth2Service; /** * Returns a value indicating whether or not the server is listening for connections. * @returns A boolean value indicating whether the server is listening. */ override get listening(): boolean; /** * Returns the bound address, family name and port where the server is listening, * or null if the server has not been started. * @returns The server bound address information. */ override address(): AddressInfo; /** * Starts the server. * @param port Port number. If omitted, it will be assigned by the operating system. * @param host Host name. * @returns A promise that resolves when the server has been started. */ override start(port?: number, host?: string): Promise<void>; /** * Stops the server. * @returns Resolves when the server has been stopped. */ override stop(): Promise<void>; } //#endregion export { RouteHandler as C, TokenRequest as D, TokenBuildOptions as E, TokenRequestIncomingMessage as O, Payload as S, StatusCodeMutableResponse as T, OAuth2Endpoints as _, HttpServer as a, Options as b, Events as c, HttpServerOptions as d, JWK$1 as f, MutableToken as g, MutableResponse as h, JWKStore as i, supportedHttpMethods as k, Header as l, MutableRedirectUri as m, OAuth2Service as n, AugmentedRequest as o, JwtTransform as p, OAuth2Issuer as r, CodeChallenge as s, OAuth2Server as t, HttpMethod as u, OAuth2EndpointsInput as v, ScopesOrTransform as w, PKCEAlgorithm as x, OAuth2Options as y };