UNPKG

@civic/auth-mcp

Version:

Civic Auth integration for MCP servers

284 lines (276 loc) 10.4 kB
import { Request, RequestHandler } from 'express'; import { IncomingMessage } from 'node:http'; import { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js'; import { JWTPayload, createRemoteJWKSet, createLocalJWKSet } from 'jose'; export { CLIAuthProvider, CLIAuthProviderOptions, CLIClient, CivicAuthProvider, CivicAuthProviderOptions, DEFAULT_CALLBACK_PORT, DEFAULT_MCP_ROUTE, DEFAULT_SCOPES, DEFAULT_WELLKNOWN_URL, InMemoryTokenPersistence, PUBLIC_CIVIC_CLIENT_ID, RestartableStreamableHTTPClientTransport, TokenAuthProvider, TokenAuthProviderOptions, TokenPersistence } from './client/index.cjs'; import '@modelcontextprotocol/sdk/client/index.js'; import '@modelcontextprotocol/sdk/client/streamableHttp.js'; import '@modelcontextprotocol/sdk/client/sse.js'; import '@modelcontextprotocol/sdk/shared/auth.js'; import '@modelcontextprotocol/sdk/client/auth.js'; /** * OAuth state information stored between authorization and callback */ interface OAuthState { /** * Original redirect URI from the client */ redirectUri: string; /** * Original state parameter from the client */ clientState?: string; /** * PKCE code challenge from the client */ codeChallenge?: string; /** * PKCE code challenge method */ codeChallengeMethod?: string; /** * Timestamp when state was created */ createdAt: number; /** * OAuth scopes requested */ scope?: string; /** * Client ID making the request */ clientId?: string; } /** * Interface for storing OAuth state between redirects */ interface StateStore { /** * Store state data */ set(key: string, state: OAuthState): Promise<void>; /** * Retrieve state data */ get(key: string): Promise<OAuthState | null>; /** * Delete state data */ delete(key: string): Promise<void>; /** * Clean up expired states */ cleanup?(): Promise<void>; } /** * In-memory implementation of OAuth state store */ declare class InMemoryStateStore implements StateStore { private states; set(key: string, state: OAuthState): Promise<void>; get(key: string): Promise<OAuthState | null>; delete(key: string): Promise<void>; cleanup(): Promise<void>; } interface CivicAuthOptions<TAuthInfo extends ExtendedAuthInfo, TRequest extends IncomingMessage = IncomingMessage> { /** * The URL to the auth server's well-known OIDC configuration * Defaults to https://auth.civic.com/oauth/.well-known/openid-configuration */ wellKnownUrl?: string; /** * OAuth scopes to support * Defaults to ['openid', 'profile', 'email'] */ scopesSupported?: string[]; /** * Header name to read the protocol from (e.g. "X-Forwarded-Proto"). * Resolution order: forceHttps > protocolHeader > req.protocol. */ protocolHeader?: string; /** * Header name to read the host from (e.g. "X-Forwarded-Host"). * Defaults to the standard "host" header. */ hostHeader?: string; /** * Base path for auth endpoints * Defaults to '/' */ basePath?: string; /** * The MCP route to protect with authentication * Defaults to '/mcp' */ mcpRoute?: string; /** * Optional callback to enrich the auth info with custom data * Called after successful token verification * @param authInfo The verified auth info from the token. Null if no token was provided. * @param request Optional request object that may contain headers or other data * @returns Enriched auth info with custom data */ onLogin?: (authInfo: ExtendedAuthInfo | null, request?: TRequest) => Promise<TAuthInfo | null>; /** * Optional OAuth client ID / Tenant ID. * When set, the access token must include *either* a "client_id" field or "tid" field that matches it. */ clientId?: string; /** * Whether to allow dynamic client registration by adding client ID as subdomain. * When true, the client ID will be added as a subdomain to the auth server URL. * When false (default), the auth server URL will be used as-is without subdomain prefixing. * Defaults to false. */ allowDynamicClientRegistration?: boolean; /** * Enable legacy OAuth mode where MCP server acts as an OAuth server. * When true, the server will expose OAuth endpoints that proxy to the underlying auth server. * Defaults to true for backward compatibility. * @deprecated This mode is deprecated. Clients should authenticate directly with the auth server. */ enableLegacyOAuth?: boolean; /** * Custom state store for managing OAuth flow state between redirects in legacy mode. * Only used when enableLegacyOAuth is true. * Defaults to in-memory store. */ stateStore?: StateStore; /** * Optional JSON Web Key Set for local JWT verification. * When provided, these keys will be used instead of fetching from the OIDC jwks_uri. * Useful for testing or air-gapped environments. */ jwks?: { keys: Array<{ kty: string; kid?: string; use?: string; alg?: string; [key: string]: unknown; }>; }; /** * Whether to disable client ID verification. * When true, the client_id or tid verification will be skipped. * Defaults to false (verification enabled). */ disableClientIdVerification?: boolean; /** * If true, forces all metadata URLs to use https even if the incoming request is http. * This is useful when sitting behind a proxy that terminates SSL. * Defaults to false. */ forceHttps?: boolean; } interface OIDCWellKnownConfiguration { issuer: string; authorization_endpoint: string; token_endpoint: string; jwks_uri: string; scopes_supported?: string[]; response_types_supported?: string[]; grant_types_supported?: string[]; token_endpoint_auth_methods_supported?: string[]; introspection_endpoint?: string; revocation_endpoint?: string; registration_endpoint?: string; } interface ExtendedAuthInfo extends AuthInfo { /** * The tenant ID from the tid claim, if present */ tenantId?: string; extra?: { sub?: string; email?: string; name?: string; picture?: string; [key: string]: unknown; }; } /** * Custom error class for all authentication errors */ declare class AuthenticationError extends Error { } /** * Custom error class for JWT verification failures */ declare class JWTVerificationError extends AuthenticationError { originalError?: Error | undefined; constructor(message: string, originalError?: Error | undefined); } type AccessTokenPayload = JWTPayload & { client_id: string | undefined; tid: string | undefined; }; /** * Core authentication functionality that can be used with any framework */ declare class McpServerAuth<TAuthInfo extends ExtendedAuthInfo, TRequest extends IncomingMessage = IncomingMessage> { protected oidcConfig: OIDCWellKnownConfiguration; protected jwks: ReturnType<typeof createRemoteJWKSet> | ReturnType<typeof createLocalJWKSet>; protected options: CivicAuthOptions<TAuthInfo, TRequest>; protected constructor(oidcConfig: OIDCWellKnownConfiguration, options: CivicAuthOptions<TAuthInfo, TRequest>); /** * Initialize the auth core by fetching OIDC configuration */ static init<TAuthInfo extends ExtendedAuthInfo, TRequest extends IncomingMessage = IncomingMessage>(options?: CivicAuthOptions<TAuthInfo, TRequest>): Promise<McpServerAuth<TAuthInfo, TRequest>>; /** * Get the OAuth Protected Resource metadata * @param resourceUrl The resource URL of the protected resource (e.g., https://my-server.com/mcp) */ getProtectedResourceMetadata(resourceUrl: string): { resource: string; authorization_servers: string[]; scopes_supported: string[]; bearer_methods_supported: string[]; }; /** * Create auth info from a token (or null) and request * @param token The JWT token (can be null) * @param payload The JWT payload if token was already verified * @param request Optional request object to pass to onLogin callback * @returns ExtendedAuthInfo if successful, null otherwise */ private createAuthInfo; /** * Extract and verify bearer token from authorization header * @param authHeader The Authorization header value * @returns Object with token and payload if valid, throws if invalid token, returns null values if no token */ private extractBearerToken; /** * Handle a request by extracting and verifying the bearer token * @param request The request object * @returns ExtendedAuthInfo if valid * @throws Error if authentication fails */ handleRequest(request: TRequest): Promise<TAuthInfo>; } interface UrlResolutionOptions { /** Header to read the protocol from. Default: none (uses forceHttps or req.protocol) */ protocolHeader?: string; /** Header to read the host from. Default: "host" (standard Host header) */ hostHeader?: string; /** Force HTTPS regardless of headers. Default: false */ forceHttps?: boolean; } /** Resolves protocol and host from request, respecting configured headers */ declare function resolveBaseUrl(req: IncomingMessage, options?: UrlResolutionOptions): string; /** * Express middleware that configures an MCP server to use Civic Auth * as its authorization server. * * This middleware: * 1. Exposes /.well-known/oauth-protected-resource metadata * 2. Validates bearer tokens using Civic's JWKS * 3. Attaches user info to the request * 4. (Legacy) Optionally exposes OAuth server endpoints for backward compatibility * * @param options Configuration options * @returns Express middleware */ declare function auth<TAuthInfo extends ExtendedAuthInfo>(options?: CivicAuthOptions<TAuthInfo, Request>): Promise<RequestHandler>; export { type AccessTokenPayload, AuthenticationError, type CivicAuthOptions, type ExtendedAuthInfo, InMemoryStateStore, JWTVerificationError, McpServerAuth, type OAuthState, type OIDCWellKnownConfiguration, type StateStore, type UrlResolutionOptions, auth, resolveBaseUrl };