@civic/auth-mcp
Version:
Civic Auth integration for MCP servers
284 lines (276 loc) • 10.4 kB
text/typescript
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 };