@cloudflare/workers-oauth-provider 0.0.0-78be0eb → 0.0.0-818a557
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/dist/oauth-provider.d.ts +20 -0
- package/dist/oauth-provider.js +159 -7
- package/package.json +1 -1
package/dist/oauth-provider.d.ts
CHANGED
|
@@ -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
|
*/
|
package/dist/oauth-provider.js
CHANGED
|
@@ -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
|
-
|
|
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,11 +1120,46 @@ 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
|
}
|
|
1054
1161
|
function generateRandomString(length) {
|
|
1055
|
-
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
1162
|
+
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
1056
1163
|
let result = "";
|
|
1057
1164
|
const values = new Uint8Array(length);
|
|
1058
1165
|
crypto.getRandomValues(values);
|
|
@@ -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
|
-
|
|
1225
|
-
|
|
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