@silkweave/auth 2.6.0 → 3.0.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.
package/build/index.mjs CHANGED
@@ -1,6 +1,3 @@
1
- import { randomBytes, randomUUID } from "crypto";
2
- import { SignJWT, jwtVerify } from "jose";
3
- import { existsSync, readFileSync, writeFileSync } from "fs";
4
1
  //#region src/errors.ts
5
2
  var AuthError = class extends Error {
6
3
  constructor(message, code, statusCode = 401) {
@@ -22,638 +19,25 @@ function extractBearerToken(header) {
22
19
  if (!header) return null;
23
20
  return /^Bearer\s+(.+)$/i.exec(header)?.[1] ?? null;
24
21
  }
25
- function buildWWWAuthenticate(error, description, resourceMetadataUrl) {
22
+ function buildWWWAuthenticate(error, description, resourceMetadataUrl, scope) {
26
23
  const parts = [];
27
24
  if (error) parts.push(`error="${error}"`);
28
25
  if (description) parts.push(`error_description="${description}"`);
29
26
  if (resourceMetadataUrl) parts.push(`resource_metadata="${resourceMetadataUrl}"`);
27
+ if (scope) parts.push(`scope="${scope}"`);
30
28
  return parts.length > 0 ? `Bearer ${parts.join(", ")}` : "Bearer";
31
29
  }
32
30
  //#endregion
33
31
  //#region src/metadata.ts
34
- function generateProtectedResourceMetadata(resourceUrl, authorizationServers) {
32
+ function generateProtectedResourceMetadata(resourceUrl, authorizationServers, scopesSupported) {
35
33
  return {
36
34
  resource: resourceUrl,
37
35
  authorization_servers: authorizationServers,
36
+ ...scopesSupported?.length ? { scopes_supported: scopesSupported } : {},
38
37
  bearer_methods_supported: ["header"]
39
38
  };
40
39
  }
41
40
  //#endregion
42
- //#region src/store/memory-store.ts
43
- function withTtl$1(map, key) {
44
- const item = map.get(key);
45
- if (!item) return;
46
- if (item.expiresAt < Date.now() / 1e3) {
47
- map.delete(key);
48
- return;
49
- }
50
- return item;
51
- }
52
- function createMemoryStore() {
53
- const authCodes = /* @__PURE__ */ new Map();
54
- const pendingAuths = /* @__PURE__ */ new Map();
55
- const pkceVerifiers = /* @__PURE__ */ new Map();
56
- const clients = /* @__PURE__ */ new Map();
57
- const refreshTokens = /* @__PURE__ */ new Map();
58
- return {
59
- async saveAuthCode(code, data) {
60
- authCodes.set(code, data);
61
- },
62
- async getAuthCode(code) {
63
- return withTtl$1(authCodes, code);
64
- },
65
- async deleteAuthCode(code) {
66
- authCodes.delete(code);
67
- },
68
- async savePendingAuth(state, data) {
69
- pendingAuths.set(state, data);
70
- },
71
- async getPendingAuth(state) {
72
- return withTtl$1(pendingAuths, state);
73
- },
74
- async deletePendingAuth(state) {
75
- pendingAuths.delete(state);
76
- },
77
- async savePkceVerifier(state, verifier) {
78
- pkceVerifiers.set(state, {
79
- verifier,
80
- expiresAt: Date.now() / 1e3 + 600
81
- });
82
- },
83
- async getPkceVerifier(state) {
84
- const item = pkceVerifiers.get(state);
85
- if (!item) return;
86
- if (item.expiresAt < Date.now() / 1e3) {
87
- pkceVerifiers.delete(state);
88
- return;
89
- }
90
- return item.verifier;
91
- },
92
- async deletePkceVerifier(state) {
93
- pkceVerifiers.delete(state);
94
- },
95
- async saveClient(clientId, data) {
96
- clients.set(clientId, data);
97
- },
98
- async getClient(clientId) {
99
- return clients.get(clientId);
100
- },
101
- async saveRefreshToken(token, data) {
102
- refreshTokens.set(token, data);
103
- },
104
- async getRefreshToken(token) {
105
- return withTtl$1(refreshTokens, token);
106
- },
107
- async deleteRefreshToken(token) {
108
- refreshTokens.delete(token);
109
- }
110
- };
111
- }
112
- //#endregion
113
- //#region src/provider/uri.ts
114
- function matchRedirectUri(uri, patterns) {
115
- return patterns.some((pattern) => {
116
- return patternToRegex(pattern).test(uri);
117
- });
118
- }
119
- function patternToRegex(pattern) {
120
- const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
121
- return new RegExp(`^${escaped}$`);
122
- }
123
- //#endregion
124
- //#region src/provider/proxy.ts
125
- const CORS_HEADERS = {
126
- "Access-Control-Allow-Origin": "*",
127
- "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
128
- "Access-Control-Allow-Headers": "*"
129
- };
130
- function generateCode() {
131
- return randomBytes(32).toString("base64url");
132
- }
133
- async function generatePkce() {
134
- const verifier = randomBytes(32).toString("base64url");
135
- const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
136
- return {
137
- verifier,
138
- challenge: Buffer.from(hash).toString("base64url")
139
- };
140
- }
141
- async function verifyPkce(verifier, challenge) {
142
- const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
143
- return Buffer.from(hash).toString("base64url") === challenge;
144
- }
145
- function jsonResponse(status, body, headers = {}) {
146
- return {
147
- status,
148
- headers: {
149
- "Content-Type": "application/json",
150
- ...CORS_HEADERS,
151
- ...headers
152
- },
153
- body
154
- };
155
- }
156
- function redirectResponse(url) {
157
- return {
158
- status: 302,
159
- headers: {
160
- Location: url,
161
- ...CORS_HEADERS
162
- },
163
- body: void 0
164
- };
165
- }
166
- function errorResponse(status, error, description) {
167
- return jsonResponse(status, {
168
- error,
169
- error_description: description
170
- });
171
- }
172
- async function signAccessToken(key, opts) {
173
- const now = Math.floor(Date.now() / 1e3);
174
- return new SignJWT({
175
- scope: opts.scopes.join(" "),
176
- email: opts.email
177
- }).setProtectedHeader({ alg: "HS256" }).setSubject(opts.sub ?? opts.email ?? opts.clientId).setIssuer(opts.issuer).setAudience(opts.issuer).setIssuedAt(now).setExpirationTime(now + opts.ttl).sign(key);
178
- }
179
- async function handleRegister(req, store, allowedRedirectUris) {
180
- const body = req.body;
181
- if (!body) return errorResponse(400, "invalid_request", "Missing request body");
182
- const redirectUris = body.redirect_uris;
183
- if (!redirectUris) return errorResponse(400, "invalid_request", "redirect_uris is required");
184
- let uris;
185
- try {
186
- uris = typeof redirectUris === "string" ? JSON.parse(redirectUris) : redirectUris;
187
- } catch {
188
- uris = [redirectUris];
189
- }
190
- if (!Array.isArray(uris) || uris.length === 0) return errorResponse(400, "invalid_request", "redirect_uris must be a non-empty array");
191
- for (const uri of uris) if (!matchRedirectUri(uri, allowedRedirectUris)) return errorResponse(400, "invalid_redirect_uri", `Redirect URI not allowed: ${uri}`);
192
- const clientId = randomUUID();
193
- const clientSecret = randomBytes(32).toString("base64url");
194
- const registration = {
195
- clientId,
196
- clientSecret,
197
- redirectUris: uris,
198
- clientName: body.client_name,
199
- createdAt: Date.now() / 1e3
200
- };
201
- await store.saveClient(clientId, registration);
202
- return jsonResponse(201, {
203
- client_id: clientId,
204
- client_secret: clientSecret,
205
- redirect_uris: uris,
206
- client_name: body.client_name,
207
- token_endpoint_auth_method: "none"
208
- });
209
- }
210
- async function resolveClient(clientId, store, allowedRedirectUris) {
211
- const existing = await store.getClient(clientId);
212
- if (existing) return existing;
213
- if (!clientId.startsWith("https://")) return errorResponse(400, "invalid_client", "Unknown client_id");
214
- try {
215
- const metaRes = await fetch(clientId);
216
- if (!metaRes.ok) return errorResponse(400, "invalid_client", "Failed to fetch client metadata document");
217
- const meta = await metaRes.json();
218
- const metaRedirectUris = meta.redirect_uris;
219
- if (!Array.isArray(metaRedirectUris) || metaRedirectUris.length === 0) return errorResponse(400, "invalid_client", "Client metadata must include redirect_uris");
220
- for (const uri of metaRedirectUris) if (!matchRedirectUri(uri, allowedRedirectUris)) return errorResponse(400, "invalid_redirect_uri", `Redirect URI not allowed: ${uri}`);
221
- const client = {
222
- clientId,
223
- clientSecret: "",
224
- redirectUris: metaRedirectUris,
225
- clientName: meta.client_name,
226
- createdAt: Date.now() / 1e3
227
- };
228
- await store.saveClient(clientId, client);
229
- return client;
230
- } catch {
231
- return errorResponse(400, "invalid_client", "Failed to fetch client metadata document");
232
- }
233
- }
234
- async function exchangeUpstreamCode(code, config, callbackPath, pkceVerifier) {
235
- const tokenBody = new URLSearchParams({
236
- grant_type: "authorization_code",
237
- code,
238
- redirect_uri: `${config.resourceUrl}${callbackPath}`,
239
- client_id: config.clientId,
240
- client_secret: config.clientSecret,
241
- code_verifier: pkceVerifier
242
- });
243
- const tokenResponse = await fetch(config.tokenUrl, {
244
- method: "POST",
245
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
246
- body: tokenBody.toString()
247
- });
248
- if (!tokenResponse.ok) {
249
- const errorBody = await tokenResponse.text();
250
- console.error("Upstream token exchange failed:", errorBody);
251
- return errorResponse(502, "upstream_error", "Failed to exchange code with upstream provider");
252
- }
253
- const tokens = await tokenResponse.json();
254
- return {
255
- accessToken: tokens.access_token,
256
- idToken: tokens.id_token
257
- };
258
- }
259
- async function fetchUserinfo(userinfoUrl, accessToken) {
260
- if (!userinfoUrl || !accessToken) return {};
261
- try {
262
- const res = await fetch(userinfoUrl, { headers: { Authorization: `Bearer ${accessToken}` } });
263
- if (res.ok) {
264
- const userinfo = await res.json();
265
- return {
266
- email: userinfo.email,
267
- sub: userinfo.sub
268
- };
269
- }
270
- } catch {}
271
- return {};
272
- }
273
- async function handleRefreshToken(body, store, getSigningKey, config) {
274
- const refreshToken = body.refresh_token;
275
- if (!refreshToken) return errorResponse(400, "invalid_request", "Missing refresh_token");
276
- const tokenData = await store.getRefreshToken(refreshToken);
277
- if (!tokenData) return errorResponse(400, "invalid_grant", "Invalid or expired refresh token");
278
- return jsonResponse(200, {
279
- access_token: await signAccessToken(await getSigningKey(), {
280
- scopes: tokenData.scopes,
281
- email: tokenData.email,
282
- sub: tokenData.sub,
283
- clientId: tokenData.clientId,
284
- issuer: config.resourceUrl,
285
- ttl: config.tokenTtl
286
- }),
287
- token_type: "Bearer",
288
- expires_in: config.tokenTtl,
289
- scope: tokenData.scopes.join(" "),
290
- refresh_token: refreshToken
291
- });
292
- }
293
- async function handleAuthorizationCode(body, store, getSigningKey, config) {
294
- const code = body.code;
295
- const redirectUri = body.redirect_uri;
296
- const clientId = body.client_id;
297
- const codeVerifier = body.code_verifier;
298
- if (!code || !codeVerifier || !clientId) return errorResponse(400, "invalid_request", "Missing required parameters");
299
- const authCode = await store.getAuthCode(code);
300
- if (!authCode) return errorResponse(400, "invalid_grant", "Invalid or expired authorization code");
301
- await store.deleteAuthCode(code);
302
- if (authCode.clientId !== clientId) return errorResponse(400, "invalid_grant", "client_id mismatch");
303
- if (redirectUri && authCode.redirectUri !== redirectUri) return errorResponse(400, "invalid_grant", "redirect_uri mismatch");
304
- if (!await verifyPkce(codeVerifier, authCode.codeChallenge)) return errorResponse(400, "invalid_grant", "PKCE verification failed");
305
- const accessToken = await signAccessToken(await getSigningKey(), {
306
- scopes: authCode.scopes,
307
- email: authCode.email,
308
- sub: authCode.sub,
309
- clientId: authCode.clientId,
310
- issuer: config.resourceUrl,
311
- ttl: config.tokenTtl
312
- });
313
- const refreshToken = randomBytes(32).toString("base64url");
314
- await store.saveRefreshToken(refreshToken, {
315
- clientId: authCode.clientId,
316
- scopes: authCode.scopes,
317
- email: authCode.email,
318
- sub: authCode.sub,
319
- expiresAt: Math.floor(Date.now() / 1e3) + 720 * 3600
320
- });
321
- return jsonResponse(200, {
322
- access_token: accessToken,
323
- token_type: "Bearer",
324
- expires_in: config.tokenTtl,
325
- scope: authCode.scopes.join(" "),
326
- refresh_token: refreshToken
327
- });
328
- }
329
- function createOAuthProxy(config) {
330
- const store = config.store ?? createMemoryStore();
331
- const callbackPath = config.callbackPath ?? "/auth/callback";
332
- const tokenTtl = config.tokenTtl ?? 3600;
333
- const scopes = config.requiredScopes ?? [];
334
- let signingKey = null;
335
- async function getSigningKey() {
336
- if (signingKey) return signingKey;
337
- if (config.signingKey) signingKey = new TextEncoder().encode(config.signingKey);
338
- else signingKey = randomBytes(32);
339
- return signingKey;
340
- }
341
- return {
342
- metadata() {
343
- return jsonResponse(200, {
344
- issuer: config.resourceUrl,
345
- authorization_endpoint: `${config.resourceUrl}/authorize`,
346
- token_endpoint: `${config.resourceUrl}/token`,
347
- registration_endpoint: `${config.resourceUrl}/register`,
348
- response_types_supported: ["code"],
349
- grant_types_supported: ["authorization_code", "refresh_token"],
350
- code_challenge_methods_supported: ["S256"],
351
- token_endpoint_auth_methods_supported: ["none", "client_secret_post"],
352
- scopes_supported: scopes
353
- }, { "Cache-Control": "max-age=3600" });
354
- },
355
- register(req) {
356
- return handleRegister(req, store, config.redirectUris);
357
- },
358
- async authorize(req) {
359
- const params = req.url.searchParams;
360
- const responseType = params.get("response_type");
361
- const clientId = params.get("client_id");
362
- const redirectUri = params.get("redirect_uri");
363
- const scope = params.get("scope") ?? "";
364
- const clientState = params.get("state") ?? "";
365
- const codeChallenge = params.get("code_challenge");
366
- const codeChallengeMethod = params.get("code_challenge_method");
367
- const resource = params.get("resource");
368
- if (responseType !== "code") return errorResponse(400, "unsupported_response_type", "Only response_type=code is supported");
369
- if (!clientId) return errorResponse(400, "invalid_request", "client_id is required");
370
- if (!redirectUri) return errorResponse(400, "invalid_request", "redirect_uri is required");
371
- if (!codeChallenge || codeChallengeMethod !== "S256") return errorResponse(400, "invalid_request", "PKCE with S256 is required");
372
- const result = await resolveClient(clientId, store, config.redirectUris);
373
- if ("status" in result) return result;
374
- if (!result.redirectUris.includes(redirectUri)) return errorResponse(400, "invalid_redirect_uri", "redirect_uri does not match registered URIs");
375
- const proxyState = randomUUID();
376
- const pkce = await generatePkce();
377
- await store.savePendingAuth(proxyState, {
378
- clientId,
379
- redirectUri,
380
- codeChallenge,
381
- codeChallengeMethod,
382
- scope,
383
- clientState,
384
- resource: resource ?? void 0,
385
- expiresAt: Date.now() / 1e3 + 600
386
- });
387
- await store.savePkceVerifier(proxyState, pkce.verifier);
388
- const upstreamScopes = scopes.length > 0 ? scopes.join(" ") : scope;
389
- const upstreamUrl = new URL(config.authorizeUrl);
390
- upstreamUrl.searchParams.set("response_type", "code");
391
- upstreamUrl.searchParams.set("client_id", config.clientId);
392
- upstreamUrl.searchParams.set("redirect_uri", `${config.resourceUrl}${callbackPath}`);
393
- upstreamUrl.searchParams.set("scope", upstreamScopes);
394
- upstreamUrl.searchParams.set("state", proxyState);
395
- upstreamUrl.searchParams.set("code_challenge", pkce.challenge);
396
- upstreamUrl.searchParams.set("code_challenge_method", "S256");
397
- if (params.has("access_type")) upstreamUrl.searchParams.set("access_type", params.get("access_type"));
398
- return redirectResponse(upstreamUrl.toString());
399
- },
400
- async callback(req) {
401
- const params = req.url.searchParams;
402
- const upstreamCode = params.get("code");
403
- const proxyState = params.get("state");
404
- const upstreamError = params.get("error");
405
- if (upstreamError) return errorResponse(400, "upstream_error", params.get("error_description") ?? upstreamError);
406
- if (!upstreamCode || !proxyState) return errorResponse(400, "invalid_request", "Missing code or state");
407
- const pending = await store.getPendingAuth(proxyState);
408
- if (!pending) return errorResponse(400, "invalid_request", "Unknown or expired state");
409
- const pkceVerifier = await store.getPkceVerifier(proxyState);
410
- if (!pkceVerifier) return errorResponse(400, "invalid_request", "Missing PKCE verifier");
411
- await store.deletePendingAuth(proxyState);
412
- await store.deletePkceVerifier(proxyState);
413
- const upstream = await exchangeUpstreamCode(upstreamCode, config, callbackPath, pkceVerifier);
414
- if ("status" in upstream) return upstream;
415
- const userinfo = await fetchUserinfo(config.userinfoUrl, upstream.accessToken);
416
- const mcpCode = generateCode();
417
- await store.saveAuthCode(mcpCode, {
418
- clientId: pending.clientId,
419
- redirectUri: pending.redirectUri,
420
- codeChallenge: pending.codeChallenge,
421
- codeChallengeMethod: pending.codeChallengeMethod,
422
- scopes: pending.scope.split(" ").filter(Boolean),
423
- upstreamAccessToken: upstream.accessToken,
424
- upstreamIdToken: upstream.idToken,
425
- email: userinfo.email,
426
- sub: userinfo.sub,
427
- expiresAt: Date.now() / 1e3 + 600
428
- });
429
- const clientRedirect = new URL(pending.redirectUri);
430
- clientRedirect.searchParams.set("code", mcpCode);
431
- if (pending.clientState) clientRedirect.searchParams.set("state", pending.clientState);
432
- return redirectResponse(clientRedirect.toString());
433
- },
434
- async token(req) {
435
- const body = req.body;
436
- if (!body) return errorResponse(400, "invalid_request", "Missing request body");
437
- const tokenConfig = {
438
- resourceUrl: config.resourceUrl,
439
- tokenTtl
440
- };
441
- if (body.grant_type === "refresh_token") return handleRefreshToken(body, store, getSigningKey, tokenConfig);
442
- if (body.grant_type !== "authorization_code") return errorResponse(400, "unsupported_grant_type", "Supported grant types: authorization_code, refresh_token");
443
- return handleAuthorizationCode(body, store, getSigningKey, tokenConfig);
444
- },
445
- async verifyToken(token) {
446
- try {
447
- const { payload } = await jwtVerify(token, await getSigningKey(), {
448
- issuer: config.resourceUrl,
449
- audience: config.resourceUrl
450
- });
451
- return {
452
- token,
453
- clientId: payload.sub ?? void 0,
454
- scopes: typeof payload.scope === "string" ? payload.scope.split(" ").filter(Boolean) : [],
455
- expiresAt: payload.exp,
456
- email: payload.email
457
- };
458
- } catch {
459
- return;
460
- }
461
- }
462
- };
463
- }
464
- //#endregion
465
- //#region src/provider/google.ts
466
- function google(options) {
467
- const provider = createOAuthProxy({
468
- authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
469
- tokenUrl: "https://oauth2.googleapis.com/token",
470
- userinfoUrl: "https://openidconnect.googleapis.com/v1/userinfo",
471
- clientId: options.clientId,
472
- clientSecret: options.clientSecret,
473
- resourceUrl: options.resourceUrl,
474
- redirectUris: options.redirectUris,
475
- requiredScopes: options.requiredScopes ?? ["openid", "email"],
476
- callbackPath: options.callbackPath,
477
- signingKey: options.signingKey,
478
- tokenTtl: options.tokenTtl,
479
- store: options.store
480
- });
481
- return {
482
- verifyToken: (token) => provider.verifyToken(token),
483
- required: true,
484
- resourceUrl: options.resourceUrl,
485
- authorizationServers: [options.resourceUrl],
486
- provider,
487
- callbackPath: options.callbackPath ?? "/auth/callback"
488
- };
489
- }
490
- //#endregion
491
- //#region src/store/json-store.ts
492
- function withTtl(obj, key) {
493
- const item = obj[key];
494
- if (!item) return;
495
- if (item.expiresAt < Date.now() / 1e3) {
496
- delete obj[key];
497
- return;
498
- }
499
- return item;
500
- }
501
- function writeJsonStore(path, store) {
502
- writeFileSync(path, JSON.stringify(store, null, 2), "utf-8");
503
- }
504
- function readJsonStore(path) {
505
- if (!existsSync(path)) return {
506
- authCodes: {},
507
- pendingAuths: {},
508
- pkceVerifiers: {},
509
- clients: {},
510
- refreshTokens: {}
511
- };
512
- const data = JSON.parse(readFileSync(path, "utf-8"));
513
- if (!data.refreshTokens) data.refreshTokens = {};
514
- return data;
515
- }
516
- function createJsonStore(path) {
517
- const store = readJsonStore(path);
518
- return {
519
- async saveAuthCode(code, data) {
520
- store.authCodes[code] = data;
521
- writeJsonStore(path, store);
522
- },
523
- async getAuthCode(code) {
524
- return withTtl(store.authCodes, code);
525
- },
526
- async deleteAuthCode(code) {
527
- delete store.authCodes[code];
528
- writeJsonStore(path, store);
529
- },
530
- async savePendingAuth(state, data) {
531
- store.pendingAuths[state] = data;
532
- writeJsonStore(path, store);
533
- },
534
- async getPendingAuth(state) {
535
- return withTtl(store.pendingAuths, state);
536
- },
537
- async deletePendingAuth(state) {
538
- delete store.pendingAuths[state];
539
- writeJsonStore(path, store);
540
- },
541
- async savePkceVerifier(state, verifier) {
542
- store.pkceVerifiers[state] = {
543
- verifier,
544
- expiresAt: Date.now() / 1e3 + 600
545
- };
546
- writeJsonStore(path, store);
547
- },
548
- async getPkceVerifier(state) {
549
- const item = store.pkceVerifiers[state];
550
- if (!item) return;
551
- if (item.expiresAt < Date.now() / 1e3) {
552
- delete store.pkceVerifiers[state];
553
- writeJsonStore(path, store);
554
- return;
555
- }
556
- return item.verifier;
557
- },
558
- async deletePkceVerifier(state) {
559
- delete store.pkceVerifiers[state];
560
- writeJsonStore(path, store);
561
- },
562
- async saveClient(clientId, data) {
563
- store.clients[clientId] = data;
564
- writeJsonStore(path, store);
565
- },
566
- async getClient(clientId) {
567
- return store.clients[clientId];
568
- },
569
- async saveRefreshToken(token, data) {
570
- store.refreshTokens[token] = data;
571
- writeJsonStore(path, store);
572
- },
573
- async getRefreshToken(token) {
574
- return withTtl(store.refreshTokens, token);
575
- },
576
- async deleteRefreshToken(token) {
577
- delete store.refreshTokens[token];
578
- writeJsonStore(path, store);
579
- }
580
- };
581
- }
582
- //#endregion
583
- //#region src/store/redis-store.ts
584
- function createRedisStore(options) {
585
- const { client, prefix = "silkweave:oauth:" } = options;
586
- function key(namespace, id) {
587
- return `${prefix}${namespace}:${id}`;
588
- }
589
- function ttlFromExpiry(expiresAt) {
590
- const seconds = Math.floor(expiresAt - Date.now() / 1e3);
591
- return seconds > 0 ? seconds : void 0;
592
- }
593
- async function save(namespace, id, data, expiresAt) {
594
- const opts = {};
595
- const ttl = expiresAt != null ? ttlFromExpiry(expiresAt) : void 0;
596
- if (ttl != null) opts.ex = ttl;
597
- await client.set(key(namespace, id), JSON.stringify(data), opts);
598
- }
599
- async function load(namespace, id) {
600
- const raw = await client.get(key(namespace, id));
601
- if (raw == null) return;
602
- return typeof raw === "string" ? JSON.parse(raw) : raw;
603
- }
604
- async function remove(namespace, id) {
605
- await client.del(key(namespace, id));
606
- }
607
- return {
608
- async saveAuthCode(code, data) {
609
- await save("auth-code", code, data, data.expiresAt);
610
- },
611
- async getAuthCode(code) {
612
- return load("auth-code", code);
613
- },
614
- async deleteAuthCode(code) {
615
- await remove("auth-code", code);
616
- },
617
- async savePendingAuth(state, data) {
618
- await save("pending-auth", state, data, data.expiresAt);
619
- },
620
- async getPendingAuth(state) {
621
- return load("pending-auth", state);
622
- },
623
- async deletePendingAuth(state) {
624
- await remove("pending-auth", state);
625
- },
626
- async savePkceVerifier(state, verifier) {
627
- const expiresAt = Date.now() / 1e3 + 600;
628
- await save("pkce-verifier", state, {
629
- verifier,
630
- expiresAt
631
- }, expiresAt);
632
- },
633
- async getPkceVerifier(state) {
634
- return (await load("pkce-verifier", state))?.verifier;
635
- },
636
- async deletePkceVerifier(state) {
637
- await remove("pkce-verifier", state);
638
- },
639
- async saveClient(clientId, data) {
640
- await save("client", clientId, data);
641
- },
642
- async getClient(clientId) {
643
- return load("client", clientId);
644
- },
645
- async saveRefreshToken(token, data) {
646
- await save("refresh-token", token, data, data.expiresAt);
647
- },
648
- async getRefreshToken(token) {
649
- return load("refresh-token", token);
650
- },
651
- async deleteRefreshToken(token) {
652
- await remove("refresh-token", token);
653
- }
654
- };
655
- }
656
- //#endregion
657
41
  //#region src/validate.ts
658
42
  async function validateToken(authorizationHeader, config, context) {
659
43
  const required = config.required ?? true;
@@ -671,12 +55,26 @@ async function validateToken(authorizationHeader, config, context) {
671
55
  }
672
56
  if (!authInfo) return buildErrorResult(invalidToken(), resourceMetadataUrl);
673
57
  if (authInfo.expiresAt && authInfo.expiresAt < Date.now() / 1e3) return buildErrorResult(invalidToken("Token has expired"), resourceMetadataUrl);
58
+ if (config.issuer && typeof authInfo.iss === "string" && authInfo.iss !== config.issuer) return buildErrorResult(invalidToken("Token issuer mismatch"), resourceMetadataUrl);
59
+ const expectedAudience = config.audience === false ? void 0 : config.audience ?? config.resourceUrl;
60
+ if (expectedAudience !== void 0 && !audienceMatches(authInfo.aud, expectedAudience)) return buildErrorResult(invalidToken("Token audience mismatch"), resourceMetadataUrl);
674
61
  if (config.requiredScopes?.length) {
675
62
  const scopes = authInfo.scopes ?? [];
676
- if (!config.requiredScopes.every((s) => scopes.includes(s))) return buildErrorResult(insufficientScope(), resourceMetadataUrl);
63
+ if (!config.requiredScopes.every((s) => scopes.includes(s))) return buildErrorResult(insufficientScope(), resourceMetadataUrl, config.requiredScopes.join(" "));
677
64
  }
678
65
  return { auth: authInfo };
679
66
  }
67
+ /**
68
+ * True when the token's `aud` claim includes one of the expected values. A token
69
+ * with no `aud` is allowed (the verifier may have bound the audience already);
70
+ * a present-but-mismatched `aud` is rejected.
71
+ */
72
+ function audienceMatches(tokenAud, expected) {
73
+ if (tokenAud === void 0 || tokenAud === null) return true;
74
+ const have = Array.isArray(tokenAud) ? tokenAud : [tokenAud];
75
+ const want = Array.isArray(expected) ? expected : [expected];
76
+ return have.some((a) => want.includes(a));
77
+ }
680
78
  function buildChallengeResult(resourceMetadataUrl) {
681
79
  return { error: {
682
80
  statusCode: 401,
@@ -690,11 +88,11 @@ function buildChallengeResult(resourceMetadataUrl) {
690
88
  }
691
89
  } };
692
90
  }
693
- function buildErrorResult(err, resourceMetadataUrl) {
91
+ function buildErrorResult(err, resourceMetadataUrl, scope) {
694
92
  return { error: {
695
93
  statusCode: err.statusCode,
696
94
  headers: {
697
- "WWW-Authenticate": buildWWWAuthenticate(err.code, err.message, resourceMetadataUrl),
95
+ "WWW-Authenticate": buildWWWAuthenticate(err.code, err.message, resourceMetadataUrl, scope),
698
96
  "Content-Type": "application/json"
699
97
  },
700
98
  body: {
@@ -704,6 +102,6 @@ function buildErrorResult(err, resourceMetadataUrl) {
704
102
  } };
705
103
  }
706
104
  //#endregion
707
- export { AuthError, buildWWWAuthenticate, createJsonStore, createMemoryStore, createOAuthProxy, createRedisStore, extractBearerToken, generateProtectedResourceMetadata, google, insufficientScope, invalidToken, matchRedirectUri, validateToken };
105
+ export { AuthError, buildWWWAuthenticate, extractBearerToken, generateProtectedResourceMetadata, insufficientScope, invalidToken, validateToken };
708
106
 
709
107
  //# sourceMappingURL=index.mjs.map