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