@cloudflare/workers-oauth-provider 0.0.1 → 0.0.3
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 +108 -5
- package/dist/oauth-provider.d.ts +76 -1
- package/dist/oauth-provider.js +215 -220
- package/package.json +9 -5
package/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
This is a TypeScript library that implements the provider side of the OAuth 2.1 protocol with PKCE support. The library is intended to be used on Cloudflare Workers.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Beta
|
|
6
6
|
|
|
7
|
-
As of March, 2025, this library is very new
|
|
7
|
+
As of March, 2025, this library is very new, prerelease software. The API is still subject to change.
|
|
8
8
|
|
|
9
9
|
## Benefits of this library
|
|
10
10
|
|
|
@@ -196,11 +196,100 @@ 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
|
-
##
|
|
199
|
+
## Token Exchange Callback
|
|
200
200
|
|
|
201
|
-
This library
|
|
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
202
|
|
|
203
|
-
|
|
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
|
+
|
|
265
|
+
## Custom Error Responses
|
|
266
|
+
|
|
267
|
+
By using the `onError` option, you can emit notifications or take other actions when an error response was to be emitted:
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
new OAuthProvider({
|
|
271
|
+
// ... other options ...
|
|
272
|
+
onError({ code, description, status, headers }) {
|
|
273
|
+
Sentry.captureMessage(/* ... */)
|
|
274
|
+
}
|
|
275
|
+
})
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
By returning a `Response` you can also override what the OAuthProvider returns to your users:
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
new OAuthProvider({
|
|
282
|
+
// ... other options ...
|
|
283
|
+
onError({ code, description, status, headers }) {
|
|
284
|
+
if (code === 'unsupported_grant_type') {
|
|
285
|
+
return new Response('...', { status, headers })
|
|
286
|
+
}
|
|
287
|
+
// returning undefined (i.e. void) uses the default Response generation
|
|
288
|
+
}
|
|
289
|
+
})
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
By default, the `onError` callback is set to ``({ status, code, description }) => console.warn(`OAuth error response: ${status} ${code} - ${description}`)``.
|
|
204
293
|
|
|
205
294
|
## Implementation Notes
|
|
206
295
|
|
|
@@ -220,3 +309,17 @@ OAuth 2.1 requires that refresh tokens are either "cryptographically bound" to t
|
|
|
220
309
|
This requirement is seemingly fundamentally flawed as it assumes that every refresh request will complete with no errors. In the real world, a transient network error, machine failure, or software fault could mean that the client fails to store the new refresh token after a refresh request. In this case, the client would be permanently unable to make any further requests, as the only token it has is no longer valid.
|
|
221
310
|
|
|
222
311
|
This library implements a compromise: At any particular time, a grant may have two valid refresh tokens. When the client uses one of them, the other one is invalidated, and a new one is generated and returned. Thus, if the client correctly uses the new refresh token each time, then older refresh tokens are continuously invalidated. But if a transient failure prevents the client from updating its token, it can always retry the request with the token it used previously.
|
|
312
|
+
|
|
313
|
+
## Written using Claude
|
|
314
|
+
|
|
315
|
+
This library (including the schema documentation) was largely written with the help of [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.
|
|
316
|
+
|
|
317
|
+
**"NOOOOOOOO!!!! You can't just use an LLM to write an auth library!"**
|
|
318
|
+
|
|
319
|
+
"haha gpus go brrr"
|
|
320
|
+
|
|
321
|
+
In all seriousness, two months ago (January 2025), I ([@kentonv](https://github.com/kentonv)) would have agreed. I was an AI skeptic. I thoughts LLMs were glorified Markov chain generators that didn't actually understand code and couldn't produce anything novel. I started this project on a lark, fully expecting the AI to produce terrible code for me to laugh at. And then, uh... the code actually looked pretty good. Not perfect, but I just told the AI to fix things, and it did. I was shocked.
|
|
322
|
+
|
|
323
|
+
To emphasize, **this is not "vibe coded"**. Every line was thoroughly reviewed and cross-referenced with relevant RFCs, by security experts with previous experience with those RFCs. I was *trying* to validate my skepticism. I ended up proving myself wrong.
|
|
324
|
+
|
|
325
|
+
Again, please check out the commit history -- especially early commits -- to understand how this went.
|
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,28 @@ 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;
|
|
131
|
+
/**
|
|
132
|
+
* Optional callback function that is called whenever the OAuthProvider returns an error response
|
|
133
|
+
* This allows the client to emit notifications or perform other actions when an error occurs.
|
|
134
|
+
*
|
|
135
|
+
* If the function returns a Response, that will be used in place of the OAuthProvider's default one.
|
|
136
|
+
*/
|
|
137
|
+
onError?: (error: {
|
|
138
|
+
code: string;
|
|
139
|
+
description: string;
|
|
140
|
+
status: number;
|
|
141
|
+
headers: Record<string, string>;
|
|
142
|
+
}) => Response | void;
|
|
68
143
|
}
|
|
69
144
|
/**
|
|
70
145
|
* Helper methods for OAuth operations provided to handler functions
|
|
@@ -456,4 +531,4 @@ declare class OAuthProvider {
|
|
|
456
531
|
fetch(request: Request, env: any, ctx: ExecutionContext): Promise<Response>;
|
|
457
532
|
}
|
|
458
533
|
|
|
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 };
|
|
534
|
+
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
|
@@ -52,8 +52,9 @@ var OAuthProviderImpl = class {
|
|
|
52
52
|
this.validateEndpoint(options.clientRegistrationEndpoint, "clientRegistrationEndpoint");
|
|
53
53
|
}
|
|
54
54
|
this.options = {
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
accessTokenTTL: DEFAULT_ACCESS_TOKEN_TTL,
|
|
56
|
+
onError: ({ status, code, description }) => console.warn(`OAuth error response: ${status} ${code} - ${description}`),
|
|
57
|
+
...options
|
|
57
58
|
};
|
|
58
59
|
}
|
|
59
60
|
/**
|
|
@@ -89,7 +90,9 @@ var OAuthProviderImpl = class {
|
|
|
89
90
|
if (typeof handler === "function" && handler.prototype instanceof WorkerEntrypoint) {
|
|
90
91
|
return { type: 1 /* WORKER_ENTRYPOINT */, handler };
|
|
91
92
|
}
|
|
92
|
-
throw new TypeError(
|
|
93
|
+
throw new TypeError(
|
|
94
|
+
`${name} must be either an ExportedHandler object with a fetch method or a class extending WorkerEntrypoint`
|
|
95
|
+
);
|
|
93
96
|
}
|
|
94
97
|
/**
|
|
95
98
|
* Main fetch handler for the Worker
|
|
@@ -132,7 +135,11 @@ var OAuthProviderImpl = class {
|
|
|
132
135
|
env.OAUTH_PROVIDER = this.createOAuthHelpers(env);
|
|
133
136
|
}
|
|
134
137
|
if (this.typedDefaultHandler.type === 0 /* EXPORTED_HANDLER */) {
|
|
135
|
-
return this.typedDefaultHandler.handler.fetch(
|
|
138
|
+
return this.typedDefaultHandler.handler.fetch(
|
|
139
|
+
request,
|
|
140
|
+
env,
|
|
141
|
+
ctx
|
|
142
|
+
);
|
|
136
143
|
} else {
|
|
137
144
|
const handler = new this.typedDefaultHandler.handler(ctx, env);
|
|
138
145
|
return handler.fetch(request);
|
|
@@ -292,20 +299,12 @@ var OAuthProviderImpl = class {
|
|
|
292
299
|
*/
|
|
293
300
|
async handleTokenRequest(request, env) {
|
|
294
301
|
if (request.method !== "POST") {
|
|
295
|
-
return createErrorResponse(
|
|
296
|
-
"invalid_request",
|
|
297
|
-
"Method not allowed",
|
|
298
|
-
405
|
|
299
|
-
);
|
|
302
|
+
return this.createErrorResponse("invalid_request", "Method not allowed", 405);
|
|
300
303
|
}
|
|
301
304
|
let contentType = request.headers.get("Content-Type") || "";
|
|
302
305
|
let body = {};
|
|
303
306
|
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
|
-
);
|
|
307
|
+
return this.createErrorResponse("invalid_request", "Content-Type must be application/x-www-form-urlencoded", 400);
|
|
309
308
|
}
|
|
310
309
|
const formData = await request.formData();
|
|
311
310
|
for (const [key, value] of formData.entries()) {
|
|
@@ -324,31 +323,19 @@ var OAuthProviderImpl = class {
|
|
|
324
323
|
clientSecret = body.client_secret || "";
|
|
325
324
|
}
|
|
326
325
|
if (!clientId) {
|
|
327
|
-
return createErrorResponse(
|
|
328
|
-
"invalid_client",
|
|
329
|
-
"Client ID is required",
|
|
330
|
-
401
|
|
331
|
-
);
|
|
326
|
+
return this.createErrorResponse("invalid_client", "Client ID is required", 401);
|
|
332
327
|
}
|
|
333
328
|
const clientInfo = await this.getClient(env, clientId);
|
|
334
329
|
if (!clientInfo) {
|
|
335
|
-
return createErrorResponse(
|
|
336
|
-
"invalid_client",
|
|
337
|
-
"Client not found",
|
|
338
|
-
401
|
|
339
|
-
);
|
|
330
|
+
return this.createErrorResponse("invalid_client", "Client not found", 401);
|
|
340
331
|
}
|
|
341
332
|
const isPublicClient = clientInfo.tokenEndpointAuthMethod === "none";
|
|
342
333
|
if (!isPublicClient) {
|
|
343
334
|
if (!clientSecret) {
|
|
344
|
-
return createErrorResponse(
|
|
345
|
-
"invalid_client",
|
|
346
|
-
"Client authentication failed: missing client_secret",
|
|
347
|
-
401
|
|
348
|
-
);
|
|
335
|
+
return this.createErrorResponse("invalid_client", "Client authentication failed: missing client_secret", 401);
|
|
349
336
|
}
|
|
350
337
|
if (!clientInfo.clientSecret) {
|
|
351
|
-
return createErrorResponse(
|
|
338
|
+
return this.createErrorResponse(
|
|
352
339
|
"invalid_client",
|
|
353
340
|
"Client authentication failed: client has no registered secret",
|
|
354
341
|
401
|
|
@@ -356,11 +343,7 @@ var OAuthProviderImpl = class {
|
|
|
356
343
|
}
|
|
357
344
|
const providedSecretHash = await hashSecret(clientSecret);
|
|
358
345
|
if (providedSecretHash !== clientInfo.clientSecret) {
|
|
359
|
-
return createErrorResponse(
|
|
360
|
-
"invalid_client",
|
|
361
|
-
"Client authentication failed: invalid client_secret",
|
|
362
|
-
401
|
|
363
|
-
);
|
|
346
|
+
return this.createErrorResponse("invalid_client", "Client authentication failed: invalid client_secret", 401);
|
|
364
347
|
}
|
|
365
348
|
}
|
|
366
349
|
const grantType = body.grant_type;
|
|
@@ -369,10 +352,7 @@ var OAuthProviderImpl = class {
|
|
|
369
352
|
} else if (grantType === "refresh_token") {
|
|
370
353
|
return this.handleRefreshTokenGrant(body, clientInfo, env);
|
|
371
354
|
} else {
|
|
372
|
-
return createErrorResponse(
|
|
373
|
-
"unsupported_grant_type",
|
|
374
|
-
"Grant type not supported"
|
|
375
|
-
);
|
|
355
|
+
return this.createErrorResponse("unsupported_grant_type", "Grant type not supported");
|
|
376
356
|
}
|
|
377
357
|
}
|
|
378
358
|
/**
|
|
@@ -388,65 +368,38 @@ var OAuthProviderImpl = class {
|
|
|
388
368
|
const redirectUri = body.redirect_uri;
|
|
389
369
|
const codeVerifier = body.code_verifier;
|
|
390
370
|
if (!code) {
|
|
391
|
-
return createErrorResponse(
|
|
392
|
-
"invalid_request",
|
|
393
|
-
"Authorization code is required"
|
|
394
|
-
);
|
|
371
|
+
return this.createErrorResponse("invalid_request", "Authorization code is required");
|
|
395
372
|
}
|
|
396
373
|
const codeParts = code.split(":");
|
|
397
374
|
if (codeParts.length !== 3) {
|
|
398
|
-
return createErrorResponse(
|
|
399
|
-
"invalid_grant",
|
|
400
|
-
"Invalid authorization code format"
|
|
401
|
-
);
|
|
375
|
+
return this.createErrorResponse("invalid_grant", "Invalid authorization code format");
|
|
402
376
|
}
|
|
403
377
|
const [userId, grantId, _] = codeParts;
|
|
404
378
|
const grantKey = `grant:${userId}:${grantId}`;
|
|
405
379
|
const grantData = await env.OAUTH_KV.get(grantKey, { type: "json" });
|
|
406
380
|
if (!grantData) {
|
|
407
|
-
return createErrorResponse(
|
|
408
|
-
"invalid_grant",
|
|
409
|
-
"Grant not found or authorization code expired"
|
|
410
|
-
);
|
|
381
|
+
return this.createErrorResponse("invalid_grant", "Grant not found or authorization code expired");
|
|
411
382
|
}
|
|
412
383
|
if (!grantData.authCodeId) {
|
|
413
|
-
return createErrorResponse(
|
|
414
|
-
"invalid_grant",
|
|
415
|
-
"Authorization code already used"
|
|
416
|
-
);
|
|
384
|
+
return this.createErrorResponse("invalid_grant", "Authorization code already used");
|
|
417
385
|
}
|
|
418
386
|
const codeHash = await hashSecret(code);
|
|
419
387
|
if (codeHash !== grantData.authCodeId) {
|
|
420
|
-
return createErrorResponse(
|
|
421
|
-
"invalid_grant",
|
|
422
|
-
"Invalid authorization code"
|
|
423
|
-
);
|
|
388
|
+
return this.createErrorResponse("invalid_grant", "Invalid authorization code");
|
|
424
389
|
}
|
|
425
390
|
if (grantData.clientId !== clientInfo.clientId) {
|
|
426
|
-
return createErrorResponse(
|
|
427
|
-
"invalid_grant",
|
|
428
|
-
"Client ID mismatch"
|
|
429
|
-
);
|
|
391
|
+
return this.createErrorResponse("invalid_grant", "Client ID mismatch");
|
|
430
392
|
}
|
|
431
393
|
const isPkceEnabled = !!grantData.codeChallenge;
|
|
432
394
|
if (!redirectUri && !isPkceEnabled) {
|
|
433
|
-
return createErrorResponse(
|
|
434
|
-
"invalid_request",
|
|
435
|
-
"redirect_uri is required when not using PKCE"
|
|
436
|
-
);
|
|
395
|
+
return this.createErrorResponse("invalid_request", "redirect_uri is required when not using PKCE");
|
|
437
396
|
}
|
|
438
397
|
if (redirectUri && !clientInfo.redirectUris.includes(redirectUri)) {
|
|
439
|
-
return createErrorResponse(
|
|
440
|
-
"invalid_grant",
|
|
441
|
-
"Invalid redirect URI"
|
|
442
|
-
);
|
|
398
|
+
return this.createErrorResponse("invalid_grant", "Invalid redirect URI");
|
|
443
399
|
}
|
|
444
400
|
if (isPkceEnabled) {
|
|
445
401
|
if (!codeVerifier) {
|
|
446
|
-
return createErrorResponse(
|
|
447
|
-
"invalid_request",
|
|
448
|
-
"code_verifier is required for PKCE"
|
|
449
|
-
);
|
|
402
|
+
return this.createErrorResponse("invalid_request", "code_verifier is required for PKCE");
|
|
450
403
|
}
|
|
451
404
|
let calculatedChallenge;
|
|
452
405
|
if (grantData.codeChallengeMethod === "S256") {
|
|
@@ -459,10 +412,7 @@ var OAuthProviderImpl = class {
|
|
|
459
412
|
calculatedChallenge = codeVerifier;
|
|
460
413
|
}
|
|
461
414
|
if (calculatedChallenge !== grantData.codeChallenge) {
|
|
462
|
-
return createErrorResponse(
|
|
463
|
-
"invalid_grant",
|
|
464
|
-
"Invalid PKCE code_verifier"
|
|
465
|
-
);
|
|
415
|
+
return this.createErrorResponse("invalid_grant", "Invalid PKCE code_verifier");
|
|
466
416
|
}
|
|
467
417
|
}
|
|
468
418
|
const accessTokenSecret = generateRandomString(TOKEN_LENGTH);
|
|
@@ -471,11 +421,53 @@ var OAuthProviderImpl = class {
|
|
|
471
421
|
const refreshToken = `${userId}:${grantId}:${refreshTokenSecret}`;
|
|
472
422
|
const accessTokenId = await generateTokenId(accessToken);
|
|
473
423
|
const refreshTokenId = await generateTokenId(refreshToken);
|
|
474
|
-
|
|
475
|
-
const accessTokenExpiresAt = now + this.options.accessTokenTTL;
|
|
424
|
+
let accessTokenTTL = this.options.accessTokenTTL;
|
|
476
425
|
const encryptionKey = await unwrapKeyWithToken(code, grantData.authCodeWrappedKey);
|
|
477
|
-
|
|
478
|
-
|
|
426
|
+
let grantEncryptionKey = encryptionKey;
|
|
427
|
+
let accessTokenEncryptionKey = encryptionKey;
|
|
428
|
+
let encryptedAccessTokenProps = grantData.encryptedProps;
|
|
429
|
+
if (this.options.tokenExchangeCallback) {
|
|
430
|
+
const decryptedProps = await decryptProps(encryptionKey, grantData.encryptedProps);
|
|
431
|
+
let grantProps = decryptedProps;
|
|
432
|
+
let accessTokenProps = decryptedProps;
|
|
433
|
+
const callbackOptions = {
|
|
434
|
+
grantType: "authorization_code",
|
|
435
|
+
clientId: clientInfo.clientId,
|
|
436
|
+
userId,
|
|
437
|
+
scope: grantData.scope,
|
|
438
|
+
props: decryptedProps
|
|
439
|
+
};
|
|
440
|
+
const callbackResult = await Promise.resolve(this.options.tokenExchangeCallback(callbackOptions));
|
|
441
|
+
if (callbackResult) {
|
|
442
|
+
if (callbackResult.newProps) {
|
|
443
|
+
grantProps = callbackResult.newProps;
|
|
444
|
+
if (!callbackResult.accessTokenProps) {
|
|
445
|
+
accessTokenProps = callbackResult.newProps;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
if (callbackResult.accessTokenProps) {
|
|
449
|
+
accessTokenProps = callbackResult.accessTokenProps;
|
|
450
|
+
}
|
|
451
|
+
if (callbackResult.accessTokenTTL !== void 0) {
|
|
452
|
+
accessTokenTTL = callbackResult.accessTokenTTL;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
const grantResult = await encryptProps(grantProps);
|
|
456
|
+
grantData.encryptedProps = grantResult.encryptedData;
|
|
457
|
+
grantEncryptionKey = grantResult.key;
|
|
458
|
+
if (accessTokenProps !== grantProps) {
|
|
459
|
+
const tokenResult = await encryptProps(accessTokenProps);
|
|
460
|
+
encryptedAccessTokenProps = tokenResult.encryptedData;
|
|
461
|
+
accessTokenEncryptionKey = tokenResult.key;
|
|
462
|
+
} else {
|
|
463
|
+
encryptedAccessTokenProps = grantData.encryptedProps;
|
|
464
|
+
accessTokenEncryptionKey = grantEncryptionKey;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
468
|
+
const accessTokenExpiresAt = now + accessTokenTTL;
|
|
469
|
+
const accessTokenWrappedKey = await wrapKeyWithToken(accessToken, accessTokenEncryptionKey);
|
|
470
|
+
const refreshTokenWrappedKey = await wrapKeyWithToken(refreshToken, grantEncryptionKey);
|
|
479
471
|
delete grantData.authCodeId;
|
|
480
472
|
delete grantData.codeChallenge;
|
|
481
473
|
delete grantData.codeChallengeMethod;
|
|
@@ -495,23 +487,24 @@ var OAuthProviderImpl = class {
|
|
|
495
487
|
grant: {
|
|
496
488
|
clientId: grantData.clientId,
|
|
497
489
|
scope: grantData.scope,
|
|
498
|
-
encryptedProps:
|
|
490
|
+
encryptedProps: encryptedAccessTokenProps
|
|
499
491
|
}
|
|
500
492
|
};
|
|
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" }
|
|
493
|
+
await env.OAUTH_KV.put(`token:${userId}:${grantId}:${accessTokenId}`, JSON.stringify(accessTokenData), {
|
|
494
|
+
expirationTtl: accessTokenTTL
|
|
514
495
|
});
|
|
496
|
+
return new Response(
|
|
497
|
+
JSON.stringify({
|
|
498
|
+
access_token: accessToken,
|
|
499
|
+
token_type: "bearer",
|
|
500
|
+
expires_in: accessTokenTTL,
|
|
501
|
+
refresh_token: refreshToken,
|
|
502
|
+
scope: grantData.scope.join(" ")
|
|
503
|
+
}),
|
|
504
|
+
{
|
|
505
|
+
headers: { "Content-Type": "application/json" }
|
|
506
|
+
}
|
|
507
|
+
);
|
|
515
508
|
}
|
|
516
509
|
/**
|
|
517
510
|
* Handles the refresh token grant type
|
|
@@ -524,41 +517,26 @@ var OAuthProviderImpl = class {
|
|
|
524
517
|
async handleRefreshTokenGrant(body, clientInfo, env) {
|
|
525
518
|
const refreshToken = body.refresh_token;
|
|
526
519
|
if (!refreshToken) {
|
|
527
|
-
return createErrorResponse(
|
|
528
|
-
"invalid_request",
|
|
529
|
-
"Refresh token is required"
|
|
530
|
-
);
|
|
520
|
+
return this.createErrorResponse("invalid_request", "Refresh token is required");
|
|
531
521
|
}
|
|
532
522
|
const tokenParts = refreshToken.split(":");
|
|
533
523
|
if (tokenParts.length !== 3) {
|
|
534
|
-
return createErrorResponse(
|
|
535
|
-
"invalid_grant",
|
|
536
|
-
"Invalid token format"
|
|
537
|
-
);
|
|
524
|
+
return this.createErrorResponse("invalid_grant", "Invalid token format");
|
|
538
525
|
}
|
|
539
526
|
const [userId, grantId, _] = tokenParts;
|
|
540
527
|
const providedTokenHash = await generateTokenId(refreshToken);
|
|
541
528
|
const grantKey = `grant:${userId}:${grantId}`;
|
|
542
529
|
const grantData = await env.OAUTH_KV.get(grantKey, { type: "json" });
|
|
543
530
|
if (!grantData) {
|
|
544
|
-
return createErrorResponse(
|
|
545
|
-
"invalid_grant",
|
|
546
|
-
"Grant not found"
|
|
547
|
-
);
|
|
531
|
+
return this.createErrorResponse("invalid_grant", "Grant not found");
|
|
548
532
|
}
|
|
549
533
|
const isCurrentToken = grantData.refreshTokenId === providedTokenHash;
|
|
550
534
|
const isPreviousToken = grantData.previousRefreshTokenId === providedTokenHash;
|
|
551
535
|
if (!isCurrentToken && !isPreviousToken) {
|
|
552
|
-
return createErrorResponse(
|
|
553
|
-
"invalid_grant",
|
|
554
|
-
"Invalid refresh token"
|
|
555
|
-
);
|
|
536
|
+
return this.createErrorResponse("invalid_grant", "Invalid refresh token");
|
|
556
537
|
}
|
|
557
538
|
if (grantData.clientId !== clientInfo.clientId) {
|
|
558
|
-
return createErrorResponse(
|
|
559
|
-
"invalid_grant",
|
|
560
|
-
"Client ID mismatch"
|
|
561
|
-
);
|
|
539
|
+
return this.createErrorResponse("invalid_grant", "Client ID mismatch");
|
|
562
540
|
}
|
|
563
541
|
const accessTokenSecret = generateRandomString(TOKEN_LENGTH);
|
|
564
542
|
const newAccessToken = `${userId}:${grantId}:${accessTokenSecret}`;
|
|
@@ -566,8 +544,7 @@ var OAuthProviderImpl = class {
|
|
|
566
544
|
const refreshTokenSecret = generateRandomString(TOKEN_LENGTH);
|
|
567
545
|
const newRefreshToken = `${userId}:${grantId}:${refreshTokenSecret}`;
|
|
568
546
|
const newRefreshTokenId = await generateTokenId(newRefreshToken);
|
|
569
|
-
|
|
570
|
-
const accessTokenExpiresAt = now + this.options.accessTokenTTL;
|
|
547
|
+
let accessTokenTTL = this.options.accessTokenTTL;
|
|
571
548
|
let wrappedKeyToUse;
|
|
572
549
|
if (isCurrentToken) {
|
|
573
550
|
wrappedKeyToUse = grantData.refreshTokenWrappedKey;
|
|
@@ -575,8 +552,60 @@ var OAuthProviderImpl = class {
|
|
|
575
552
|
wrappedKeyToUse = grantData.previousRefreshTokenWrappedKey;
|
|
576
553
|
}
|
|
577
554
|
const encryptionKey = await unwrapKeyWithToken(refreshToken, wrappedKeyToUse);
|
|
578
|
-
|
|
579
|
-
|
|
555
|
+
let grantEncryptionKey = encryptionKey;
|
|
556
|
+
let accessTokenEncryptionKey = encryptionKey;
|
|
557
|
+
let encryptedAccessTokenProps = grantData.encryptedProps;
|
|
558
|
+
if (this.options.tokenExchangeCallback) {
|
|
559
|
+
const decryptedProps = await decryptProps(encryptionKey, grantData.encryptedProps);
|
|
560
|
+
let grantProps = decryptedProps;
|
|
561
|
+
let accessTokenProps = decryptedProps;
|
|
562
|
+
const callbackOptions = {
|
|
563
|
+
grantType: "refresh_token",
|
|
564
|
+
clientId: clientInfo.clientId,
|
|
565
|
+
userId,
|
|
566
|
+
scope: grantData.scope,
|
|
567
|
+
props: decryptedProps
|
|
568
|
+
};
|
|
569
|
+
const callbackResult = await Promise.resolve(this.options.tokenExchangeCallback(callbackOptions));
|
|
570
|
+
let grantPropsChanged = false;
|
|
571
|
+
if (callbackResult) {
|
|
572
|
+
if (callbackResult.newProps) {
|
|
573
|
+
grantProps = callbackResult.newProps;
|
|
574
|
+
grantPropsChanged = true;
|
|
575
|
+
if (!callbackResult.accessTokenProps) {
|
|
576
|
+
accessTokenProps = callbackResult.newProps;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
if (callbackResult.accessTokenProps) {
|
|
580
|
+
accessTokenProps = callbackResult.accessTokenProps;
|
|
581
|
+
}
|
|
582
|
+
if (callbackResult.accessTokenTTL !== void 0) {
|
|
583
|
+
accessTokenTTL = callbackResult.accessTokenTTL;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
if (grantPropsChanged) {
|
|
587
|
+
const grantResult = await encryptProps(grantProps);
|
|
588
|
+
grantData.encryptedProps = grantResult.encryptedData;
|
|
589
|
+
if (grantResult.key !== encryptionKey) {
|
|
590
|
+
grantEncryptionKey = grantResult.key;
|
|
591
|
+
wrappedKeyToUse = await wrapKeyWithToken(refreshToken, grantEncryptionKey);
|
|
592
|
+
} else {
|
|
593
|
+
grantEncryptionKey = grantResult.key;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
if (accessTokenProps !== grantProps) {
|
|
597
|
+
const tokenResult = await encryptProps(accessTokenProps);
|
|
598
|
+
encryptedAccessTokenProps = tokenResult.encryptedData;
|
|
599
|
+
accessTokenEncryptionKey = tokenResult.key;
|
|
600
|
+
} else {
|
|
601
|
+
encryptedAccessTokenProps = grantData.encryptedProps;
|
|
602
|
+
accessTokenEncryptionKey = grantEncryptionKey;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
606
|
+
const accessTokenExpiresAt = now + accessTokenTTL;
|
|
607
|
+
const accessTokenWrappedKey = await wrapKeyWithToken(newAccessToken, accessTokenEncryptionKey);
|
|
608
|
+
const newRefreshTokenWrappedKey = await wrapKeyWithToken(newRefreshToken, grantEncryptionKey);
|
|
580
609
|
grantData.previousRefreshTokenId = providedTokenHash;
|
|
581
610
|
grantData.previousRefreshTokenWrappedKey = wrappedKeyToUse;
|
|
582
611
|
grantData.refreshTokenId = newRefreshTokenId;
|
|
@@ -592,23 +621,24 @@ var OAuthProviderImpl = class {
|
|
|
592
621
|
grant: {
|
|
593
622
|
clientId: grantData.clientId,
|
|
594
623
|
scope: grantData.scope,
|
|
595
|
-
encryptedProps:
|
|
624
|
+
encryptedProps: encryptedAccessTokenProps
|
|
596
625
|
}
|
|
597
626
|
};
|
|
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" }
|
|
627
|
+
await env.OAUTH_KV.put(`token:${userId}:${grantId}:${accessTokenId}`, JSON.stringify(accessTokenData), {
|
|
628
|
+
expirationTtl: accessTokenTTL
|
|
611
629
|
});
|
|
630
|
+
return new Response(
|
|
631
|
+
JSON.stringify({
|
|
632
|
+
access_token: newAccessToken,
|
|
633
|
+
token_type: "bearer",
|
|
634
|
+
expires_in: accessTokenTTL,
|
|
635
|
+
refresh_token: newRefreshToken,
|
|
636
|
+
scope: grantData.scope.join(" ")
|
|
637
|
+
}),
|
|
638
|
+
{
|
|
639
|
+
headers: { "Content-Type": "application/json" }
|
|
640
|
+
}
|
|
641
|
+
);
|
|
612
642
|
}
|
|
613
643
|
/**
|
|
614
644
|
* Handles the dynamic client registration endpoint (RFC 7591)
|
|
@@ -618,44 +648,24 @@ var OAuthProviderImpl = class {
|
|
|
618
648
|
*/
|
|
619
649
|
async handleClientRegistration(request, env) {
|
|
620
650
|
if (!this.options.clientRegistrationEndpoint) {
|
|
621
|
-
return createErrorResponse(
|
|
622
|
-
"not_implemented",
|
|
623
|
-
"Client registration is not enabled",
|
|
624
|
-
501
|
|
625
|
-
);
|
|
651
|
+
return this.createErrorResponse("not_implemented", "Client registration is not enabled", 501);
|
|
626
652
|
}
|
|
627
653
|
if (request.method !== "POST") {
|
|
628
|
-
return createErrorResponse(
|
|
629
|
-
"invalid_request",
|
|
630
|
-
"Method not allowed",
|
|
631
|
-
405
|
|
632
|
-
);
|
|
654
|
+
return this.createErrorResponse("invalid_request", "Method not allowed", 405);
|
|
633
655
|
}
|
|
634
656
|
const contentLength = parseInt(request.headers.get("Content-Length") || "0", 10);
|
|
635
657
|
if (contentLength > 1048576) {
|
|
636
|
-
return createErrorResponse(
|
|
637
|
-
"invalid_request",
|
|
638
|
-
"Request payload too large, must be under 1 MiB",
|
|
639
|
-
413
|
|
640
|
-
);
|
|
658
|
+
return this.createErrorResponse("invalid_request", "Request payload too large, must be under 1 MiB", 413);
|
|
641
659
|
}
|
|
642
660
|
let clientMetadata;
|
|
643
661
|
try {
|
|
644
662
|
const text = await request.text();
|
|
645
663
|
if (text.length > 1048576) {
|
|
646
|
-
return createErrorResponse(
|
|
647
|
-
"invalid_request",
|
|
648
|
-
"Request payload too large, must be under 1 MiB",
|
|
649
|
-
413
|
|
650
|
-
);
|
|
664
|
+
return this.createErrorResponse("invalid_request", "Request payload too large, must be under 1 MiB", 413);
|
|
651
665
|
}
|
|
652
666
|
clientMetadata = JSON.parse(text);
|
|
653
667
|
} catch (error) {
|
|
654
|
-
return createErrorResponse(
|
|
655
|
-
"invalid_request",
|
|
656
|
-
"Invalid JSON payload",
|
|
657
|
-
400
|
|
658
|
-
);
|
|
668
|
+
return this.createErrorResponse("invalid_request", "Invalid JSON payload", 400);
|
|
659
669
|
}
|
|
660
670
|
const validateStringField = (field) => {
|
|
661
671
|
if (field === void 0) {
|
|
@@ -683,10 +693,7 @@ var OAuthProviderImpl = class {
|
|
|
683
693
|
const authMethod = validateStringField(clientMetadata.token_endpoint_auth_method) || "client_secret_basic";
|
|
684
694
|
const isPublicClient = authMethod === "none";
|
|
685
695
|
if (isPublicClient && this.options.disallowPublicClientRegistration) {
|
|
686
|
-
return createErrorResponse(
|
|
687
|
-
"invalid_client_metadata",
|
|
688
|
-
"Public client registration is not allowed"
|
|
689
|
-
);
|
|
696
|
+
return this.createErrorResponse("invalid_client_metadata", "Public client registration is not allowed");
|
|
690
697
|
}
|
|
691
698
|
const clientId = generateRandomString(16);
|
|
692
699
|
let clientSecret;
|
|
@@ -720,7 +727,7 @@ var OAuthProviderImpl = class {
|
|
|
720
727
|
clientInfo.clientSecret = hashedSecret;
|
|
721
728
|
}
|
|
722
729
|
} catch (error) {
|
|
723
|
-
return createErrorResponse(
|
|
730
|
+
return this.createErrorResponse(
|
|
724
731
|
"invalid_client_metadata",
|
|
725
732
|
error instanceof Error ? error.message : "Invalid client metadata"
|
|
726
733
|
);
|
|
@@ -760,49 +767,34 @@ var OAuthProviderImpl = class {
|
|
|
760
767
|
async handleApiRequest(request, env, ctx) {
|
|
761
768
|
const authHeader = request.headers.get("Authorization");
|
|
762
769
|
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
|
-
);
|
|
770
|
+
return this.createErrorResponse("invalid_token", "Missing or invalid access token", 401, {
|
|
771
|
+
"WWW-Authenticate": 'Bearer realm="OAuth", error="invalid_token", error_description="Missing or invalid access token"'
|
|
772
|
+
});
|
|
769
773
|
}
|
|
770
774
|
const accessToken = authHeader.substring(7);
|
|
771
775
|
const tokenParts = accessToken.split(":");
|
|
772
776
|
if (tokenParts.length !== 3) {
|
|
773
|
-
return createErrorResponse(
|
|
774
|
-
"invalid_token"
|
|
775
|
-
|
|
776
|
-
401,
|
|
777
|
-
{ "WWW-Authenticate": 'Bearer realm="OAuth", error="invalid_token"' }
|
|
778
|
-
);
|
|
777
|
+
return this.createErrorResponse("invalid_token", "Invalid token format", 401, {
|
|
778
|
+
"WWW-Authenticate": 'Bearer realm="OAuth", error="invalid_token"'
|
|
779
|
+
});
|
|
779
780
|
}
|
|
780
781
|
const [userId, grantId, _] = tokenParts;
|
|
781
782
|
const accessTokenId = await generateTokenId(accessToken);
|
|
782
783
|
const tokenKey = `token:${userId}:${grantId}:${accessTokenId}`;
|
|
783
784
|
const tokenData = await env.OAUTH_KV.get(tokenKey, { type: "json" });
|
|
784
785
|
if (!tokenData) {
|
|
785
|
-
return createErrorResponse(
|
|
786
|
-
"invalid_token"
|
|
787
|
-
|
|
788
|
-
401,
|
|
789
|
-
{ "WWW-Authenticate": 'Bearer realm="OAuth", error="invalid_token"' }
|
|
790
|
-
);
|
|
786
|
+
return this.createErrorResponse("invalid_token", "Invalid access token", 401, {
|
|
787
|
+
"WWW-Authenticate": 'Bearer realm="OAuth", error="invalid_token"'
|
|
788
|
+
});
|
|
791
789
|
}
|
|
792
790
|
const now = Math.floor(Date.now() / 1e3);
|
|
793
791
|
if (tokenData.expiresAt < now) {
|
|
794
|
-
return createErrorResponse(
|
|
795
|
-
"invalid_token"
|
|
796
|
-
|
|
797
|
-
401,
|
|
798
|
-
{ "WWW-Authenticate": 'Bearer realm="OAuth", error="invalid_token"' }
|
|
799
|
-
);
|
|
792
|
+
return this.createErrorResponse("invalid_token", "Access token expired", 401, {
|
|
793
|
+
"WWW-Authenticate": 'Bearer realm="OAuth", error="invalid_token"'
|
|
794
|
+
});
|
|
800
795
|
}
|
|
801
796
|
const encryptionKey = await unwrapKeyWithToken(accessToken, tokenData.wrappedEncryptionKey);
|
|
802
|
-
const decryptedProps = await decryptProps(
|
|
803
|
-
encryptionKey,
|
|
804
|
-
tokenData.grant.encryptedProps
|
|
805
|
-
);
|
|
797
|
+
const decryptedProps = await decryptProps(encryptionKey, tokenData.grant.encryptedProps);
|
|
806
798
|
ctx.props = decryptedProps;
|
|
807
799
|
if (!env.OAUTH_PROVIDER) {
|
|
808
800
|
env.OAUTH_PROVIDER = this.createOAuthHelpers(env);
|
|
@@ -836,22 +828,32 @@ var OAuthProviderImpl = class {
|
|
|
836
828
|
const clientKey = `client:${clientId}`;
|
|
837
829
|
return env.OAUTH_KV.get(clientKey, { type: "json" });
|
|
838
830
|
}
|
|
831
|
+
/**
|
|
832
|
+
* Helper function to create OAuth error responses
|
|
833
|
+
* @param code - OAuth error code (e.g., 'invalid_request', 'invalid_token')
|
|
834
|
+
* @param description - Human-readable error description
|
|
835
|
+
* @param status - HTTP status code (default: 400)
|
|
836
|
+
* @param headers - Additional headers to include
|
|
837
|
+
* @returns A Response object with the error
|
|
838
|
+
*/
|
|
839
|
+
createErrorResponse(code, description, status = 400, headers = {}) {
|
|
840
|
+
const customErrorResponse = this.options.onError?.({ code, description, status, headers });
|
|
841
|
+
if (customErrorResponse) return customErrorResponse;
|
|
842
|
+
const body = JSON.stringify({
|
|
843
|
+
error: code,
|
|
844
|
+
error_description: description
|
|
845
|
+
});
|
|
846
|
+
return new Response(body, {
|
|
847
|
+
status,
|
|
848
|
+
headers: {
|
|
849
|
+
"Content-Type": "application/json",
|
|
850
|
+
...headers
|
|
851
|
+
}
|
|
852
|
+
});
|
|
853
|
+
}
|
|
839
854
|
};
|
|
840
855
|
var DEFAULT_ACCESS_TOKEN_TTL = 60 * 60;
|
|
841
856
|
var TOKEN_LENGTH = 32;
|
|
842
|
-
function createErrorResponse(code, description, status = 400, headers = {}) {
|
|
843
|
-
const body = JSON.stringify({
|
|
844
|
-
error: code,
|
|
845
|
-
error_description: description
|
|
846
|
-
});
|
|
847
|
-
return new Response(body, {
|
|
848
|
-
status,
|
|
849
|
-
headers: {
|
|
850
|
-
"Content-Type": "application/json",
|
|
851
|
-
...headers
|
|
852
|
-
}
|
|
853
|
-
});
|
|
854
|
-
}
|
|
855
857
|
async function hashSecret(secret) {
|
|
856
858
|
return generateTokenId(secret);
|
|
857
859
|
}
|
|
@@ -972,11 +974,7 @@ async function deriveKeyFromToken(tokenStr) {
|
|
|
972
974
|
false,
|
|
973
975
|
["sign"]
|
|
974
976
|
);
|
|
975
|
-
const hmacResult = await crypto.subtle.sign(
|
|
976
|
-
"HMAC",
|
|
977
|
-
hmacKey,
|
|
978
|
-
encoder.encode(tokenStr)
|
|
979
|
-
);
|
|
977
|
+
const hmacResult = await crypto.subtle.sign("HMAC", hmacKey, encoder.encode(tokenStr));
|
|
980
978
|
return await crypto.subtle.importKey(
|
|
981
979
|
"raw",
|
|
982
980
|
hmacResult,
|
|
@@ -988,12 +986,7 @@ async function deriveKeyFromToken(tokenStr) {
|
|
|
988
986
|
}
|
|
989
987
|
async function wrapKeyWithToken(tokenStr, keyToWrap) {
|
|
990
988
|
const wrappingKey = await deriveKeyFromToken(tokenStr);
|
|
991
|
-
const wrappedKeyBuffer = await crypto.subtle.wrapKey(
|
|
992
|
-
"raw",
|
|
993
|
-
keyToWrap,
|
|
994
|
-
wrappingKey,
|
|
995
|
-
{ name: "AES-KW" }
|
|
996
|
-
);
|
|
989
|
+
const wrappedKeyBuffer = await crypto.subtle.wrapKey("raw", keyToWrap, wrappingKey, { name: "AES-KW" });
|
|
997
990
|
return arrayBufferToBase64(wrappedKeyBuffer);
|
|
998
991
|
}
|
|
999
992
|
async function unwrapKeyWithToken(tokenStr, wrappedKeyBase64) {
|
|
@@ -1319,9 +1312,11 @@ var OAuthHelpersImpl = class {
|
|
|
1319
1312
|
}
|
|
1320
1313
|
const result = await this.env.OAUTH_KV.list(listOptions);
|
|
1321
1314
|
if (result.keys.length > 0) {
|
|
1322
|
-
await Promise.all(
|
|
1323
|
-
|
|
1324
|
-
|
|
1315
|
+
await Promise.all(
|
|
1316
|
+
result.keys.map((key) => {
|
|
1317
|
+
return this.env.OAUTH_KV.delete(key.name);
|
|
1318
|
+
})
|
|
1319
|
+
);
|
|
1325
1320
|
}
|
|
1326
1321
|
if (result.list_complete) {
|
|
1327
1322
|
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.3",
|
|
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,23 @@
|
|
|
11
11
|
],
|
|
12
12
|
"type": "module",
|
|
13
13
|
"publishConfig": {
|
|
14
|
-
"access": "
|
|
14
|
+
"access": "public"
|
|
15
15
|
},
|
|
16
16
|
"scripts": {
|
|
17
17
|
"build": "tsup",
|
|
18
|
-
"
|
|
19
|
-
"
|
|
18
|
+
"build:watch": "tsup --watch",
|
|
19
|
+
"test": "vitest run",
|
|
20
|
+
"test:watch": "vitest",
|
|
21
|
+
"prepublishOnly": "npm run build",
|
|
22
|
+
"prettier": "prettier -w ."
|
|
20
23
|
},
|
|
21
24
|
"dependencies": {
|
|
22
25
|
"@cloudflare/workers-types": "^4.20250311.0"
|
|
23
26
|
},
|
|
24
27
|
"devDependencies": {
|
|
28
|
+
"prettier": "^3.5.3",
|
|
25
29
|
"tsup": "^8.4.0",
|
|
26
30
|
"typescript": "^5.8.2",
|
|
27
|
-
"vitest": "^
|
|
31
|
+
"vitest": "^3.0.8"
|
|
28
32
|
}
|
|
29
33
|
}
|