@cloudflare/workers-oauth-provider 0.0.13 → 0.1.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/dist/oauth-provider.d.ts +20 -0
- package/dist/oauth-provider.js +122 -4
- 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
|
});
|
|
@@ -965,6 +1012,17 @@ var OAuthProviderImpl = class {
|
|
|
965
1012
|
"WWW-Authenticate": 'Bearer realm="OAuth", error="invalid_token"'
|
|
966
1013
|
});
|
|
967
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
|
+
}
|
|
968
1026
|
const encryptionKey = await unwrapKeyWithToken(accessToken, tokenData.wrappedEncryptionKey);
|
|
969
1027
|
const decryptedProps = await decryptProps(encryptionKey, tokenData.grant.encryptedProps);
|
|
970
1028
|
ctx.props = decryptedProps;
|
|
@@ -975,6 +1033,17 @@ var OAuthProviderImpl = class {
|
|
|
975
1033
|
"WWW-Authenticate": 'Bearer realm="OAuth", error="invalid_token"'
|
|
976
1034
|
});
|
|
977
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
|
+
}
|
|
978
1047
|
ctx.props = ext.props;
|
|
979
1048
|
}
|
|
980
1049
|
if (!env.OAUTH_PROVIDER) {
|
|
@@ -1051,6 +1120,41 @@ var OAuthProviderImpl = class {
|
|
|
1051
1120
|
};
|
|
1052
1121
|
var DEFAULT_ACCESS_TOKEN_TTL = 60 * 60;
|
|
1053
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
|
+
}
|
|
1054
1158
|
async function hashSecret(secret) {
|
|
1055
1159
|
return generateTokenId(secret);
|
|
1056
1160
|
}
|
|
@@ -1244,7 +1348,13 @@ var OAuthHelpersImpl = class {
|
|
|
1244
1348
|
const state = url.searchParams.get("state") || "";
|
|
1245
1349
|
const codeChallenge = url.searchParams.get("code_challenge") || void 0;
|
|
1246
1350
|
const codeChallengeMethod = url.searchParams.get("code_challenge_method") || "plain";
|
|
1351
|
+
const resourceParams = url.searchParams.getAll("resource");
|
|
1352
|
+
const resourceParam = resourceParams.length > 0 ? resourceParams.length === 1 ? resourceParams[0] : resourceParams : void 0;
|
|
1247
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");
|
|
1357
|
+
}
|
|
1248
1358
|
if (responseType === "token" && !this.provider.options.allowImplicitFlow) {
|
|
1249
1359
|
throw new Error("The implicit grant flow is not enabled for this provider");
|
|
1250
1360
|
}
|
|
@@ -1268,7 +1378,8 @@ var OAuthHelpersImpl = class {
|
|
|
1268
1378
|
scope,
|
|
1269
1379
|
state,
|
|
1270
1380
|
codeChallenge,
|
|
1271
|
-
codeChallengeMethod
|
|
1381
|
+
codeChallengeMethod,
|
|
1382
|
+
resource
|
|
1272
1383
|
};
|
|
1273
1384
|
}
|
|
1274
1385
|
/**
|
|
@@ -1307,6 +1418,10 @@ var OAuthHelpersImpl = class {
|
|
|
1307
1418
|
const accessTokenTTL = this.provider.options.accessTokenTTL || DEFAULT_ACCESS_TOKEN_TTL;
|
|
1308
1419
|
const accessTokenExpiresAt = now + accessTokenTTL;
|
|
1309
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
|
+
}
|
|
1310
1425
|
const grant = {
|
|
1311
1426
|
id: grantId,
|
|
1312
1427
|
clientId: options.request.clientId,
|
|
@@ -1314,7 +1429,8 @@ var OAuthHelpersImpl = class {
|
|
|
1314
1429
|
scope: options.scope,
|
|
1315
1430
|
metadata: options.metadata,
|
|
1316
1431
|
encryptedProps: encryptedData,
|
|
1317
|
-
createdAt: now
|
|
1432
|
+
createdAt: now,
|
|
1433
|
+
resource: options.request.resource
|
|
1318
1434
|
};
|
|
1319
1435
|
const grantKey = `grant:${options.userId}:${grantId}`;
|
|
1320
1436
|
await this.env.OAUTH_KV.put(grantKey, JSON.stringify(grant));
|
|
@@ -1324,6 +1440,7 @@ var OAuthHelpersImpl = class {
|
|
|
1324
1440
|
userId: options.userId,
|
|
1325
1441
|
createdAt: now,
|
|
1326
1442
|
expiresAt: accessTokenExpiresAt,
|
|
1443
|
+
audience,
|
|
1327
1444
|
wrappedEncryptionKey: accessTokenWrappedKey,
|
|
1328
1445
|
grant: {
|
|
1329
1446
|
clientId: options.request.clientId,
|
|
@@ -1366,7 +1483,8 @@ var OAuthHelpersImpl = class {
|
|
|
1366
1483
|
// Store the wrapped key
|
|
1367
1484
|
// Store PKCE parameters if provided
|
|
1368
1485
|
codeChallenge: options.request.codeChallenge,
|
|
1369
|
-
codeChallengeMethod: options.request.codeChallengeMethod
|
|
1486
|
+
codeChallengeMethod: options.request.codeChallengeMethod,
|
|
1487
|
+
resource: options.request.resource
|
|
1370
1488
|
};
|
|
1371
1489
|
const grantKey = `grant:${options.userId}:${grantId}`;
|
|
1372
1490
|
const codeExpiresIn = 600;
|