@cloudflare/workers-oauth-provider 0.0.0-b7784c7 → 0.0.0-f2b1372

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.
@@ -94,6 +94,12 @@ interface ResolveExternalTokenResult {
94
94
  * These properties are set in the execution context (ctx.props) when the external token is validated
95
95
  */
96
96
  props: any;
97
+ /**
98
+ * Audience claim from the external token (RFC 7519 Section 4.1.3)
99
+ * If provided, will be validated against the resource server identity
100
+ *
101
+ */
102
+ audience?: string | string[];
97
103
  }
98
104
  interface OAuthProviderOptions {
99
105
  /**
@@ -304,6 +310,10 @@ interface AuthRequest {
304
310
  * PKCE code challenge method (plain or S256)
305
311
  */
306
312
  codeChallengeMethod?: string;
313
+ /**
314
+ * Resource parameter indicating target resource(s) (RFC 8707)
315
+ */
316
+ resource?: string | string[];
307
317
  }
308
318
  /**
309
319
  * OAuth client registration information
@@ -472,6 +482,11 @@ interface Grant {
472
482
  * Only present during the authorization code exchange process
473
483
  */
474
484
  codeChallengeMethod?: string;
485
+ /**
486
+ * Resource parameter from authorization request (RFC 8707 Section 2.1)
487
+ * Indicates the protected resource(s) for which access is requested
488
+ */
489
+ resource?: string | string[];
475
490
  }
476
491
  /**
477
492
  * Token record stored in KV
@@ -500,6 +515,11 @@ interface Token {
500
515
  * Unix timestamp when the token expires
501
516
  */
502
517
  expiresAt: number;
518
+ /**
519
+ * Intended audience for this token (RFC 7519 Section 4.1.3)
520
+ * Can be a single string or array of strings
521
+ */
522
+ audience?: string | string[];
503
523
  /**
504
524
  * The encryption key for props, wrapped with this token
505
525
  */
@@ -231,7 +231,8 @@ var OAuthProviderImpl = class {
231
231
  }
232
232
  const formData = await request.formData();
233
233
  for (const [key, value] of formData.entries()) {
234
- body[key] = value;
234
+ const allValues = formData.getAll(key);
235
+ body[key] = allValues.length > 1 ? allValues : value;
235
236
  }
236
237
  const authHeader = request.headers.get("Authorization");
237
238
  let clientId = "";
@@ -549,12 +550,32 @@ var OAuthProviderImpl = class {
549
550
  grantData.expiresAt = expiresAt;
550
551
  }
551
552
  await this.saveGrantWithTTL(env, grantKey, grantData, now);
553
+ if (body.resource && grantData.resource) {
554
+ const requestedResources = Array.isArray(body.resource) ? body.resource : [body.resource];
555
+ const grantedResources = Array.isArray(grantData.resource) ? grantData.resource : [grantData.resource];
556
+ for (const requested of requestedResources) {
557
+ if (!grantedResources.includes(requested)) {
558
+ return this.createErrorResponse(
559
+ "invalid_target",
560
+ "Requested resource was not included in the authorization request"
561
+ );
562
+ }
563
+ }
564
+ }
565
+ const audience = parseResourceParameter(body.resource || grantData.resource);
566
+ if ((body.resource || grantData.resource) && !audience) {
567
+ return this.createErrorResponse(
568
+ "invalid_target",
569
+ "The resource parameter must be a valid absolute URI without a fragment"
570
+ );
571
+ }
552
572
  const accessTokenData = {
553
573
  id: accessTokenId,
554
574
  grantId,
555
575
  userId,
556
576
  createdAt: now,
557
577
  expiresAt: accessTokenExpiresAt,
578
+ audience,
558
579
  wrappedEncryptionKey: accessTokenWrappedKey,
559
580
  grant: {
560
581
  clientId: grantData.clientId,
@@ -574,6 +595,9 @@ var OAuthProviderImpl = class {
574
595
  if (refreshToken) {
575
596
  tokenResponse.refresh_token = refreshToken;
576
597
  }
598
+ if (audience) {
599
+ tokenResponse.resource = audience;
600
+ }
577
601
  return new Response(JSON.stringify(tokenResponse), {
578
602
  headers: { "Content-Type": "application/json" }
579
603
  });
@@ -701,12 +725,32 @@ var OAuthProviderImpl = class {
701
725
  grantData.refreshTokenId = newRefreshTokenId;
702
726
  grantData.refreshTokenWrappedKey = newRefreshTokenWrappedKey;
703
727
  await this.saveGrantWithTTL(env, grantKey, grantData, now);
728
+ if (body.resource && grantData.resource) {
729
+ const requestedResources = Array.isArray(body.resource) ? body.resource : [body.resource];
730
+ const grantedResources = Array.isArray(grantData.resource) ? grantData.resource : [grantData.resource];
731
+ for (const requested of requestedResources) {
732
+ if (!grantedResources.includes(requested)) {
733
+ return this.createErrorResponse(
734
+ "invalid_target",
735
+ "Requested resource was not included in the authorization request"
736
+ );
737
+ }
738
+ }
739
+ }
740
+ const audience = parseResourceParameter(body.resource || grantData.resource);
741
+ if ((body.resource || grantData.resource) && !audience) {
742
+ return this.createErrorResponse(
743
+ "invalid_target",
744
+ "The resource parameter must be a valid absolute URI without a fragment"
745
+ );
746
+ }
704
747
  const accessTokenData = {
705
748
  id: accessTokenId,
706
749
  grantId,
707
750
  userId,
708
751
  createdAt: now,
709
752
  expiresAt: accessTokenExpiresAt,
753
+ audience,
710
754
  wrappedEncryptionKey: accessTokenWrappedKey,
711
755
  grant: {
712
756
  clientId: grantData.clientId,
@@ -724,6 +768,9 @@ var OAuthProviderImpl = class {
724
768
  refresh_token: newRefreshToken,
725
769
  scope: grantData.scope.join(" ")
726
770
  };
771
+ if (audience) {
772
+ tokenResponse.resource = audience;
773
+ }
727
774
  return new Response(JSON.stringify(tokenResponse), {
728
775
  headers: { "Content-Type": "application/json" }
729
776
  });
@@ -876,6 +923,9 @@ var OAuthProviderImpl = class {
876
923
  if (!redirectUris || redirectUris.length === 0) {
877
924
  throw new Error("At least one redirect URI is required");
878
925
  }
926
+ for (const uri of redirectUris) {
927
+ validateRedirectUriScheme(uri);
928
+ }
879
929
  clientInfo = {
880
930
  clientId,
881
931
  redirectUris,
@@ -962,6 +1012,17 @@ var OAuthProviderImpl = class {
962
1012
  "WWW-Authenticate": 'Bearer realm="OAuth", error="invalid_token"'
963
1013
  });
964
1014
  }
1015
+ if (tokenData.audience) {
1016
+ const requestUrl = new URL(request.url);
1017
+ const resourceServer = `${requestUrl.protocol}//${requestUrl.host}`;
1018
+ const audiences = Array.isArray(tokenData.audience) ? tokenData.audience : [tokenData.audience];
1019
+ const matches = audiences.some((aud) => audienceMatches(resourceServer, aud));
1020
+ if (!matches) {
1021
+ return this.createErrorResponse("invalid_token", "Token audience does not match resource server", 401, {
1022
+ "WWW-Authenticate": 'Bearer realm="OAuth", error="invalid_token", error_description="Invalid audience"'
1023
+ });
1024
+ }
1025
+ }
965
1026
  const encryptionKey = await unwrapKeyWithToken(accessToken, tokenData.wrappedEncryptionKey);
966
1027
  const decryptedProps = await decryptProps(encryptionKey, tokenData.grant.encryptedProps);
967
1028
  ctx.props = decryptedProps;
@@ -972,6 +1033,17 @@ var OAuthProviderImpl = class {
972
1033
  "WWW-Authenticate": 'Bearer realm="OAuth", error="invalid_token"'
973
1034
  });
974
1035
  }
1036
+ if (ext.audience) {
1037
+ const requestUrl = new URL(request.url);
1038
+ const resourceServer = `${requestUrl.protocol}//${requestUrl.host}`;
1039
+ const audiences = Array.isArray(ext.audience) ? ext.audience : [ext.audience];
1040
+ const matches = audiences.some((aud) => audienceMatches(resourceServer, aud));
1041
+ if (!matches) {
1042
+ return this.createErrorResponse("invalid_token", "Token audience does not match resource server", 401, {
1043
+ "WWW-Authenticate": 'Bearer realm="OAuth", error="invalid_token", error_description="Invalid audience"'
1044
+ });
1045
+ }
1046
+ }
975
1047
  ctx.props = ext.props;
976
1048
  }
977
1049
  if (!env.OAUTH_PROVIDER) {
@@ -1048,6 +1120,41 @@ var OAuthProviderImpl = class {
1048
1120
  };
1049
1121
  var DEFAULT_ACCESS_TOKEN_TTL = 60 * 60;
1050
1122
  var TOKEN_LENGTH = 32;
1123
+ function validateResourceUri(uri) {
1124
+ if (!uri || typeof uri !== "string") {
1125
+ return false;
1126
+ }
1127
+ try {
1128
+ const parsed = new URL(uri);
1129
+ if (!parsed.protocol) {
1130
+ return false;
1131
+ }
1132
+ if (parsed.hash) {
1133
+ return false;
1134
+ }
1135
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
1136
+ return false;
1137
+ }
1138
+ return true;
1139
+ } catch {
1140
+ return false;
1141
+ }
1142
+ }
1143
+ function audienceMatches(resourceServerUrl, audienceValue) {
1144
+ return resourceServerUrl === audienceValue;
1145
+ }
1146
+ function parseResourceParameter(value) {
1147
+ if (!value) {
1148
+ return void 0;
1149
+ }
1150
+ const uris = Array.isArray(value) ? value : [value];
1151
+ for (const uri of uris) {
1152
+ if (typeof uri !== "string" || !validateResourceUri(uri)) {
1153
+ return void 0;
1154
+ }
1155
+ }
1156
+ return value;
1157
+ }
1051
1158
  async function hashSecret(secret) {
1052
1159
  return generateTokenId(secret);
1053
1160
  }
@@ -1069,6 +1176,26 @@ async function generateTokenId(token) {
1069
1176
  const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
1070
1177
  return hashHex;
1071
1178
  }
1179
+ function validateRedirectUriScheme(redirectUri) {
1180
+ const dangerousSchemes = ["javascript:", "data:", "vbscript:", "file:", "mailto:", "blob:"];
1181
+ const normalized = redirectUri.trim();
1182
+ for (let i = 0; i < normalized.length; i++) {
1183
+ const code = normalized.charCodeAt(i);
1184
+ if (code >= 0 && code <= 31 || code >= 127 && code <= 159) {
1185
+ throw new Error("Invalid redirect URI");
1186
+ }
1187
+ }
1188
+ const colonIndex = normalized.indexOf(":");
1189
+ if (colonIndex === -1) {
1190
+ throw new Error("Invalid redirect URI");
1191
+ }
1192
+ const scheme = normalized.substring(0, colonIndex + 1).toLowerCase();
1193
+ for (const dangerousScheme of dangerousSchemes) {
1194
+ if (scheme === dangerousScheme) {
1195
+ throw new Error("Invalid redirect URI");
1196
+ }
1197
+ }
1198
+ }
1072
1199
  function base64UrlEncode(str) {
1073
1200
  return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
1074
1201
  }
@@ -1221,8 +1348,12 @@ var OAuthHelpersImpl = class {
1221
1348
  const state = url.searchParams.get("state") || "";
1222
1349
  const codeChallenge = url.searchParams.get("code_challenge") || void 0;
1223
1350
  const codeChallengeMethod = url.searchParams.get("code_challenge_method") || "plain";
1224
- if (redirectUri.startsWith("javascript:") || redirectUri.startsWith("data:") || redirectUri.startsWith("vbscript:") || redirectUri.startsWith("file:") || redirectUri.startsWith("mailto:") || redirectUri.startsWith("blob:")) {
1225
- throw new Error("Invalid redirect URI");
1351
+ const resourceParams = url.searchParams.getAll("resource");
1352
+ const resourceParam = resourceParams.length > 0 ? resourceParams.length === 1 ? resourceParams[0] : resourceParams : void 0;
1353
+ validateRedirectUriScheme(redirectUri);
1354
+ const resource = parseResourceParameter(resourceParam);
1355
+ if (resourceParam && !resource) {
1356
+ throw new Error("The resource parameter must be a valid absolute URI without a fragment");
1226
1357
  }
1227
1358
  if (responseType === "token" && !this.provider.options.allowImplicitFlow) {
1228
1359
  throw new Error("The implicit grant flow is not enabled for this provider");
@@ -1247,7 +1378,8 @@ var OAuthHelpersImpl = class {
1247
1378
  scope,
1248
1379
  state,
1249
1380
  codeChallenge,
1250
- codeChallengeMethod
1381
+ codeChallengeMethod,
1382
+ resource
1251
1383
  };
1252
1384
  }
1253
1385
  /**
@@ -1266,6 +1398,16 @@ var OAuthHelpersImpl = class {
1266
1398
  * @returns A Promise resolving to an object containing the redirect URL
1267
1399
  */
1268
1400
  async completeAuthorization(options) {
1401
+ const { clientId, redirectUri } = options.request;
1402
+ if (!clientId || !redirectUri) {
1403
+ throw new Error("Client ID and Redirect URI are required in the authorization request.");
1404
+ }
1405
+ const clientInfo = await this.lookupClient(clientId);
1406
+ if (!clientInfo || !clientInfo.redirectUris.includes(redirectUri)) {
1407
+ throw new Error(
1408
+ "Invalid redirect URI. The redirect URI provided does not match any registered URI for this client."
1409
+ );
1410
+ }
1269
1411
  const grantId = generateRandomString(16);
1270
1412
  const { encryptedData, key: encryptionKey } = await encryptProps(options.props);
1271
1413
  const now = Math.floor(Date.now() / 1e3);
@@ -1276,6 +1418,10 @@ var OAuthHelpersImpl = class {
1276
1418
  const accessTokenTTL = this.provider.options.accessTokenTTL || DEFAULT_ACCESS_TOKEN_TTL;
1277
1419
  const accessTokenExpiresAt = now + accessTokenTTL;
1278
1420
  const accessTokenWrappedKey = await wrapKeyWithToken(accessToken, encryptionKey);
1421
+ const audience = parseResourceParameter(options.request.resource);
1422
+ if (options.request.resource && !audience) {
1423
+ throw new Error("The resource parameter must be a valid absolute URI without a fragment");
1424
+ }
1279
1425
  const grant = {
1280
1426
  id: grantId,
1281
1427
  clientId: options.request.clientId,
@@ -1283,7 +1429,8 @@ var OAuthHelpersImpl = class {
1283
1429
  scope: options.scope,
1284
1430
  metadata: options.metadata,
1285
1431
  encryptedProps: encryptedData,
1286
- createdAt: now
1432
+ createdAt: now,
1433
+ resource: options.request.resource
1287
1434
  };
1288
1435
  const grantKey = `grant:${options.userId}:${grantId}`;
1289
1436
  await this.env.OAUTH_KV.put(grantKey, JSON.stringify(grant));
@@ -1293,6 +1440,7 @@ var OAuthHelpersImpl = class {
1293
1440
  userId: options.userId,
1294
1441
  createdAt: now,
1295
1442
  expiresAt: accessTokenExpiresAt,
1443
+ audience,
1296
1444
  wrappedEncryptionKey: accessTokenWrappedKey,
1297
1445
  grant: {
1298
1446
  clientId: options.request.clientId,
@@ -1335,7 +1483,8 @@ var OAuthHelpersImpl = class {
1335
1483
  // Store the wrapped key
1336
1484
  // Store PKCE parameters if provided
1337
1485
  codeChallenge: options.request.codeChallenge,
1338
- codeChallengeMethod: options.request.codeChallengeMethod
1486
+ codeChallengeMethod: options.request.codeChallengeMethod,
1487
+ resource: options.request.resource
1339
1488
  };
1340
1489
  const grantKey = `grant:${options.userId}:${grantId}`;
1341
1490
  const codeExpiresIn = 600;
@@ -1372,6 +1521,9 @@ var OAuthHelpersImpl = class {
1372
1521
  registrationDate: Math.floor(Date.now() / 1e3),
1373
1522
  tokenEndpointAuthMethod
1374
1523
  };
1524
+ for (const uri of newClient.redirectUris) {
1525
+ validateRedirectUriScheme(uri);
1526
+ }
1375
1527
  let clientSecret;
1376
1528
  if (!isPublicClient) {
1377
1529
  clientSecret = generateRandomString(32);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudflare/workers-oauth-provider",
3
- "version": "0.0.0-b7784c7",
3
+ "version": "0.0.0-f2b1372",
4
4
  "description": "OAuth provider for Cloudflare Workers",
5
5
  "main": "dist/oauth-provider.js",
6
6
  "types": "dist/oauth-provider.d.ts",