mcpresso-oauth-server
Version:
Production-ready OAuth 2.1 server implementation for Model Context Protocol (MCP) with PKCE support
447 lines (415 loc) • 15.2 kB
text/typescript
// MCP OAuth 2.1 Types - Focused on Model Context Protocol requirements
// Based on: https://modelcontextprotocol.io/specification/draft/basic/authorization
import type { CorsOptions } from 'cors'
export interface OAuthClient {
id: string
secret?: string // Optional for public clients
name: string
type: 'confidential' | 'public'
redirectUris: string[]
scopes: string[]
grantTypes: string[]
createdAt: Date
updatedAt: Date
}
export interface OAuthUser {
id: string
username: string
email?: string
scopes: string[]
createdAt: Date
updatedAt: Date
// Add optional custom properties for extended user data
[key: string]: any
}
// User authentication context passed to authentication callbacks
export interface UserAuthContext {
clientId: string
scope?: string
resource?: string
redirectUri: string
ipAddress: string
userAgent?: string
}
// User authentication callbacks for custom login logic
export interface UserAuthCallbacks {
/**
* Authenticate a user by username/email and password.
* This callback is invoked when the user submits their credentials.
*
* @param credentials - The user's login credentials
* @param context - Authentication context (client, scope, etc.)
* @returns Promise<OAuthUser | null> - The authenticated user or null if invalid
*/
authenticateUser?: (
credentials: { username: string; password: string },
context: UserAuthContext
) => Promise<OAuthUser | null>
/**
* Get the currently authenticated user from session/context.
* This callback is invoked during the authorization flow to determine
* if a user is already logged in.
*
* @param sessionData - Session data (cookies, tokens, etc.)
* @param context - Authentication context
* @returns Promise<OAuthUser | null> - The current user or null if not authenticated
*/
getCurrentUser?: (
sessionData: any,
context: UserAuthContext
) => Promise<OAuthUser | null>
/**
* Render or redirect to a custom login page.
* If not provided, a basic HTML login form will be used.
*
* @param context - Authentication context
* @param error - Optional error message to display
* @returns Promise<string | { redirect: string }> - HTML content or redirect info
*/
renderLoginPage?: (
context: UserAuthContext,
error?: string
) => Promise<string | { redirect: string }>
/**
* Render or redirect to a custom consent/authorization page.
* If not provided, automatic consent will be granted.
*
* @param user - The authenticated user
* @param context - Authentication context
* @returns Promise<boolean | string | { redirect: string }> - Consent result or custom page
*/
renderConsentPage?: (
user: OAuthUser,
context: UserAuthContext
) => Promise<boolean | string | { redirect: string }>
}
export interface AuthorizationCode {
code: string
clientId: string
userId: string
redirectUri: string
scope: string
resource?: string;
codeChallenge?: string
codeChallengeMethod?: 'S256' | 'plain'
expiresAt: Date
createdAt: Date
}
export interface AccessToken {
token: string
clientId: string
userId?: string // Optional for client credentials flow
scope: string
expiresAt: Date
createdAt: Date
// MCP-specific: Resource indicator for audience binding
audience?: string
}
export interface RefreshToken {
token: string
accessTokenId: string
clientId: string
userId?: string
scope: string
expiresAt: Date
createdAt: Date
// MCP-specific: Resource indicator for audience binding
audience?: string
}
// MCP OAuth Request/Response Types
export interface AuthorizationRequest {
response_type: 'code'
client_id: string
redirect_uri: string
scope?: string
state?: string
// MCP-specific: Resource indicator (REQUIRED)
resource: string
// PKCE (REQUIRED for MCP)
code_challenge: string
code_challenge_method: 'S256' | 'plain'
}
export interface TokenRequest {
grant_type: 'authorization_code' | 'refresh_token' | 'client_credentials'
client_id: string
client_secret?: string
code?: string
redirect_uri?: string
refresh_token?: string
scope?: string
// MCP-specific: Resource indicator (REQUIRED)
resource: string
// PKCE verification (REQUIRED for authorization_code)
code_verifier?: string
}
export type TokenType = 'Bearer'
export type CodeChallengeMethod = 'S256' | 'plain'
export interface TokenResponse {
access_token: string
token_type: TokenType
expires_in: number
refresh_token?: string
scope: string
}
export interface TokenIntrospectionResponse {
active: boolean
scope?: string
client_id?: string
username?: string
exp?: number
aud?: string // MCP-specific: audience validation
}
export interface UserInfoResponse {
sub: string
name?: string
email?: string
scope?: string
}
// Dynamic Client Registration (RFC 7591)
export interface ClientRegistrationRequest {
redirect_uris: string[]
client_name?: string
client_uri?: string
logo_uri?: string
scope?: string
grant_types?: string[]
response_types?: string[]
token_endpoint_auth_method?: string
token_endpoint_auth_signing_alg?: string
contacts?: string[]
policy_uri?: string
terms_of_service_uri?: string
jwks_uri?: string
jwks?: any
software_id?: string
software_version?: string
}
export interface ClientRegistrationResponse {
client_id: string
client_secret?: string
client_id_issued_at?: number
client_secret_expires_at?: number
redirect_uris: string[]
client_name?: string
client_uri?: string
logo_uri?: string
scope?: string
grant_types?: string[]
response_types?: string[]
token_endpoint_auth_method?: string
token_endpoint_auth_signing_alg?: string
contacts?: string[]
policy_uri?: string
terms_of_service_uri?: string
jwks_uri?: string
jwks?: any
software_id?: string
software_version?: string
}
// MCP OAuth Error Types
export interface OAuthError {
error: string
error_description?: string
error_uri?: string
state?: string
}
// HTTP Server Configuration
export interface HTTPServerConfig {
cors?: CorsOptions
trustProxy?: boolean | string | string[] | number
jsonLimit?: string
urlencodedLimit?: string
enableCompression?: boolean
enableHelmet?: boolean
enableRateLimit?: boolean
rateLimitConfig?: {
windowMs?: number
max?: number
message?: string
standardHeaders?: boolean
legacyHeaders?: boolean
}
}
/**
* Configuration for the MCP OAuth 2.1 Server.
*
* @property issuer - The OAuth 2.1 issuer URL (should be the public base URL of your auth server). Example: 'https://auth.example.com'.
* @property serverUrl - The public base URL of your OAuth server (used for discovery and resource indicators).
* @property authorizationEndpoint - Full URL to the /authorize endpoint. Example: 'https://auth.example.com/authorize'.
* @property tokenEndpoint - Full URL to the /token endpoint.
* @property userinfoEndpoint - Full URL to the /userinfo endpoint.
* @property jwksEndpoint - Full URL to the JWKS endpoint.
* @property introspectionEndpoint - Full URL to the /introspect endpoint.
* @property revocationEndpoint - Full URL to the /revoke endpoint.
* @property requireResourceIndicator - If true, the 'resource' parameter is required in all auth/token requests (MCP best practice). Default: true for MCP, false for dev.
* @property requirePkce - If true, PKCE is required for all authorization code flows. Default: true (MCP requirement).
* @property allowRefreshTokens - If true, refresh tokens are issued and accepted. Default: true.
* @property allowDynamicClientRegistration - If true, clients can register via the /register endpoint (RFC 7591). Default: true for dev, false for prod.
* @property accessTokenLifetime - Access token lifetime in seconds. Default: 3600 (1 hour).
* @property refreshTokenLifetime - Refresh token lifetime in seconds. Default: 2592000 (30 days).
* @property authorizationCodeLifetime - Authorization code lifetime in seconds. Default: 600 (10 minutes).
* @property supportedGrantTypes - List of supported OAuth grant types. Example: ['authorization_code', 'refresh_token', 'client_credentials'].
* @property supportedResponseTypes - List of supported OAuth response types. Example: ['code'].
* @property supportedScopes - List of supported scopes. Example: ['read', 'write', 'admin'].
* @property supportedCodeChallengeMethods - Supported PKCE code challenge methods. Example: ['S256', 'plain'].
* @property jwtSecret - Secret for signing JWTs (use a strong, random value in production!).
* @property jwtAlgorithm - JWT signing algorithm. Example: 'HS256'.
* @property http - HTTP server configuration (CORS, rate limiting, etc). See HTTPServerConfig.
*/
export interface MCPOAuthConfig {
/** OAuth 2.1 issuer URL (public base URL of your auth server). */
issuer: string
/** Public base URL of your OAuth server (used for discovery and resource indicators). */
serverUrl: string
/** Full URL to the /authorize endpoint. */
authorizationEndpoint: string
/** Full URL to the /token endpoint. */
tokenEndpoint: string
/** Full URL to the /userinfo endpoint. */
userinfoEndpoint: string
/** Full URL to the JWKS endpoint. */
jwksEndpoint: string
/** Full URL to the /introspect endpoint. */
introspectionEndpoint: string
/** Full URL to the /revoke endpoint. */
revocationEndpoint: string
/** Require 'resource' parameter in all auth/token requests (MCP best practice). */
requireResourceIndicator: boolean
/** Require PKCE for all authorization code flows (MCP requirement). */
requirePkce: boolean
/** Issue and accept refresh tokens. */
allowRefreshTokens: boolean
/** Allow dynamic client registration via /register (RFC 7591). */
allowDynamicClientRegistration: boolean
/** Access token lifetime in seconds. */
accessTokenLifetime: number
/** Refresh token lifetime in seconds. */
refreshTokenLifetime: number
/** Authorization code lifetime in seconds. */
authorizationCodeLifetime: number
/** Supported OAuth grant types. */
supportedGrantTypes: readonly string[]
/** Supported OAuth response types. */
supportedResponseTypes: readonly string[]
/** Supported scopes. */
supportedScopes: readonly string[]
/** Supported PKCE code challenge methods. */
supportedCodeChallengeMethods: readonly string[]
/** Secret for signing JWTs (use a strong, random value in production!). */
jwtSecret: string
/** JWT signing algorithm. */
jwtAlgorithm: string
/** HTTP server configuration (CORS, rate limiting, etc). */
http?: HTTPServerConfig
/** User authentication callbacks for custom login logic. */
auth?: UserAuthCallbacks
}
/**
* Input type for MCPOAuthServer: only issuer, serverUrl, and jwtSecret are required, all others are optional.
*/
export type MCPOAuthConfigInput = Pick<MCPOAuthConfig, 'issuer' | 'serverUrl' | 'jwtSecret'> & Partial<Omit<MCPOAuthConfig, 'issuer' | 'serverUrl' | 'jwtSecret'>>;
/**
* HTTP server configuration for the MCP OAuth server.
*
* @property cors - CORS configuration (see 'cors' package for options).
* @property trustProxy - Trust proxy headers (true if behind a reverse proxy).
* @property jsonLimit - Max JSON body size (e.g. '10mb').
* @property urlencodedLimit - Max urlencoded body size (e.g. '10mb').
* @property enableCompression - Enable gzip compression. Default: true.
* @property enableHelmet - Enable helmet security headers. Default: true.
* @property enableRateLimit - Enable rate limiting. Default: true.
* @property rateLimitConfig - Rate limiting options (windowMs, max, etc).
*/
export interface HTTPServerConfig {
/** CORS configuration (see 'cors' package for options). */
cors?: import('cors').CorsOptions
/** Trust proxy headers (true if behind a reverse proxy). */
trustProxy?: boolean | string | string[] | number
/** Max JSON body size (e.g. '10mb'). */
jsonLimit?: string
/** Max urlencoded body size (e.g. '10mb'). */
urlencodedLimit?: string
/** Enable gzip compression. Default: true. */
enableCompression?: boolean
/** Enable helmet security headers. Default: true. */
enableHelmet?: boolean
/** Enable rate limiting. Default: true. */
enableRateLimit?: boolean
/** Rate limiting options (windowMs, max, etc). */
rateLimitConfig?: {
/** Time window in ms. Default: 15 minutes. */
windowMs?: number
/** Max requests per window. Default: 100. */
max?: number
/** Message to return when rate limited. */
message?: string
/** Use standard rate limit headers. */
standardHeaders?: boolean
/** Use legacy rate limit headers. */
legacyHeaders?: boolean
}
}
// MCP Protected Resource Metadata (RFC 9728)
export interface MCPProtectedResourceMetadata {
resource: string
authorization_servers: string[]
scopes_supported?: string[]
bearer_methods_supported?: string[]
}
// MCP Authorization Server Metadata (RFC 8414)
export interface MCPAuthorizationServerMetadata {
issuer: string
authorization_endpoint: string
token_endpoint: string
userinfo_endpoint?: string
jwks_uri: string
revocation_endpoint?: string
introspection_endpoint?: string
registration_endpoint?: string // Dynamic client registration
grant_types_supported: string[]
response_types_supported: string[]
scopes_supported: string[]
token_endpoint_auth_methods_supported: string[]
code_challenge_methods_supported: string[]
resource_indicators_supported?: boolean
}
// Storage interface for MCP OAuth server
export interface MCPOAuthStorage {
// Client management
createClient(client: OAuthClient): Promise<void>
getClient(clientId: string): Promise<OAuthClient | null>
listClients(): Promise<OAuthClient[]>
updateClient(clientId: string, updates: Partial<OAuthClient>): Promise<void>
deleteClient(clientId: string): Promise<void>
// User management
createUser(user: OAuthUser): Promise<void>
getUser(userId: string): Promise<OAuthUser | null>
getUserByUsername(username: string): Promise<OAuthUser | null>
listUsers(): Promise<OAuthUser[]>
updateUser(userId: string, updates: Partial<OAuthUser>): Promise<void>
deleteUser(userId: string): Promise<void>
// Authorization codes
createAuthorizationCode(code: AuthorizationCode): Promise<void>
getAuthorizationCode(code: string): Promise<AuthorizationCode | null>
deleteAuthorizationCode(code: string): Promise<void>
cleanupExpiredCodes(): Promise<void>
// Access tokens
createAccessToken(token: AccessToken): Promise<void>
getAccessToken(token: string): Promise<AccessToken | null>
deleteAccessToken(token: string): Promise<void>
cleanupExpiredTokens(): Promise<void>
// Refresh tokens
createRefreshToken(token: RefreshToken): Promise<void>
getRefreshToken(token: string): Promise<RefreshToken | null>
deleteRefreshToken(token: string): Promise<void>
deleteRefreshTokensByAccessToken(accessTokenId: string): Promise<void>
cleanupExpiredRefreshTokens(): Promise<void>
// Utility methods
getStats(): {
clients: number
users: number
authorizationCodes: number
accessTokens: number
refreshTokens: number
}
}