@leanmcp/auth 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,496 @@
1
+ import { Router } from 'express';
2
+
3
+ /**
4
+ * JWT Utilities for Stateless OAuth
5
+ *
6
+ * Provides cryptographic functions for:
7
+ * - JWT signing and verification (HS256)
8
+ * - Upstream token encryption/decryption (AES-256-GCM)
9
+ */
10
+ /**
11
+ * Encrypted token structure for upstream credentials
12
+ */
13
+ interface EncryptedToken {
14
+ ciphertext: string;
15
+ iv: string;
16
+ tag: string;
17
+ }
18
+ /**
19
+ * JWT payload structure (RFC 7519 + custom claims)
20
+ */
21
+ interface JWTPayload {
22
+ iss: string;
23
+ sub: string;
24
+ aud: string | string[];
25
+ exp: number;
26
+ iat: number;
27
+ jti?: string;
28
+ scope?: string;
29
+ client_id?: string;
30
+ name?: string;
31
+ email?: string;
32
+ picture?: string;
33
+ upstream_provider?: string;
34
+ upstream_token?: EncryptedToken;
35
+ upstream_refresh_token?: EncryptedToken;
36
+ }
37
+
38
+ /**
39
+ * Server-side OAuth types for MCP Authorization
40
+ *
41
+ * Types for Authorization Server, Dynamic Client Registration,
42
+ * and Token Verification per MCP spec.
43
+ */
44
+ /**
45
+ * Client registration request (RFC 7591 Section 2)
46
+ */
47
+ interface ClientRegistrationRequest {
48
+ /** Array of redirect URIs */
49
+ redirect_uris?: string[];
50
+ /** OAuth 2.0 grant types */
51
+ grant_types?: ('authorization_code' | 'refresh_token' | 'client_credentials')[];
52
+ /** OAuth 2.0 response types */
53
+ response_types?: ('code' | 'token')[];
54
+ /** Client name */
55
+ client_name?: string;
56
+ /** Client URI */
57
+ client_uri?: string;
58
+ /** Logo URI */
59
+ logo_uri?: string;
60
+ /** Token endpoint auth method */
61
+ token_endpoint_auth_method?: 'none' | 'client_secret_post' | 'client_secret_basic';
62
+ /** Scope */
63
+ scope?: string;
64
+ /** Contacts */
65
+ contacts?: string[];
66
+ /** Software ID */
67
+ software_id?: string;
68
+ /** Software version */
69
+ software_version?: string;
70
+ }
71
+ /**
72
+ * Client registration response (RFC 7591 Section 3.2.1)
73
+ */
74
+ interface ClientRegistrationResponse {
75
+ /** Issued client ID */
76
+ client_id: string;
77
+ /** Issued client secret (for confidential clients) */
78
+ client_secret?: string;
79
+ /** Timestamp when client_id was issued */
80
+ client_id_issued_at?: number;
81
+ /** Timestamp when client_secret expires (0 = never) */
82
+ client_secret_expires_at?: number;
83
+ /** All registration request fields echoed back */
84
+ redirect_uris?: string[];
85
+ grant_types?: string[];
86
+ response_types?: string[];
87
+ client_name?: string;
88
+ token_endpoint_auth_method?: string;
89
+ }
90
+ /**
91
+ * Registered client stored in memory/storage
92
+ */
93
+ interface RegisteredClient {
94
+ client_id: string;
95
+ client_secret?: string;
96
+ redirect_uris: string[];
97
+ grant_types: string[];
98
+ response_types: string[];
99
+ client_name?: string;
100
+ token_endpoint_auth_method: string;
101
+ created_at: number;
102
+ expires_at?: number;
103
+ }
104
+ /**
105
+ * OAuth 2.0 Authorization Server Metadata (RFC 8414)
106
+ */
107
+ interface AuthorizationServerMetadata {
108
+ /** Authorization server's issuer identifier URL */
109
+ issuer: string;
110
+ /** URL of the authorization endpoint */
111
+ authorization_endpoint: string;
112
+ /** URL of the token endpoint */
113
+ token_endpoint: string;
114
+ /** URL of the dynamic client registration endpoint */
115
+ registration_endpoint?: string;
116
+ /** URL of the JWKS endpoint */
117
+ jwks_uri?: string;
118
+ /** Supported scopes */
119
+ scopes_supported?: string[];
120
+ /** Supported response types */
121
+ response_types_supported: string[];
122
+ /** Supported grant types */
123
+ grant_types_supported?: string[];
124
+ /** Supported PKCE code challenge methods */
125
+ code_challenge_methods_supported?: string[];
126
+ /** Supported token endpoint auth methods */
127
+ token_endpoint_auth_methods_supported?: string[];
128
+ /** Service documentation URL */
129
+ service_documentation?: string;
130
+ }
131
+ /**
132
+ * OAuth 2.0 Protected Resource Metadata (RFC 9728)
133
+ */
134
+ interface ProtectedResourceMetadata {
135
+ /** Canonical resource identifier */
136
+ resource: string;
137
+ /** Authorization servers that can authorize access */
138
+ authorization_servers: string[];
139
+ /** Scopes supported by this resource */
140
+ scopes_supported?: string[];
141
+ /** Resource documentation URL */
142
+ resource_documentation?: string;
143
+ /** Token endpoint auth methods supported */
144
+ token_endpoint_auth_methods_supported?: string[];
145
+ /** Introspection endpoint */
146
+ introspection_endpoint?: string;
147
+ }
148
+ /**
149
+ * Token claims extracted from access token
150
+ */
151
+ interface TokenClaims {
152
+ /** Subject (user ID) */
153
+ sub: string;
154
+ /** Issuer */
155
+ iss: string;
156
+ /** Audience (resource server) */
157
+ aud: string | string[];
158
+ /** Expiration timestamp */
159
+ exp: number;
160
+ /** Issued at timestamp */
161
+ iat: number;
162
+ /** Scopes (space-separated or array) */
163
+ scope?: string | string[];
164
+ /** Client ID */
165
+ client_id?: string;
166
+ /** Additional claims */
167
+ [key: string]: unknown;
168
+ }
169
+ /**
170
+ * Token verification result
171
+ */
172
+ interface TokenVerificationResult {
173
+ /** Whether token is valid */
174
+ valid: boolean;
175
+ /** Extracted claims (if valid) */
176
+ claims?: JWTPayload;
177
+ /** Decrypted upstream token (if present and valid) */
178
+ upstreamToken?: string;
179
+ /** Error message (if invalid) */
180
+ error?: string;
181
+ /** Error code */
182
+ errorCode?: 'invalid_token' | 'expired_token' | 'insufficient_scope';
183
+ }
184
+
185
+ interface OAuthAuthorizationServerOptions {
186
+ /** Issuer URL (e.g., https://auth.example.com) */
187
+ issuer: string;
188
+ /** Session secret for signing state parameters */
189
+ sessionSecret: string;
190
+ /** JWT signing secret (for HS256) */
191
+ jwtSigningSecret?: string;
192
+ /** JWT encryption secret (32 bytes for AES-256) */
193
+ jwtEncryptionSecret?: Buffer;
194
+ /** Upstream OAuth provider configuration (e.g., GitHub, Google) */
195
+ upstreamProvider?: {
196
+ id: string;
197
+ authorizationEndpoint: string;
198
+ tokenEndpoint: string;
199
+ clientId: string;
200
+ clientSecret: string;
201
+ scopes?: string[];
202
+ userInfoEndpoint?: string;
203
+ };
204
+ /** Scopes to advertise */
205
+ scopesSupported?: string[];
206
+ /** Enable dynamic client registration */
207
+ enableDCR?: boolean;
208
+ /** Token TTL in seconds (default: 3600) */
209
+ tokenTTL?: number;
210
+ /** Refresh token TTL in seconds (default: 2592000 = 30 days) */
211
+ refreshTokenTTL?: number;
212
+ /** Custom token mapper */
213
+ tokenMapper?: (upstreamTokens: {
214
+ access_token: string;
215
+ refresh_token?: string;
216
+ expires_in?: number;
217
+ }, userInfo: Record<string, unknown>) => Promise<{
218
+ access_token: string;
219
+ refresh_token?: string;
220
+ expires_in?: number;
221
+ }>;
222
+ }
223
+ /**
224
+ * DCR options
225
+ */
226
+ interface DynamicClientRegistrationOptions {
227
+ /** Client ID prefix */
228
+ clientIdPrefix?: string;
229
+ /** Client secret length in bytes */
230
+ clientSecretLength?: number;
231
+ /** Client TTL in seconds (0 = never expires) */
232
+ clientTTL?: number;
233
+ }
234
+ /**
235
+ * Token verifier options
236
+ */
237
+ interface TokenVerifierOptions {
238
+ /** Expected audience (resource server URL) */
239
+ audience: string;
240
+ /** Expected issuer */
241
+ issuer: string;
242
+ /** JWKS URI for RS256/ES256 signature verification */
243
+ jwksUri?: string;
244
+ /** Symmetric secret for HS256 tokens */
245
+ secret?: string;
246
+ /** Encryption secret for decrypting upstream tokens (32 bytes) */
247
+ encryptionSecret?: Buffer;
248
+ /** Clock tolerance in seconds for exp/nbf checks */
249
+ clockTolerance?: number;
250
+ }
251
+
252
+ /**
253
+ * OAuth Authorization Server
254
+ *
255
+ * MCP-compliant OAuth 2.1 authorization server that can proxy to
256
+ * external providers like GitHub, Google, etc.
257
+ *
258
+ * Implements:
259
+ * - RFC 8414: Authorization Server Metadata
260
+ * - RFC 7591: Dynamic Client Registration
261
+ * - RFC 8707: Resource Indicators
262
+ * - OAuth 2.1 with PKCE
263
+ */
264
+
265
+ /**
266
+ * OAuth Authorization Server for MCP
267
+ *
268
+ * Creates Express routes for a complete OAuth 2.1 authorization server
269
+ * that can proxy authentication to external providers.
270
+ *
271
+ * @example
272
+ * ```typescript
273
+ * import express from 'express';
274
+ * import { OAuthAuthorizationServer } from '@leanmcp/auth/server';
275
+ *
276
+ * const app = express();
277
+ *
278
+ * const authServer = new OAuthAuthorizationServer({
279
+ * issuer: 'https://mcp.example.com',
280
+ * sessionSecret: process.env.SESSION_SECRET!,
281
+ * upstreamProvider: {
282
+ * id: 'github',
283
+ * authorizationEndpoint: 'https://github.com/login/oauth/authorize',
284
+ * tokenEndpoint: 'https://github.com/login/oauth/access_token',
285
+ * clientId: process.env.GITHUB_CLIENT_ID!,
286
+ * clientSecret: process.env.GITHUB_CLIENT_SECRET!,
287
+ * scopes: ['read:user', 'repo'],
288
+ * userInfoEndpoint: 'https://api.github.com/user',
289
+ * },
290
+ * scopesSupported: ['read:user', 'repo'],
291
+ * });
292
+ *
293
+ * app.use(authServer.getRouter());
294
+ * ```
295
+ */
296
+ declare class OAuthAuthorizationServer {
297
+ private options;
298
+ private dcr;
299
+ constructor(options: OAuthAuthorizationServerOptions);
300
+ /**
301
+ * Generate state parameter with HMAC signature
302
+ */
303
+ private generateState;
304
+ /**
305
+ * Verify state parameter signature
306
+ */
307
+ private verifyState;
308
+ /**
309
+ * Generate an authorization code
310
+ */
311
+ private generateAuthCode;
312
+ /**
313
+ * Generate JWT access token with encrypted upstream credentials
314
+ */
315
+ private generateAccessToken;
316
+ /**
317
+ * Get authorization server metadata (RFC 8414)
318
+ */
319
+ getMetadata(): AuthorizationServerMetadata;
320
+ /**
321
+ * Get Express router with all OAuth endpoints
322
+ */
323
+ getRouter(): Router;
324
+ /**
325
+ * Handle authorization request
326
+ */
327
+ private handleAuthorize;
328
+ /**
329
+ * Handle callback from upstream provider
330
+ */
331
+ private handleCallback;
332
+ /**
333
+ * Handle token request
334
+ */
335
+ private handleToken;
336
+ /**
337
+ * Handle authorization code grant
338
+ */
339
+ private handleAuthCodeGrant;
340
+ }
341
+
342
+ /**
343
+ * Dynamic Client Registration (RFC 7591) - Stateless Implementation
344
+ *
345
+ * Uses signed JWTs as client credentials for serverless compatibility.
346
+ * No storage required - all client data is encoded in the credentials.
347
+ */
348
+
349
+ /**
350
+ * Dynamic Client Registration handler (Stateless)
351
+ *
352
+ * Client credentials are JWTs signed by the server:
353
+ * - client_id: JWT containing client metadata
354
+ * - client_secret: HMAC signature derived from client_id
355
+ *
356
+ * This eliminates the need for storage while maintaining security.
357
+ *
358
+ * @example
359
+ * ```typescript
360
+ * const dcr = new DynamicClientRegistration({
361
+ * signingSecret: process.env.DCR_SIGNING_SECRET,
362
+ * clientIdPrefix: 'chatgpt_',
363
+ * clientTTL: 0, // Never expires
364
+ * });
365
+ *
366
+ * // Register a client
367
+ * const { client_id, client_secret } = dcr.register({
368
+ * redirect_uris: ['https://chatgpt.com/callback'],
369
+ * grant_types: ['authorization_code'],
370
+ * });
371
+ *
372
+ * // Validate credentials (no storage lookup needed)
373
+ * if (dcr.validate(client_id, client_secret)) {
374
+ * // Valid client
375
+ * }
376
+ * ```
377
+ */
378
+ declare class DynamicClientRegistration {
379
+ private options;
380
+ constructor(options: DynamicClientRegistrationOptions & {
381
+ signingSecret: string;
382
+ });
383
+ /**
384
+ * Register a new OAuth client (stateless)
385
+ *
386
+ * @param request - Client registration request per RFC 7591
387
+ * @returns Client registration response with JWT credentials
388
+ */
389
+ register(request: ClientRegistrationRequest): ClientRegistrationResponse;
390
+ /**
391
+ * Validate client credentials (stateless)
392
+ *
393
+ * @param clientId - Client ID (JWT with prefix)
394
+ * @param clientSecret - Client secret (optional for public clients)
395
+ * @returns Whether credentials are valid
396
+ */
397
+ validate(clientId: string, clientSecret?: string): boolean;
398
+ /**
399
+ * Get a registered client by ID (stateless)
400
+ */
401
+ getClient(clientId: string): RegisteredClient | undefined;
402
+ /**
403
+ * Validate redirect URI for a client
404
+ */
405
+ validateRedirectUri(clientId: string, redirectUri: string): boolean;
406
+ /**
407
+ * Delete a client (no-op in stateless mode)
408
+ *
409
+ * In stateless mode, clients cannot be truly "deleted" because
410
+ * their credentials are self-contained. They will expire naturally
411
+ * or when the signing secret is rotated.
412
+ */
413
+ delete(clientId: string): boolean;
414
+ /**
415
+ * List all registered clients (not supported in stateless mode)
416
+ */
417
+ listClients(): RegisteredClient[];
418
+ /**
419
+ * Clear all clients (no-op in stateless mode)
420
+ */
421
+ clearAll(): void;
422
+ /**
423
+ * Extract JWT from prefixed client_id
424
+ */
425
+ private extractJWT;
426
+ /**
427
+ * Derive client secret from client_id JWT
428
+ *
429
+ * Uses HMAC to create a deterministic secret that can be
430
+ * validated without storage.
431
+ */
432
+ private deriveClientSecret;
433
+ }
434
+
435
+ /**
436
+ * Token Verifier
437
+ *
438
+ * Verifies JWT access tokens on the resource server side.
439
+ */
440
+
441
+ /**
442
+ * Token Verifier for resource servers
443
+ *
444
+ * @example
445
+ * ```typescript
446
+ * const verifier = new TokenVerifier({
447
+ * audience: 'https://mcp.example.com',
448
+ * issuer: 'https://auth.example.com',
449
+ * secret: process.env.JWT_SIGNING_SECRET,
450
+ * encryptionSecret: Buffer.from(process.env.JWT_ENCRYPTION_SECRET, 'hex'),
451
+ * });
452
+ *
453
+ * const result = await verifier.verify(accessToken);
454
+ * if (result.valid) {
455
+ * console.log('User:', result.claims.sub);
456
+ * console.log('Upstream token:', result.upstreamToken);
457
+ * } else {
458
+ * console.error('Invalid token:', result.error);
459
+ * }
460
+ * ```
461
+ */
462
+ declare class TokenVerifier {
463
+ private options;
464
+ constructor(options: TokenVerifierOptions & {
465
+ encryptionSecret?: Buffer;
466
+ });
467
+ /**
468
+ * Verify a JWT access token
469
+ *
470
+ * @param token - The JWT access token to verify
471
+ * @returns Verification result with claims and decrypted upstream token if valid
472
+ */
473
+ verify(token: string): Promise<TokenVerificationResult & {
474
+ upstreamToken?: string;
475
+ }>;
476
+ /**
477
+ * Generate WWW-Authenticate header for 401 responses
478
+ *
479
+ * Per RFC 9728, this should include the resource_metadata URL
480
+ *
481
+ * @param options - Header options
482
+ * @returns WWW-Authenticate header value
483
+ */
484
+ static getWWWAuthenticateHeader(options: {
485
+ resourceMetadataUrl: string;
486
+ error?: string;
487
+ errorDescription?: string;
488
+ scope?: string;
489
+ }): string;
490
+ /**
491
+ * Check if token has required scopes
492
+ */
493
+ hasScopes(claims: JWTPayload, requiredScopes: string[]): boolean;
494
+ }
495
+
496
+ export { type AuthorizationServerMetadata, type ClientRegistrationRequest, type ClientRegistrationResponse, DynamicClientRegistration, type DynamicClientRegistrationOptions, OAuthAuthorizationServer, type OAuthAuthorizationServerOptions, type ProtectedResourceMetadata, type RegisteredClient, type TokenClaims, type TokenVerificationResult, TokenVerifier, type TokenVerifierOptions };