@silkweave/auth 1.3.1 → 1.3.2

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.js CHANGED
@@ -15,9 +15,6 @@ function invalidToken(message = "Invalid token") {
15
15
  function insufficientScope(message = "Insufficient scope") {
16
16
  return new AuthError(message, "insufficient_scope", 403);
17
17
  }
18
- function missingToken(message = "No authorization provided") {
19
- return new AuthError(message, "missing_token", 401);
20
- }
21
18
 
22
19
  // src/extract.ts
23
20
  function extractBearerToken(header) {
@@ -50,6 +47,10 @@ function generateProtectedResourceMetadata(resourceUrl, authorizationServers) {
50
47
  };
51
48
  }
52
49
 
50
+ // src/provider/proxy.ts
51
+ import { randomBytes, randomUUID } from "crypto";
52
+ import { SignJWT, jwtVerify } from "jose";
53
+
53
54
  // src/store/memory-store.ts
54
55
  function withTtl(map, key) {
55
56
  const item = map.get(key);
@@ -122,10 +123,6 @@ function createMemoryStore() {
122
123
  };
123
124
  }
124
125
 
125
- // src/provider/proxy.ts
126
- import { randomBytes, randomUUID } from "crypto";
127
- import { SignJWT, jwtVerify } from "jose";
128
-
129
126
  // src/provider/uri.ts
130
127
  function matchRedirectUri(uri, patterns) {
131
128
  return patterns.some((pattern) => {
@@ -144,6 +141,233 @@ var CORS_HEADERS = {
144
141
  "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
145
142
  "Access-Control-Allow-Headers": "*"
146
143
  };
144
+ function generateCode() {
145
+ return randomBytes(32).toString("base64url");
146
+ }
147
+ async function generatePkce() {
148
+ const verifier = randomBytes(32).toString("base64url");
149
+ const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
150
+ const challenge = Buffer.from(hash).toString("base64url");
151
+ return { verifier, challenge };
152
+ }
153
+ async function verifyPkce(verifier, challenge) {
154
+ const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
155
+ const computed = Buffer.from(hash).toString("base64url");
156
+ return computed === challenge;
157
+ }
158
+ function jsonResponse(status, body, headers = {}) {
159
+ return {
160
+ status,
161
+ headers: { "Content-Type": "application/json", ...CORS_HEADERS, ...headers },
162
+ body
163
+ };
164
+ }
165
+ function redirectResponse(url) {
166
+ return {
167
+ status: 302,
168
+ headers: { Location: url, ...CORS_HEADERS },
169
+ body: void 0
170
+ };
171
+ }
172
+ function errorResponse(status, error, description) {
173
+ return jsonResponse(status, { error, error_description: description });
174
+ }
175
+ async function signAccessToken(key, opts) {
176
+ const now = Math.floor(Date.now() / 1e3);
177
+ return new SignJWT({ scope: opts.scopes.join(" "), email: opts.email }).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) {
182
+ return errorResponse(400, "invalid_request", "Missing request body");
183
+ }
184
+ const redirectUris = body.redirect_uris;
185
+ if (!redirectUris) {
186
+ return errorResponse(400, "invalid_request", "redirect_uris is required");
187
+ }
188
+ let uris;
189
+ try {
190
+ uris = typeof redirectUris === "string" ? JSON.parse(redirectUris) : redirectUris;
191
+ } catch {
192
+ uris = [redirectUris];
193
+ }
194
+ if (!Array.isArray(uris) || uris.length === 0) {
195
+ return errorResponse(400, "invalid_request", "redirect_uris must be a non-empty array");
196
+ }
197
+ for (const uri of uris) {
198
+ if (!matchRedirectUri(uri, allowedRedirectUris)) {
199
+ return errorResponse(400, "invalid_redirect_uri", `Redirect URI not allowed: ${uri}`);
200
+ }
201
+ }
202
+ const clientId = randomUUID();
203
+ const clientSecret = randomBytes(32).toString("base64url");
204
+ const registration = {
205
+ clientId,
206
+ clientSecret,
207
+ redirectUris: uris,
208
+ clientName: body.client_name,
209
+ createdAt: Date.now() / 1e3
210
+ };
211
+ await store.saveClient(clientId, registration);
212
+ return jsonResponse(201, {
213
+ client_id: clientId,
214
+ client_secret: clientSecret,
215
+ redirect_uris: uris,
216
+ client_name: body.client_name,
217
+ token_endpoint_auth_method: "none"
218
+ });
219
+ }
220
+ async function resolveClient(clientId, store, allowedRedirectUris) {
221
+ const existing = await store.getClient(clientId);
222
+ if (existing) {
223
+ return existing;
224
+ }
225
+ if (!clientId.startsWith("https://")) {
226
+ return errorResponse(400, "invalid_client", "Unknown client_id");
227
+ }
228
+ try {
229
+ const metaRes = await fetch(clientId);
230
+ if (!metaRes.ok) {
231
+ return errorResponse(400, "invalid_client", "Failed to fetch client metadata document");
232
+ }
233
+ const meta = await metaRes.json();
234
+ const metaRedirectUris = meta.redirect_uris;
235
+ if (!Array.isArray(metaRedirectUris) || metaRedirectUris.length === 0) {
236
+ return errorResponse(400, "invalid_client", "Client metadata must include redirect_uris");
237
+ }
238
+ for (const uri of metaRedirectUris) {
239
+ if (!matchRedirectUri(uri, allowedRedirectUris)) {
240
+ return errorResponse(400, "invalid_redirect_uri", `Redirect URI not allowed: ${uri}`);
241
+ }
242
+ }
243
+ const client = {
244
+ clientId,
245
+ clientSecret: "",
246
+ redirectUris: metaRedirectUris,
247
+ clientName: meta.client_name,
248
+ createdAt: Date.now() / 1e3
249
+ };
250
+ await store.saveClient(clientId, client);
251
+ return client;
252
+ } catch {
253
+ return errorResponse(400, "invalid_client", "Failed to fetch client metadata document");
254
+ }
255
+ }
256
+ async function exchangeUpstreamCode(code, config, callbackPath, pkceVerifier) {
257
+ const tokenBody = new URLSearchParams({
258
+ grant_type: "authorization_code",
259
+ code,
260
+ redirect_uri: `${config.resourceUrl}${callbackPath}`,
261
+ client_id: config.clientId,
262
+ client_secret: config.clientSecret,
263
+ code_verifier: pkceVerifier
264
+ });
265
+ const tokenResponse = await fetch(config.tokenUrl, {
266
+ method: "POST",
267
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
268
+ body: tokenBody.toString()
269
+ });
270
+ if (!tokenResponse.ok) {
271
+ const errorBody = await tokenResponse.text();
272
+ console.error("Upstream token exchange failed:", errorBody);
273
+ return errorResponse(502, "upstream_error", "Failed to exchange code with upstream provider");
274
+ }
275
+ const tokens = await tokenResponse.json();
276
+ return {
277
+ accessToken: tokens.access_token,
278
+ idToken: tokens.id_token
279
+ };
280
+ }
281
+ async function fetchUserinfo(userinfoUrl, accessToken) {
282
+ if (!userinfoUrl || !accessToken) {
283
+ return {};
284
+ }
285
+ try {
286
+ const res = await fetch(userinfoUrl, {
287
+ headers: { Authorization: `Bearer ${accessToken}` }
288
+ });
289
+ if (res.ok) {
290
+ const userinfo = await res.json();
291
+ return { email: userinfo.email, sub: userinfo.sub };
292
+ }
293
+ } catch {
294
+ }
295
+ return {};
296
+ }
297
+ async function handleRefreshToken(body, store, getSigningKey, config) {
298
+ const refreshToken = body.refresh_token;
299
+ if (!refreshToken) {
300
+ return errorResponse(400, "invalid_request", "Missing refresh_token");
301
+ }
302
+ const tokenData = await store.getRefreshToken(refreshToken);
303
+ if (!tokenData) {
304
+ return errorResponse(400, "invalid_grant", "Invalid or expired refresh token");
305
+ }
306
+ const key = await getSigningKey();
307
+ const accessToken = await signAccessToken(key, {
308
+ scopes: tokenData.scopes,
309
+ email: tokenData.email,
310
+ sub: tokenData.sub,
311
+ clientId: tokenData.clientId,
312
+ issuer: config.resourceUrl,
313
+ ttl: config.tokenTtl
314
+ });
315
+ return jsonResponse(200, {
316
+ access_token: accessToken,
317
+ token_type: "Bearer",
318
+ expires_in: config.tokenTtl,
319
+ scope: tokenData.scopes.join(" "),
320
+ refresh_token: refreshToken
321
+ });
322
+ }
323
+ async function handleAuthorizationCode(body, store, getSigningKey, config) {
324
+ const code = body.code;
325
+ const redirectUri = body.redirect_uri;
326
+ const clientId = body.client_id;
327
+ const codeVerifier = body.code_verifier;
328
+ if (!code || !codeVerifier || !clientId) {
329
+ return errorResponse(400, "invalid_request", "Missing required parameters");
330
+ }
331
+ const authCode = await store.getAuthCode(code);
332
+ if (!authCode) {
333
+ return errorResponse(400, "invalid_grant", "Invalid or expired authorization code");
334
+ }
335
+ await store.deleteAuthCode(code);
336
+ if (authCode.clientId !== clientId) {
337
+ return errorResponse(400, "invalid_grant", "client_id mismatch");
338
+ }
339
+ if (redirectUri && authCode.redirectUri !== redirectUri) {
340
+ return errorResponse(400, "invalid_grant", "redirect_uri mismatch");
341
+ }
342
+ const pkceValid = await verifyPkce(codeVerifier, authCode.codeChallenge);
343
+ if (!pkceValid) {
344
+ return errorResponse(400, "invalid_grant", "PKCE verification failed");
345
+ }
346
+ const key = await getSigningKey();
347
+ const accessToken = await signAccessToken(key, {
348
+ scopes: authCode.scopes,
349
+ email: authCode.email,
350
+ sub: authCode.sub,
351
+ clientId: authCode.clientId,
352
+ issuer: config.resourceUrl,
353
+ ttl: config.tokenTtl
354
+ });
355
+ const refreshToken = randomBytes(32).toString("base64url");
356
+ await store.saveRefreshToken(refreshToken, {
357
+ clientId: authCode.clientId,
358
+ scopes: authCode.scopes,
359
+ email: authCode.email,
360
+ sub: authCode.sub,
361
+ expiresAt: Math.floor(Date.now() / 1e3) + 30 * 24 * 3600
362
+ });
363
+ return jsonResponse(200, {
364
+ access_token: accessToken,
365
+ token_type: "Bearer",
366
+ expires_in: config.tokenTtl,
367
+ scope: authCode.scopes.join(" "),
368
+ refresh_token: refreshToken
369
+ });
370
+ }
147
371
  function createOAuthProxy(config) {
148
372
  const store = config.store ?? createMemoryStore();
149
373
  const callbackPath = config.callbackPath ?? "/auth/callback";
@@ -161,38 +385,7 @@ function createOAuthProxy(config) {
161
385
  }
162
386
  return signingKey;
163
387
  }
164
- function generateCode() {
165
- return randomBytes(32).toString("base64url");
166
- }
167
- async function generatePkce() {
168
- const verifier = randomBytes(32).toString("base64url");
169
- const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
170
- const challenge = Buffer.from(hash).toString("base64url");
171
- return { verifier, challenge };
172
- }
173
- async function verifyPkce(verifier, challenge) {
174
- const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
175
- const computed = Buffer.from(hash).toString("base64url");
176
- return computed === challenge;
177
- }
178
- function jsonResponse(status, body, headers = {}) {
179
- return {
180
- status,
181
- headers: { "Content-Type": "application/json", ...CORS_HEADERS, ...headers },
182
- body
183
- };
184
- }
185
- function redirectResponse(url) {
186
- return {
187
- status: 302,
188
- headers: { Location: url, ...CORS_HEADERS },
189
- body: void 0
190
- };
191
- }
192
- function errorResponse(status, error, description) {
193
- return jsonResponse(status, { error, error_description: description });
194
- }
195
- const provider = {
388
+ return {
196
389
  metadata() {
197
390
  return jsonResponse(200, {
198
391
  issuer: config.resourceUrl,
@@ -206,46 +399,8 @@ function createOAuthProxy(config) {
206
399
  scopes_supported: scopes
207
400
  }, { "Cache-Control": "max-age=3600" });
208
401
  },
209
- async register(req) {
210
- const body = req.body;
211
- if (!body) {
212
- return errorResponse(400, "invalid_request", "Missing request body");
213
- }
214
- const redirectUris = body.redirect_uris;
215
- if (!redirectUris) {
216
- return errorResponse(400, "invalid_request", "redirect_uris is required");
217
- }
218
- let uris;
219
- try {
220
- uris = typeof redirectUris === "string" ? JSON.parse(redirectUris) : redirectUris;
221
- } catch {
222
- uris = [redirectUris];
223
- }
224
- if (!Array.isArray(uris) || uris.length === 0) {
225
- return errorResponse(400, "invalid_request", "redirect_uris must be a non-empty array");
226
- }
227
- for (const uri of uris) {
228
- if (!matchRedirectUri(uri, config.redirectUris)) {
229
- return errorResponse(400, "invalid_redirect_uri", `Redirect URI not allowed: ${uri}`);
230
- }
231
- }
232
- const clientId = randomUUID();
233
- const clientSecret = randomBytes(32).toString("base64url");
234
- const registration = {
235
- clientId,
236
- clientSecret,
237
- redirectUris: uris,
238
- clientName: body.client_name,
239
- createdAt: Date.now() / 1e3
240
- };
241
- await store.saveClient(clientId, registration);
242
- return jsonResponse(201, {
243
- client_id: clientId,
244
- client_secret: clientSecret,
245
- redirect_uris: uris,
246
- client_name: body.client_name,
247
- token_endpoint_auth_method: "none"
248
- });
402
+ register(req) {
403
+ return handleRegister(req, store, config.redirectUris);
249
404
  },
250
405
  async authorize(req) {
251
406
  const params = req.url.searchParams;
@@ -269,38 +424,11 @@ function createOAuthProxy(config) {
269
424
  if (!codeChallenge || codeChallengeMethod !== "S256") {
270
425
  return errorResponse(400, "invalid_request", "PKCE with S256 is required");
271
426
  }
272
- let client = await store.getClient(clientId);
273
- if (!client && clientId.startsWith("https://")) {
274
- try {
275
- const metaRes = await fetch(clientId);
276
- if (!metaRes.ok) {
277
- return errorResponse(400, "invalid_client", "Failed to fetch client metadata document");
278
- }
279
- const meta = await metaRes.json();
280
- const metaRedirectUris = meta.redirect_uris;
281
- if (!Array.isArray(metaRedirectUris) || metaRedirectUris.length === 0) {
282
- return errorResponse(400, "invalid_client", "Client metadata must include redirect_uris");
283
- }
284
- for (const uri of metaRedirectUris) {
285
- if (!matchRedirectUri(uri, config.redirectUris)) {
286
- return errorResponse(400, "invalid_redirect_uri", `Redirect URI not allowed: ${uri}`);
287
- }
288
- }
289
- client = {
290
- clientId,
291
- clientSecret: "",
292
- redirectUris: metaRedirectUris,
293
- clientName: meta.client_name,
294
- createdAt: Date.now() / 1e3
295
- };
296
- await store.saveClient(clientId, client);
297
- } catch {
298
- return errorResponse(400, "invalid_client", "Failed to fetch client metadata document");
299
- }
300
- }
301
- if (!client) {
302
- return errorResponse(400, "invalid_client", "Unknown client_id");
427
+ const result = await resolveClient(clientId, store, config.redirectUris);
428
+ if ("status" in result) {
429
+ return result;
303
430
  }
431
+ const client = result;
304
432
  if (!client.redirectUris.includes(redirectUri)) {
305
433
  return errorResponse(400, "invalid_redirect_uri", "redirect_uri does not match registered URIs");
306
434
  }
@@ -352,42 +480,11 @@ function createOAuthProxy(config) {
352
480
  }
353
481
  await store.deletePendingAuth(proxyState);
354
482
  await store.deletePkceVerifier(proxyState);
355
- const tokenBody = new URLSearchParams({
356
- grant_type: "authorization_code",
357
- code: upstreamCode,
358
- redirect_uri: `${config.resourceUrl}${callbackPath}`,
359
- client_id: config.clientId,
360
- client_secret: config.clientSecret,
361
- code_verifier: pkceVerifier
362
- });
363
- const tokenResponse = await fetch(config.tokenUrl, {
364
- method: "POST",
365
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
366
- body: tokenBody.toString()
367
- });
368
- if (!tokenResponse.ok) {
369
- const errorBody = await tokenResponse.text();
370
- console.error("Upstream token exchange failed:", errorBody);
371
- return errorResponse(502, "upstream_error", "Failed to exchange code with upstream provider");
372
- }
373
- const tokens = await tokenResponse.json();
374
- const upstreamAccessToken = tokens.access_token;
375
- const upstreamIdToken = tokens.id_token;
376
- let email;
377
- let sub;
378
- if (config.userinfoUrl && upstreamAccessToken) {
379
- try {
380
- const userinfoResponse = await fetch(config.userinfoUrl, {
381
- headers: { Authorization: `Bearer ${upstreamAccessToken}` }
382
- });
383
- if (userinfoResponse.ok) {
384
- const userinfo = await userinfoResponse.json();
385
- email = userinfo.email;
386
- sub = userinfo.sub;
387
- }
388
- } catch {
389
- }
483
+ const upstream = await exchangeUpstreamCode(upstreamCode, config, callbackPath, pkceVerifier);
484
+ if ("status" in upstream) {
485
+ return upstream;
390
486
  }
487
+ const userinfo = await fetchUserinfo(config.userinfoUrl, upstream.accessToken);
391
488
  const mcpCode = generateCode();
392
489
  await store.saveAuthCode(mcpCode, {
393
490
  clientId: pending.clientId,
@@ -395,10 +492,10 @@ function createOAuthProxy(config) {
395
492
  codeChallenge: pending.codeChallenge,
396
493
  codeChallengeMethod: pending.codeChallengeMethod,
397
494
  scopes: pending.scope.split(" ").filter(Boolean),
398
- upstreamAccessToken,
399
- upstreamIdToken,
400
- email,
401
- sub,
495
+ upstreamAccessToken: upstream.accessToken,
496
+ upstreamIdToken: upstream.idToken,
497
+ email: userinfo.email,
498
+ sub: userinfo.sub,
402
499
  expiresAt: Date.now() / 1e3 + 600
403
500
  });
404
501
  const clientRedirect = new URL(pending.redirectUri);
@@ -413,76 +510,14 @@ function createOAuthProxy(config) {
413
510
  if (!body) {
414
511
  return errorResponse(400, "invalid_request", "Missing request body");
415
512
  }
416
- const grantType = body.grant_type;
417
- if (grantType === "refresh_token") {
418
- const refreshToken2 = body.refresh_token;
419
- if (!refreshToken2) {
420
- return errorResponse(400, "invalid_request", "Missing refresh_token");
421
- }
422
- const tokenData = await store.getRefreshToken(refreshToken2);
423
- if (!tokenData) {
424
- return errorResponse(400, "invalid_grant", "Invalid or expired refresh token");
425
- }
426
- const key2 = await getSigningKey();
427
- const now2 = Math.floor(Date.now() / 1e3);
428
- const accessToken2 = await new SignJWT({
429
- scope: tokenData.scopes.join(" "),
430
- email: tokenData.email
431
- }).setProtectedHeader({ alg: "HS256" }).setSubject(tokenData.sub ?? tokenData.email ?? tokenData.clientId).setIssuer(config.resourceUrl).setAudience(config.resourceUrl).setIssuedAt(now2).setExpirationTime(now2 + tokenTtl).sign(key2);
432
- return jsonResponse(200, {
433
- access_token: accessToken2,
434
- token_type: "Bearer",
435
- expires_in: tokenTtl,
436
- scope: tokenData.scopes.join(" "),
437
- refresh_token: refreshToken2
438
- });
513
+ const tokenConfig = { resourceUrl: config.resourceUrl, tokenTtl };
514
+ if (body.grant_type === "refresh_token") {
515
+ return handleRefreshToken(body, store, getSigningKey, tokenConfig);
439
516
  }
440
- if (grantType !== "authorization_code") {
517
+ if (body.grant_type !== "authorization_code") {
441
518
  return errorResponse(400, "unsupported_grant_type", "Supported grant types: authorization_code, refresh_token");
442
519
  }
443
- const code = body.code;
444
- const redirectUri = body.redirect_uri;
445
- const clientId = body.client_id;
446
- const codeVerifier = body.code_verifier;
447
- if (!code || !codeVerifier || !clientId) {
448
- return errorResponse(400, "invalid_request", "Missing required parameters");
449
- }
450
- const authCode = await store.getAuthCode(code);
451
- if (!authCode) {
452
- return errorResponse(400, "invalid_grant", "Invalid or expired authorization code");
453
- }
454
- await store.deleteAuthCode(code);
455
- if (authCode.clientId !== clientId) {
456
- return errorResponse(400, "invalid_grant", "client_id mismatch");
457
- }
458
- if (redirectUri && authCode.redirectUri !== redirectUri) {
459
- return errorResponse(400, "invalid_grant", "redirect_uri mismatch");
460
- }
461
- const pkceValid = await verifyPkce(codeVerifier, authCode.codeChallenge);
462
- if (!pkceValid) {
463
- return errorResponse(400, "invalid_grant", "PKCE verification failed");
464
- }
465
- const key = await getSigningKey();
466
- const now = Math.floor(Date.now() / 1e3);
467
- const accessToken = await new SignJWT({
468
- scope: authCode.scopes.join(" "),
469
- email: authCode.email
470
- }).setProtectedHeader({ alg: "HS256" }).setSubject(authCode.sub ?? authCode.email ?? authCode.clientId).setIssuer(config.resourceUrl).setAudience(config.resourceUrl).setIssuedAt(now).setExpirationTime(now + tokenTtl).sign(key);
471
- const refreshToken = randomBytes(32).toString("base64url");
472
- await store.saveRefreshToken(refreshToken, {
473
- clientId: authCode.clientId,
474
- scopes: authCode.scopes,
475
- email: authCode.email,
476
- sub: authCode.sub,
477
- expiresAt: now + 30 * 24 * 3600
478
- });
479
- return jsonResponse(200, {
480
- access_token: accessToken,
481
- token_type: "Bearer",
482
- expires_in: tokenTtl,
483
- scope: authCode.scopes.join(" "),
484
- refresh_token: refreshToken
485
- });
520
+ return handleAuthorizationCode(body, store, getSigningKey, tokenConfig);
486
521
  },
487
522
  async verifyToken(token) {
488
523
  try {
@@ -503,7 +538,6 @@ function createOAuthProxy(config) {
503
538
  }
504
539
  }
505
540
  };
506
- return provider;
507
541
  }
508
542
 
509
543
  // src/provider/google.ts
@@ -779,7 +813,6 @@ export {
779
813
  insufficientScope,
780
814
  invalidToken,
781
815
  matchRedirectUri,
782
- missingToken,
783
816
  validateToken
784
817
  };
785
818
  //# sourceMappingURL=index.js.map