@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,847 @@
1
+ import {
2
+ __name,
3
+ __require
4
+ } from "../chunk-LPEX4YW6.mjs";
5
+
6
+ // src/server/authorization-server.ts
7
+ import { randomUUID as randomUUID2, createHmac as createHmac2, randomBytes as randomBytes2, createHash } from "crypto";
8
+ import express from "express";
9
+
10
+ // src/server/dcr.ts
11
+ import { randomUUID } from "crypto";
12
+
13
+ // src/server/jwt-utils.ts
14
+ import { createHmac, randomBytes, createCipheriv, createDecipheriv } from "crypto";
15
+ function encryptUpstreamToken(plaintext, secret) {
16
+ if (secret.length !== 32) {
17
+ throw new Error("Encryption secret must be 32 bytes (256 bits)");
18
+ }
19
+ const iv = randomBytes(12);
20
+ const cipher = createCipheriv("aes-256-gcm", secret, iv);
21
+ let ciphertext = cipher.update(plaintext, "utf8", "base64");
22
+ ciphertext += cipher.final("base64");
23
+ const tag = cipher.getAuthTag();
24
+ return {
25
+ ciphertext,
26
+ iv: iv.toString("base64"),
27
+ tag: tag.toString("base64")
28
+ };
29
+ }
30
+ __name(encryptUpstreamToken, "encryptUpstreamToken");
31
+ function decryptUpstreamToken(encrypted, secret) {
32
+ if (secret.length !== 32) {
33
+ throw new Error("Encryption secret must be 32 bytes (256 bits)");
34
+ }
35
+ try {
36
+ const iv = Buffer.from(encrypted.iv, "base64");
37
+ const tag = Buffer.from(encrypted.tag, "base64");
38
+ const decipher = createDecipheriv("aes-256-gcm", secret, iv);
39
+ decipher.setAuthTag(tag);
40
+ let plaintext = decipher.update(encrypted.ciphertext, "base64", "utf8");
41
+ plaintext += decipher.final("utf8");
42
+ return plaintext;
43
+ } catch (error) {
44
+ throw new Error(`Failed to decrypt upstream token: ${error.message}`);
45
+ }
46
+ }
47
+ __name(decryptUpstreamToken, "decryptUpstreamToken");
48
+ function signJWT(payload, secret) {
49
+ const header = {
50
+ alg: "HS256",
51
+ typ: "JWT"
52
+ };
53
+ const encodedHeader = base64UrlEncode(JSON.stringify(header));
54
+ const encodedPayload = base64UrlEncode(JSON.stringify(payload));
55
+ const signatureInput = `${encodedHeader}.${encodedPayload}`;
56
+ const signature = createHmac("sha256", secret).update(signatureInput).digest("base64url");
57
+ return `${encodedHeader}.${encodedPayload}.${signature}`;
58
+ }
59
+ __name(signJWT, "signJWT");
60
+ function verifyJWT(token, secret) {
61
+ const parts = token.split(".");
62
+ if (parts.length !== 3) {
63
+ throw new Error("Invalid JWT format");
64
+ }
65
+ const [encodedHeader, encodedPayload, signature] = parts;
66
+ const signatureInput = `${encodedHeader}.${encodedPayload}`;
67
+ const expectedSignature = createHmac("sha256", secret).update(signatureInput).digest("base64url");
68
+ if (signature !== expectedSignature) {
69
+ throw new Error("Invalid JWT signature");
70
+ }
71
+ const header = JSON.parse(base64UrlDecode(encodedHeader));
72
+ const payload = JSON.parse(base64UrlDecode(encodedPayload));
73
+ if (header.alg !== "HS256") {
74
+ throw new Error(`Unsupported algorithm: ${header.alg}`);
75
+ }
76
+ const now = Math.floor(Date.now() / 1e3);
77
+ if (payload.exp && now > payload.exp) {
78
+ throw new Error("JWT expired");
79
+ }
80
+ if (payload.iat && now < payload.iat) {
81
+ throw new Error("JWT not yet valid");
82
+ }
83
+ return payload;
84
+ }
85
+ __name(verifyJWT, "verifyJWT");
86
+ function base64UrlEncode(str) {
87
+ return Buffer.from(str, "utf8").toString("base64url");
88
+ }
89
+ __name(base64UrlEncode, "base64UrlEncode");
90
+ function base64UrlDecode(str) {
91
+ return Buffer.from(str, "base64url").toString("utf8");
92
+ }
93
+ __name(base64UrlDecode, "base64UrlDecode");
94
+ function generateJTI() {
95
+ return randomBytes(16).toString("hex");
96
+ }
97
+ __name(generateJTI, "generateJTI");
98
+
99
+ // src/server/dcr.ts
100
+ var DynamicClientRegistration = class {
101
+ static {
102
+ __name(this, "DynamicClientRegistration");
103
+ }
104
+ options;
105
+ constructor(options) {
106
+ if (!options.signingSecret) {
107
+ throw new Error("signingSecret is required for stateless DCR");
108
+ }
109
+ this.options = {
110
+ clientIdPrefix: options.clientIdPrefix ?? "mcp_",
111
+ clientSecretLength: options.clientSecretLength ?? 32,
112
+ clientTTL: options.clientTTL ?? 0,
113
+ signingSecret: options.signingSecret
114
+ };
115
+ }
116
+ /**
117
+ * Register a new OAuth client (stateless)
118
+ *
119
+ * @param request - Client registration request per RFC 7591
120
+ * @returns Client registration response with JWT credentials
121
+ */
122
+ register(request) {
123
+ const now = Date.now();
124
+ const nowSeconds = Math.floor(now / 1e3);
125
+ const expiresAt = this.options.clientTTL > 0 ? now + this.options.clientTTL * 1e3 : void 0;
126
+ const expSeconds = expiresAt ? Math.floor(expiresAt / 1e3) : 0;
127
+ const authMethod = request.token_endpoint_auth_method ?? "client_secret_post";
128
+ const credentialPayload = {
129
+ sub: randomUUID(),
130
+ iss: "leanmcp-dcr",
131
+ aud: "leanmcp-oauth",
132
+ iat: nowSeconds,
133
+ exp: expSeconds || void 0,
134
+ redirect_uris: request.redirect_uris ?? [],
135
+ grant_types: request.grant_types ?? [
136
+ "authorization_code"
137
+ ],
138
+ response_types: request.response_types ?? [
139
+ "code"
140
+ ],
141
+ client_name: request.client_name,
142
+ token_endpoint_auth_method: authMethod
143
+ };
144
+ const clientIdJWT = signJWT(credentialPayload, this.options.signingSecret);
145
+ const clientId = `${this.options.clientIdPrefix}${clientIdJWT}`;
146
+ const response = {
147
+ client_id: clientId,
148
+ client_id_issued_at: nowSeconds
149
+ };
150
+ if (authMethod !== "none") {
151
+ const clientSecret = this.deriveClientSecret(clientIdJWT);
152
+ response.client_secret = clientSecret;
153
+ response.client_secret_expires_at = expSeconds;
154
+ }
155
+ if (request.redirect_uris) response.redirect_uris = request.redirect_uris;
156
+ if (request.grant_types) response.grant_types = request.grant_types;
157
+ if (request.response_types) response.response_types = request.response_types;
158
+ if (request.client_name) response.client_name = request.client_name;
159
+ if (authMethod) response.token_endpoint_auth_method = authMethod;
160
+ return response;
161
+ }
162
+ /**
163
+ * Validate client credentials (stateless)
164
+ *
165
+ * @param clientId - Client ID (JWT with prefix)
166
+ * @param clientSecret - Client secret (optional for public clients)
167
+ * @returns Whether credentials are valid
168
+ */
169
+ validate(clientId, clientSecret) {
170
+ try {
171
+ const jwtWithoutPrefix = this.extractJWT(clientId);
172
+ if (!jwtWithoutPrefix) return false;
173
+ const payload = verifyJWT(jwtWithoutPrefix, this.options.signingSecret);
174
+ if (payload.token_endpoint_auth_method && payload.token_endpoint_auth_method !== "none") {
175
+ if (!clientSecret) return false;
176
+ const expectedSecret = this.deriveClientSecret(jwtWithoutPrefix);
177
+ return clientSecret === expectedSecret;
178
+ }
179
+ return true;
180
+ } catch (error) {
181
+ return false;
182
+ }
183
+ }
184
+ /**
185
+ * Get a registered client by ID (stateless)
186
+ */
187
+ getClient(clientId) {
188
+ try {
189
+ const jwtWithoutPrefix = this.extractJWT(clientId);
190
+ if (!jwtWithoutPrefix) return void 0;
191
+ const payload = verifyJWT(jwtWithoutPrefix, this.options.signingSecret);
192
+ return {
193
+ client_id: clientId,
194
+ client_secret: payload.token_endpoint_auth_method !== "none" ? this.deriveClientSecret(jwtWithoutPrefix) : void 0,
195
+ redirect_uris: payload.redirect_uris,
196
+ grant_types: payload.grant_types,
197
+ response_types: payload.response_types,
198
+ client_name: payload.client_name,
199
+ token_endpoint_auth_method: payload.token_endpoint_auth_method,
200
+ created_at: payload.iat * 1e3,
201
+ expires_at: payload.exp ? payload.exp * 1e3 : void 0
202
+ };
203
+ } catch (error) {
204
+ return void 0;
205
+ }
206
+ }
207
+ /**
208
+ * Validate redirect URI for a client
209
+ */
210
+ validateRedirectUri(clientId, redirectUri) {
211
+ const client = this.getClient(clientId);
212
+ if (!client) return false;
213
+ if (client.redirect_uris.length === 0) return true;
214
+ return client.redirect_uris.includes(redirectUri);
215
+ }
216
+ /**
217
+ * Delete a client (no-op in stateless mode)
218
+ *
219
+ * In stateless mode, clients cannot be truly "deleted" because
220
+ * their credentials are self-contained. They will expire naturally
221
+ * or when the signing secret is rotated.
222
+ */
223
+ delete(clientId) {
224
+ return this.getClient(clientId) !== void 0;
225
+ }
226
+ /**
227
+ * List all registered clients (not supported in stateless mode)
228
+ */
229
+ listClients() {
230
+ throw new Error("listClients() is not supported in stateless DCR mode");
231
+ }
232
+ /**
233
+ * Clear all clients (no-op in stateless mode)
234
+ */
235
+ clearAll() {
236
+ }
237
+ /**
238
+ * Extract JWT from prefixed client_id
239
+ */
240
+ extractJWT(clientId) {
241
+ if (!clientId.startsWith(this.options.clientIdPrefix)) {
242
+ return null;
243
+ }
244
+ return clientId.slice(this.options.clientIdPrefix.length);
245
+ }
246
+ /**
247
+ * Derive client secret from client_id JWT
248
+ *
249
+ * Uses HMAC to create a deterministic secret that can be
250
+ * validated without storage.
251
+ */
252
+ deriveClientSecret(clientIdJWT) {
253
+ const { createHmac: createHmac3 } = __require("crypto");
254
+ return createHmac3("sha256", this.options.signingSecret).update(clientIdJWT).digest("hex");
255
+ }
256
+ };
257
+
258
+ // src/server/authorization-server.ts
259
+ var pendingAuthRequests = /* @__PURE__ */ new Map();
260
+ var pendingTokenExchanges = /* @__PURE__ */ new Map();
261
+ var CLEANUP_INTERVAL_MS = 60 * 1e3;
262
+ var REQUEST_TTL_MS = 10 * 60 * 1e3;
263
+ function cleanupExpiredRequests() {
264
+ const now = Date.now();
265
+ for (const [key, request] of pendingAuthRequests.entries()) {
266
+ if (now - request.createdAt > REQUEST_TTL_MS) {
267
+ pendingAuthRequests.delete(key);
268
+ }
269
+ }
270
+ for (const [key, exchange] of pendingTokenExchanges.entries()) {
271
+ if (now - exchange.createdAt > REQUEST_TTL_MS) {
272
+ pendingTokenExchanges.delete(key);
273
+ }
274
+ }
275
+ }
276
+ __name(cleanupExpiredRequests, "cleanupExpiredRequests");
277
+ setInterval(cleanupExpiredRequests, CLEANUP_INTERVAL_MS);
278
+ var OAuthAuthorizationServer = class {
279
+ static {
280
+ __name(this, "OAuthAuthorizationServer");
281
+ }
282
+ options;
283
+ dcr;
284
+ constructor(options) {
285
+ this.options = {
286
+ enableDCR: true,
287
+ tokenTTL: 3600,
288
+ refreshTokenTTL: 2592e3,
289
+ ...options
290
+ };
291
+ const jwtSigningSecret = this.options.jwtSigningSecret || this.options.sessionSecret;
292
+ this.dcr = new DynamicClientRegistration({
293
+ clientIdPrefix: "mcp_",
294
+ clientSecretLength: 32,
295
+ clientTTL: 0,
296
+ signingSecret: jwtSigningSecret
297
+ });
298
+ }
299
+ /**
300
+ * Generate state parameter with HMAC signature
301
+ */
302
+ generateState() {
303
+ const nonce = randomUUID2();
304
+ const signature = createHmac2("sha256", this.options.sessionSecret).update(nonce).digest("hex").substring(0, 8);
305
+ return `${nonce}.${signature}`;
306
+ }
307
+ /**
308
+ * Verify state parameter signature
309
+ */
310
+ verifyState(state) {
311
+ const [nonce, signature] = state.split(".");
312
+ if (!nonce || !signature) return false;
313
+ const expectedSignature = createHmac2("sha256", this.options.sessionSecret).update(nonce).digest("hex").substring(0, 8);
314
+ return signature === expectedSignature;
315
+ }
316
+ /**
317
+ * Generate an authorization code
318
+ */
319
+ generateAuthCode() {
320
+ return randomBytes2(32).toString("hex");
321
+ }
322
+ /**
323
+ * Generate JWT access token with encrypted upstream credentials
324
+ */
325
+ generateAccessToken(claims, upstreamToken) {
326
+ const now = Math.floor(Date.now() / 1e3);
327
+ const jwtSigningSecret = this.options.jwtSigningSecret || this.options.sessionSecret;
328
+ const jwtEncryptionSecret = this.options.jwtEncryptionSecret || Buffer.from(this.options.sessionSecret.padEnd(32, "0").slice(0, 32));
329
+ let encryptedUpstreamToken;
330
+ if (upstreamToken) {
331
+ encryptedUpstreamToken = encryptUpstreamToken(upstreamToken, jwtEncryptionSecret);
332
+ }
333
+ const payload = {
334
+ sub: claims.sub || "unknown",
335
+ iss: this.options.issuer,
336
+ aud: claims.aud || this.options.issuer,
337
+ iat: now,
338
+ exp: now + (this.options.tokenTTL || 3600),
339
+ jti: generateJTI(),
340
+ scope: claims.scope,
341
+ client_id: claims.client_id,
342
+ name: claims.name,
343
+ email: claims.email,
344
+ picture: claims.picture,
345
+ upstream_provider: claims.upstream_provider,
346
+ upstream_token: encryptedUpstreamToken
347
+ };
348
+ return signJWT(payload, jwtSigningSecret);
349
+ }
350
+ /**
351
+ * Get authorization server metadata (RFC 8414)
352
+ */
353
+ getMetadata() {
354
+ const issuer = this.options.issuer;
355
+ return {
356
+ issuer,
357
+ authorization_endpoint: `${issuer}/oauth/authorize`,
358
+ token_endpoint: `${issuer}/oauth/token`,
359
+ registration_endpoint: this.options.enableDCR ? `${issuer}/oauth/register` : void 0,
360
+ scopes_supported: this.options.scopesSupported || [],
361
+ response_types_supported: [
362
+ "code"
363
+ ],
364
+ grant_types_supported: [
365
+ "authorization_code",
366
+ "refresh_token"
367
+ ],
368
+ code_challenge_methods_supported: [
369
+ "S256"
370
+ ],
371
+ token_endpoint_auth_methods_supported: [
372
+ "client_secret_post",
373
+ "client_secret_basic",
374
+ "none"
375
+ ]
376
+ };
377
+ }
378
+ /**
379
+ * Get Express router with all OAuth endpoints
380
+ */
381
+ getRouter() {
382
+ const router = express.Router();
383
+ router.get("/.well-known/oauth-authorization-server", (_req, res) => {
384
+ res.json(this.getMetadata());
385
+ });
386
+ if (this.options.enableDCR) {
387
+ router.post("/oauth/register", express.json(), (req, res) => {
388
+ try {
389
+ const response = this.dcr.register(req.body);
390
+ res.status(201).json(response);
391
+ } catch (error) {
392
+ res.status(400).json({
393
+ error: "invalid_client_metadata",
394
+ error_description: error.message
395
+ });
396
+ }
397
+ });
398
+ }
399
+ router.get("/oauth/authorize", (req, res) => {
400
+ this.handleAuthorize(req, res);
401
+ });
402
+ router.get("/oauth/callback", async (req, res) => {
403
+ await this.handleCallback(req, res);
404
+ });
405
+ router.post("/oauth/token", express.urlencoded({
406
+ extended: true
407
+ }), async (req, res) => {
408
+ await this.handleToken(req, res);
409
+ });
410
+ return router;
411
+ }
412
+ /**
413
+ * Handle authorization request
414
+ */
415
+ handleAuthorize(req, res) {
416
+ const { response_type, client_id, redirect_uri, scope, state, code_challenge, code_challenge_method, resource } = req.query;
417
+ if (response_type !== "code") {
418
+ res.status(400).json({
419
+ error: "unsupported_response_type",
420
+ error_description: 'Only "code" response type is supported'
421
+ });
422
+ return;
423
+ }
424
+ if (!client_id) {
425
+ res.status(400).json({
426
+ error: "invalid_request",
427
+ error_description: "client_id is required"
428
+ });
429
+ return;
430
+ }
431
+ const client = this.dcr.getClient(client_id);
432
+ if (!client) {
433
+ res.status(400).json({
434
+ error: "invalid_client",
435
+ error_description: "Unknown client_id"
436
+ });
437
+ return;
438
+ }
439
+ if (redirect_uri && !this.dcr.validateRedirectUri(client_id, redirect_uri)) {
440
+ res.status(400).json({
441
+ error: "invalid_request",
442
+ error_description: "Invalid redirect_uri"
443
+ });
444
+ return;
445
+ }
446
+ if (!code_challenge || code_challenge_method !== "S256") {
447
+ res.status(400).json({
448
+ error: "invalid_request",
449
+ error_description: "PKCE with S256 is required"
450
+ });
451
+ return;
452
+ }
453
+ if (!this.options.upstreamProvider) {
454
+ res.status(500).json({
455
+ error: "server_error",
456
+ error_description: "No upstream provider configured"
457
+ });
458
+ return;
459
+ }
460
+ const proxyState = this.generateState();
461
+ const pendingRequest = {
462
+ clientId: client_id,
463
+ redirectUri: redirect_uri || client.redirect_uris[0],
464
+ scope: scope || "",
465
+ state,
466
+ codeChallenge: code_challenge,
467
+ codeChallengeMethod: code_challenge_method,
468
+ resource,
469
+ proxyState,
470
+ createdAt: Date.now()
471
+ };
472
+ pendingAuthRequests.set(proxyState, pendingRequest);
473
+ const upstream = this.options.upstreamProvider;
474
+ const authUrl = new URL(upstream.authorizationEndpoint);
475
+ authUrl.searchParams.set("client_id", upstream.clientId);
476
+ authUrl.searchParams.set("redirect_uri", `${this.options.issuer}/oauth/callback`);
477
+ authUrl.searchParams.set("response_type", "code");
478
+ authUrl.searchParams.set("state", proxyState);
479
+ authUrl.searchParams.set("scope", upstream.scopes?.join(" ") || scope || "");
480
+ res.redirect(authUrl.toString());
481
+ }
482
+ /**
483
+ * Handle callback from upstream provider
484
+ */
485
+ async handleCallback(req, res) {
486
+ const { code, state, error, error_description } = req.query;
487
+ if (error) {
488
+ const pending2 = state ? pendingAuthRequests.get(state) : void 0;
489
+ pendingAuthRequests.delete(state || "");
490
+ if (pending2) {
491
+ const redirectUri = new URL(pending2.redirectUri);
492
+ redirectUri.searchParams.set("error", error);
493
+ if (error_description) {
494
+ redirectUri.searchParams.set("error_description", error_description);
495
+ }
496
+ if (pending2.state) {
497
+ redirectUri.searchParams.set("state", pending2.state);
498
+ }
499
+ res.redirect(redirectUri.toString());
500
+ } else {
501
+ res.status(400).json({
502
+ error,
503
+ error_description
504
+ });
505
+ }
506
+ return;
507
+ }
508
+ if (!state || !this.verifyState(state)) {
509
+ res.status(400).json({
510
+ error: "invalid_request",
511
+ error_description: "Invalid state parameter"
512
+ });
513
+ return;
514
+ }
515
+ const pending = pendingAuthRequests.get(state);
516
+ if (!pending) {
517
+ res.status(400).json({
518
+ error: "invalid_request",
519
+ error_description: "State not found - request may have expired"
520
+ });
521
+ return;
522
+ }
523
+ pendingAuthRequests.delete(state);
524
+ if (!code) {
525
+ res.status(400).json({
526
+ error: "invalid_request",
527
+ error_description: "Missing authorization code"
528
+ });
529
+ return;
530
+ }
531
+ try {
532
+ const upstream = this.options.upstreamProvider;
533
+ const tokenResponse = await fetch(upstream.tokenEndpoint, {
534
+ method: "POST",
535
+ headers: {
536
+ "Accept": "application/json",
537
+ "Content-Type": "application/x-www-form-urlencoded"
538
+ },
539
+ body: new URLSearchParams({
540
+ grant_type: "authorization_code",
541
+ code,
542
+ redirect_uri: `${this.options.issuer}/oauth/callback`,
543
+ client_id: upstream.clientId,
544
+ client_secret: upstream.clientSecret
545
+ })
546
+ });
547
+ if (!tokenResponse.ok) {
548
+ const errorBody = await tokenResponse.text();
549
+ throw new Error(`Token exchange failed: ${errorBody}`);
550
+ }
551
+ const upstreamTokens = await tokenResponse.json();
552
+ let userInfo = {};
553
+ if (upstream.userInfoEndpoint && upstreamTokens.access_token) {
554
+ const userInfoResponse = await fetch(upstream.userInfoEndpoint, {
555
+ headers: {
556
+ "Authorization": `Bearer ${upstreamTokens.access_token}`,
557
+ "Accept": "application/json"
558
+ }
559
+ });
560
+ if (userInfoResponse.ok) {
561
+ userInfo = await userInfoResponse.json();
562
+ }
563
+ }
564
+ const authCode = this.generateAuthCode();
565
+ pendingTokenExchanges.set(authCode, {
566
+ clientId: pending.clientId,
567
+ redirectUri: pending.redirectUri,
568
+ scope: pending.scope,
569
+ resource: pending.resource,
570
+ upstreamTokens,
571
+ userInfo,
572
+ createdAt: Date.now(),
573
+ codeChallenge: pending.codeChallenge,
574
+ codeChallengeMethod: pending.codeChallengeMethod
575
+ });
576
+ const redirectUri = new URL(pending.redirectUri);
577
+ redirectUri.searchParams.set("code", authCode);
578
+ if (pending.state) {
579
+ redirectUri.searchParams.set("state", pending.state);
580
+ }
581
+ res.redirect(redirectUri.toString());
582
+ } catch (error2) {
583
+ console.error("Auth callback error:", error2);
584
+ const redirectUri = new URL(pending.redirectUri);
585
+ redirectUri.searchParams.set("error", "server_error");
586
+ redirectUri.searchParams.set("error_description", error2.message);
587
+ if (pending.state) {
588
+ redirectUri.searchParams.set("state", pending.state);
589
+ }
590
+ res.redirect(redirectUri.toString());
591
+ }
592
+ }
593
+ /**
594
+ * Handle token request
595
+ */
596
+ async handleToken(req, res) {
597
+ const { grant_type, code, redirect_uri, client_id, client_secret, code_verifier } = req.body;
598
+ let clientId = client_id;
599
+ const authHeader = req.headers.authorization;
600
+ if (authHeader?.startsWith("Basic ")) {
601
+ const credentials = Buffer.from(authHeader.slice(6), "base64").toString();
602
+ const [basicClientId, basicSecret] = credentials.split(":");
603
+ clientId = basicClientId;
604
+ if (!this.dcr.validate(clientId, basicSecret)) {
605
+ res.status(401).json({
606
+ error: "invalid_client",
607
+ error_description: "Invalid client credentials"
608
+ });
609
+ return;
610
+ }
611
+ } else if (client_id) {
612
+ if (!this.dcr.validate(client_id, client_secret)) {
613
+ res.status(401).json({
614
+ error: "invalid_client",
615
+ error_description: "Invalid client credentials"
616
+ });
617
+ return;
618
+ }
619
+ } else {
620
+ res.status(401).json({
621
+ error: "invalid_client",
622
+ error_description: "Client authentication required"
623
+ });
624
+ return;
625
+ }
626
+ if (grant_type === "authorization_code") {
627
+ await this.handleAuthCodeGrant(res, clientId, code, redirect_uri, code_verifier);
628
+ } else if (grant_type === "refresh_token") {
629
+ res.status(400).json({
630
+ error: "unsupported_grant_type",
631
+ error_description: "Refresh token grant not yet implemented"
632
+ });
633
+ } else {
634
+ res.status(400).json({
635
+ error: "unsupported_grant_type",
636
+ error_description: `Grant type "${grant_type}" not supported`
637
+ });
638
+ }
639
+ }
640
+ /**
641
+ * Handle authorization code grant
642
+ */
643
+ async handleAuthCodeGrant(res, clientId, code, redirectUri, codeVerifier) {
644
+ if (!code) {
645
+ res.status(400).json({
646
+ error: "invalid_request",
647
+ error_description: "Missing authorization code"
648
+ });
649
+ return;
650
+ }
651
+ const pending = pendingTokenExchanges.get(code);
652
+ if (!pending) {
653
+ res.status(400).json({
654
+ error: "invalid_grant",
655
+ error_description: "Invalid or expired authorization code"
656
+ });
657
+ return;
658
+ }
659
+ pendingTokenExchanges.delete(code);
660
+ if (pending.clientId !== clientId) {
661
+ res.status(400).json({
662
+ error: "invalid_grant",
663
+ error_description: "Client mismatch"
664
+ });
665
+ return;
666
+ }
667
+ if (redirectUri && pending.redirectUri !== redirectUri) {
668
+ res.status(400).json({
669
+ error: "invalid_grant",
670
+ error_description: "Redirect URI mismatch"
671
+ });
672
+ return;
673
+ }
674
+ if (pending.codeChallenge) {
675
+ if (!codeVerifier) {
676
+ res.status(400).json({
677
+ error: "invalid_request",
678
+ error_description: "Missing code_verifier"
679
+ });
680
+ return;
681
+ }
682
+ if (pending.codeChallengeMethod === "S256") {
683
+ const calculatedChallenge = createHash("sha256").update(codeVerifier).digest("base64url");
684
+ if (calculatedChallenge !== pending.codeChallenge) {
685
+ res.status(400).json({
686
+ error: "invalid_grant",
687
+ error_description: "PKCE verification failed"
688
+ });
689
+ return;
690
+ }
691
+ } else {
692
+ res.status(400).json({
693
+ error: "invalid_request",
694
+ error_description: "Unsupported PKCE method"
695
+ });
696
+ return;
697
+ }
698
+ }
699
+ let tokens = pending.upstreamTokens;
700
+ if (this.options.tokenMapper) {
701
+ tokens = await this.options.tokenMapper(pending.upstreamTokens, pending.userInfo);
702
+ }
703
+ const userId = pending.userInfo.id || pending.userInfo.sub || pending.userInfo.login || "unknown";
704
+ const accessToken = this.generateAccessToken({
705
+ sub: userId,
706
+ aud: pending.resource || this.options.issuer,
707
+ scope: pending.scope,
708
+ client_id: clientId,
709
+ // Include upstream user info
710
+ name: pending.userInfo.name,
711
+ email: pending.userInfo.email,
712
+ picture: pending.userInfo.avatar_url || pending.userInfo.picture,
713
+ upstream_provider: this.options.upstreamProvider?.id
714
+ }, tokens.access_token);
715
+ res.json({
716
+ access_token: accessToken,
717
+ token_type: "Bearer",
718
+ expires_in: this.options.tokenTTL || 3600,
719
+ scope: pending.scope,
720
+ // Include upstream refresh token if available
721
+ refresh_token: tokens.refresh_token
722
+ });
723
+ }
724
+ };
725
+
726
+ // src/server/token-verifier.ts
727
+ var TokenVerifier = class {
728
+ static {
729
+ __name(this, "TokenVerifier");
730
+ }
731
+ options;
732
+ constructor(options) {
733
+ if (!options.secret) {
734
+ throw new Error("JWT signing secret is required");
735
+ }
736
+ this.options = {
737
+ audience: options.audience,
738
+ issuer: options.issuer,
739
+ secret: options.secret,
740
+ clockTolerance: options.clockTolerance ?? 60,
741
+ encryptionSecret: options.encryptionSecret
742
+ };
743
+ }
744
+ /**
745
+ * Verify a JWT access token
746
+ *
747
+ * @param token - The JWT access token to verify
748
+ * @returns Verification result with claims and decrypted upstream token if valid
749
+ */
750
+ async verify(token) {
751
+ if (!token) {
752
+ return {
753
+ valid: false,
754
+ error: "No token provided",
755
+ errorCode: "invalid_token"
756
+ };
757
+ }
758
+ try {
759
+ const payload = verifyJWT(token, this.options.secret);
760
+ if (this.options.issuer && payload.iss !== this.options.issuer) {
761
+ return {
762
+ valid: false,
763
+ error: `Invalid issuer: expected ${this.options.issuer}, got ${payload.iss}`,
764
+ errorCode: "invalid_token"
765
+ };
766
+ }
767
+ if (this.options.audience) {
768
+ const audiences = Array.isArray(payload.aud) ? payload.aud : [
769
+ payload.aud
770
+ ];
771
+ if (!audiences.includes(this.options.audience)) {
772
+ return {
773
+ valid: false,
774
+ error: `Invalid audience: expected ${this.options.audience}`,
775
+ errorCode: "invalid_token"
776
+ };
777
+ }
778
+ }
779
+ let upstreamToken;
780
+ if (payload.upstream_token && this.options.encryptionSecret) {
781
+ try {
782
+ upstreamToken = decryptUpstreamToken(payload.upstream_token, this.options.encryptionSecret);
783
+ } catch (error) {
784
+ return {
785
+ valid: false,
786
+ error: `Failed to decrypt upstream token: ${error.message}`,
787
+ errorCode: "invalid_token"
788
+ };
789
+ }
790
+ }
791
+ return {
792
+ valid: true,
793
+ claims: payload,
794
+ upstreamToken
795
+ };
796
+ } catch (error) {
797
+ if (error.message.includes("expired")) {
798
+ return {
799
+ valid: false,
800
+ error: error.message,
801
+ errorCode: "expired_token"
802
+ };
803
+ }
804
+ return {
805
+ valid: false,
806
+ error: error.message,
807
+ errorCode: "invalid_token"
808
+ };
809
+ }
810
+ }
811
+ /**
812
+ * Generate WWW-Authenticate header for 401 responses
813
+ *
814
+ * Per RFC 9728, this should include the resource_metadata URL
815
+ *
816
+ * @param options - Header options
817
+ * @returns WWW-Authenticate header value
818
+ */
819
+ static getWWWAuthenticateHeader(options) {
820
+ const parts = [
821
+ `Bearer resource_metadata="${options.resourceMetadataUrl}"`
822
+ ];
823
+ if (options.error) {
824
+ parts.push(`error="${options.error}"`);
825
+ }
826
+ if (options.errorDescription) {
827
+ parts.push(`error_description="${options.errorDescription}"`);
828
+ }
829
+ if (options.scope) {
830
+ parts.push(`scope="${options.scope}"`);
831
+ }
832
+ return parts.join(", ");
833
+ }
834
+ /**
835
+ * Check if token has required scopes
836
+ */
837
+ hasScopes(claims, requiredScopes) {
838
+ if (requiredScopes.length === 0) return true;
839
+ const tokenScopes = typeof claims.scope === "string" ? claims.scope.split(" ") : claims.scope || [];
840
+ return requiredScopes.every((scope) => tokenScopes.includes(scope));
841
+ }
842
+ };
843
+ export {
844
+ DynamicClientRegistration,
845
+ OAuthAuthorizationServer,
846
+ TokenVerifier
847
+ };