@cloudflare/workers-oauth-provider 0.0.0-9cd9ab4 → 0.0.0-a204c64
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.js +155 -54
- package/package.json +1 -1
package/dist/oauth-provider.js
CHANGED
|
@@ -142,7 +142,16 @@ var OAuthProviderImpl = class {
|
|
|
142
142
|
return this.addCorsHeaders(response, request);
|
|
143
143
|
}
|
|
144
144
|
if (this.isTokenEndpoint(url)) {
|
|
145
|
-
const
|
|
145
|
+
const parsed = await this.parseTokenEndpointRequest(request, env);
|
|
146
|
+
if (parsed instanceof Response) {
|
|
147
|
+
return this.addCorsHeaders(parsed, request);
|
|
148
|
+
}
|
|
149
|
+
let response;
|
|
150
|
+
if (parsed.isRevocationRequest) {
|
|
151
|
+
response = await this.handleRevocationRequest(parsed.body, env);
|
|
152
|
+
} else {
|
|
153
|
+
response = await this.handleTokenRequest(parsed.body, parsed.clientInfo, env);
|
|
154
|
+
}
|
|
146
155
|
return this.addCorsHeaders(response, request);
|
|
147
156
|
}
|
|
148
157
|
if (this.options.clientRegistrationEndpoint && this.isClientRegistrationEndpoint(url)) {
|
|
@@ -206,6 +215,67 @@ var OAuthProviderImpl = class {
|
|
|
206
215
|
if (!this.options.clientRegistrationEndpoint) return false;
|
|
207
216
|
return this.matchEndpoint(url, this.options.clientRegistrationEndpoint);
|
|
208
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* Parses and validates a token endpoint request (used for both token exchange and revocation)
|
|
220
|
+
* @param request - The HTTP request to parse
|
|
221
|
+
* @returns Promise with parsed body and client info, or error response
|
|
222
|
+
*/
|
|
223
|
+
async parseTokenEndpointRequest(request, env) {
|
|
224
|
+
if (request.method !== "POST") {
|
|
225
|
+
return this.createErrorResponse("invalid_request", "Method not allowed", 405);
|
|
226
|
+
}
|
|
227
|
+
let contentType = request.headers.get("Content-Type") || "";
|
|
228
|
+
let body = {};
|
|
229
|
+
if (!contentType.includes("application/x-www-form-urlencoded")) {
|
|
230
|
+
return this.createErrorResponse("invalid_request", "Content-Type must be application/x-www-form-urlencoded", 400);
|
|
231
|
+
}
|
|
232
|
+
const formData = await request.formData();
|
|
233
|
+
for (const [key, value] of formData.entries()) {
|
|
234
|
+
body[key] = value;
|
|
235
|
+
}
|
|
236
|
+
const authHeader = request.headers.get("Authorization");
|
|
237
|
+
let clientId = "";
|
|
238
|
+
let clientSecret = "";
|
|
239
|
+
if (authHeader && authHeader.startsWith("Basic ")) {
|
|
240
|
+
const credentials = atob(authHeader.substring(6));
|
|
241
|
+
const [id, secret] = credentials.split(":", 2);
|
|
242
|
+
clientId = decodeURIComponent(id);
|
|
243
|
+
clientSecret = decodeURIComponent(secret || "");
|
|
244
|
+
} else {
|
|
245
|
+
clientId = body.client_id;
|
|
246
|
+
clientSecret = body.client_secret || "";
|
|
247
|
+
}
|
|
248
|
+
if (!clientId) {
|
|
249
|
+
return this.createErrorResponse("invalid_client", "Client ID is required", 401);
|
|
250
|
+
}
|
|
251
|
+
const clientInfo = await this.getClient(env, clientId);
|
|
252
|
+
if (!clientInfo) {
|
|
253
|
+
return this.createErrorResponse("invalid_client", "Client not found", 401);
|
|
254
|
+
}
|
|
255
|
+
const isPublicClient = clientInfo.tokenEndpointAuthMethod === "none";
|
|
256
|
+
if (!isPublicClient) {
|
|
257
|
+
if (!clientSecret) {
|
|
258
|
+
return this.createErrorResponse("invalid_client", "Client authentication failed: missing client_secret", 401);
|
|
259
|
+
}
|
|
260
|
+
if (!clientInfo.clientSecret) {
|
|
261
|
+
return this.createErrorResponse(
|
|
262
|
+
"invalid_client",
|
|
263
|
+
"Client authentication failed: client has no registered secret",
|
|
264
|
+
401
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
const providedSecretHash = await hashSecret(clientSecret);
|
|
268
|
+
if (providedSecretHash !== clientInfo.clientSecret) {
|
|
269
|
+
return this.createErrorResponse("invalid_client", "Client authentication failed: invalid client_secret", 401);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
const isRevocationRequest = !body.grant_type && !!body.token;
|
|
273
|
+
return {
|
|
274
|
+
body,
|
|
275
|
+
clientInfo,
|
|
276
|
+
isRevocationRequest
|
|
277
|
+
};
|
|
278
|
+
}
|
|
209
279
|
/**
|
|
210
280
|
* Checks if a URL matches a specific API route
|
|
211
281
|
* @param url - The URL to check
|
|
@@ -329,59 +399,12 @@ var OAuthProviderImpl = class {
|
|
|
329
399
|
/**
|
|
330
400
|
* Handles client authentication and token issuance via the token endpoint
|
|
331
401
|
* Supports authorization_code and refresh_token grant types
|
|
332
|
-
* @param
|
|
402
|
+
* @param body - The parsed request body
|
|
403
|
+
* @param clientInfo - The authenticated client information
|
|
333
404
|
* @param env - Cloudflare Worker environment variables
|
|
334
405
|
* @returns Response with token data or error
|
|
335
406
|
*/
|
|
336
|
-
async handleTokenRequest(
|
|
337
|
-
if (request.method !== "POST") {
|
|
338
|
-
return this.createErrorResponse("invalid_request", "Method not allowed", 405);
|
|
339
|
-
}
|
|
340
|
-
let contentType = request.headers.get("Content-Type") || "";
|
|
341
|
-
let body = {};
|
|
342
|
-
if (!contentType.includes("application/x-www-form-urlencoded")) {
|
|
343
|
-
return this.createErrorResponse("invalid_request", "Content-Type must be application/x-www-form-urlencoded", 400);
|
|
344
|
-
}
|
|
345
|
-
const formData = await request.formData();
|
|
346
|
-
for (const [key, value] of formData.entries()) {
|
|
347
|
-
body[key] = value;
|
|
348
|
-
}
|
|
349
|
-
const authHeader = request.headers.get("Authorization");
|
|
350
|
-
let clientId = "";
|
|
351
|
-
let clientSecret = "";
|
|
352
|
-
if (authHeader && authHeader.startsWith("Basic ")) {
|
|
353
|
-
const credentials = atob(authHeader.substring(6));
|
|
354
|
-
const [id, secret] = credentials.split(":", 2);
|
|
355
|
-
clientId = decodeURIComponent(id);
|
|
356
|
-
clientSecret = decodeURIComponent(secret || "");
|
|
357
|
-
} else {
|
|
358
|
-
clientId = body.client_id;
|
|
359
|
-
clientSecret = body.client_secret || "";
|
|
360
|
-
}
|
|
361
|
-
if (!clientId) {
|
|
362
|
-
return this.createErrorResponse("invalid_client", "Client ID is required", 401);
|
|
363
|
-
}
|
|
364
|
-
const clientInfo = await this.getClient(env, clientId);
|
|
365
|
-
if (!clientInfo) {
|
|
366
|
-
return this.createErrorResponse("invalid_client", "Client not found", 401);
|
|
367
|
-
}
|
|
368
|
-
const isPublicClient = clientInfo.tokenEndpointAuthMethod === "none";
|
|
369
|
-
if (!isPublicClient) {
|
|
370
|
-
if (!clientSecret) {
|
|
371
|
-
return this.createErrorResponse("invalid_client", "Client authentication failed: missing client_secret", 401);
|
|
372
|
-
}
|
|
373
|
-
if (!clientInfo.clientSecret) {
|
|
374
|
-
return this.createErrorResponse(
|
|
375
|
-
"invalid_client",
|
|
376
|
-
"Client authentication failed: client has no registered secret",
|
|
377
|
-
401
|
|
378
|
-
);
|
|
379
|
-
}
|
|
380
|
-
const providedSecretHash = await hashSecret(clientSecret);
|
|
381
|
-
if (providedSecretHash !== clientInfo.clientSecret) {
|
|
382
|
-
return this.createErrorResponse("invalid_client", "Client authentication failed: invalid client_secret", 401);
|
|
383
|
-
}
|
|
384
|
-
}
|
|
407
|
+
async handleTokenRequest(body, clientInfo, env) {
|
|
385
408
|
const grantType = body.grant_type;
|
|
386
409
|
if (grantType === "authorization_code") {
|
|
387
410
|
return this.handleAuthorizationCodeGrant(body, clientInfo, env);
|
|
@@ -679,6 +702,86 @@ var OAuthProviderImpl = class {
|
|
|
679
702
|
}
|
|
680
703
|
);
|
|
681
704
|
}
|
|
705
|
+
/**
|
|
706
|
+
* Handles OAuth 2.0 token revocation requests (RFC 7009)
|
|
707
|
+
* @param body - The parsed request body containing revocation parameters
|
|
708
|
+
* @param env - Cloudflare Worker environment variables
|
|
709
|
+
* @returns Response confirming revocation or error
|
|
710
|
+
*/
|
|
711
|
+
async handleRevocationRequest(body, env) {
|
|
712
|
+
return this.revokeToken(body, env);
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* - Access tokens: Revokes only the specific token
|
|
716
|
+
* - Refresh tokens: Revokes the entire grant (access + refresh tokens)
|
|
717
|
+
* @param body - The parsed request body containing token parameter
|
|
718
|
+
* @param env - Cloudflare Worker environment variables
|
|
719
|
+
* @returns Response confirming revocation or error
|
|
720
|
+
*/
|
|
721
|
+
async revokeToken(body, env) {
|
|
722
|
+
const token = body.token;
|
|
723
|
+
if (!token) {
|
|
724
|
+
return this.createErrorResponse("invalid_request", "Token parameter is required");
|
|
725
|
+
}
|
|
726
|
+
const tokenParts = token.split(":");
|
|
727
|
+
if (tokenParts.length !== 3) {
|
|
728
|
+
return new Response("", { status: 200 });
|
|
729
|
+
}
|
|
730
|
+
const [userId, grantId, _] = tokenParts;
|
|
731
|
+
const tokenId = await generateTokenId(token);
|
|
732
|
+
const isAccessToken = await this.validateAccessToken(tokenId, userId, grantId, env);
|
|
733
|
+
const isRefreshToken = await this.validateRefreshToken(tokenId, userId, grantId, env);
|
|
734
|
+
if (isAccessToken) {
|
|
735
|
+
await this.revokeSpecificAccessToken(tokenId, userId, grantId, env);
|
|
736
|
+
} else if (isRefreshToken) {
|
|
737
|
+
await this.createOAuthHelpers(env).revokeGrant(grantId, userId);
|
|
738
|
+
}
|
|
739
|
+
return new Response("", { status: 500 });
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Revokes a specific access token without affecting the refresh token
|
|
743
|
+
* @param tokenId - The hashed token ID
|
|
744
|
+
* @param userId - The user ID extracted from the token
|
|
745
|
+
* @param grantId - The grant ID extracted from the token
|
|
746
|
+
* @param env - Cloudflare Worker environment variables
|
|
747
|
+
*/
|
|
748
|
+
async revokeSpecificAccessToken(tokenId, userId, grantId, env) {
|
|
749
|
+
const tokenKey = `token:${userId}:${grantId}:${tokenId}`;
|
|
750
|
+
await env.OAUTH_KV.delete(tokenKey);
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Validates if a token is a valid access token
|
|
754
|
+
* @param tokenId - The hashed token ID
|
|
755
|
+
* @param userId - The user ID extracted from the token
|
|
756
|
+
* @param grantId - The grant ID extracted from the token
|
|
757
|
+
* @param env - Cloudflare Worker environment variables
|
|
758
|
+
* @returns Promise<boolean> indicating if the token is valid
|
|
759
|
+
*/
|
|
760
|
+
async validateAccessToken(tokenId, userId, grantId, env) {
|
|
761
|
+
const tokenKey = `token:${userId}:${grantId}:${tokenId}`;
|
|
762
|
+
const tokenData = await env.OAUTH_KV.get(tokenKey, { type: "json" });
|
|
763
|
+
if (!tokenData) {
|
|
764
|
+
return false;
|
|
765
|
+
}
|
|
766
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
767
|
+
return tokenData.expiresAt >= now;
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* Validates if a token is a valid refresh token
|
|
771
|
+
* @param tokenId - The hashed token ID
|
|
772
|
+
* @param userId - The user ID extracted from the token
|
|
773
|
+
* @param grantId - The grant ID extracted from the token
|
|
774
|
+
* @param env - Cloudflare Worker environment variables
|
|
775
|
+
* @returns Promise<boolean> indicating if the token is valid
|
|
776
|
+
*/
|
|
777
|
+
async validateRefreshToken(tokenId, userId, grantId, env) {
|
|
778
|
+
const grantKey = `grant:${userId}:${grantId}`;
|
|
779
|
+
const grantData = await env.OAUTH_KV.get(grantKey, { type: "json" });
|
|
780
|
+
if (!grantData) {
|
|
781
|
+
return false;
|
|
782
|
+
}
|
|
783
|
+
return grantData.refreshTokenId === tokenId || grantData.previousRefreshTokenId === tokenId;
|
|
784
|
+
}
|
|
682
785
|
/**
|
|
683
786
|
* Handles the dynamic client registration endpoint (RFC 7591)
|
|
684
787
|
* @param request - The HTTP request
|
|
@@ -1077,9 +1180,7 @@ var OAuthHelpersImpl = class {
|
|
|
1077
1180
|
if (clientId) {
|
|
1078
1181
|
const clientInfo = await this.lookupClient(clientId);
|
|
1079
1182
|
if (!clientInfo) {
|
|
1080
|
-
throw new Error(
|
|
1081
|
-
`Invalid client. The clientId provided does not match to this client.`
|
|
1082
|
-
);
|
|
1183
|
+
throw new Error(`Invalid client. The clientId provided does not match to this client.`);
|
|
1083
1184
|
}
|
|
1084
1185
|
if (clientInfo && redirectUri) {
|
|
1085
1186
|
if (!clientInfo.redirectUris.includes(redirectUri)) {
|
package/package.json
CHANGED