@cloudflare/workers-oauth-provider 0.0.0-6ab489d → 0.0.0-75d3fe2

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.
@@ -142,7 +142,16 @@ var OAuthProviderImpl = class {
142
142
  return this.addCorsHeaders(response, request);
143
143
  }
144
144
  if (this.isTokenEndpoint(url)) {
145
- const response = await this.handleTokenRequest(request, env);
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 request - The HTTP request
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(request, env) {
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: 200 });
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
@@ -1071,15 +1174,16 @@ var OAuthHelpersImpl = class {
1071
1174
  const state = url.searchParams.get("state") || "";
1072
1175
  const codeChallenge = url.searchParams.get("code_challenge") || void 0;
1073
1176
  const codeChallengeMethod = url.searchParams.get("code_challenge_method") || "plain";
1177
+ if (!redirectUri.startsWith("http://") && !redirectUri.startsWith("https://")) {
1178
+ throw new Error("Invalid redirect URI");
1179
+ }
1074
1180
  if (responseType === "token" && !this.provider.options.allowImplicitFlow) {
1075
1181
  throw new Error("The implicit grant flow is not enabled for this provider");
1076
1182
  }
1077
1183
  if (clientId) {
1078
1184
  const clientInfo = await this.lookupClient(clientId);
1079
1185
  if (!clientInfo) {
1080
- throw new Error(
1081
- `Invalid client. The clientId provided does not match to this client.`
1082
- );
1186
+ throw new Error(`Invalid client. The clientId provided does not match to this client.`);
1083
1187
  }
1084
1188
  if (clientInfo && redirectUri) {
1085
1189
  if (!clientInfo.redirectUris.includes(redirectUri)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudflare/workers-oauth-provider",
3
- "version": "0.0.0-6ab489d",
3
+ "version": "0.0.0-75d3fe2",
4
4
  "description": "OAuth provider for Cloudflare Workers",
5
5
  "main": "dist/oauth-provider.js",
6
6
  "types": "dist/oauth-provider.d.ts",
@@ -26,11 +26,12 @@
26
26
  },
27
27
  "devDependencies": {
28
28
  "@changesets/changelog-github": "^0.5.1",
29
- "@changesets/cli": "^2.29.5",
29
+ "@changesets/cli": "^2.29.7",
30
30
  "@cloudflare/workers-types": "^4.20250807.0",
31
+ "pkg-pr-new": "^0.0.59",
31
32
  "prettier": "^3.6.2",
32
33
  "tsup": "^8.5.0",
33
- "tsx": "^4.20.3",
34
+ "tsx": "^4.20.5",
34
35
  "typescript": "^5.9.2",
35
36
  "vitest": "^3.2.4"
36
37
  },