@cloudflare/workers-oauth-provider 0.0.1 → 0.0.2
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/README.md +66 -0
- package/dist/oauth-provider.d.ts +64 -1
- package/dist/oauth-provider.js +187 -203
- package/package.json +7 -5
package/README.md
CHANGED
|
@@ -196,6 +196,72 @@ The `env.OAUTH_PROVIDER` object available to the fetch handlers provides some me
|
|
|
196
196
|
|
|
197
197
|
See the `OAuthHelpers` interface definition for full API details.
|
|
198
198
|
|
|
199
|
+
## Token Exchange Callback
|
|
200
|
+
|
|
201
|
+
This library allows you to update the `props` value during token exchanges by configuring a callback function. This is useful for scenarios where the application needs to perform additional processing when tokens are issued or refreshed.
|
|
202
|
+
|
|
203
|
+
For example, if your application is also a client to some other OAuth API, you might want to perform an equivalent upstream token exchange and store the result in the `props`. The callback can be used to update the props for both the grant record and specific access tokens.
|
|
204
|
+
|
|
205
|
+
To use this feature, provide a `tokenExchangeCallback` in your OAuthProvider options:
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
new OAuthProvider({
|
|
209
|
+
// ... other options ...
|
|
210
|
+
tokenExchangeCallback: async (options) => {
|
|
211
|
+
// options.grantType is either 'authorization_code' or 'refresh_token'
|
|
212
|
+
// options.props contains the current props
|
|
213
|
+
// options.clientId, options.userId, and options.scope are also available
|
|
214
|
+
|
|
215
|
+
if (options.grantType === 'authorization_code') {
|
|
216
|
+
// For authorization code exchange, might want to obtain upstream tokens
|
|
217
|
+
const upstreamTokens = await exchangeUpstreamToken(options.props.someCode);
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
// Update the props stored in the access token
|
|
221
|
+
accessTokenProps: {
|
|
222
|
+
...options.props,
|
|
223
|
+
upstreamAccessToken: upstreamTokens.access_token
|
|
224
|
+
},
|
|
225
|
+
// Update the props stored in the grant (for future token refreshes)
|
|
226
|
+
newProps: {
|
|
227
|
+
...options.props,
|
|
228
|
+
upstreamRefreshToken: upstreamTokens.refresh_token
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (options.grantType === 'refresh_token') {
|
|
234
|
+
// For refresh token exchanges, might want to refresh upstream tokens too
|
|
235
|
+
const upstreamTokens = await refreshUpstreamToken(options.props.upstreamRefreshToken);
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
accessTokenProps: {
|
|
239
|
+
...options.props,
|
|
240
|
+
upstreamAccessToken: upstreamTokens.access_token
|
|
241
|
+
},
|
|
242
|
+
newProps: {
|
|
243
|
+
...options.props,
|
|
244
|
+
upstreamRefreshToken: upstreamTokens.refresh_token || options.props.upstreamRefreshToken
|
|
245
|
+
},
|
|
246
|
+
// Optionally override the default access token TTL to match the upstream token
|
|
247
|
+
accessTokenTTL: upstreamTokens.expires_in
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
The callback can:
|
|
255
|
+
- Return both `accessTokenProps` and `newProps` to update both
|
|
256
|
+
- Return only `accessTokenProps` to update just the current access token
|
|
257
|
+
- Return only `newProps` to update both the grant and access token (the access token inherits these props)
|
|
258
|
+
- Return `accessTokenTTL` to override the default TTL for this specific access token
|
|
259
|
+
- Return nothing to keep the original props unchanged
|
|
260
|
+
|
|
261
|
+
The `accessTokenTTL` override is particularly useful when the application is also an OAuth client to another service and wants to match its access token TTL to the upstream access token TTL. This helps prevent situations where the downstream token is still valid but the upstream token has expired.
|
|
262
|
+
|
|
263
|
+
The `props` values are end-to-end encrypted, so they can safely contain sensitive information.
|
|
264
|
+
|
|
199
265
|
## Written by Claude
|
|
200
266
|
|
|
201
267
|
This library (including the schema documentation) was largely written by [Claude](https://claude.ai), the AI model by Anthropic. Claude's output was thoroughly reviewed by Cloudflare engineers with careful attention paid to security and compliance with standards. Many improvements were made on the initial output, mostly again by prompting Claude (and reviewing the results). Check out the commit history to see how Claude was prompted and what code it produced.
|
package/dist/oauth-provider.d.ts
CHANGED
|
@@ -9,6 +9,59 @@ type WorkerEntrypointWithFetch = WorkerEntrypoint & Pick<Required<WorkerEntrypoi
|
|
|
9
9
|
/**
|
|
10
10
|
* Configuration options for the OAuth Provider
|
|
11
11
|
*/
|
|
12
|
+
/**
|
|
13
|
+
* Result of a token exchange callback function.
|
|
14
|
+
* Allows updating the props stored in both the access token and the grant.
|
|
15
|
+
*/
|
|
16
|
+
interface TokenExchangeCallbackResult {
|
|
17
|
+
/**
|
|
18
|
+
* New props to be stored specifically with the access token.
|
|
19
|
+
* If not provided but newProps is, the access token will use newProps.
|
|
20
|
+
* If neither is provided, the original props will be used.
|
|
21
|
+
*/
|
|
22
|
+
accessTokenProps?: any;
|
|
23
|
+
/**
|
|
24
|
+
* New props to replace the props stored in the grant itself.
|
|
25
|
+
* These props will be used for all future token refreshes.
|
|
26
|
+
* If accessTokenProps is not provided, these props will also be used for the current access token.
|
|
27
|
+
* If not provided, the original props will be used.
|
|
28
|
+
*/
|
|
29
|
+
newProps?: any;
|
|
30
|
+
/**
|
|
31
|
+
* Override the default access token TTL (time-to-live) for this specific token.
|
|
32
|
+
* This is especially useful when the application is also an OAuth client to another service
|
|
33
|
+
* and wants to match its access token TTL to the upstream access token TTL.
|
|
34
|
+
* Value should be in seconds.
|
|
35
|
+
*/
|
|
36
|
+
accessTokenTTL?: number;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Options for token exchange callback functions
|
|
40
|
+
*/
|
|
41
|
+
interface TokenExchangeCallbackOptions {
|
|
42
|
+
/**
|
|
43
|
+
* The type of grant being processed.
|
|
44
|
+
* 'authorization_code' for initial code exchange,
|
|
45
|
+
* 'refresh_token' for refresh token exchange.
|
|
46
|
+
*/
|
|
47
|
+
grantType: 'authorization_code' | 'refresh_token';
|
|
48
|
+
/**
|
|
49
|
+
* Client that received this grant
|
|
50
|
+
*/
|
|
51
|
+
clientId: string;
|
|
52
|
+
/**
|
|
53
|
+
* User who authorized this grant
|
|
54
|
+
*/
|
|
55
|
+
userId: string;
|
|
56
|
+
/**
|
|
57
|
+
* List of scopes that were granted
|
|
58
|
+
*/
|
|
59
|
+
scope: string[];
|
|
60
|
+
/**
|
|
61
|
+
* Application-specific properties currently associated with this grant
|
|
62
|
+
*/
|
|
63
|
+
props: any;
|
|
64
|
+
}
|
|
12
65
|
interface OAuthProviderOptions {
|
|
13
66
|
/**
|
|
14
67
|
* URL(s) for API routes. Requests with URLs starting with any of these prefixes
|
|
@@ -65,6 +118,16 @@ interface OAuthProviderOptions {
|
|
|
65
118
|
* Defaults to false.
|
|
66
119
|
*/
|
|
67
120
|
disallowPublicClientRegistration?: boolean;
|
|
121
|
+
/**
|
|
122
|
+
* Optional callback function that is called during token exchange.
|
|
123
|
+
* This allows updating the props stored in both the access token and the grant.
|
|
124
|
+
* For example, if the application itself is also a client to some other OAuth API,
|
|
125
|
+
* it may want to perform the equivalent upstream token exchange, and store the result in the props.
|
|
126
|
+
*
|
|
127
|
+
* The callback can return new props values that will be stored with the token or grant.
|
|
128
|
+
* If the callback returns nothing or undefined for a props field, the original props will be used.
|
|
129
|
+
*/
|
|
130
|
+
tokenExchangeCallback?: (options: TokenExchangeCallbackOptions) => Promise<TokenExchangeCallbackResult | void> | TokenExchangeCallbackResult | void;
|
|
68
131
|
}
|
|
69
132
|
/**
|
|
70
133
|
* Helper methods for OAuth operations provided to handler functions
|
|
@@ -456,4 +519,4 @@ declare class OAuthProvider {
|
|
|
456
519
|
fetch(request: Request, env: any, ctx: ExecutionContext): Promise<Response>;
|
|
457
520
|
}
|
|
458
521
|
|
|
459
|
-
export { type AuthRequest, type ClientInfo, type CompleteAuthorizationOptions, type Grant, type GrantSummary, type ListOptions, type ListResult, type OAuthHelpers, OAuthProvider, type OAuthProviderOptions, type Token, OAuthProvider as default };
|
|
522
|
+
export { type AuthRequest, type ClientInfo, type CompleteAuthorizationOptions, type Grant, type GrantSummary, type ListOptions, type ListResult, type OAuthHelpers, OAuthProvider, type OAuthProviderOptions, type Token, type TokenExchangeCallbackOptions, type TokenExchangeCallbackResult, OAuthProvider as default };
|
package/dist/oauth-provider.js
CHANGED
|
@@ -89,7 +89,9 @@ var OAuthProviderImpl = class {
|
|
|
89
89
|
if (typeof handler === "function" && handler.prototype instanceof WorkerEntrypoint) {
|
|
90
90
|
return { type: 1 /* WORKER_ENTRYPOINT */, handler };
|
|
91
91
|
}
|
|
92
|
-
throw new TypeError(
|
|
92
|
+
throw new TypeError(
|
|
93
|
+
`${name} must be either an ExportedHandler object with a fetch method or a class extending WorkerEntrypoint`
|
|
94
|
+
);
|
|
93
95
|
}
|
|
94
96
|
/**
|
|
95
97
|
* Main fetch handler for the Worker
|
|
@@ -132,7 +134,11 @@ var OAuthProviderImpl = class {
|
|
|
132
134
|
env.OAUTH_PROVIDER = this.createOAuthHelpers(env);
|
|
133
135
|
}
|
|
134
136
|
if (this.typedDefaultHandler.type === 0 /* EXPORTED_HANDLER */) {
|
|
135
|
-
return this.typedDefaultHandler.handler.fetch(
|
|
137
|
+
return this.typedDefaultHandler.handler.fetch(
|
|
138
|
+
request,
|
|
139
|
+
env,
|
|
140
|
+
ctx
|
|
141
|
+
);
|
|
136
142
|
} else {
|
|
137
143
|
const handler = new this.typedDefaultHandler.handler(ctx, env);
|
|
138
144
|
return handler.fetch(request);
|
|
@@ -292,20 +298,12 @@ var OAuthProviderImpl = class {
|
|
|
292
298
|
*/
|
|
293
299
|
async handleTokenRequest(request, env) {
|
|
294
300
|
if (request.method !== "POST") {
|
|
295
|
-
return createErrorResponse(
|
|
296
|
-
"invalid_request",
|
|
297
|
-
"Method not allowed",
|
|
298
|
-
405
|
|
299
|
-
);
|
|
301
|
+
return createErrorResponse("invalid_request", "Method not allowed", 405);
|
|
300
302
|
}
|
|
301
303
|
let contentType = request.headers.get("Content-Type") || "";
|
|
302
304
|
let body = {};
|
|
303
305
|
if (!contentType.includes("application/x-www-form-urlencoded")) {
|
|
304
|
-
return createErrorResponse(
|
|
305
|
-
"invalid_request",
|
|
306
|
-
"Content-Type must be application/x-www-form-urlencoded",
|
|
307
|
-
400
|
|
308
|
-
);
|
|
306
|
+
return createErrorResponse("invalid_request", "Content-Type must be application/x-www-form-urlencoded", 400);
|
|
309
307
|
}
|
|
310
308
|
const formData = await request.formData();
|
|
311
309
|
for (const [key, value] of formData.entries()) {
|
|
@@ -324,28 +322,16 @@ var OAuthProviderImpl = class {
|
|
|
324
322
|
clientSecret = body.client_secret || "";
|
|
325
323
|
}
|
|
326
324
|
if (!clientId) {
|
|
327
|
-
return createErrorResponse(
|
|
328
|
-
"invalid_client",
|
|
329
|
-
"Client ID is required",
|
|
330
|
-
401
|
|
331
|
-
);
|
|
325
|
+
return createErrorResponse("invalid_client", "Client ID is required", 401);
|
|
332
326
|
}
|
|
333
327
|
const clientInfo = await this.getClient(env, clientId);
|
|
334
328
|
if (!clientInfo) {
|
|
335
|
-
return createErrorResponse(
|
|
336
|
-
"invalid_client",
|
|
337
|
-
"Client not found",
|
|
338
|
-
401
|
|
339
|
-
);
|
|
329
|
+
return createErrorResponse("invalid_client", "Client not found", 401);
|
|
340
330
|
}
|
|
341
331
|
const isPublicClient = clientInfo.tokenEndpointAuthMethod === "none";
|
|
342
332
|
if (!isPublicClient) {
|
|
343
333
|
if (!clientSecret) {
|
|
344
|
-
return createErrorResponse(
|
|
345
|
-
"invalid_client",
|
|
346
|
-
"Client authentication failed: missing client_secret",
|
|
347
|
-
401
|
|
348
|
-
);
|
|
334
|
+
return createErrorResponse("invalid_client", "Client authentication failed: missing client_secret", 401);
|
|
349
335
|
}
|
|
350
336
|
if (!clientInfo.clientSecret) {
|
|
351
337
|
return createErrorResponse(
|
|
@@ -356,11 +342,7 @@ var OAuthProviderImpl = class {
|
|
|
356
342
|
}
|
|
357
343
|
const providedSecretHash = await hashSecret(clientSecret);
|
|
358
344
|
if (providedSecretHash !== clientInfo.clientSecret) {
|
|
359
|
-
return createErrorResponse(
|
|
360
|
-
"invalid_client",
|
|
361
|
-
"Client authentication failed: invalid client_secret",
|
|
362
|
-
401
|
|
363
|
-
);
|
|
345
|
+
return createErrorResponse("invalid_client", "Client authentication failed: invalid client_secret", 401);
|
|
364
346
|
}
|
|
365
347
|
}
|
|
366
348
|
const grantType = body.grant_type;
|
|
@@ -369,10 +351,7 @@ var OAuthProviderImpl = class {
|
|
|
369
351
|
} else if (grantType === "refresh_token") {
|
|
370
352
|
return this.handleRefreshTokenGrant(body, clientInfo, env);
|
|
371
353
|
} else {
|
|
372
|
-
return createErrorResponse(
|
|
373
|
-
"unsupported_grant_type",
|
|
374
|
-
"Grant type not supported"
|
|
375
|
-
);
|
|
354
|
+
return createErrorResponse("unsupported_grant_type", "Grant type not supported");
|
|
376
355
|
}
|
|
377
356
|
}
|
|
378
357
|
/**
|
|
@@ -388,65 +367,38 @@ var OAuthProviderImpl = class {
|
|
|
388
367
|
const redirectUri = body.redirect_uri;
|
|
389
368
|
const codeVerifier = body.code_verifier;
|
|
390
369
|
if (!code) {
|
|
391
|
-
return createErrorResponse(
|
|
392
|
-
"invalid_request",
|
|
393
|
-
"Authorization code is required"
|
|
394
|
-
);
|
|
370
|
+
return createErrorResponse("invalid_request", "Authorization code is required");
|
|
395
371
|
}
|
|
396
372
|
const codeParts = code.split(":");
|
|
397
373
|
if (codeParts.length !== 3) {
|
|
398
|
-
return createErrorResponse(
|
|
399
|
-
"invalid_grant",
|
|
400
|
-
"Invalid authorization code format"
|
|
401
|
-
);
|
|
374
|
+
return createErrorResponse("invalid_grant", "Invalid authorization code format");
|
|
402
375
|
}
|
|
403
376
|
const [userId, grantId, _] = codeParts;
|
|
404
377
|
const grantKey = `grant:${userId}:${grantId}`;
|
|
405
378
|
const grantData = await env.OAUTH_KV.get(grantKey, { type: "json" });
|
|
406
379
|
if (!grantData) {
|
|
407
|
-
return createErrorResponse(
|
|
408
|
-
"invalid_grant",
|
|
409
|
-
"Grant not found or authorization code expired"
|
|
410
|
-
);
|
|
380
|
+
return createErrorResponse("invalid_grant", "Grant not found or authorization code expired");
|
|
411
381
|
}
|
|
412
382
|
if (!grantData.authCodeId) {
|
|
413
|
-
return createErrorResponse(
|
|
414
|
-
"invalid_grant",
|
|
415
|
-
"Authorization code already used"
|
|
416
|
-
);
|
|
383
|
+
return createErrorResponse("invalid_grant", "Authorization code already used");
|
|
417
384
|
}
|
|
418
385
|
const codeHash = await hashSecret(code);
|
|
419
386
|
if (codeHash !== grantData.authCodeId) {
|
|
420
|
-
return createErrorResponse(
|
|
421
|
-
"invalid_grant",
|
|
422
|
-
"Invalid authorization code"
|
|
423
|
-
);
|
|
387
|
+
return createErrorResponse("invalid_grant", "Invalid authorization code");
|
|
424
388
|
}
|
|
425
389
|
if (grantData.clientId !== clientInfo.clientId) {
|
|
426
|
-
return createErrorResponse(
|
|
427
|
-
"invalid_grant",
|
|
428
|
-
"Client ID mismatch"
|
|
429
|
-
);
|
|
390
|
+
return createErrorResponse("invalid_grant", "Client ID mismatch");
|
|
430
391
|
}
|
|
431
392
|
const isPkceEnabled = !!grantData.codeChallenge;
|
|
432
393
|
if (!redirectUri && !isPkceEnabled) {
|
|
433
|
-
return createErrorResponse(
|
|
434
|
-
"invalid_request",
|
|
435
|
-
"redirect_uri is required when not using PKCE"
|
|
436
|
-
);
|
|
394
|
+
return createErrorResponse("invalid_request", "redirect_uri is required when not using PKCE");
|
|
437
395
|
}
|
|
438
396
|
if (redirectUri && !clientInfo.redirectUris.includes(redirectUri)) {
|
|
439
|
-
return createErrorResponse(
|
|
440
|
-
"invalid_grant",
|
|
441
|
-
"Invalid redirect URI"
|
|
442
|
-
);
|
|
397
|
+
return createErrorResponse("invalid_grant", "Invalid redirect URI");
|
|
443
398
|
}
|
|
444
399
|
if (isPkceEnabled) {
|
|
445
400
|
if (!codeVerifier) {
|
|
446
|
-
return createErrorResponse(
|
|
447
|
-
"invalid_request",
|
|
448
|
-
"code_verifier is required for PKCE"
|
|
449
|
-
);
|
|
401
|
+
return createErrorResponse("invalid_request", "code_verifier is required for PKCE");
|
|
450
402
|
}
|
|
451
403
|
let calculatedChallenge;
|
|
452
404
|
if (grantData.codeChallengeMethod === "S256") {
|
|
@@ -459,10 +411,7 @@ var OAuthProviderImpl = class {
|
|
|
459
411
|
calculatedChallenge = codeVerifier;
|
|
460
412
|
}
|
|
461
413
|
if (calculatedChallenge !== grantData.codeChallenge) {
|
|
462
|
-
return createErrorResponse(
|
|
463
|
-
"invalid_grant",
|
|
464
|
-
"Invalid PKCE code_verifier"
|
|
465
|
-
);
|
|
414
|
+
return createErrorResponse("invalid_grant", "Invalid PKCE code_verifier");
|
|
466
415
|
}
|
|
467
416
|
}
|
|
468
417
|
const accessTokenSecret = generateRandomString(TOKEN_LENGTH);
|
|
@@ -471,11 +420,53 @@ var OAuthProviderImpl = class {
|
|
|
471
420
|
const refreshToken = `${userId}:${grantId}:${refreshTokenSecret}`;
|
|
472
421
|
const accessTokenId = await generateTokenId(accessToken);
|
|
473
422
|
const refreshTokenId = await generateTokenId(refreshToken);
|
|
474
|
-
|
|
475
|
-
const accessTokenExpiresAt = now + this.options.accessTokenTTL;
|
|
423
|
+
let accessTokenTTL = this.options.accessTokenTTL;
|
|
476
424
|
const encryptionKey = await unwrapKeyWithToken(code, grantData.authCodeWrappedKey);
|
|
477
|
-
|
|
478
|
-
|
|
425
|
+
let grantEncryptionKey = encryptionKey;
|
|
426
|
+
let accessTokenEncryptionKey = encryptionKey;
|
|
427
|
+
let encryptedAccessTokenProps = grantData.encryptedProps;
|
|
428
|
+
if (this.options.tokenExchangeCallback) {
|
|
429
|
+
const decryptedProps = await decryptProps(encryptionKey, grantData.encryptedProps);
|
|
430
|
+
let grantProps = decryptedProps;
|
|
431
|
+
let accessTokenProps = decryptedProps;
|
|
432
|
+
const callbackOptions = {
|
|
433
|
+
grantType: "authorization_code",
|
|
434
|
+
clientId: clientInfo.clientId,
|
|
435
|
+
userId,
|
|
436
|
+
scope: grantData.scope,
|
|
437
|
+
props: decryptedProps
|
|
438
|
+
};
|
|
439
|
+
const callbackResult = await Promise.resolve(this.options.tokenExchangeCallback(callbackOptions));
|
|
440
|
+
if (callbackResult) {
|
|
441
|
+
if (callbackResult.newProps) {
|
|
442
|
+
grantProps = callbackResult.newProps;
|
|
443
|
+
if (!callbackResult.accessTokenProps) {
|
|
444
|
+
accessTokenProps = callbackResult.newProps;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (callbackResult.accessTokenProps) {
|
|
448
|
+
accessTokenProps = callbackResult.accessTokenProps;
|
|
449
|
+
}
|
|
450
|
+
if (callbackResult.accessTokenTTL !== void 0) {
|
|
451
|
+
accessTokenTTL = callbackResult.accessTokenTTL;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
const grantResult = await encryptProps(grantProps);
|
|
455
|
+
grantData.encryptedProps = grantResult.encryptedData;
|
|
456
|
+
grantEncryptionKey = grantResult.key;
|
|
457
|
+
if (accessTokenProps !== grantProps) {
|
|
458
|
+
const tokenResult = await encryptProps(accessTokenProps);
|
|
459
|
+
encryptedAccessTokenProps = tokenResult.encryptedData;
|
|
460
|
+
accessTokenEncryptionKey = tokenResult.key;
|
|
461
|
+
} else {
|
|
462
|
+
encryptedAccessTokenProps = grantData.encryptedProps;
|
|
463
|
+
accessTokenEncryptionKey = grantEncryptionKey;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
467
|
+
const accessTokenExpiresAt = now + accessTokenTTL;
|
|
468
|
+
const accessTokenWrappedKey = await wrapKeyWithToken(accessToken, accessTokenEncryptionKey);
|
|
469
|
+
const refreshTokenWrappedKey = await wrapKeyWithToken(refreshToken, grantEncryptionKey);
|
|
479
470
|
delete grantData.authCodeId;
|
|
480
471
|
delete grantData.codeChallenge;
|
|
481
472
|
delete grantData.codeChallengeMethod;
|
|
@@ -495,23 +486,24 @@ var OAuthProviderImpl = class {
|
|
|
495
486
|
grant: {
|
|
496
487
|
clientId: grantData.clientId,
|
|
497
488
|
scope: grantData.scope,
|
|
498
|
-
encryptedProps:
|
|
489
|
+
encryptedProps: encryptedAccessTokenProps
|
|
499
490
|
}
|
|
500
491
|
};
|
|
501
|
-
await env.OAUTH_KV.put(
|
|
502
|
-
|
|
503
|
-
JSON.stringify(accessTokenData),
|
|
504
|
-
{ expirationTtl: this.options.accessTokenTTL }
|
|
505
|
-
);
|
|
506
|
-
return new Response(JSON.stringify({
|
|
507
|
-
access_token: accessToken,
|
|
508
|
-
token_type: "bearer",
|
|
509
|
-
expires_in: this.options.accessTokenTTL,
|
|
510
|
-
refresh_token: refreshToken,
|
|
511
|
-
scope: grantData.scope.join(" ")
|
|
512
|
-
}), {
|
|
513
|
-
headers: { "Content-Type": "application/json" }
|
|
492
|
+
await env.OAUTH_KV.put(`token:${userId}:${grantId}:${accessTokenId}`, JSON.stringify(accessTokenData), {
|
|
493
|
+
expirationTtl: accessTokenTTL
|
|
514
494
|
});
|
|
495
|
+
return new Response(
|
|
496
|
+
JSON.stringify({
|
|
497
|
+
access_token: accessToken,
|
|
498
|
+
token_type: "bearer",
|
|
499
|
+
expires_in: accessTokenTTL,
|
|
500
|
+
refresh_token: refreshToken,
|
|
501
|
+
scope: grantData.scope.join(" ")
|
|
502
|
+
}),
|
|
503
|
+
{
|
|
504
|
+
headers: { "Content-Type": "application/json" }
|
|
505
|
+
}
|
|
506
|
+
);
|
|
515
507
|
}
|
|
516
508
|
/**
|
|
517
509
|
* Handles the refresh token grant type
|
|
@@ -524,41 +516,26 @@ var OAuthProviderImpl = class {
|
|
|
524
516
|
async handleRefreshTokenGrant(body, clientInfo, env) {
|
|
525
517
|
const refreshToken = body.refresh_token;
|
|
526
518
|
if (!refreshToken) {
|
|
527
|
-
return createErrorResponse(
|
|
528
|
-
"invalid_request",
|
|
529
|
-
"Refresh token is required"
|
|
530
|
-
);
|
|
519
|
+
return createErrorResponse("invalid_request", "Refresh token is required");
|
|
531
520
|
}
|
|
532
521
|
const tokenParts = refreshToken.split(":");
|
|
533
522
|
if (tokenParts.length !== 3) {
|
|
534
|
-
return createErrorResponse(
|
|
535
|
-
"invalid_grant",
|
|
536
|
-
"Invalid token format"
|
|
537
|
-
);
|
|
523
|
+
return createErrorResponse("invalid_grant", "Invalid token format");
|
|
538
524
|
}
|
|
539
525
|
const [userId, grantId, _] = tokenParts;
|
|
540
526
|
const providedTokenHash = await generateTokenId(refreshToken);
|
|
541
527
|
const grantKey = `grant:${userId}:${grantId}`;
|
|
542
528
|
const grantData = await env.OAUTH_KV.get(grantKey, { type: "json" });
|
|
543
529
|
if (!grantData) {
|
|
544
|
-
return createErrorResponse(
|
|
545
|
-
"invalid_grant",
|
|
546
|
-
"Grant not found"
|
|
547
|
-
);
|
|
530
|
+
return createErrorResponse("invalid_grant", "Grant not found");
|
|
548
531
|
}
|
|
549
532
|
const isCurrentToken = grantData.refreshTokenId === providedTokenHash;
|
|
550
533
|
const isPreviousToken = grantData.previousRefreshTokenId === providedTokenHash;
|
|
551
534
|
if (!isCurrentToken && !isPreviousToken) {
|
|
552
|
-
return createErrorResponse(
|
|
553
|
-
"invalid_grant",
|
|
554
|
-
"Invalid refresh token"
|
|
555
|
-
);
|
|
535
|
+
return createErrorResponse("invalid_grant", "Invalid refresh token");
|
|
556
536
|
}
|
|
557
537
|
if (grantData.clientId !== clientInfo.clientId) {
|
|
558
|
-
return createErrorResponse(
|
|
559
|
-
"invalid_grant",
|
|
560
|
-
"Client ID mismatch"
|
|
561
|
-
);
|
|
538
|
+
return createErrorResponse("invalid_grant", "Client ID mismatch");
|
|
562
539
|
}
|
|
563
540
|
const accessTokenSecret = generateRandomString(TOKEN_LENGTH);
|
|
564
541
|
const newAccessToken = `${userId}:${grantId}:${accessTokenSecret}`;
|
|
@@ -566,8 +543,7 @@ var OAuthProviderImpl = class {
|
|
|
566
543
|
const refreshTokenSecret = generateRandomString(TOKEN_LENGTH);
|
|
567
544
|
const newRefreshToken = `${userId}:${grantId}:${refreshTokenSecret}`;
|
|
568
545
|
const newRefreshTokenId = await generateTokenId(newRefreshToken);
|
|
569
|
-
|
|
570
|
-
const accessTokenExpiresAt = now + this.options.accessTokenTTL;
|
|
546
|
+
let accessTokenTTL = this.options.accessTokenTTL;
|
|
571
547
|
let wrappedKeyToUse;
|
|
572
548
|
if (isCurrentToken) {
|
|
573
549
|
wrappedKeyToUse = grantData.refreshTokenWrappedKey;
|
|
@@ -575,8 +551,60 @@ var OAuthProviderImpl = class {
|
|
|
575
551
|
wrappedKeyToUse = grantData.previousRefreshTokenWrappedKey;
|
|
576
552
|
}
|
|
577
553
|
const encryptionKey = await unwrapKeyWithToken(refreshToken, wrappedKeyToUse);
|
|
578
|
-
|
|
579
|
-
|
|
554
|
+
let grantEncryptionKey = encryptionKey;
|
|
555
|
+
let accessTokenEncryptionKey = encryptionKey;
|
|
556
|
+
let encryptedAccessTokenProps = grantData.encryptedProps;
|
|
557
|
+
if (this.options.tokenExchangeCallback) {
|
|
558
|
+
const decryptedProps = await decryptProps(encryptionKey, grantData.encryptedProps);
|
|
559
|
+
let grantProps = decryptedProps;
|
|
560
|
+
let accessTokenProps = decryptedProps;
|
|
561
|
+
const callbackOptions = {
|
|
562
|
+
grantType: "refresh_token",
|
|
563
|
+
clientId: clientInfo.clientId,
|
|
564
|
+
userId,
|
|
565
|
+
scope: grantData.scope,
|
|
566
|
+
props: decryptedProps
|
|
567
|
+
};
|
|
568
|
+
const callbackResult = await Promise.resolve(this.options.tokenExchangeCallback(callbackOptions));
|
|
569
|
+
let grantPropsChanged = false;
|
|
570
|
+
if (callbackResult) {
|
|
571
|
+
if (callbackResult.newProps) {
|
|
572
|
+
grantProps = callbackResult.newProps;
|
|
573
|
+
grantPropsChanged = true;
|
|
574
|
+
if (!callbackResult.accessTokenProps) {
|
|
575
|
+
accessTokenProps = callbackResult.newProps;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
if (callbackResult.accessTokenProps) {
|
|
579
|
+
accessTokenProps = callbackResult.accessTokenProps;
|
|
580
|
+
}
|
|
581
|
+
if (callbackResult.accessTokenTTL !== void 0) {
|
|
582
|
+
accessTokenTTL = callbackResult.accessTokenTTL;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
if (grantPropsChanged) {
|
|
586
|
+
const grantResult = await encryptProps(grantProps);
|
|
587
|
+
grantData.encryptedProps = grantResult.encryptedData;
|
|
588
|
+
if (grantResult.key !== encryptionKey) {
|
|
589
|
+
grantEncryptionKey = grantResult.key;
|
|
590
|
+
wrappedKeyToUse = await wrapKeyWithToken(refreshToken, grantEncryptionKey);
|
|
591
|
+
} else {
|
|
592
|
+
grantEncryptionKey = grantResult.key;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
if (accessTokenProps !== grantProps) {
|
|
596
|
+
const tokenResult = await encryptProps(accessTokenProps);
|
|
597
|
+
encryptedAccessTokenProps = tokenResult.encryptedData;
|
|
598
|
+
accessTokenEncryptionKey = tokenResult.key;
|
|
599
|
+
} else {
|
|
600
|
+
encryptedAccessTokenProps = grantData.encryptedProps;
|
|
601
|
+
accessTokenEncryptionKey = grantEncryptionKey;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
605
|
+
const accessTokenExpiresAt = now + accessTokenTTL;
|
|
606
|
+
const accessTokenWrappedKey = await wrapKeyWithToken(newAccessToken, accessTokenEncryptionKey);
|
|
607
|
+
const newRefreshTokenWrappedKey = await wrapKeyWithToken(newRefreshToken, grantEncryptionKey);
|
|
580
608
|
grantData.previousRefreshTokenId = providedTokenHash;
|
|
581
609
|
grantData.previousRefreshTokenWrappedKey = wrappedKeyToUse;
|
|
582
610
|
grantData.refreshTokenId = newRefreshTokenId;
|
|
@@ -592,23 +620,24 @@ var OAuthProviderImpl = class {
|
|
|
592
620
|
grant: {
|
|
593
621
|
clientId: grantData.clientId,
|
|
594
622
|
scope: grantData.scope,
|
|
595
|
-
encryptedProps:
|
|
623
|
+
encryptedProps: encryptedAccessTokenProps
|
|
596
624
|
}
|
|
597
625
|
};
|
|
598
|
-
await env.OAUTH_KV.put(
|
|
599
|
-
|
|
600
|
-
JSON.stringify(accessTokenData),
|
|
601
|
-
{ expirationTtl: this.options.accessTokenTTL }
|
|
602
|
-
);
|
|
603
|
-
return new Response(JSON.stringify({
|
|
604
|
-
access_token: newAccessToken,
|
|
605
|
-
token_type: "bearer",
|
|
606
|
-
expires_in: this.options.accessTokenTTL,
|
|
607
|
-
refresh_token: newRefreshToken,
|
|
608
|
-
scope: grantData.scope.join(" ")
|
|
609
|
-
}), {
|
|
610
|
-
headers: { "Content-Type": "application/json" }
|
|
626
|
+
await env.OAUTH_KV.put(`token:${userId}:${grantId}:${accessTokenId}`, JSON.stringify(accessTokenData), {
|
|
627
|
+
expirationTtl: accessTokenTTL
|
|
611
628
|
});
|
|
629
|
+
return new Response(
|
|
630
|
+
JSON.stringify({
|
|
631
|
+
access_token: newAccessToken,
|
|
632
|
+
token_type: "bearer",
|
|
633
|
+
expires_in: accessTokenTTL,
|
|
634
|
+
refresh_token: newRefreshToken,
|
|
635
|
+
scope: grantData.scope.join(" ")
|
|
636
|
+
}),
|
|
637
|
+
{
|
|
638
|
+
headers: { "Content-Type": "application/json" }
|
|
639
|
+
}
|
|
640
|
+
);
|
|
612
641
|
}
|
|
613
642
|
/**
|
|
614
643
|
* Handles the dynamic client registration endpoint (RFC 7591)
|
|
@@ -618,44 +647,24 @@ var OAuthProviderImpl = class {
|
|
|
618
647
|
*/
|
|
619
648
|
async handleClientRegistration(request, env) {
|
|
620
649
|
if (!this.options.clientRegistrationEndpoint) {
|
|
621
|
-
return createErrorResponse(
|
|
622
|
-
"not_implemented",
|
|
623
|
-
"Client registration is not enabled",
|
|
624
|
-
501
|
|
625
|
-
);
|
|
650
|
+
return createErrorResponse("not_implemented", "Client registration is not enabled", 501);
|
|
626
651
|
}
|
|
627
652
|
if (request.method !== "POST") {
|
|
628
|
-
return createErrorResponse(
|
|
629
|
-
"invalid_request",
|
|
630
|
-
"Method not allowed",
|
|
631
|
-
405
|
|
632
|
-
);
|
|
653
|
+
return createErrorResponse("invalid_request", "Method not allowed", 405);
|
|
633
654
|
}
|
|
634
655
|
const contentLength = parseInt(request.headers.get("Content-Length") || "0", 10);
|
|
635
656
|
if (contentLength > 1048576) {
|
|
636
|
-
return createErrorResponse(
|
|
637
|
-
"invalid_request",
|
|
638
|
-
"Request payload too large, must be under 1 MiB",
|
|
639
|
-
413
|
|
640
|
-
);
|
|
657
|
+
return createErrorResponse("invalid_request", "Request payload too large, must be under 1 MiB", 413);
|
|
641
658
|
}
|
|
642
659
|
let clientMetadata;
|
|
643
660
|
try {
|
|
644
661
|
const text = await request.text();
|
|
645
662
|
if (text.length > 1048576) {
|
|
646
|
-
return createErrorResponse(
|
|
647
|
-
"invalid_request",
|
|
648
|
-
"Request payload too large, must be under 1 MiB",
|
|
649
|
-
413
|
|
650
|
-
);
|
|
663
|
+
return createErrorResponse("invalid_request", "Request payload too large, must be under 1 MiB", 413);
|
|
651
664
|
}
|
|
652
665
|
clientMetadata = JSON.parse(text);
|
|
653
666
|
} catch (error) {
|
|
654
|
-
return createErrorResponse(
|
|
655
|
-
"invalid_request",
|
|
656
|
-
"Invalid JSON payload",
|
|
657
|
-
400
|
|
658
|
-
);
|
|
667
|
+
return createErrorResponse("invalid_request", "Invalid JSON payload", 400);
|
|
659
668
|
}
|
|
660
669
|
const validateStringField = (field) => {
|
|
661
670
|
if (field === void 0) {
|
|
@@ -683,10 +692,7 @@ var OAuthProviderImpl = class {
|
|
|
683
692
|
const authMethod = validateStringField(clientMetadata.token_endpoint_auth_method) || "client_secret_basic";
|
|
684
693
|
const isPublicClient = authMethod === "none";
|
|
685
694
|
if (isPublicClient && this.options.disallowPublicClientRegistration) {
|
|
686
|
-
return createErrorResponse(
|
|
687
|
-
"invalid_client_metadata",
|
|
688
|
-
"Public client registration is not allowed"
|
|
689
|
-
);
|
|
695
|
+
return createErrorResponse("invalid_client_metadata", "Public client registration is not allowed");
|
|
690
696
|
}
|
|
691
697
|
const clientId = generateRandomString(16);
|
|
692
698
|
let clientSecret;
|
|
@@ -760,49 +766,34 @@ var OAuthProviderImpl = class {
|
|
|
760
766
|
async handleApiRequest(request, env, ctx) {
|
|
761
767
|
const authHeader = request.headers.get("Authorization");
|
|
762
768
|
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
763
|
-
return createErrorResponse(
|
|
764
|
-
"invalid_token",
|
|
765
|
-
|
|
766
|
-
401,
|
|
767
|
-
{ "WWW-Authenticate": 'Bearer realm="OAuth", error="invalid_token", error_description="Missing or invalid access token"' }
|
|
768
|
-
);
|
|
769
|
+
return createErrorResponse("invalid_token", "Missing or invalid access token", 401, {
|
|
770
|
+
"WWW-Authenticate": 'Bearer realm="OAuth", error="invalid_token", error_description="Missing or invalid access token"'
|
|
771
|
+
});
|
|
769
772
|
}
|
|
770
773
|
const accessToken = authHeader.substring(7);
|
|
771
774
|
const tokenParts = accessToken.split(":");
|
|
772
775
|
if (tokenParts.length !== 3) {
|
|
773
|
-
return createErrorResponse(
|
|
774
|
-
"invalid_token"
|
|
775
|
-
|
|
776
|
-
401,
|
|
777
|
-
{ "WWW-Authenticate": 'Bearer realm="OAuth", error="invalid_token"' }
|
|
778
|
-
);
|
|
776
|
+
return createErrorResponse("invalid_token", "Invalid token format", 401, {
|
|
777
|
+
"WWW-Authenticate": 'Bearer realm="OAuth", error="invalid_token"'
|
|
778
|
+
});
|
|
779
779
|
}
|
|
780
780
|
const [userId, grantId, _] = tokenParts;
|
|
781
781
|
const accessTokenId = await generateTokenId(accessToken);
|
|
782
782
|
const tokenKey = `token:${userId}:${grantId}:${accessTokenId}`;
|
|
783
783
|
const tokenData = await env.OAUTH_KV.get(tokenKey, { type: "json" });
|
|
784
784
|
if (!tokenData) {
|
|
785
|
-
return createErrorResponse(
|
|
786
|
-
"invalid_token"
|
|
787
|
-
|
|
788
|
-
401,
|
|
789
|
-
{ "WWW-Authenticate": 'Bearer realm="OAuth", error="invalid_token"' }
|
|
790
|
-
);
|
|
785
|
+
return createErrorResponse("invalid_token", "Invalid access token", 401, {
|
|
786
|
+
"WWW-Authenticate": 'Bearer realm="OAuth", error="invalid_token"'
|
|
787
|
+
});
|
|
791
788
|
}
|
|
792
789
|
const now = Math.floor(Date.now() / 1e3);
|
|
793
790
|
if (tokenData.expiresAt < now) {
|
|
794
|
-
return createErrorResponse(
|
|
795
|
-
"invalid_token"
|
|
796
|
-
|
|
797
|
-
401,
|
|
798
|
-
{ "WWW-Authenticate": 'Bearer realm="OAuth", error="invalid_token"' }
|
|
799
|
-
);
|
|
791
|
+
return createErrorResponse("invalid_token", "Access token expired", 401, {
|
|
792
|
+
"WWW-Authenticate": 'Bearer realm="OAuth", error="invalid_token"'
|
|
793
|
+
});
|
|
800
794
|
}
|
|
801
795
|
const encryptionKey = await unwrapKeyWithToken(accessToken, tokenData.wrappedEncryptionKey);
|
|
802
|
-
const decryptedProps = await decryptProps(
|
|
803
|
-
encryptionKey,
|
|
804
|
-
tokenData.grant.encryptedProps
|
|
805
|
-
);
|
|
796
|
+
const decryptedProps = await decryptProps(encryptionKey, tokenData.grant.encryptedProps);
|
|
806
797
|
ctx.props = decryptedProps;
|
|
807
798
|
if (!env.OAUTH_PROVIDER) {
|
|
808
799
|
env.OAUTH_PROVIDER = this.createOAuthHelpers(env);
|
|
@@ -972,11 +963,7 @@ async function deriveKeyFromToken(tokenStr) {
|
|
|
972
963
|
false,
|
|
973
964
|
["sign"]
|
|
974
965
|
);
|
|
975
|
-
const hmacResult = await crypto.subtle.sign(
|
|
976
|
-
"HMAC",
|
|
977
|
-
hmacKey,
|
|
978
|
-
encoder.encode(tokenStr)
|
|
979
|
-
);
|
|
966
|
+
const hmacResult = await crypto.subtle.sign("HMAC", hmacKey, encoder.encode(tokenStr));
|
|
980
967
|
return await crypto.subtle.importKey(
|
|
981
968
|
"raw",
|
|
982
969
|
hmacResult,
|
|
@@ -988,12 +975,7 @@ async function deriveKeyFromToken(tokenStr) {
|
|
|
988
975
|
}
|
|
989
976
|
async function wrapKeyWithToken(tokenStr, keyToWrap) {
|
|
990
977
|
const wrappingKey = await deriveKeyFromToken(tokenStr);
|
|
991
|
-
const wrappedKeyBuffer = await crypto.subtle.wrapKey(
|
|
992
|
-
"raw",
|
|
993
|
-
keyToWrap,
|
|
994
|
-
wrappingKey,
|
|
995
|
-
{ name: "AES-KW" }
|
|
996
|
-
);
|
|
978
|
+
const wrappedKeyBuffer = await crypto.subtle.wrapKey("raw", keyToWrap, wrappingKey, { name: "AES-KW" });
|
|
997
979
|
return arrayBufferToBase64(wrappedKeyBuffer);
|
|
998
980
|
}
|
|
999
981
|
async function unwrapKeyWithToken(tokenStr, wrappedKeyBase64) {
|
|
@@ -1319,9 +1301,11 @@ var OAuthHelpersImpl = class {
|
|
|
1319
1301
|
}
|
|
1320
1302
|
const result = await this.env.OAUTH_KV.list(listOptions);
|
|
1321
1303
|
if (result.keys.length > 0) {
|
|
1322
|
-
await Promise.all(
|
|
1323
|
-
|
|
1324
|
-
|
|
1304
|
+
await Promise.all(
|
|
1305
|
+
result.keys.map((key) => {
|
|
1306
|
+
return this.env.OAUTH_KV.delete(key.name);
|
|
1307
|
+
})
|
|
1308
|
+
);
|
|
1325
1309
|
}
|
|
1326
1310
|
if (result.list_complete) {
|
|
1327
1311
|
allTokensDeleted = true;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudflare/workers-oauth-provider",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "OAuth provider for Cloudflare Workers",
|
|
5
5
|
"main": "dist/oauth-provider.js",
|
|
6
6
|
"types": "dist/oauth-provider.d.ts",
|
|
@@ -11,19 +11,21 @@
|
|
|
11
11
|
],
|
|
12
12
|
"type": "module",
|
|
13
13
|
"publishConfig": {
|
|
14
|
-
"access": "
|
|
14
|
+
"access": "public"
|
|
15
15
|
},
|
|
16
16
|
"scripts": {
|
|
17
17
|
"build": "tsup",
|
|
18
|
-
"test": "vitest",
|
|
19
|
-
"prepublishOnly": "npm run build"
|
|
18
|
+
"test": "vitest run",
|
|
19
|
+
"prepublishOnly": "npm run build",
|
|
20
|
+
"prettier": "prettier -w ."
|
|
20
21
|
},
|
|
21
22
|
"dependencies": {
|
|
22
23
|
"@cloudflare/workers-types": "^4.20250311.0"
|
|
23
24
|
},
|
|
24
25
|
"devDependencies": {
|
|
26
|
+
"prettier": "^3.5.3",
|
|
25
27
|
"tsup": "^8.4.0",
|
|
26
28
|
"typescript": "^5.8.2",
|
|
27
|
-
"vitest": "^
|
|
29
|
+
"vitest": "^3.0.8"
|
|
28
30
|
}
|
|
29
31
|
}
|