@cloudflare/workers-oauth-provider 0.0.13 → 0.2.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 +65 -46
- package/dist/oauth-provider.d.ts +563 -507
- package/dist/oauth-provider.js +1549 -1547
- package/package.json +11 -11
package/dist/oauth-provider.js
CHANGED
|
@@ -1,1579 +1,1581 @@
|
|
|
1
|
-
var __typeError = (msg) => {
|
|
2
|
-
throw TypeError(msg);
|
|
3
|
-
};
|
|
4
|
-
var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
|
|
5
|
-
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
|
|
6
|
-
var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
|
|
7
|
-
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
|
|
8
|
-
|
|
9
|
-
// src/oauth-provider.ts
|
|
10
1
|
import { WorkerEntrypoint } from "cloudflare:workers";
|
|
11
|
-
|
|
2
|
+
|
|
3
|
+
//#region src/oauth-provider.ts
|
|
4
|
+
if (!(typeof Cloudflare !== "undefined" && Cloudflare.compatibilityFlags?.global_fetch_strictly_public === true)) console.warn("CIMD (Client ID Metadata Document) is disabled: add '\"compatibility_flags\": [\"global_fetch_strictly_public\"]' to your wrangler.jsonc to enable. See: https://developers.cloudflare.com/workers/configuration/compatibility-flags/#global-fetch-strictly-public");
|
|
5
|
+
/**
|
|
6
|
+
* Enum representing the type of handler (ExportedHandler or WorkerEntrypoint)
|
|
7
|
+
*/
|
|
8
|
+
var HandlerType = /* @__PURE__ */ function(HandlerType$1) {
|
|
9
|
+
HandlerType$1[HandlerType$1["EXPORTED_HANDLER"] = 0] = "EXPORTED_HANDLER";
|
|
10
|
+
HandlerType$1[HandlerType$1["WORKER_ENTRYPOINT"] = 1] = "WORKER_ENTRYPOINT";
|
|
11
|
+
return HandlerType$1;
|
|
12
|
+
}(HandlerType || {});
|
|
13
|
+
/**
|
|
14
|
+
* OAuth 2.0 Provider implementation for Cloudflare Workers
|
|
15
|
+
* Implements authorization code flow with support for refresh tokens
|
|
16
|
+
* and dynamic client registration.
|
|
17
|
+
*/
|
|
12
18
|
var OAuthProvider = class {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
19
|
+
#impl;
|
|
20
|
+
/**
|
|
21
|
+
* Creates a new OAuth provider instance
|
|
22
|
+
* @param options - Configuration options for the provider
|
|
23
|
+
*/
|
|
24
|
+
constructor(options) {
|
|
25
|
+
this.#impl = new OAuthProviderImpl(options);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Main fetch handler for the Worker
|
|
29
|
+
* Routes requests to the appropriate handler based on the URL
|
|
30
|
+
* @param request - The HTTP request
|
|
31
|
+
* @param env - Cloudflare Worker environment variables
|
|
32
|
+
* @param ctx - Cloudflare Worker execution context
|
|
33
|
+
* @returns A Promise resolving to an HTTP Response
|
|
34
|
+
*/
|
|
35
|
+
fetch(request, env, ctx) {
|
|
36
|
+
return this.#impl.fetch(request, env, ctx);
|
|
37
|
+
}
|
|
32
38
|
};
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
* This method is not private because `OAuthHelpers` needs to call it. Note that since
|
|
1018
|
-
* `OAuthProviderImpl` is not exposed outside this module, this is still effectively
|
|
1019
|
-
* module-private.
|
|
1020
|
-
* @param env - Cloudflare Worker environment variables
|
|
1021
|
-
* @param clientId - The client ID to look up
|
|
1022
|
-
* @returns The client information, or null if not found
|
|
1023
|
-
*/
|
|
1024
|
-
getClient(env, clientId) {
|
|
1025
|
-
const clientKey = `client:${clientId}`;
|
|
1026
|
-
return env.OAUTH_KV.get(clientKey, { type: "json" });
|
|
1027
|
-
}
|
|
1028
|
-
/**
|
|
1029
|
-
* Helper function to create OAuth error responses
|
|
1030
|
-
* @param code - OAuth error code (e.g., 'invalid_request', 'invalid_token')
|
|
1031
|
-
* @param description - Human-readable error description
|
|
1032
|
-
* @param status - HTTP status code (default: 400)
|
|
1033
|
-
* @param headers - Additional headers to include
|
|
1034
|
-
* @returns A Response object with the error
|
|
1035
|
-
*/
|
|
1036
|
-
createErrorResponse(code, description, status = 400, headers = {}) {
|
|
1037
|
-
const customErrorResponse = this.options.onError?.({ code, description, status, headers });
|
|
1038
|
-
if (customErrorResponse) return customErrorResponse;
|
|
1039
|
-
const body = JSON.stringify({
|
|
1040
|
-
error: code,
|
|
1041
|
-
error_description: description
|
|
1042
|
-
});
|
|
1043
|
-
return new Response(body, {
|
|
1044
|
-
status,
|
|
1045
|
-
headers: {
|
|
1046
|
-
"Content-Type": "application/json",
|
|
1047
|
-
...headers
|
|
1048
|
-
}
|
|
1049
|
-
});
|
|
1050
|
-
}
|
|
39
|
+
/**
|
|
40
|
+
* Implementation class backing OAuthProvider.
|
|
41
|
+
*
|
|
42
|
+
* We use a PImpl pattern in `OAuthProvider` to make sure we don't inadvertently export any private
|
|
43
|
+
* methods over RPC. Unfortunately, declaring a method "private" in TypeScript is merely a type
|
|
44
|
+
* annotation, and does not actually prevent the method from being called from outside the class,
|
|
45
|
+
* including over RPC.
|
|
46
|
+
*/
|
|
47
|
+
var OAuthProviderImpl = class OAuthProviderImpl {
|
|
48
|
+
/**
|
|
49
|
+
* Creates a new OAuth provider instance
|
|
50
|
+
* @param options - Configuration options for the provider
|
|
51
|
+
*/
|
|
52
|
+
constructor(options) {
|
|
53
|
+
this.typedApiHandlers = [];
|
|
54
|
+
const hasSingleHandlerConfig = !!(options.apiRoute && options.apiHandler);
|
|
55
|
+
const hasMultiHandlerConfig = !!options.apiHandlers;
|
|
56
|
+
if (hasSingleHandlerConfig && hasMultiHandlerConfig) throw new TypeError("Cannot use both apiRoute/apiHandler and apiHandlers. Use either apiRoute + apiHandler OR apiHandlers, not both.");
|
|
57
|
+
if (!hasSingleHandlerConfig && !hasMultiHandlerConfig) throw new TypeError("Must provide either apiRoute + apiHandler OR apiHandlers. No API route configuration provided.");
|
|
58
|
+
this.typedDefaultHandler = this.validateHandler(options.defaultHandler, "defaultHandler");
|
|
59
|
+
if (hasSingleHandlerConfig) {
|
|
60
|
+
const apiHandler = this.validateHandler(options.apiHandler, "apiHandler");
|
|
61
|
+
if (Array.isArray(options.apiRoute)) options.apiRoute.forEach((route, index) => {
|
|
62
|
+
this.validateEndpoint(route, `apiRoute[${index}]`);
|
|
63
|
+
this.typedApiHandlers.push([route, apiHandler]);
|
|
64
|
+
});
|
|
65
|
+
else {
|
|
66
|
+
this.validateEndpoint(options.apiRoute, "apiRoute");
|
|
67
|
+
this.typedApiHandlers.push([options.apiRoute, apiHandler]);
|
|
68
|
+
}
|
|
69
|
+
} else for (const [route, handler] of Object.entries(options.apiHandlers)) {
|
|
70
|
+
this.validateEndpoint(route, `apiHandlers key: ${route}`);
|
|
71
|
+
this.typedApiHandlers.push([route, this.validateHandler(handler, `apiHandlers[${route}]`)]);
|
|
72
|
+
}
|
|
73
|
+
this.validateEndpoint(options.authorizeEndpoint, "authorizeEndpoint");
|
|
74
|
+
this.validateEndpoint(options.tokenEndpoint, "tokenEndpoint");
|
|
75
|
+
if (options.clientRegistrationEndpoint) this.validateEndpoint(options.clientRegistrationEndpoint, "clientRegistrationEndpoint");
|
|
76
|
+
this.options = {
|
|
77
|
+
accessTokenTTL: DEFAULT_ACCESS_TOKEN_TTL,
|
|
78
|
+
onError: ({ status, code, description }) => console.warn(`OAuth error response: ${status} ${code} - ${description}`),
|
|
79
|
+
...options
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Validates that an endpoint is either an absolute path or a full URL
|
|
84
|
+
* @param endpoint - The endpoint to validate
|
|
85
|
+
* @param name - The name of the endpoint property for error messages
|
|
86
|
+
* @throws TypeError if the endpoint is invalid
|
|
87
|
+
*/
|
|
88
|
+
validateEndpoint(endpoint, name) {
|
|
89
|
+
if (this.isPath(endpoint)) {
|
|
90
|
+
if (!endpoint.startsWith("/")) throw new TypeError(`${name} path must be an absolute path starting with /`);
|
|
91
|
+
} else try {
|
|
92
|
+
new URL(endpoint);
|
|
93
|
+
} catch (e) {
|
|
94
|
+
throw new TypeError(`${name} must be either an absolute path starting with / or a valid URL`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Validates that a handler is either an ExportedHandler or a class extending WorkerEntrypoint
|
|
99
|
+
* @param handler - The handler to validate
|
|
100
|
+
* @param name - The name of the handler property for error messages
|
|
101
|
+
* @returns The type of the handler (EXPORTED_HANDLER or WORKER_ENTRYPOINT)
|
|
102
|
+
* @throws TypeError if the handler is invalid
|
|
103
|
+
*/
|
|
104
|
+
validateHandler(handler, name) {
|
|
105
|
+
if (typeof handler === "object" && handler !== null && typeof handler.fetch === "function") return {
|
|
106
|
+
type: HandlerType.EXPORTED_HANDLER,
|
|
107
|
+
handler
|
|
108
|
+
};
|
|
109
|
+
if (typeof handler === "function" && handler.prototype instanceof WorkerEntrypoint) return {
|
|
110
|
+
type: HandlerType.WORKER_ENTRYPOINT,
|
|
111
|
+
handler
|
|
112
|
+
};
|
|
113
|
+
throw new TypeError(`${name} must be either an ExportedHandler object with a fetch method or a class extending WorkerEntrypoint`);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Main fetch handler for the Worker
|
|
117
|
+
* Routes requests to the appropriate handler based on the URL
|
|
118
|
+
* @param request - The HTTP request
|
|
119
|
+
* @param env - Cloudflare Worker environment variables
|
|
120
|
+
* @param ctx - Cloudflare Worker execution context
|
|
121
|
+
* @returns A Promise resolving to an HTTP Response
|
|
122
|
+
*/
|
|
123
|
+
async fetch(request, env, ctx) {
|
|
124
|
+
const url = new URL(request.url);
|
|
125
|
+
if (request.method === "OPTIONS") {
|
|
126
|
+
if (this.isApiRequest(url) || url.pathname === "/.well-known/oauth-authorization-server" || this.isTokenEndpoint(url) || this.options.clientRegistrationEndpoint && this.isClientRegistrationEndpoint(url)) return this.addCorsHeaders(new Response(null, {
|
|
127
|
+
status: 204,
|
|
128
|
+
headers: { "Content-Length": "0" }
|
|
129
|
+
}), request);
|
|
130
|
+
}
|
|
131
|
+
if (url.pathname === "/.well-known/oauth-authorization-server") {
|
|
132
|
+
const response = await this.handleMetadataDiscovery(url);
|
|
133
|
+
return this.addCorsHeaders(response, request);
|
|
134
|
+
}
|
|
135
|
+
if (this.isTokenEndpoint(url)) {
|
|
136
|
+
const parsed = await this.parseTokenEndpointRequest(request, env);
|
|
137
|
+
if (parsed instanceof Response) return this.addCorsHeaders(parsed, request);
|
|
138
|
+
let response;
|
|
139
|
+
if (parsed.isRevocationRequest) response = await this.handleRevocationRequest(parsed.body, env);
|
|
140
|
+
else response = await this.handleTokenRequest(parsed.body, parsed.clientInfo, env);
|
|
141
|
+
return this.addCorsHeaders(response, request);
|
|
142
|
+
}
|
|
143
|
+
if (this.options.clientRegistrationEndpoint && this.isClientRegistrationEndpoint(url)) {
|
|
144
|
+
const response = await this.handleClientRegistration(request, env);
|
|
145
|
+
return this.addCorsHeaders(response, request);
|
|
146
|
+
}
|
|
147
|
+
if (this.isApiRequest(url)) {
|
|
148
|
+
const response = await this.handleApiRequest(request, env, ctx);
|
|
149
|
+
return this.addCorsHeaders(response, request);
|
|
150
|
+
}
|
|
151
|
+
if (!env.OAUTH_PROVIDER) env.OAUTH_PROVIDER = this.createOAuthHelpers(env);
|
|
152
|
+
if (this.typedDefaultHandler.type === HandlerType.EXPORTED_HANDLER) return this.typedDefaultHandler.handler.fetch(request, env, ctx);
|
|
153
|
+
else return new this.typedDefaultHandler.handler(ctx, env).fetch(request);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Decodes a token and returns token data with decrypted props
|
|
157
|
+
* @param token - The granted token
|
|
158
|
+
* @param env - Cloudflare Worker environment variables
|
|
159
|
+
* @returns Promise resolving to token data with decrypted props, or null if token is invalid
|
|
160
|
+
*/
|
|
161
|
+
async unwrapToken(token, env) {
|
|
162
|
+
const parts = token.split(":");
|
|
163
|
+
if (!(parts.length === 3)) return null;
|
|
164
|
+
const [userId, grantId] = parts;
|
|
165
|
+
const id = await generateTokenId(token);
|
|
166
|
+
const tokenData = await env.OAUTH_KV.get(`token:${userId}:${grantId}:${id}`, { type: "json" });
|
|
167
|
+
if (!tokenData) return null;
|
|
168
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
169
|
+
if (tokenData.expiresAt < now) return null;
|
|
170
|
+
const decryptedProps = await decryptProps(await unwrapKeyWithToken(token, tokenData.wrappedEncryptionKey), tokenData.grant.encryptedProps);
|
|
171
|
+
const { grant } = tokenData;
|
|
172
|
+
return {
|
|
173
|
+
id: tokenData.id,
|
|
174
|
+
grantId: tokenData.grantId,
|
|
175
|
+
userId: tokenData.userId,
|
|
176
|
+
createdAt: tokenData.createdAt,
|
|
177
|
+
expiresAt: tokenData.expiresAt,
|
|
178
|
+
audience: tokenData.audience,
|
|
179
|
+
grant: {
|
|
180
|
+
clientId: grant.clientId,
|
|
181
|
+
scope: grant.scope,
|
|
182
|
+
props: decryptedProps
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Determines if an endpoint configuration is a path or a full URL
|
|
188
|
+
* @param endpoint - The endpoint configuration
|
|
189
|
+
* @returns True if the endpoint is a path (starts with /), false if it's a full URL
|
|
190
|
+
*/
|
|
191
|
+
isPath(endpoint) {
|
|
192
|
+
return endpoint.startsWith("/");
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Matches a URL against an endpoint pattern that can be a full URL or just a path
|
|
196
|
+
* @param url - The URL to check
|
|
197
|
+
* @param endpoint - The endpoint pattern (full URL or path)
|
|
198
|
+
* @returns True if the URL matches the endpoint pattern
|
|
199
|
+
*/
|
|
200
|
+
matchEndpoint(url, endpoint) {
|
|
201
|
+
if (this.isPath(endpoint)) return url.pathname === endpoint;
|
|
202
|
+
else {
|
|
203
|
+
const endpointUrl = new URL(endpoint);
|
|
204
|
+
return url.hostname === endpointUrl.hostname && url.pathname === endpointUrl.pathname;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Checks if a URL matches the configured token endpoint
|
|
209
|
+
* @param url - The URL to check
|
|
210
|
+
* @returns True if the URL matches the token endpoint
|
|
211
|
+
*/
|
|
212
|
+
isTokenEndpoint(url) {
|
|
213
|
+
return this.matchEndpoint(url, this.options.tokenEndpoint);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Checks if a URL matches the configured client registration endpoint
|
|
217
|
+
* @param url - The URL to check
|
|
218
|
+
* @returns True if the URL matches the client registration endpoint
|
|
219
|
+
*/
|
|
220
|
+
isClientRegistrationEndpoint(url) {
|
|
221
|
+
if (!this.options.clientRegistrationEndpoint) return false;
|
|
222
|
+
return this.matchEndpoint(url, this.options.clientRegistrationEndpoint);
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Parses and validates a token endpoint request (used for both token exchange and revocation)
|
|
226
|
+
* @param request - The HTTP request to parse
|
|
227
|
+
* @returns Promise with parsed body and client info, or error response
|
|
228
|
+
*/
|
|
229
|
+
async parseTokenEndpointRequest(request, env) {
|
|
230
|
+
if (request.method !== "POST") return this.createErrorResponse("invalid_request", "Method not allowed", 405);
|
|
231
|
+
let contentType = request.headers.get("Content-Type") || "";
|
|
232
|
+
let body = {};
|
|
233
|
+
if (!contentType.includes("application/x-www-form-urlencoded")) return this.createErrorResponse("invalid_request", "Content-Type must be application/x-www-form-urlencoded", 400);
|
|
234
|
+
const formData = await request.formData();
|
|
235
|
+
for (const [key, value] of formData.entries()) {
|
|
236
|
+
const allValues = formData.getAll(key);
|
|
237
|
+
body[key] = allValues.length > 1 ? allValues : value;
|
|
238
|
+
}
|
|
239
|
+
const authHeader = request.headers.get("Authorization");
|
|
240
|
+
let clientId = "";
|
|
241
|
+
let clientSecret = "";
|
|
242
|
+
if (authHeader && authHeader.startsWith("Basic ")) {
|
|
243
|
+
const [id, secret] = atob(authHeader.substring(6)).split(":", 2);
|
|
244
|
+
clientId = decodeURIComponent(id);
|
|
245
|
+
clientSecret = decodeURIComponent(secret || "");
|
|
246
|
+
} else {
|
|
247
|
+
clientId = body.client_id;
|
|
248
|
+
clientSecret = body.client_secret || "";
|
|
249
|
+
}
|
|
250
|
+
if (!clientId) return this.createErrorResponse("invalid_client", "Client ID is required", 401);
|
|
251
|
+
const clientInfo = await this.getClient(env, clientId);
|
|
252
|
+
if (!clientInfo) return this.createErrorResponse("invalid_client", "Client not found", 401);
|
|
253
|
+
if (!(clientInfo.tokenEndpointAuthMethod === "none")) {
|
|
254
|
+
if (!clientSecret) return this.createErrorResponse("invalid_client", "Client authentication failed: missing client_secret", 401);
|
|
255
|
+
if (!clientInfo.clientSecret) return this.createErrorResponse("invalid_client", "Client authentication failed: client has no registered secret", 401);
|
|
256
|
+
if (await hashSecret(clientSecret) !== clientInfo.clientSecret) return this.createErrorResponse("invalid_client", "Client authentication failed: invalid client_secret", 401);
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
body,
|
|
260
|
+
clientInfo,
|
|
261
|
+
isRevocationRequest: !body.grant_type && !!body.token
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Checks if a URL matches a specific API route
|
|
266
|
+
* @param url - The URL to check
|
|
267
|
+
* @param route - The API route to check against
|
|
268
|
+
* @returns True if the URL matches the API route
|
|
269
|
+
*/
|
|
270
|
+
matchApiRoute(url, route) {
|
|
271
|
+
if (this.isPath(route)) return url.pathname.startsWith(route);
|
|
272
|
+
else {
|
|
273
|
+
const apiUrl = new URL(route);
|
|
274
|
+
return url.hostname === apiUrl.hostname && url.pathname.startsWith(apiUrl.pathname);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Checks if a URL is an API request based on the configured API route(s)
|
|
279
|
+
* @param url - The URL to check
|
|
280
|
+
* @returns True if the URL matches any of the API routes
|
|
281
|
+
*/
|
|
282
|
+
isApiRequest(url) {
|
|
283
|
+
for (const [route, _] of this.typedApiHandlers) if (this.matchApiRoute(url, route)) return true;
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Finds the appropriate API handler for a URL
|
|
288
|
+
* @param url - The URL to find a handler for
|
|
289
|
+
* @returns The TypedHandler for the URL, or undefined if no handler matches
|
|
290
|
+
*/
|
|
291
|
+
findApiHandlerForUrl(url) {
|
|
292
|
+
for (const [route, handler] of this.typedApiHandlers) if (this.matchApiRoute(url, route)) return handler;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Gets the full URL for an endpoint, using the provided request URL's
|
|
296
|
+
* origin for endpoints specified as just paths
|
|
297
|
+
* @param endpoint - The endpoint configuration (path or full URL)
|
|
298
|
+
* @param requestUrl - The URL of the incoming request
|
|
299
|
+
* @returns The full URL for the endpoint
|
|
300
|
+
*/
|
|
301
|
+
getFullEndpointUrl(endpoint, requestUrl) {
|
|
302
|
+
if (this.isPath(endpoint)) return `${requestUrl.origin}${endpoint}`;
|
|
303
|
+
else return endpoint;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Adds CORS headers to a response
|
|
307
|
+
* @param response - The response to add CORS headers to
|
|
308
|
+
* @param request - The original request
|
|
309
|
+
* @returns A new Response with CORS headers added
|
|
310
|
+
*/
|
|
311
|
+
addCorsHeaders(response, request) {
|
|
312
|
+
const origin = request.headers.get("Origin");
|
|
313
|
+
if (!origin) return response;
|
|
314
|
+
const newResponse = new Response(response.body, response);
|
|
315
|
+
newResponse.headers.set("Access-Control-Allow-Origin", origin);
|
|
316
|
+
newResponse.headers.set("Access-Control-Allow-Methods", "*");
|
|
317
|
+
newResponse.headers.set("Access-Control-Allow-Headers", "Authorization, *");
|
|
318
|
+
newResponse.headers.set("Access-Control-Max-Age", "86400");
|
|
319
|
+
return newResponse;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Handles the OAuth metadata discovery endpoint
|
|
323
|
+
* Implements RFC 8414 for OAuth Server Metadata
|
|
324
|
+
* @param requestUrl - The URL of the incoming request
|
|
325
|
+
* @returns Response with OAuth server metadata
|
|
326
|
+
*/
|
|
327
|
+
async handleMetadataDiscovery(requestUrl) {
|
|
328
|
+
const tokenEndpoint = this.getFullEndpointUrl(this.options.tokenEndpoint, requestUrl);
|
|
329
|
+
const authorizeEndpoint = this.getFullEndpointUrl(this.options.authorizeEndpoint, requestUrl);
|
|
330
|
+
let registrationEndpoint = void 0;
|
|
331
|
+
if (this.options.clientRegistrationEndpoint) registrationEndpoint = this.getFullEndpointUrl(this.options.clientRegistrationEndpoint, requestUrl);
|
|
332
|
+
const responseTypesSupported = ["code"];
|
|
333
|
+
if (this.options.allowImplicitFlow) responseTypesSupported.push("token");
|
|
334
|
+
const metadata = {
|
|
335
|
+
issuer: new URL(tokenEndpoint).origin,
|
|
336
|
+
authorization_endpoint: authorizeEndpoint,
|
|
337
|
+
token_endpoint: tokenEndpoint,
|
|
338
|
+
registration_endpoint: registrationEndpoint,
|
|
339
|
+
scopes_supported: this.options.scopesSupported,
|
|
340
|
+
response_types_supported: responseTypesSupported,
|
|
341
|
+
response_modes_supported: ["query"],
|
|
342
|
+
grant_types_supported: ["authorization_code", "refresh_token"],
|
|
343
|
+
token_endpoint_auth_methods_supported: [
|
|
344
|
+
"client_secret_basic",
|
|
345
|
+
"client_secret_post",
|
|
346
|
+
"none"
|
|
347
|
+
],
|
|
348
|
+
revocation_endpoint: tokenEndpoint,
|
|
349
|
+
code_challenge_methods_supported: ["plain", "S256"],
|
|
350
|
+
client_id_metadata_document_supported: this.hasGlobalFetchStrictlyPublic()
|
|
351
|
+
};
|
|
352
|
+
return new Response(JSON.stringify(metadata), { headers: { "Content-Type": "application/json" } });
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Handles client authentication and token issuance via the token endpoint
|
|
356
|
+
* Supports authorization_code and refresh_token grant types
|
|
357
|
+
* @param body - The parsed request body
|
|
358
|
+
* @param clientInfo - The authenticated client information
|
|
359
|
+
* @param env - Cloudflare Worker environment variables
|
|
360
|
+
* @returns Response with token data or error
|
|
361
|
+
*/
|
|
362
|
+
async handleTokenRequest(body, clientInfo, env) {
|
|
363
|
+
const grantType = body.grant_type;
|
|
364
|
+
if (grantType === "authorization_code") return this.handleAuthorizationCodeGrant(body, clientInfo, env);
|
|
365
|
+
else if (grantType === "refresh_token") return this.handleRefreshTokenGrant(body, clientInfo, env);
|
|
366
|
+
else return this.createErrorResponse("unsupported_grant_type", "Grant type not supported");
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Handles the authorization code grant type
|
|
370
|
+
* Exchanges an authorization code for access and refresh tokens
|
|
371
|
+
* @param body - The parsed request body
|
|
372
|
+
* @param clientInfo - The authenticated client information
|
|
373
|
+
* @param env - Cloudflare Worker environment variables
|
|
374
|
+
* @returns Response with token data or error
|
|
375
|
+
*/
|
|
376
|
+
async handleAuthorizationCodeGrant(body, clientInfo, env) {
|
|
377
|
+
const code = body.code;
|
|
378
|
+
const redirectUri = body.redirect_uri;
|
|
379
|
+
const codeVerifier = body.code_verifier;
|
|
380
|
+
if (!code) return this.createErrorResponse("invalid_request", "Authorization code is required");
|
|
381
|
+
const codeParts = code.split(":");
|
|
382
|
+
if (codeParts.length !== 3) return this.createErrorResponse("invalid_grant", "Invalid authorization code format");
|
|
383
|
+
const [userId, grantId, _] = codeParts;
|
|
384
|
+
const grantKey = `grant:${userId}:${grantId}`;
|
|
385
|
+
const grantData = await env.OAUTH_KV.get(grantKey, { type: "json" });
|
|
386
|
+
if (!grantData) return this.createErrorResponse("invalid_grant", "Grant not found or authorization code expired");
|
|
387
|
+
if (!grantData.authCodeId) return this.createErrorResponse("invalid_grant", "Authorization code already used");
|
|
388
|
+
if (await hashSecret(code) !== grantData.authCodeId) return this.createErrorResponse("invalid_grant", "Invalid authorization code");
|
|
389
|
+
if (grantData.clientId !== clientInfo.clientId) return this.createErrorResponse("invalid_grant", "Client ID mismatch");
|
|
390
|
+
const isPkceEnabled = !!grantData.codeChallenge;
|
|
391
|
+
if (!redirectUri && !isPkceEnabled) return this.createErrorResponse("invalid_request", "redirect_uri is required when not using PKCE");
|
|
392
|
+
if (redirectUri && !clientInfo.redirectUris.includes(redirectUri)) return this.createErrorResponse("invalid_grant", "Invalid redirect URI");
|
|
393
|
+
if (!isPkceEnabled && codeVerifier) return this.createErrorResponse("invalid_request", "code_verifier provided for a flow that did not use PKCE");
|
|
394
|
+
if (isPkceEnabled) {
|
|
395
|
+
if (!codeVerifier) return this.createErrorResponse("invalid_request", "code_verifier is required for PKCE");
|
|
396
|
+
let calculatedChallenge;
|
|
397
|
+
if (grantData.codeChallengeMethod === "S256") {
|
|
398
|
+
const data = new TextEncoder().encode(codeVerifier);
|
|
399
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
400
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
401
|
+
calculatedChallenge = base64UrlEncode(String.fromCharCode(...hashArray));
|
|
402
|
+
} else calculatedChallenge = codeVerifier;
|
|
403
|
+
if (calculatedChallenge !== grantData.codeChallenge) return this.createErrorResponse("invalid_grant", "Invalid PKCE code_verifier");
|
|
404
|
+
}
|
|
405
|
+
const accessToken = `${userId}:${grantId}:${generateRandomString(TOKEN_LENGTH)}`;
|
|
406
|
+
const accessTokenId = await generateTokenId(accessToken);
|
|
407
|
+
let accessTokenTTL = this.options.accessTokenTTL;
|
|
408
|
+
let refreshTokenTTL = this.options.refreshTokenTTL;
|
|
409
|
+
const encryptionKey = await unwrapKeyWithToken(code, grantData.authCodeWrappedKey);
|
|
410
|
+
let grantEncryptionKey = encryptionKey;
|
|
411
|
+
let accessTokenEncryptionKey = encryptionKey;
|
|
412
|
+
let encryptedAccessTokenProps = grantData.encryptedProps;
|
|
413
|
+
if (this.options.tokenExchangeCallback) {
|
|
414
|
+
const decryptedProps = await decryptProps(encryptionKey, grantData.encryptedProps);
|
|
415
|
+
let grantProps = decryptedProps;
|
|
416
|
+
let accessTokenProps = decryptedProps;
|
|
417
|
+
const callbackOptions = {
|
|
418
|
+
grantType: "authorization_code",
|
|
419
|
+
clientId: clientInfo.clientId,
|
|
420
|
+
userId,
|
|
421
|
+
scope: grantData.scope,
|
|
422
|
+
props: decryptedProps
|
|
423
|
+
};
|
|
424
|
+
const callbackResult = await Promise.resolve(this.options.tokenExchangeCallback(callbackOptions));
|
|
425
|
+
if (callbackResult) {
|
|
426
|
+
if (callbackResult.newProps) {
|
|
427
|
+
grantProps = callbackResult.newProps;
|
|
428
|
+
if (!callbackResult.accessTokenProps) accessTokenProps = callbackResult.newProps;
|
|
429
|
+
}
|
|
430
|
+
if (callbackResult.accessTokenProps) accessTokenProps = callbackResult.accessTokenProps;
|
|
431
|
+
if (callbackResult.accessTokenTTL !== void 0) accessTokenTTL = callbackResult.accessTokenTTL;
|
|
432
|
+
if ("refreshTokenTTL" in callbackResult) refreshTokenTTL = callbackResult.refreshTokenTTL;
|
|
433
|
+
}
|
|
434
|
+
const grantResult = await encryptProps(grantProps);
|
|
435
|
+
grantData.encryptedProps = grantResult.encryptedData;
|
|
436
|
+
grantEncryptionKey = grantResult.key;
|
|
437
|
+
if (accessTokenProps !== grantProps) {
|
|
438
|
+
const tokenResult = await encryptProps(accessTokenProps);
|
|
439
|
+
encryptedAccessTokenProps = tokenResult.encryptedData;
|
|
440
|
+
accessTokenEncryptionKey = tokenResult.key;
|
|
441
|
+
} else {
|
|
442
|
+
encryptedAccessTokenProps = grantData.encryptedProps;
|
|
443
|
+
accessTokenEncryptionKey = grantEncryptionKey;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
447
|
+
const accessTokenExpiresAt = now + accessTokenTTL;
|
|
448
|
+
const useRefreshToken = refreshTokenTTL !== 0;
|
|
449
|
+
const accessTokenWrappedKey = await wrapKeyWithToken(accessToken, accessTokenEncryptionKey);
|
|
450
|
+
delete grantData.authCodeId;
|
|
451
|
+
delete grantData.codeChallenge;
|
|
452
|
+
delete grantData.codeChallengeMethod;
|
|
453
|
+
delete grantData.authCodeWrappedKey;
|
|
454
|
+
let refreshToken;
|
|
455
|
+
if (useRefreshToken) {
|
|
456
|
+
refreshToken = `${userId}:${grantId}:${generateRandomString(TOKEN_LENGTH)}`;
|
|
457
|
+
const refreshTokenId = await generateTokenId(refreshToken);
|
|
458
|
+
const refreshTokenWrappedKey = await wrapKeyWithToken(refreshToken, grantEncryptionKey);
|
|
459
|
+
const expiresAt = refreshTokenTTL !== void 0 ? now + refreshTokenTTL : void 0;
|
|
460
|
+
grantData.refreshTokenId = refreshTokenId;
|
|
461
|
+
grantData.refreshTokenWrappedKey = refreshTokenWrappedKey;
|
|
462
|
+
grantData.previousRefreshTokenId = void 0;
|
|
463
|
+
grantData.previousRefreshTokenWrappedKey = void 0;
|
|
464
|
+
grantData.expiresAt = expiresAt;
|
|
465
|
+
}
|
|
466
|
+
await this.saveGrantWithTTL(env, grantKey, grantData, now);
|
|
467
|
+
if (body.resource && grantData.resource) {
|
|
468
|
+
const requestedResources = Array.isArray(body.resource) ? body.resource : [body.resource];
|
|
469
|
+
const grantedResources = Array.isArray(grantData.resource) ? grantData.resource : [grantData.resource];
|
|
470
|
+
for (const requested of requestedResources) if (!grantedResources.includes(requested)) return this.createErrorResponse("invalid_target", "Requested resource was not included in the authorization request");
|
|
471
|
+
}
|
|
472
|
+
const audience = parseResourceParameter(body.resource || grantData.resource);
|
|
473
|
+
if ((body.resource || grantData.resource) && !audience) return this.createErrorResponse("invalid_target", "The resource parameter must be a valid absolute URI without a fragment");
|
|
474
|
+
const accessTokenData = {
|
|
475
|
+
id: accessTokenId,
|
|
476
|
+
grantId,
|
|
477
|
+
userId,
|
|
478
|
+
createdAt: now,
|
|
479
|
+
expiresAt: accessTokenExpiresAt,
|
|
480
|
+
audience,
|
|
481
|
+
wrappedEncryptionKey: accessTokenWrappedKey,
|
|
482
|
+
grant: {
|
|
483
|
+
clientId: grantData.clientId,
|
|
484
|
+
scope: grantData.scope,
|
|
485
|
+
encryptedProps: encryptedAccessTokenProps
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
await env.OAUTH_KV.put(`token:${userId}:${grantId}:${accessTokenId}`, JSON.stringify(accessTokenData), { expirationTtl: accessTokenTTL });
|
|
489
|
+
const tokenResponse = {
|
|
490
|
+
access_token: accessToken,
|
|
491
|
+
token_type: "bearer",
|
|
492
|
+
expires_in: accessTokenTTL,
|
|
493
|
+
scope: grantData.scope.join(" ")
|
|
494
|
+
};
|
|
495
|
+
if (refreshToken) tokenResponse.refresh_token = refreshToken;
|
|
496
|
+
if (audience) tokenResponse.resource = audience;
|
|
497
|
+
return new Response(JSON.stringify(tokenResponse), { headers: { "Content-Type": "application/json" } });
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Handles the refresh token grant type
|
|
501
|
+
* Issues a new access token using a refresh token
|
|
502
|
+
* @param body - The parsed request body
|
|
503
|
+
* @param clientInfo - The authenticated client information
|
|
504
|
+
* @param env - Cloudflare Worker environment variables
|
|
505
|
+
* @returns Response with token data or error
|
|
506
|
+
*/
|
|
507
|
+
async handleRefreshTokenGrant(body, clientInfo, env) {
|
|
508
|
+
const refreshToken = body.refresh_token;
|
|
509
|
+
if (!refreshToken) return this.createErrorResponse("invalid_request", "Refresh token is required");
|
|
510
|
+
const tokenParts = refreshToken.split(":");
|
|
511
|
+
if (tokenParts.length !== 3) return this.createErrorResponse("invalid_grant", "Invalid token format");
|
|
512
|
+
const [userId, grantId, _] = tokenParts;
|
|
513
|
+
const providedTokenHash = await generateTokenId(refreshToken);
|
|
514
|
+
const grantKey = `grant:${userId}:${grantId}`;
|
|
515
|
+
const grantData = await env.OAUTH_KV.get(grantKey, { type: "json" });
|
|
516
|
+
if (!grantData) return this.createErrorResponse("invalid_grant", "Grant not found");
|
|
517
|
+
const isCurrentToken = grantData.refreshTokenId === providedTokenHash;
|
|
518
|
+
const isPreviousToken = grantData.previousRefreshTokenId === providedTokenHash;
|
|
519
|
+
if (!isCurrentToken && !isPreviousToken) return this.createErrorResponse("invalid_grant", "Invalid refresh token");
|
|
520
|
+
if (grantData.clientId !== clientInfo.clientId) return this.createErrorResponse("invalid_grant", "Client ID mismatch");
|
|
521
|
+
if (grantData.expiresAt !== void 0) {
|
|
522
|
+
if (Math.floor(Date.now() / 1e3) >= grantData.expiresAt) return this.createErrorResponse("invalid_grant", "Refresh token has expired");
|
|
523
|
+
}
|
|
524
|
+
const newAccessToken = `${userId}:${grantId}:${generateRandomString(TOKEN_LENGTH)}`;
|
|
525
|
+
const accessTokenId = await generateTokenId(newAccessToken);
|
|
526
|
+
let accessTokenTTL = this.options.accessTokenTTL;
|
|
527
|
+
let wrappedKeyToUse;
|
|
528
|
+
if (isCurrentToken) wrappedKeyToUse = grantData.refreshTokenWrappedKey;
|
|
529
|
+
else wrappedKeyToUse = grantData.previousRefreshTokenWrappedKey;
|
|
530
|
+
const encryptionKey = await unwrapKeyWithToken(refreshToken, wrappedKeyToUse);
|
|
531
|
+
let grantEncryptionKey = encryptionKey;
|
|
532
|
+
let accessTokenEncryptionKey = encryptionKey;
|
|
533
|
+
let encryptedAccessTokenProps = grantData.encryptedProps;
|
|
534
|
+
let grantPropsChanged = false;
|
|
535
|
+
if (this.options.tokenExchangeCallback) {
|
|
536
|
+
const decryptedProps = await decryptProps(encryptionKey, grantData.encryptedProps);
|
|
537
|
+
let grantProps = decryptedProps;
|
|
538
|
+
let accessTokenProps = decryptedProps;
|
|
539
|
+
const callbackOptions = {
|
|
540
|
+
grantType: "refresh_token",
|
|
541
|
+
clientId: clientInfo.clientId,
|
|
542
|
+
userId,
|
|
543
|
+
scope: grantData.scope,
|
|
544
|
+
props: decryptedProps
|
|
545
|
+
};
|
|
546
|
+
const callbackResult = await Promise.resolve(this.options.tokenExchangeCallback(callbackOptions));
|
|
547
|
+
if (callbackResult) {
|
|
548
|
+
if (callbackResult.newProps) {
|
|
549
|
+
grantProps = callbackResult.newProps;
|
|
550
|
+
grantPropsChanged = true;
|
|
551
|
+
if (!callbackResult.accessTokenProps) accessTokenProps = callbackResult.newProps;
|
|
552
|
+
}
|
|
553
|
+
if (callbackResult.accessTokenProps) accessTokenProps = callbackResult.accessTokenProps;
|
|
554
|
+
if (callbackResult.accessTokenTTL !== void 0) accessTokenTTL = callbackResult.accessTokenTTL;
|
|
555
|
+
if ("refreshTokenTTL" in callbackResult) return this.createErrorResponse("invalid_request", "refreshTokenTTL cannot be changed during refresh token exchange");
|
|
556
|
+
}
|
|
557
|
+
if (grantPropsChanged) {
|
|
558
|
+
const grantResult = await encryptProps(grantProps);
|
|
559
|
+
grantData.encryptedProps = grantResult.encryptedData;
|
|
560
|
+
if (grantResult.key !== encryptionKey) {
|
|
561
|
+
grantEncryptionKey = grantResult.key;
|
|
562
|
+
wrappedKeyToUse = await wrapKeyWithToken(refreshToken, grantEncryptionKey);
|
|
563
|
+
} else grantEncryptionKey = grantResult.key;
|
|
564
|
+
}
|
|
565
|
+
if (accessTokenProps !== grantProps) {
|
|
566
|
+
const tokenResult = await encryptProps(accessTokenProps);
|
|
567
|
+
encryptedAccessTokenProps = tokenResult.encryptedData;
|
|
568
|
+
accessTokenEncryptionKey = tokenResult.key;
|
|
569
|
+
} else {
|
|
570
|
+
encryptedAccessTokenProps = grantData.encryptedProps;
|
|
571
|
+
accessTokenEncryptionKey = grantEncryptionKey;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
575
|
+
if (grantData.expiresAt !== void 0) {
|
|
576
|
+
const remainingRefreshTokenLifetime = grantData.expiresAt - now;
|
|
577
|
+
if (remainingRefreshTokenLifetime > 0) accessTokenTTL = Math.min(accessTokenTTL, remainingRefreshTokenLifetime);
|
|
578
|
+
}
|
|
579
|
+
const accessTokenExpiresAt = now + accessTokenTTL;
|
|
580
|
+
const accessTokenWrappedKey = await wrapKeyWithToken(newAccessToken, accessTokenEncryptionKey);
|
|
581
|
+
const newRefreshToken = `${userId}:${grantId}:${generateRandomString(TOKEN_LENGTH)}`;
|
|
582
|
+
const newRefreshTokenId = await generateTokenId(newRefreshToken);
|
|
583
|
+
const newRefreshTokenWrappedKey = await wrapKeyWithToken(newRefreshToken, grantEncryptionKey);
|
|
584
|
+
grantData.previousRefreshTokenId = providedTokenHash;
|
|
585
|
+
grantData.previousRefreshTokenWrappedKey = wrappedKeyToUse;
|
|
586
|
+
grantData.refreshTokenId = newRefreshTokenId;
|
|
587
|
+
grantData.refreshTokenWrappedKey = newRefreshTokenWrappedKey;
|
|
588
|
+
await this.saveGrantWithTTL(env, grantKey, grantData, now);
|
|
589
|
+
if (body.resource && grantData.resource) {
|
|
590
|
+
const requestedResources = Array.isArray(body.resource) ? body.resource : [body.resource];
|
|
591
|
+
const grantedResources = Array.isArray(grantData.resource) ? grantData.resource : [grantData.resource];
|
|
592
|
+
for (const requested of requestedResources) if (!grantedResources.includes(requested)) return this.createErrorResponse("invalid_target", "Requested resource was not included in the authorization request");
|
|
593
|
+
}
|
|
594
|
+
const audience = parseResourceParameter(body.resource || grantData.resource);
|
|
595
|
+
if ((body.resource || grantData.resource) && !audience) return this.createErrorResponse("invalid_target", "The resource parameter must be a valid absolute URI without a fragment");
|
|
596
|
+
const accessTokenData = {
|
|
597
|
+
id: accessTokenId,
|
|
598
|
+
grantId,
|
|
599
|
+
userId,
|
|
600
|
+
createdAt: now,
|
|
601
|
+
expiresAt: accessTokenExpiresAt,
|
|
602
|
+
audience,
|
|
603
|
+
wrappedEncryptionKey: accessTokenWrappedKey,
|
|
604
|
+
grant: {
|
|
605
|
+
clientId: grantData.clientId,
|
|
606
|
+
scope: grantData.scope,
|
|
607
|
+
encryptedProps: encryptedAccessTokenProps
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
await env.OAUTH_KV.put(`token:${userId}:${grantId}:${accessTokenId}`, JSON.stringify(accessTokenData), { expirationTtl: accessTokenTTL });
|
|
611
|
+
const tokenResponse = {
|
|
612
|
+
access_token: newAccessToken,
|
|
613
|
+
token_type: "bearer",
|
|
614
|
+
expires_in: accessTokenTTL,
|
|
615
|
+
refresh_token: newRefreshToken,
|
|
616
|
+
scope: grantData.scope.join(" ")
|
|
617
|
+
};
|
|
618
|
+
if (audience) tokenResponse.resource = audience;
|
|
619
|
+
return new Response(JSON.stringify(tokenResponse), { headers: { "Content-Type": "application/json" } });
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Handles OAuth 2.0 token revocation requests (RFC 7009)
|
|
623
|
+
* @param body - The parsed request body containing revocation parameters
|
|
624
|
+
* @param env - Cloudflare Worker environment variables
|
|
625
|
+
* @returns Response confirming revocation or error
|
|
626
|
+
*/
|
|
627
|
+
async handleRevocationRequest(body, env) {
|
|
628
|
+
return this.revokeToken(body, env);
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* - Access tokens: Revokes only the specific token
|
|
632
|
+
* - Refresh tokens: Revokes the entire grant (access + refresh tokens)
|
|
633
|
+
* @param body - The parsed request body containing token parameter
|
|
634
|
+
* @param env - Cloudflare Worker environment variables
|
|
635
|
+
* @returns Response confirming revocation or error
|
|
636
|
+
*/
|
|
637
|
+
async revokeToken(body, env) {
|
|
638
|
+
const token = body.token;
|
|
639
|
+
if (!token) return this.createErrorResponse("invalid_request", "Token parameter is required");
|
|
640
|
+
const tokenParts = token.split(":");
|
|
641
|
+
if (tokenParts.length !== 3) return new Response("", { status: 200 });
|
|
642
|
+
const [userId, grantId, _] = tokenParts;
|
|
643
|
+
const tokenId = await generateTokenId(token);
|
|
644
|
+
const isAccessToken = await this.validateAccessToken(tokenId, userId, grantId, env);
|
|
645
|
+
const isRefreshToken = await this.validateRefreshToken(tokenId, userId, grantId, env);
|
|
646
|
+
if (isAccessToken) await this.revokeSpecificAccessToken(tokenId, userId, grantId, env);
|
|
647
|
+
else if (isRefreshToken) await this.createOAuthHelpers(env).revokeGrant(grantId, userId);
|
|
648
|
+
return new Response("", { status: 200 });
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Revokes a specific access token without affecting the refresh token
|
|
652
|
+
* @param tokenId - The hashed token ID
|
|
653
|
+
* @param userId - The user ID extracted from the token
|
|
654
|
+
* @param grantId - The grant ID extracted from the token
|
|
655
|
+
* @param env - Cloudflare Worker environment variables
|
|
656
|
+
*/
|
|
657
|
+
async revokeSpecificAccessToken(tokenId, userId, grantId, env) {
|
|
658
|
+
const tokenKey = `token:${userId}:${grantId}:${tokenId}`;
|
|
659
|
+
await env.OAUTH_KV.delete(tokenKey);
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Validates if a token is a valid access token
|
|
663
|
+
* @param tokenId - The hashed token ID
|
|
664
|
+
* @param userId - The user ID extracted from the token
|
|
665
|
+
* @param grantId - The grant ID extracted from the token
|
|
666
|
+
* @param env - Cloudflare Worker environment variables
|
|
667
|
+
* @returns Promise<boolean> indicating if the token is valid
|
|
668
|
+
*/
|
|
669
|
+
async validateAccessToken(tokenId, userId, grantId, env) {
|
|
670
|
+
const tokenKey = `token:${userId}:${grantId}:${tokenId}`;
|
|
671
|
+
const tokenData = await env.OAUTH_KV.get(tokenKey, { type: "json" });
|
|
672
|
+
if (!tokenData) return false;
|
|
673
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
674
|
+
return tokenData.expiresAt >= now;
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Validates if a token is a valid refresh token
|
|
678
|
+
* @param tokenId - The hashed token ID
|
|
679
|
+
* @param userId - The user ID extracted from the token
|
|
680
|
+
* @param grantId - The grant ID extracted from the token
|
|
681
|
+
* @param env - Cloudflare Worker environment variables
|
|
682
|
+
* @returns Promise<boolean> indicating if the token is valid
|
|
683
|
+
*/
|
|
684
|
+
async validateRefreshToken(tokenId, userId, grantId, env) {
|
|
685
|
+
const grantKey = `grant:${userId}:${grantId}`;
|
|
686
|
+
const grantData = await env.OAUTH_KV.get(grantKey, { type: "json" });
|
|
687
|
+
if (!grantData) return false;
|
|
688
|
+
return grantData.refreshTokenId === tokenId || grantData.previousRefreshTokenId === tokenId;
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Handles the dynamic client registration endpoint (RFC 7591)
|
|
692
|
+
* @param request - The HTTP request
|
|
693
|
+
* @param env - Cloudflare Worker environment variables
|
|
694
|
+
* @returns Response with client registration data or error
|
|
695
|
+
*/
|
|
696
|
+
async handleClientRegistration(request, env) {
|
|
697
|
+
if (!this.options.clientRegistrationEndpoint) return this.createErrorResponse("not_implemented", "Client registration is not enabled", 501);
|
|
698
|
+
if (request.method !== "POST") return this.createErrorResponse("invalid_request", "Method not allowed", 405);
|
|
699
|
+
if (parseInt(request.headers.get("Content-Length") || "0", 10) > 1048576) return this.createErrorResponse("invalid_request", "Request payload too large, must be under 1 MiB", 413);
|
|
700
|
+
let clientMetadata;
|
|
701
|
+
try {
|
|
702
|
+
const text = await request.text();
|
|
703
|
+
if (text.length > 1048576) return this.createErrorResponse("invalid_request", "Request payload too large, must be under 1 MiB", 413);
|
|
704
|
+
clientMetadata = JSON.parse(text);
|
|
705
|
+
} catch (error) {
|
|
706
|
+
return this.createErrorResponse("invalid_request", "Invalid JSON payload", 400);
|
|
707
|
+
}
|
|
708
|
+
const authMethod = OAuthProviderImpl.validateStringField(clientMetadata.token_endpoint_auth_method) || "client_secret_basic";
|
|
709
|
+
const isPublicClient = authMethod === "none";
|
|
710
|
+
if (isPublicClient && this.options.disallowPublicClientRegistration) return this.createErrorResponse("invalid_client_metadata", "Public client registration is not allowed");
|
|
711
|
+
const clientId = generateRandomString(16);
|
|
712
|
+
let clientSecret;
|
|
713
|
+
let hashedSecret;
|
|
714
|
+
if (!isPublicClient) {
|
|
715
|
+
clientSecret = generateRandomString(32);
|
|
716
|
+
hashedSecret = await hashSecret(clientSecret);
|
|
717
|
+
}
|
|
718
|
+
let clientInfo;
|
|
719
|
+
try {
|
|
720
|
+
const redirectUris = OAuthProviderImpl.validateStringArray(clientMetadata.redirect_uris);
|
|
721
|
+
if (!redirectUris || redirectUris.length === 0) throw new Error("At least one redirect URI is required");
|
|
722
|
+
for (const uri of redirectUris) validateRedirectUriScheme(uri);
|
|
723
|
+
clientInfo = {
|
|
724
|
+
clientId,
|
|
725
|
+
redirectUris,
|
|
726
|
+
clientName: OAuthProviderImpl.validateStringField(clientMetadata.client_name),
|
|
727
|
+
logoUri: OAuthProviderImpl.validateStringField(clientMetadata.logo_uri),
|
|
728
|
+
clientUri: OAuthProviderImpl.validateStringField(clientMetadata.client_uri),
|
|
729
|
+
policyUri: OAuthProviderImpl.validateStringField(clientMetadata.policy_uri),
|
|
730
|
+
tosUri: OAuthProviderImpl.validateStringField(clientMetadata.tos_uri),
|
|
731
|
+
jwksUri: OAuthProviderImpl.validateStringField(clientMetadata.jwks_uri),
|
|
732
|
+
contacts: OAuthProviderImpl.validateStringArray(clientMetadata.contacts),
|
|
733
|
+
grantTypes: OAuthProviderImpl.validateStringArray(clientMetadata.grant_types) || ["authorization_code", "refresh_token"],
|
|
734
|
+
responseTypes: OAuthProviderImpl.validateStringArray(clientMetadata.response_types) || ["code"],
|
|
735
|
+
registrationDate: Math.floor(Date.now() / 1e3),
|
|
736
|
+
tokenEndpointAuthMethod: authMethod
|
|
737
|
+
};
|
|
738
|
+
if (!isPublicClient && hashedSecret) clientInfo.clientSecret = hashedSecret;
|
|
739
|
+
} catch (error) {
|
|
740
|
+
return this.createErrorResponse("invalid_client_metadata", error instanceof Error ? error.message : "Invalid client metadata");
|
|
741
|
+
}
|
|
742
|
+
await env.OAUTH_KV.put(`client:${clientId}`, JSON.stringify(clientInfo));
|
|
743
|
+
const response = {
|
|
744
|
+
client_id: clientInfo.clientId,
|
|
745
|
+
redirect_uris: clientInfo.redirectUris,
|
|
746
|
+
client_name: clientInfo.clientName,
|
|
747
|
+
logo_uri: clientInfo.logoUri,
|
|
748
|
+
client_uri: clientInfo.clientUri,
|
|
749
|
+
policy_uri: clientInfo.policyUri,
|
|
750
|
+
tos_uri: clientInfo.tosUri,
|
|
751
|
+
jwks_uri: clientInfo.jwksUri,
|
|
752
|
+
contacts: clientInfo.contacts,
|
|
753
|
+
grant_types: clientInfo.grantTypes,
|
|
754
|
+
response_types: clientInfo.responseTypes,
|
|
755
|
+
token_endpoint_auth_method: clientInfo.tokenEndpointAuthMethod,
|
|
756
|
+
registration_client_uri: `${this.options.clientRegistrationEndpoint}/${clientId}`,
|
|
757
|
+
client_id_issued_at: clientInfo.registrationDate
|
|
758
|
+
};
|
|
759
|
+
if (clientSecret) response.client_secret = clientSecret;
|
|
760
|
+
return new Response(JSON.stringify(response), {
|
|
761
|
+
status: 201,
|
|
762
|
+
headers: { "Content-Type": "application/json" }
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* Handles API requests by validating the access token and calling the API handler
|
|
767
|
+
* @param request - The HTTP request
|
|
768
|
+
* @param env - Cloudflare Worker environment variables
|
|
769
|
+
* @param ctx - Cloudflare Worker execution context
|
|
770
|
+
* @returns Response from the API handler or error
|
|
771
|
+
*/
|
|
772
|
+
async handleApiRequest(request, env, ctx) {
|
|
773
|
+
const authHeader = request.headers.get("Authorization");
|
|
774
|
+
if (!authHeader || !authHeader.startsWith("Bearer ")) return this.createErrorResponse("invalid_token", "Missing or invalid access token", 401, { "WWW-Authenticate": "Bearer realm=\"OAuth\", error=\"invalid_token\", error_description=\"Missing or invalid access token\"" });
|
|
775
|
+
const accessToken = authHeader.substring(7);
|
|
776
|
+
const parts = accessToken.split(":");
|
|
777
|
+
const isPossiblyInternalFormat = parts.length === 3;
|
|
778
|
+
let tokenData = null;
|
|
779
|
+
let userId = "";
|
|
780
|
+
let grantId = "";
|
|
781
|
+
if (isPossiblyInternalFormat) {
|
|
782
|
+
[userId, grantId] = parts;
|
|
783
|
+
const id = await generateTokenId(accessToken);
|
|
784
|
+
tokenData = await env.OAUTH_KV.get(`token:${userId}:${grantId}:${id}`, { type: "json" });
|
|
785
|
+
}
|
|
786
|
+
if (!tokenData && !this.options.resolveExternalToken) return this.createErrorResponse("invalid_token", "Invalid access token", 401, { "WWW-Authenticate": "Bearer realm=\"OAuth\", error=\"invalid_token\"" });
|
|
787
|
+
if (tokenData) {
|
|
788
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
789
|
+
if (tokenData.expiresAt < now) return this.createErrorResponse("invalid_token", "Access token expired", 401, { "WWW-Authenticate": "Bearer realm=\"OAuth\", error=\"invalid_token\"" });
|
|
790
|
+
if (tokenData.audience) {
|
|
791
|
+
const requestUrl = new URL(request.url);
|
|
792
|
+
const resourceServer = `${requestUrl.protocol}//${requestUrl.host}`;
|
|
793
|
+
if (!(Array.isArray(tokenData.audience) ? tokenData.audience : [tokenData.audience]).some((aud) => audienceMatches(resourceServer, aud))) return this.createErrorResponse("invalid_token", "Token audience does not match resource server", 401, { "WWW-Authenticate": "Bearer realm=\"OAuth\", error=\"invalid_token\", error_description=\"Invalid audience\"" });
|
|
794
|
+
}
|
|
795
|
+
ctx.props = await decryptProps(await unwrapKeyWithToken(accessToken, tokenData.wrappedEncryptionKey), tokenData.grant.encryptedProps);
|
|
796
|
+
} else if (this.options.resolveExternalToken) {
|
|
797
|
+
const ext = await this.options.resolveExternalToken({
|
|
798
|
+
token: accessToken,
|
|
799
|
+
request,
|
|
800
|
+
env
|
|
801
|
+
});
|
|
802
|
+
if (!ext) return this.createErrorResponse("invalid_token", "Invalid access token", 401, { "WWW-Authenticate": "Bearer realm=\"OAuth\", error=\"invalid_token\"" });
|
|
803
|
+
if (ext.audience) {
|
|
804
|
+
const requestUrl = new URL(request.url);
|
|
805
|
+
const resourceServer = `${requestUrl.protocol}//${requestUrl.host}`;
|
|
806
|
+
if (!(Array.isArray(ext.audience) ? ext.audience : [ext.audience]).some((aud) => audienceMatches(resourceServer, aud))) return this.createErrorResponse("invalid_token", "Token audience does not match resource server", 401, { "WWW-Authenticate": "Bearer realm=\"OAuth\", error=\"invalid_token\", error_description=\"Invalid audience\"" });
|
|
807
|
+
}
|
|
808
|
+
ctx.props = ext.props;
|
|
809
|
+
}
|
|
810
|
+
if (!env.OAUTH_PROVIDER) env.OAUTH_PROVIDER = this.createOAuthHelpers(env);
|
|
811
|
+
const url = new URL(request.url);
|
|
812
|
+
const apiHandler = this.findApiHandlerForUrl(url);
|
|
813
|
+
if (!apiHandler) return this.createErrorResponse("invalid_request", "No handler found for API route", 404);
|
|
814
|
+
if (apiHandler.type === HandlerType.EXPORTED_HANDLER) return apiHandler.handler.fetch(request, env, ctx);
|
|
815
|
+
else return new apiHandler.handler(ctx, env).fetch(request);
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* Creates the helper methods object for OAuth operations
|
|
819
|
+
* This is passed to the handler functions to allow them to interact with the OAuth system
|
|
820
|
+
* @param env - Cloudflare Worker environment variables
|
|
821
|
+
* @returns An instance of OAuthHelpers
|
|
822
|
+
*/
|
|
823
|
+
createOAuthHelpers(env) {
|
|
824
|
+
return new OAuthHelpersImpl(env, this);
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Saves a grant to KV with appropriate TTL based on expiration
|
|
828
|
+
* @param env - The environment bindings
|
|
829
|
+
* @param grantKey - The KV key for the grant
|
|
830
|
+
* @param grantData - The grant data to save
|
|
831
|
+
* @param now - Current timestamp in seconds
|
|
832
|
+
*/
|
|
833
|
+
async saveGrantWithTTL(env, grantKey, grantData, now) {
|
|
834
|
+
const kvOptions = grantData.expiresAt !== void 0 ? { expiration: grantData.expiresAt } : {};
|
|
835
|
+
await env.OAUTH_KV.put(grantKey, JSON.stringify(grantData), kvOptions);
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* Fetches client information from KV storage or via CIMD (Client ID Metadata Document)
|
|
839
|
+
* This method is not private because `OAuthHelpers` needs to call it. Note that since
|
|
840
|
+
* `OAuthProviderImpl` is not exposed outside this module, this is still effectively
|
|
841
|
+
* module-private.
|
|
842
|
+
*
|
|
843
|
+
* Supports CIMD: If clientId is an HTTPS URL with a non-root path, the metadata
|
|
844
|
+
* document will be fetched from that URL instead of looking up in KV storage.
|
|
845
|
+
*
|
|
846
|
+
* @param env - Cloudflare Worker environment variables
|
|
847
|
+
* @param clientId - The client ID to look up (can be a regular ID or an HTTPS URL for CIMD)
|
|
848
|
+
* @returns The client information, or null if not found
|
|
849
|
+
*/
|
|
850
|
+
async getClient(env, clientId) {
|
|
851
|
+
if (this.isClientMetadataUrl(clientId)) {
|
|
852
|
+
if (!this.hasGlobalFetchStrictlyPublic()) throw new Error(`Client ID "${clientId}" appears to be a CIMD URL, but the 'global_fetch_strictly_public' compatibility flag is not enabled. Add this flag to your wrangler.jsonc to enable CIMD support.`);
|
|
853
|
+
return this.fetchClientMetadataDocument(clientId);
|
|
854
|
+
}
|
|
855
|
+
const clientKey = `client:${clientId}`;
|
|
856
|
+
return env.OAUTH_KV.get(clientKey, { type: "json" });
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Checks if the global_fetch_strictly_public compatibility flag is enabled.
|
|
860
|
+
* This flag is required for CIMD to prevent SSRF attacks.
|
|
861
|
+
* See: https://developers.cloudflare.com/workers/configuration/compatibility-flags/#global-fetch-strictly-public
|
|
862
|
+
*/
|
|
863
|
+
hasGlobalFetchStrictlyPublic() {
|
|
864
|
+
return !!(typeof Cloudflare !== "undefined" && Cloudflare.compatibilityFlags ? Cloudflare.compatibilityFlags : null)?.global_fetch_strictly_public;
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Checks if a client_id is a CIMD URL (HTTPS with non-root path)
|
|
868
|
+
*/
|
|
869
|
+
isClientMetadataUrl(clientId) {
|
|
870
|
+
try {
|
|
871
|
+
const url = new URL(clientId);
|
|
872
|
+
return url.protocol === "https:" && url.pathname !== "/";
|
|
873
|
+
} catch {
|
|
874
|
+
return false;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
static {
|
|
878
|
+
this.CIMD_MAX_SIZE_BYTES = 5 * 1024;
|
|
879
|
+
}
|
|
880
|
+
static {
|
|
881
|
+
this.CIMD_FETCH_TIMEOUT_MS = 1e4;
|
|
882
|
+
}
|
|
883
|
+
static {
|
|
884
|
+
this.CIMD_ALLOWED_AUTH_METHODS = ["none", "private_key_jwt"];
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* Validates that a field is a string or undefined
|
|
888
|
+
* @param field - The field value to validate
|
|
889
|
+
* @param fieldName - Name of the field for error messages
|
|
890
|
+
* @returns The validated string or undefined
|
|
891
|
+
* @throws Error if field is not a string or undefined
|
|
892
|
+
*/
|
|
893
|
+
static validateStringField(field, fieldName) {
|
|
894
|
+
if (field === void 0) return void 0;
|
|
895
|
+
if (typeof field !== "string") throw new Error(fieldName ? `Invalid ${fieldName}: expected string, got ${typeof field}` : "Field must be a string");
|
|
896
|
+
return field;
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* Validates that a field is a string array or undefined
|
|
900
|
+
* @param arr - The array to validate
|
|
901
|
+
* @param fieldName - Name of the field for error messages
|
|
902
|
+
* @returns The validated string array or undefined
|
|
903
|
+
* @throws Error if field is not a string array or undefined
|
|
904
|
+
*/
|
|
905
|
+
static validateStringArray(arr, fieldName) {
|
|
906
|
+
if (arr === void 0) return void 0;
|
|
907
|
+
if (!Array.isArray(arr)) throw new Error(fieldName ? `Invalid ${fieldName}: expected array, got ${typeof arr}` : "Field must be an array");
|
|
908
|
+
if (!arr.every((item) => typeof item === "string")) throw new Error(fieldName ? `Invalid ${fieldName}: array must contain only strings` : "All array elements must be strings");
|
|
909
|
+
return arr;
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* Fetches and validates a Client ID Metadata Document from the given URL
|
|
913
|
+
* Per the MCP spec, the client_id in the document must match the URL exactly
|
|
914
|
+
*
|
|
915
|
+
* Uses Cloudflare HTTP cache for caching (via cacheEverything option).
|
|
916
|
+
* Response size is limited to 5KB per IETF spec.
|
|
917
|
+
*
|
|
918
|
+
* @param metadataUrl - The HTTPS URL to fetch metadata from
|
|
919
|
+
* @returns The client information
|
|
920
|
+
* @throws Error if fetch fails or validation fails
|
|
921
|
+
*/
|
|
922
|
+
async fetchClientMetadataDocument(metadataUrl) {
|
|
923
|
+
const abortController = new AbortController();
|
|
924
|
+
const timeoutId = setTimeout(() => abortController.abort(), OAuthProviderImpl.CIMD_FETCH_TIMEOUT_MS);
|
|
925
|
+
try {
|
|
926
|
+
const response = await fetch(metadataUrl, {
|
|
927
|
+
headers: { Accept: "application/json" },
|
|
928
|
+
signal: abortController.signal,
|
|
929
|
+
cf: { cacheEverything: true }
|
|
930
|
+
});
|
|
931
|
+
clearTimeout(timeoutId);
|
|
932
|
+
if (!response.ok) throw new Error(`Failed to fetch client metadata: HTTP ${response.status}`);
|
|
933
|
+
const contentLength = response.headers.get("content-length");
|
|
934
|
+
if (contentLength && parseInt(contentLength, 10) > OAuthProviderImpl.CIMD_MAX_SIZE_BYTES) throw new Error(`Client metadata exceeds size limit: ${contentLength} bytes (max ${OAuthProviderImpl.CIMD_MAX_SIZE_BYTES})`);
|
|
935
|
+
const rawMetadata = await this.readJsonWithSizeLimit(response, OAuthProviderImpl.CIMD_MAX_SIZE_BYTES);
|
|
936
|
+
const clientId = OAuthProviderImpl.validateStringField(rawMetadata.client_id, "client_id");
|
|
937
|
+
const redirectUris = OAuthProviderImpl.validateStringArray(rawMetadata.redirect_uris, "redirect_uris");
|
|
938
|
+
const tokenEndpointAuthMethod = OAuthProviderImpl.validateStringField(rawMetadata.token_endpoint_auth_method, "token_endpoint_auth_method");
|
|
939
|
+
if (clientId !== metadataUrl) throw new Error(`client_id "${clientId}" does not match metadata URL "${metadataUrl}"`);
|
|
940
|
+
if (!redirectUris || redirectUris.length === 0) throw new Error("redirect_uris is required and must not be empty");
|
|
941
|
+
if (tokenEndpointAuthMethod && !OAuthProviderImpl.CIMD_ALLOWED_AUTH_METHODS.includes(tokenEndpointAuthMethod)) throw new Error(`token_endpoint_auth_method "${tokenEndpointAuthMethod}" is not allowed for CIMD clients. Allowed methods: ${OAuthProviderImpl.CIMD_ALLOWED_AUTH_METHODS.join(", ")}`);
|
|
942
|
+
return {
|
|
943
|
+
clientId,
|
|
944
|
+
redirectUris,
|
|
945
|
+
clientName: OAuthProviderImpl.validateStringField(rawMetadata.client_name, "client_name"),
|
|
946
|
+
clientUri: OAuthProviderImpl.validateStringField(rawMetadata.client_uri, "client_uri"),
|
|
947
|
+
logoUri: OAuthProviderImpl.validateStringField(rawMetadata.logo_uri, "logo_uri"),
|
|
948
|
+
policyUri: OAuthProviderImpl.validateStringField(rawMetadata.policy_uri, "policy_uri"),
|
|
949
|
+
tosUri: OAuthProviderImpl.validateStringField(rawMetadata.tos_uri, "tos_uri"),
|
|
950
|
+
jwksUri: OAuthProviderImpl.validateStringField(rawMetadata.jwks_uri, "jwks_uri"),
|
|
951
|
+
contacts: OAuthProviderImpl.validateStringArray(rawMetadata.contacts, "contacts"),
|
|
952
|
+
grantTypes: OAuthProviderImpl.validateStringArray(rawMetadata.grant_types, "grant_types") || ["authorization_code"],
|
|
953
|
+
responseTypes: OAuthProviderImpl.validateStringArray(rawMetadata.response_types, "response_types") || ["code"],
|
|
954
|
+
tokenEndpointAuthMethod: tokenEndpointAuthMethod || "none"
|
|
955
|
+
};
|
|
956
|
+
} finally {
|
|
957
|
+
clearTimeout(timeoutId);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
/**
|
|
961
|
+
* Reads JSON from a response with a size limit to prevent DoS attacks.
|
|
962
|
+
* Streams the response body and aborts if it exceeds the limit.
|
|
963
|
+
*
|
|
964
|
+
* @param response - The fetch response
|
|
965
|
+
* @param maxBytes - Maximum allowed size in bytes
|
|
966
|
+
* @returns Parsed JSON object
|
|
967
|
+
* @throws Error if response body is null, size exceeded, or JSON parse failed
|
|
968
|
+
*/
|
|
969
|
+
async readJsonWithSizeLimit(response, maxBytes) {
|
|
970
|
+
const reader = response.body?.getReader();
|
|
971
|
+
if (!reader) throw new Error("Response body is null");
|
|
972
|
+
const chunks = [];
|
|
973
|
+
let totalSize = 0;
|
|
974
|
+
while (true) {
|
|
975
|
+
const { done, value } = await reader.read();
|
|
976
|
+
if (done) break;
|
|
977
|
+
if (value) {
|
|
978
|
+
totalSize += value.length;
|
|
979
|
+
if (totalSize > maxBytes) {
|
|
980
|
+
await reader.cancel();
|
|
981
|
+
throw new Error(`Response exceeded size limit of ${maxBytes} bytes`);
|
|
982
|
+
}
|
|
983
|
+
chunks.push(value);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
const allChunks = new Uint8Array(totalSize);
|
|
987
|
+
let position = 0;
|
|
988
|
+
for (const chunk of chunks) {
|
|
989
|
+
allChunks.set(chunk, position);
|
|
990
|
+
position += chunk.length;
|
|
991
|
+
}
|
|
992
|
+
const text = new TextDecoder().decode(allChunks);
|
|
993
|
+
return JSON.parse(text);
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Helper function to create OAuth error responses
|
|
997
|
+
* @param code - OAuth error code (e.g., 'invalid_request', 'invalid_token')
|
|
998
|
+
* @param description - Human-readable error description
|
|
999
|
+
* @param status - HTTP status code (default: 400)
|
|
1000
|
+
* @param headers - Additional headers to include
|
|
1001
|
+
* @returns A Response object with the error
|
|
1002
|
+
*/
|
|
1003
|
+
createErrorResponse(code, description, status = 400, headers = {}) {
|
|
1004
|
+
const customErrorResponse = this.options.onError?.({
|
|
1005
|
+
code,
|
|
1006
|
+
description,
|
|
1007
|
+
status,
|
|
1008
|
+
headers
|
|
1009
|
+
});
|
|
1010
|
+
if (customErrorResponse) return customErrorResponse;
|
|
1011
|
+
const body = JSON.stringify({
|
|
1012
|
+
error: code,
|
|
1013
|
+
error_description: description
|
|
1014
|
+
});
|
|
1015
|
+
return new Response(body, {
|
|
1016
|
+
status,
|
|
1017
|
+
headers: {
|
|
1018
|
+
"Content-Type": "application/json",
|
|
1019
|
+
...headers
|
|
1020
|
+
}
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1051
1023
|
};
|
|
1052
|
-
|
|
1053
|
-
|
|
1024
|
+
/**
|
|
1025
|
+
* Default expiration time for access tokens (1 hour in seconds)
|
|
1026
|
+
*/
|
|
1027
|
+
const DEFAULT_ACCESS_TOKEN_TTL = 3600;
|
|
1028
|
+
/**
|
|
1029
|
+
* Length of generated token strings
|
|
1030
|
+
*/
|
|
1031
|
+
const TOKEN_LENGTH = 32;
|
|
1032
|
+
/**
|
|
1033
|
+
* Validates a resource URI per RFC 8707 Section 2
|
|
1034
|
+
* @param uri - The URI string to validate
|
|
1035
|
+
* @returns true if valid, false otherwise
|
|
1036
|
+
*/
|
|
1037
|
+
function validateResourceUri(uri) {
|
|
1038
|
+
if (!uri || typeof uri !== "string") return false;
|
|
1039
|
+
try {
|
|
1040
|
+
const parsed = new URL(uri);
|
|
1041
|
+
if (!parsed.protocol) return false;
|
|
1042
|
+
if (parsed.hash) return false;
|
|
1043
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
|
|
1044
|
+
return true;
|
|
1045
|
+
} catch {
|
|
1046
|
+
return false;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
/**
|
|
1050
|
+
* Checks if a resource server matches an audience claim
|
|
1051
|
+
* RFC 7519 Section 4.1.3: audience values are case-sensitive strings
|
|
1052
|
+
* @param resourceServerUrl - The resource server URL (from request)
|
|
1053
|
+
* @param audienceValue - The audience value from token
|
|
1054
|
+
* @returns true if they match, false otherwise
|
|
1055
|
+
*/
|
|
1056
|
+
function audienceMatches(resourceServerUrl, audienceValue) {
|
|
1057
|
+
return resourceServerUrl === audienceValue;
|
|
1058
|
+
}
|
|
1059
|
+
/**
|
|
1060
|
+
* Parses and validates the resource parameter from a token request (RFC 8707)
|
|
1061
|
+
* Handles single string or array of strings (from multiple form parameters)
|
|
1062
|
+
* @param value - The resource parameter value from the request body
|
|
1063
|
+
* @returns The validated value as string, string array, or undefined if validation fails
|
|
1064
|
+
*/
|
|
1065
|
+
function parseResourceParameter(value) {
|
|
1066
|
+
if (!value) return;
|
|
1067
|
+
const uris = Array.isArray(value) ? value : [value];
|
|
1068
|
+
for (const uri of uris) if (typeof uri !== "string" || !validateResourceUri(uri)) return;
|
|
1069
|
+
return value;
|
|
1070
|
+
}
|
|
1071
|
+
/**
|
|
1072
|
+
* Hashes a secret value using SHA-256
|
|
1073
|
+
* @param secret - The secret value to hash
|
|
1074
|
+
* @returns A hex string representation of the hash
|
|
1075
|
+
*/
|
|
1054
1076
|
async function hashSecret(secret) {
|
|
1055
|
-
|
|
1077
|
+
return generateTokenId(secret);
|
|
1056
1078
|
}
|
|
1079
|
+
/**
|
|
1080
|
+
* Generates a cryptographically secure random string
|
|
1081
|
+
* @param length - The length of the string to generate
|
|
1082
|
+
* @returns A random string of the specified length
|
|
1083
|
+
*/
|
|
1057
1084
|
function generateRandomString(length) {
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
}
|
|
1065
|
-
return result;
|
|
1085
|
+
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
1086
|
+
let result = "";
|
|
1087
|
+
const values = new Uint8Array(length);
|
|
1088
|
+
crypto.getRandomValues(values);
|
|
1089
|
+
for (let i = 0; i < length; i++) result += characters.charAt(values[i] % 64);
|
|
1090
|
+
return result;
|
|
1066
1091
|
}
|
|
1092
|
+
/**
|
|
1093
|
+
* Generates a token ID by hashing the token value using SHA-256
|
|
1094
|
+
* @param token - The token to hash
|
|
1095
|
+
* @returns A hex string representation of the hash
|
|
1096
|
+
*/
|
|
1067
1097
|
async function generateTokenId(token) {
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
1072
|
-
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1073
|
-
return hashHex;
|
|
1098
|
+
const data = new TextEncoder().encode(token);
|
|
1099
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
1100
|
+
return Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1074
1101
|
}
|
|
1102
|
+
/**
|
|
1103
|
+
* Validates that a redirect URI does not use a dangerous pseudo-scheme.
|
|
1104
|
+
* Normalizes the URI by trimming whitespace and checking the scheme in a
|
|
1105
|
+
* case-insensitive manner to prevent bypass attacks.
|
|
1106
|
+
* Per RFC 3986, control characters are explicitly disallowed in URIs and
|
|
1107
|
+
* will cause rejection rather than silent removal.
|
|
1108
|
+
* @param redirectUri - The redirect URI to validate
|
|
1109
|
+
* @throws Error if the URI uses a blacklisted scheme or contains control characters
|
|
1110
|
+
*/
|
|
1075
1111
|
function validateRedirectUriScheme(redirectUri) {
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
}
|
|
1112
|
+
const dangerousSchemes = [
|
|
1113
|
+
"javascript:",
|
|
1114
|
+
"data:",
|
|
1115
|
+
"vbscript:",
|
|
1116
|
+
"file:",
|
|
1117
|
+
"mailto:",
|
|
1118
|
+
"blob:"
|
|
1119
|
+
];
|
|
1120
|
+
const normalized = redirectUri.trim();
|
|
1121
|
+
for (let i = 0; i < normalized.length; i++) {
|
|
1122
|
+
const code = normalized.charCodeAt(i);
|
|
1123
|
+
if (code >= 0 && code <= 31 || code >= 127 && code <= 159) throw new Error("Invalid redirect URI");
|
|
1124
|
+
}
|
|
1125
|
+
const colonIndex = normalized.indexOf(":");
|
|
1126
|
+
if (colonIndex === -1) throw new Error("Invalid redirect URI");
|
|
1127
|
+
const scheme = normalized.substring(0, colonIndex + 1).toLowerCase();
|
|
1128
|
+
for (const dangerousScheme of dangerousSchemes) if (scheme === dangerousScheme) throw new Error("Invalid redirect URI");
|
|
1094
1129
|
}
|
|
1130
|
+
/**
|
|
1131
|
+
* Encodes a string as base64url (URL-safe base64)
|
|
1132
|
+
* @param str - The string to encode
|
|
1133
|
+
* @returns The base64url encoded string
|
|
1134
|
+
*/
|
|
1095
1135
|
function base64UrlEncode(str) {
|
|
1096
|
-
|
|
1136
|
+
return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
1097
1137
|
}
|
|
1138
|
+
/**
|
|
1139
|
+
* Encodes an ArrayBuffer as base64 string
|
|
1140
|
+
* @param buffer - The ArrayBuffer to encode
|
|
1141
|
+
* @returns The base64 encoded string
|
|
1142
|
+
*/
|
|
1098
1143
|
function arrayBufferToBase64(buffer) {
|
|
1099
|
-
|
|
1144
|
+
return btoa(String.fromCharCode(...new Uint8Array(buffer)));
|
|
1100
1145
|
}
|
|
1146
|
+
/**
|
|
1147
|
+
* Decodes a base64 string to an ArrayBuffer
|
|
1148
|
+
* @param base64 - The base64 string to decode
|
|
1149
|
+
* @returns The decoded ArrayBuffer
|
|
1150
|
+
*/
|
|
1101
1151
|
function base64ToArrayBuffer(base64) {
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
}
|
|
1107
|
-
return bytes.buffer;
|
|
1152
|
+
const binaryString = atob(base64);
|
|
1153
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
1154
|
+
for (let i = 0; i < binaryString.length; i++) bytes[i] = binaryString.charCodeAt(i);
|
|
1155
|
+
return bytes.buffer;
|
|
1108
1156
|
}
|
|
1157
|
+
/**
|
|
1158
|
+
* Encrypts props data with a newly generated key
|
|
1159
|
+
* @param data - The data to encrypt
|
|
1160
|
+
* @returns An object containing the encrypted data and the generated key
|
|
1161
|
+
*/
|
|
1109
1162
|
async function encryptProps(data) {
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
{
|
|
1125
|
-
name: "AES-GCM",
|
|
1126
|
-
iv
|
|
1127
|
-
},
|
|
1128
|
-
key,
|
|
1129
|
-
encodedData
|
|
1130
|
-
);
|
|
1131
|
-
return {
|
|
1132
|
-
encryptedData: arrayBufferToBase64(encryptedBuffer),
|
|
1133
|
-
key
|
|
1134
|
-
};
|
|
1163
|
+
const key = await crypto.subtle.generateKey({
|
|
1164
|
+
name: "AES-GCM",
|
|
1165
|
+
length: 256
|
|
1166
|
+
}, true, ["encrypt", "decrypt"]);
|
|
1167
|
+
const iv = new Uint8Array(12);
|
|
1168
|
+
const jsonData = JSON.stringify(data);
|
|
1169
|
+
const encodedData = new TextEncoder().encode(jsonData);
|
|
1170
|
+
return {
|
|
1171
|
+
encryptedData: arrayBufferToBase64(await crypto.subtle.encrypt({
|
|
1172
|
+
name: "AES-GCM",
|
|
1173
|
+
iv
|
|
1174
|
+
}, key, encodedData)),
|
|
1175
|
+
key
|
|
1176
|
+
};
|
|
1135
1177
|
}
|
|
1178
|
+
/**
|
|
1179
|
+
* Decrypts encrypted props data using the provided key
|
|
1180
|
+
* @param key - The CryptoKey to use for decryption
|
|
1181
|
+
* @param encryptedData - The encrypted data as a base64 string
|
|
1182
|
+
* @returns The decrypted data object
|
|
1183
|
+
*/
|
|
1136
1184
|
async function decryptProps(key, encryptedData) {
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
encryptedBuffer
|
|
1146
|
-
);
|
|
1147
|
-
const decoder = new TextDecoder();
|
|
1148
|
-
const jsonData = decoder.decode(decryptedBuffer);
|
|
1149
|
-
return JSON.parse(jsonData);
|
|
1185
|
+
const encryptedBuffer = base64ToArrayBuffer(encryptedData);
|
|
1186
|
+
const iv = new Uint8Array(12);
|
|
1187
|
+
const decryptedBuffer = await crypto.subtle.decrypt({
|
|
1188
|
+
name: "AES-GCM",
|
|
1189
|
+
iv
|
|
1190
|
+
}, key, encryptedBuffer);
|
|
1191
|
+
const jsonData = new TextDecoder().decode(decryptedBuffer);
|
|
1192
|
+
return JSON.parse(jsonData);
|
|
1150
1193
|
}
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1194
|
+
const WRAPPING_KEY_HMAC_KEY = new Uint8Array([
|
|
1195
|
+
34,
|
|
1196
|
+
126,
|
|
1197
|
+
38,
|
|
1198
|
+
134,
|
|
1199
|
+
141,
|
|
1200
|
+
241,
|
|
1201
|
+
225,
|
|
1202
|
+
109,
|
|
1203
|
+
128,
|
|
1204
|
+
112,
|
|
1205
|
+
234,
|
|
1206
|
+
23,
|
|
1207
|
+
151,
|
|
1208
|
+
91,
|
|
1209
|
+
71,
|
|
1210
|
+
166,
|
|
1211
|
+
130,
|
|
1212
|
+
24,
|
|
1213
|
+
250,
|
|
1214
|
+
135,
|
|
1215
|
+
40,
|
|
1216
|
+
174,
|
|
1217
|
+
222,
|
|
1218
|
+
133,
|
|
1219
|
+
181,
|
|
1220
|
+
29,
|
|
1221
|
+
74,
|
|
1222
|
+
217,
|
|
1223
|
+
150,
|
|
1224
|
+
202,
|
|
1225
|
+
202,
|
|
1226
|
+
67
|
|
1184
1227
|
]);
|
|
1228
|
+
/**
|
|
1229
|
+
* Derives a wrapping key from a token string
|
|
1230
|
+
* This intentionally uses a different method than token ID generation
|
|
1231
|
+
* to ensure the token ID cannot be used to derive the wrapping key
|
|
1232
|
+
* @param tokenStr - The token string to use as key material
|
|
1233
|
+
* @returns A Promise resolving to the derived CryptoKey
|
|
1234
|
+
*/
|
|
1185
1235
|
async function deriveKeyFromToken(tokenStr) {
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
);
|
|
1194
|
-
const hmacResult = await crypto.subtle.sign("HMAC", hmacKey, encoder.encode(tokenStr));
|
|
1195
|
-
return await crypto.subtle.importKey(
|
|
1196
|
-
"raw",
|
|
1197
|
-
hmacResult,
|
|
1198
|
-
{ name: "AES-KW" },
|
|
1199
|
-
false,
|
|
1200
|
-
// not extractable
|
|
1201
|
-
["wrapKey", "unwrapKey"]
|
|
1202
|
-
);
|
|
1236
|
+
const encoder = new TextEncoder();
|
|
1237
|
+
const hmacKey = await crypto.subtle.importKey("raw", WRAPPING_KEY_HMAC_KEY, {
|
|
1238
|
+
name: "HMAC",
|
|
1239
|
+
hash: "SHA-256"
|
|
1240
|
+
}, false, ["sign"]);
|
|
1241
|
+
const hmacResult = await crypto.subtle.sign("HMAC", hmacKey, encoder.encode(tokenStr));
|
|
1242
|
+
return await crypto.subtle.importKey("raw", hmacResult, { name: "AES-KW" }, false, ["wrapKey", "unwrapKey"]);
|
|
1203
1243
|
}
|
|
1244
|
+
/**
|
|
1245
|
+
* Wraps an encryption key using a token-derived key
|
|
1246
|
+
* @param tokenStr - The token string to use for key wrapping
|
|
1247
|
+
* @param keyToWrap - The encryption key to wrap
|
|
1248
|
+
* @returns A Promise resolving to the wrapped key as a base64 string
|
|
1249
|
+
*/
|
|
1204
1250
|
async function wrapKeyWithToken(tokenStr, keyToWrap) {
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
return arrayBufferToBase64(wrappedKeyBuffer);
|
|
1251
|
+
const wrappingKey = await deriveKeyFromToken(tokenStr);
|
|
1252
|
+
return arrayBufferToBase64(await crypto.subtle.wrapKey("raw", keyToWrap, wrappingKey, { name: "AES-KW" }));
|
|
1208
1253
|
}
|
|
1254
|
+
/**
|
|
1255
|
+
* Unwraps an encryption key using a token-derived key
|
|
1256
|
+
* @param tokenStr - The token string used for key wrapping
|
|
1257
|
+
* @param wrappedKeyBase64 - The wrapped key as a base64 string
|
|
1258
|
+
* @returns A Promise resolving to the unwrapped CryptoKey
|
|
1259
|
+
*/
|
|
1209
1260
|
async function unwrapKeyWithToken(tokenStr, wrappedKeyBase64) {
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
"raw",
|
|
1214
|
-
wrappedKeyBuffer,
|
|
1215
|
-
wrappingKey,
|
|
1216
|
-
{ name: "AES-KW" },
|
|
1217
|
-
{ name: "AES-GCM" },
|
|
1218
|
-
true,
|
|
1219
|
-
// extractable
|
|
1220
|
-
["encrypt", "decrypt"]
|
|
1221
|
-
);
|
|
1261
|
+
const wrappingKey = await deriveKeyFromToken(tokenStr);
|
|
1262
|
+
const wrappedKeyBuffer = base64ToArrayBuffer(wrappedKeyBase64);
|
|
1263
|
+
return await crypto.subtle.unwrapKey("raw", wrappedKeyBuffer, wrappingKey, { name: "AES-KW" }, { name: "AES-GCM" }, true, ["encrypt", "decrypt"]);
|
|
1222
1264
|
}
|
|
1265
|
+
/**
|
|
1266
|
+
* Class that implements the OAuth helper methods
|
|
1267
|
+
* Provides methods for OAuth operations needed by handlers
|
|
1268
|
+
*/
|
|
1223
1269
|
var OAuthHelpersImpl = class {
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
metadata: grantData.metadata,
|
|
1528
|
-
createdAt: grantData.createdAt,
|
|
1529
|
-
expiresAt: grantData.expiresAt
|
|
1530
|
-
};
|
|
1531
|
-
grantSummaries.push(summary);
|
|
1532
|
-
}
|
|
1533
|
-
});
|
|
1534
|
-
await Promise.all(promises);
|
|
1535
|
-
return {
|
|
1536
|
-
items: grantSummaries,
|
|
1537
|
-
cursor: response.list_complete ? void 0 : response.cursor
|
|
1538
|
-
};
|
|
1539
|
-
}
|
|
1540
|
-
/**
|
|
1541
|
-
* Revokes an authorization grant and all its associated access tokens
|
|
1542
|
-
* @param grantId - The ID of the grant to revoke
|
|
1543
|
-
* @param userId - The ID of the user who owns the grant
|
|
1544
|
-
* @returns A Promise resolving when the revocation is confirmed.
|
|
1545
|
-
*/
|
|
1546
|
-
async revokeGrant(grantId, userId) {
|
|
1547
|
-
const grantKey = `grant:${userId}:${grantId}`;
|
|
1548
|
-
const tokenPrefix = `token:${userId}:${grantId}:`;
|
|
1549
|
-
let cursor;
|
|
1550
|
-
let allTokensDeleted = false;
|
|
1551
|
-
while (!allTokensDeleted) {
|
|
1552
|
-
const listOptions = {
|
|
1553
|
-
prefix: tokenPrefix
|
|
1554
|
-
};
|
|
1555
|
-
if (cursor) {
|
|
1556
|
-
listOptions.cursor = cursor;
|
|
1557
|
-
}
|
|
1558
|
-
const result = await this.env.OAUTH_KV.list(listOptions);
|
|
1559
|
-
if (result.keys.length > 0) {
|
|
1560
|
-
await Promise.all(
|
|
1561
|
-
result.keys.map((key) => {
|
|
1562
|
-
return this.env.OAUTH_KV.delete(key.name);
|
|
1563
|
-
})
|
|
1564
|
-
);
|
|
1565
|
-
}
|
|
1566
|
-
if (result.list_complete) {
|
|
1567
|
-
allTokensDeleted = true;
|
|
1568
|
-
} else {
|
|
1569
|
-
cursor = result.cursor;
|
|
1570
|
-
}
|
|
1571
|
-
}
|
|
1572
|
-
await this.env.OAUTH_KV.delete(grantKey);
|
|
1573
|
-
}
|
|
1270
|
+
/**
|
|
1271
|
+
* Creates a new OAuthHelpers instance
|
|
1272
|
+
* @param env - Cloudflare Worker environment variables
|
|
1273
|
+
* @param provider - Reference to the parent provider instance
|
|
1274
|
+
*/
|
|
1275
|
+
constructor(env, provider) {
|
|
1276
|
+
this.env = env;
|
|
1277
|
+
this.provider = provider;
|
|
1278
|
+
}
|
|
1279
|
+
/**
|
|
1280
|
+
* Parses an OAuth authorization request from the HTTP request
|
|
1281
|
+
* @param request - The HTTP request containing OAuth parameters
|
|
1282
|
+
* @returns The parsed authorization request parameters
|
|
1283
|
+
*/
|
|
1284
|
+
async parseAuthRequest(request) {
|
|
1285
|
+
const url = new URL(request.url);
|
|
1286
|
+
const responseType = url.searchParams.get("response_type") || "";
|
|
1287
|
+
const clientId = url.searchParams.get("client_id") || "";
|
|
1288
|
+
const redirectUri = url.searchParams.get("redirect_uri") || "";
|
|
1289
|
+
const scope = (url.searchParams.get("scope") || "").split(" ").filter(Boolean);
|
|
1290
|
+
const state = url.searchParams.get("state") || "";
|
|
1291
|
+
const codeChallenge = url.searchParams.get("code_challenge") || void 0;
|
|
1292
|
+
const codeChallengeMethod = url.searchParams.get("code_challenge_method") || "plain";
|
|
1293
|
+
const resourceParams = url.searchParams.getAll("resource");
|
|
1294
|
+
const resourceParam = resourceParams.length > 0 ? resourceParams.length === 1 ? resourceParams[0] : resourceParams : void 0;
|
|
1295
|
+
validateRedirectUriScheme(redirectUri);
|
|
1296
|
+
const resource = parseResourceParameter(resourceParam);
|
|
1297
|
+
if (resourceParam && !resource) throw new Error("The resource parameter must be a valid absolute URI without a fragment");
|
|
1298
|
+
if (responseType === "token" && !this.provider.options.allowImplicitFlow) throw new Error("The implicit grant flow is not enabled for this provider");
|
|
1299
|
+
if (clientId) {
|
|
1300
|
+
const clientInfo = await this.lookupClient(clientId);
|
|
1301
|
+
if (!clientInfo) throw new Error(`Invalid client. The clientId provided does not match to this client.`);
|
|
1302
|
+
if (clientInfo && redirectUri) {
|
|
1303
|
+
if (!clientInfo.redirectUris.includes(redirectUri)) throw new Error(`Invalid redirect URI. The redirect URI provided does not match any registered URI for this client.`);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
return {
|
|
1307
|
+
responseType,
|
|
1308
|
+
clientId,
|
|
1309
|
+
redirectUri,
|
|
1310
|
+
scope,
|
|
1311
|
+
state,
|
|
1312
|
+
codeChallenge,
|
|
1313
|
+
codeChallengeMethod,
|
|
1314
|
+
resource
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
/**
|
|
1318
|
+
* Looks up a client by its client ID
|
|
1319
|
+
* @param clientId - The client ID to look up
|
|
1320
|
+
* @returns A Promise resolving to the client info, or null if not found
|
|
1321
|
+
*/
|
|
1322
|
+
async lookupClient(clientId) {
|
|
1323
|
+
return await this.provider.getClient(this.env, clientId);
|
|
1324
|
+
}
|
|
1325
|
+
/**
|
|
1326
|
+
* Completes an authorization request by creating a grant and either:
|
|
1327
|
+
* - For authorization code flow: generating an authorization code
|
|
1328
|
+
* - For implicit flow: generating an access token directly
|
|
1329
|
+
* @param options - Options specifying the grant details
|
|
1330
|
+
* @returns A Promise resolving to an object containing the redirect URL
|
|
1331
|
+
*/
|
|
1332
|
+
async completeAuthorization(options) {
|
|
1333
|
+
const { clientId, redirectUri } = options.request;
|
|
1334
|
+
if (!clientId || !redirectUri) throw new Error("Client ID and Redirect URI are required in the authorization request.");
|
|
1335
|
+
const clientInfo = await this.lookupClient(clientId);
|
|
1336
|
+
if (!clientInfo || !clientInfo.redirectUris.includes(redirectUri)) throw new Error("Invalid redirect URI. The redirect URI provided does not match any registered URI for this client.");
|
|
1337
|
+
const grantId = generateRandomString(16);
|
|
1338
|
+
const { encryptedData, key: encryptionKey } = await encryptProps(options.props);
|
|
1339
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1340
|
+
if (options.request.responseType === "token") {
|
|
1341
|
+
const accessTokenSecret = generateRandomString(TOKEN_LENGTH);
|
|
1342
|
+
const accessToken = `${options.userId}:${grantId}:${accessTokenSecret}`;
|
|
1343
|
+
const accessTokenId = await generateTokenId(accessToken);
|
|
1344
|
+
const accessTokenTTL = this.provider.options.accessTokenTTL || DEFAULT_ACCESS_TOKEN_TTL;
|
|
1345
|
+
const accessTokenExpiresAt = now + accessTokenTTL;
|
|
1346
|
+
const accessTokenWrappedKey = await wrapKeyWithToken(accessToken, encryptionKey);
|
|
1347
|
+
const audience = parseResourceParameter(options.request.resource);
|
|
1348
|
+
if (options.request.resource && !audience) throw new Error("The resource parameter must be a valid absolute URI without a fragment");
|
|
1349
|
+
const grant = {
|
|
1350
|
+
id: grantId,
|
|
1351
|
+
clientId: options.request.clientId,
|
|
1352
|
+
userId: options.userId,
|
|
1353
|
+
scope: options.scope,
|
|
1354
|
+
metadata: options.metadata,
|
|
1355
|
+
encryptedProps: encryptedData,
|
|
1356
|
+
createdAt: now,
|
|
1357
|
+
resource: options.request.resource
|
|
1358
|
+
};
|
|
1359
|
+
const grantKey = `grant:${options.userId}:${grantId}`;
|
|
1360
|
+
await this.env.OAUTH_KV.put(grantKey, JSON.stringify(grant));
|
|
1361
|
+
const accessTokenData = {
|
|
1362
|
+
id: accessTokenId,
|
|
1363
|
+
grantId,
|
|
1364
|
+
userId: options.userId,
|
|
1365
|
+
createdAt: now,
|
|
1366
|
+
expiresAt: accessTokenExpiresAt,
|
|
1367
|
+
audience,
|
|
1368
|
+
wrappedEncryptionKey: accessTokenWrappedKey,
|
|
1369
|
+
grant: {
|
|
1370
|
+
clientId: options.request.clientId,
|
|
1371
|
+
scope: options.scope,
|
|
1372
|
+
encryptedProps: encryptedData
|
|
1373
|
+
}
|
|
1374
|
+
};
|
|
1375
|
+
await this.env.OAUTH_KV.put(`token:${options.userId}:${grantId}:${accessTokenId}`, JSON.stringify(accessTokenData), { expirationTtl: accessTokenTTL });
|
|
1376
|
+
const redirectUrl = new URL(options.request.redirectUri);
|
|
1377
|
+
const fragment = new URLSearchParams();
|
|
1378
|
+
fragment.set("access_token", accessToken);
|
|
1379
|
+
fragment.set("token_type", "bearer");
|
|
1380
|
+
fragment.set("expires_in", accessTokenTTL.toString());
|
|
1381
|
+
fragment.set("scope", options.scope.join(" "));
|
|
1382
|
+
if (options.request.state) fragment.set("state", options.request.state);
|
|
1383
|
+
redirectUrl.hash = fragment.toString();
|
|
1384
|
+
return { redirectTo: redirectUrl.toString() };
|
|
1385
|
+
} else {
|
|
1386
|
+
const authCodeSecret = generateRandomString(32);
|
|
1387
|
+
const authCode = `${options.userId}:${grantId}:${authCodeSecret}`;
|
|
1388
|
+
const authCodeId = await hashSecret(authCode);
|
|
1389
|
+
const authCodeWrappedKey = await wrapKeyWithToken(authCode, encryptionKey);
|
|
1390
|
+
const grant = {
|
|
1391
|
+
id: grantId,
|
|
1392
|
+
clientId: options.request.clientId,
|
|
1393
|
+
userId: options.userId,
|
|
1394
|
+
scope: options.scope,
|
|
1395
|
+
metadata: options.metadata,
|
|
1396
|
+
encryptedProps: encryptedData,
|
|
1397
|
+
createdAt: now,
|
|
1398
|
+
authCodeId,
|
|
1399
|
+
authCodeWrappedKey,
|
|
1400
|
+
codeChallenge: options.request.codeChallenge,
|
|
1401
|
+
codeChallengeMethod: options.request.codeChallengeMethod,
|
|
1402
|
+
resource: options.request.resource
|
|
1403
|
+
};
|
|
1404
|
+
const grantKey = `grant:${options.userId}:${grantId}`;
|
|
1405
|
+
await this.env.OAUTH_KV.put(grantKey, JSON.stringify(grant), { expirationTtl: 600 });
|
|
1406
|
+
const redirectUrl = new URL(options.request.redirectUri);
|
|
1407
|
+
redirectUrl.searchParams.set("code", authCode);
|
|
1408
|
+
if (options.request.state) redirectUrl.searchParams.set("state", options.request.state);
|
|
1409
|
+
return { redirectTo: redirectUrl.toString() };
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
/**
|
|
1413
|
+
* Creates a new OAuth client
|
|
1414
|
+
* @param clientInfo - Partial client information to create the client with
|
|
1415
|
+
* @returns A Promise resolving to the created client info
|
|
1416
|
+
*/
|
|
1417
|
+
async createClient(clientInfo) {
|
|
1418
|
+
const clientId = generateRandomString(16);
|
|
1419
|
+
const tokenEndpointAuthMethod = clientInfo.tokenEndpointAuthMethod || "client_secret_basic";
|
|
1420
|
+
const isPublicClient = tokenEndpointAuthMethod === "none";
|
|
1421
|
+
const newClient = {
|
|
1422
|
+
clientId,
|
|
1423
|
+
redirectUris: clientInfo.redirectUris || [],
|
|
1424
|
+
clientName: clientInfo.clientName,
|
|
1425
|
+
logoUri: clientInfo.logoUri,
|
|
1426
|
+
clientUri: clientInfo.clientUri,
|
|
1427
|
+
policyUri: clientInfo.policyUri,
|
|
1428
|
+
tosUri: clientInfo.tosUri,
|
|
1429
|
+
jwksUri: clientInfo.jwksUri,
|
|
1430
|
+
contacts: clientInfo.contacts,
|
|
1431
|
+
grantTypes: clientInfo.grantTypes || ["authorization_code", "refresh_token"],
|
|
1432
|
+
responseTypes: clientInfo.responseTypes || ["code"],
|
|
1433
|
+
registrationDate: Math.floor(Date.now() / 1e3),
|
|
1434
|
+
tokenEndpointAuthMethod
|
|
1435
|
+
};
|
|
1436
|
+
for (const uri of newClient.redirectUris) validateRedirectUriScheme(uri);
|
|
1437
|
+
let clientSecret;
|
|
1438
|
+
if (!isPublicClient) {
|
|
1439
|
+
clientSecret = generateRandomString(32);
|
|
1440
|
+
newClient.clientSecret = await hashSecret(clientSecret);
|
|
1441
|
+
}
|
|
1442
|
+
await this.env.OAUTH_KV.put(`client:${clientId}`, JSON.stringify(newClient));
|
|
1443
|
+
const clientResponse = { ...newClient };
|
|
1444
|
+
if (!isPublicClient && clientSecret) clientResponse.clientSecret = clientSecret;
|
|
1445
|
+
return clientResponse;
|
|
1446
|
+
}
|
|
1447
|
+
/**
|
|
1448
|
+
* Lists all registered OAuth clients with pagination support
|
|
1449
|
+
* @param options - Optional pagination parameters (limit and cursor)
|
|
1450
|
+
* @returns A Promise resolving to the list result with items and optional cursor
|
|
1451
|
+
*/
|
|
1452
|
+
async listClients(options) {
|
|
1453
|
+
const listOptions = { prefix: "client:" };
|
|
1454
|
+
if (options?.limit !== void 0) listOptions.limit = options.limit;
|
|
1455
|
+
if (options?.cursor !== void 0) listOptions.cursor = options.cursor;
|
|
1456
|
+
const response = await this.env.OAUTH_KV.list(listOptions);
|
|
1457
|
+
const clients = [];
|
|
1458
|
+
const promises = response.keys.map(async (key) => {
|
|
1459
|
+
const clientId = key.name.substring(7);
|
|
1460
|
+
const client = await this.provider.getClient(this.env, clientId);
|
|
1461
|
+
if (client) clients.push(client);
|
|
1462
|
+
});
|
|
1463
|
+
await Promise.all(promises);
|
|
1464
|
+
return {
|
|
1465
|
+
items: clients,
|
|
1466
|
+
cursor: response.list_complete ? void 0 : response.cursor
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
/**
|
|
1470
|
+
* Updates an existing OAuth client
|
|
1471
|
+
* @param clientId - The ID of the client to update
|
|
1472
|
+
* @param updates - Partial client information with fields to update
|
|
1473
|
+
* @returns A Promise resolving to the updated client info, or null if not found
|
|
1474
|
+
*/
|
|
1475
|
+
async updateClient(clientId, updates) {
|
|
1476
|
+
const client = await this.provider.getClient(this.env, clientId);
|
|
1477
|
+
if (!client) return null;
|
|
1478
|
+
let authMethod = updates.tokenEndpointAuthMethod || client.tokenEndpointAuthMethod || "client_secret_basic";
|
|
1479
|
+
const isPublicClient = authMethod === "none";
|
|
1480
|
+
let secretToStore = client.clientSecret;
|
|
1481
|
+
let originalSecret = void 0;
|
|
1482
|
+
if (isPublicClient) secretToStore = void 0;
|
|
1483
|
+
else if (updates.clientSecret) {
|
|
1484
|
+
originalSecret = updates.clientSecret;
|
|
1485
|
+
secretToStore = await hashSecret(updates.clientSecret);
|
|
1486
|
+
}
|
|
1487
|
+
const updatedClient = {
|
|
1488
|
+
...client,
|
|
1489
|
+
...updates,
|
|
1490
|
+
clientId: client.clientId,
|
|
1491
|
+
tokenEndpointAuthMethod: authMethod
|
|
1492
|
+
};
|
|
1493
|
+
if (!isPublicClient && secretToStore) updatedClient.clientSecret = secretToStore;
|
|
1494
|
+
else delete updatedClient.clientSecret;
|
|
1495
|
+
await this.env.OAUTH_KV.put(`client:${clientId}`, JSON.stringify(updatedClient));
|
|
1496
|
+
const response = { ...updatedClient };
|
|
1497
|
+
if (!isPublicClient && originalSecret) response.clientSecret = originalSecret;
|
|
1498
|
+
return response;
|
|
1499
|
+
}
|
|
1500
|
+
/**
|
|
1501
|
+
* Deletes an OAuth client
|
|
1502
|
+
* @param clientId - The ID of the client to delete
|
|
1503
|
+
* @returns A Promise resolving when the deletion is confirmed.
|
|
1504
|
+
*/
|
|
1505
|
+
async deleteClient(clientId) {
|
|
1506
|
+
await this.env.OAUTH_KV.delete(`client:${clientId}`);
|
|
1507
|
+
}
|
|
1508
|
+
/**
|
|
1509
|
+
* Lists all authorization grants for a specific user with pagination support
|
|
1510
|
+
* Returns a summary of each grant without sensitive information
|
|
1511
|
+
* @param userId - The ID of the user whose grants to list
|
|
1512
|
+
* @param options - Optional pagination parameters (limit and cursor)
|
|
1513
|
+
* @returns A Promise resolving to the list result with grant summaries and optional cursor
|
|
1514
|
+
*/
|
|
1515
|
+
async listUserGrants(userId, options) {
|
|
1516
|
+
const listOptions = { prefix: `grant:${userId}:` };
|
|
1517
|
+
if (options?.limit !== void 0) listOptions.limit = options.limit;
|
|
1518
|
+
if (options?.cursor !== void 0) listOptions.cursor = options.cursor;
|
|
1519
|
+
const response = await this.env.OAUTH_KV.list(listOptions);
|
|
1520
|
+
const grantSummaries = [];
|
|
1521
|
+
const promises = response.keys.map(async (key) => {
|
|
1522
|
+
const grantData = await this.env.OAUTH_KV.get(key.name, { type: "json" });
|
|
1523
|
+
if (grantData) {
|
|
1524
|
+
const summary = {
|
|
1525
|
+
id: grantData.id,
|
|
1526
|
+
clientId: grantData.clientId,
|
|
1527
|
+
userId: grantData.userId,
|
|
1528
|
+
scope: grantData.scope,
|
|
1529
|
+
metadata: grantData.metadata,
|
|
1530
|
+
createdAt: grantData.createdAt,
|
|
1531
|
+
expiresAt: grantData.expiresAt
|
|
1532
|
+
};
|
|
1533
|
+
grantSummaries.push(summary);
|
|
1534
|
+
}
|
|
1535
|
+
});
|
|
1536
|
+
await Promise.all(promises);
|
|
1537
|
+
return {
|
|
1538
|
+
items: grantSummaries,
|
|
1539
|
+
cursor: response.list_complete ? void 0 : response.cursor
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
/**
|
|
1543
|
+
* Revokes an authorization grant and all its associated access tokens
|
|
1544
|
+
* @param grantId - The ID of the grant to revoke
|
|
1545
|
+
* @param userId - The ID of the user who owns the grant
|
|
1546
|
+
* @returns A Promise resolving when the revocation is confirmed.
|
|
1547
|
+
*/
|
|
1548
|
+
async revokeGrant(grantId, userId) {
|
|
1549
|
+
const grantKey = `grant:${userId}:${grantId}`;
|
|
1550
|
+
const tokenPrefix = `token:${userId}:${grantId}:`;
|
|
1551
|
+
let cursor;
|
|
1552
|
+
let allTokensDeleted = false;
|
|
1553
|
+
while (!allTokensDeleted) {
|
|
1554
|
+
const listOptions = { prefix: tokenPrefix };
|
|
1555
|
+
if (cursor) listOptions.cursor = cursor;
|
|
1556
|
+
const result = await this.env.OAUTH_KV.list(listOptions);
|
|
1557
|
+
if (result.keys.length > 0) await Promise.all(result.keys.map((key) => {
|
|
1558
|
+
return this.env.OAUTH_KV.delete(key.name);
|
|
1559
|
+
}));
|
|
1560
|
+
if (result.list_complete) allTokensDeleted = true;
|
|
1561
|
+
else cursor = result.cursor;
|
|
1562
|
+
}
|
|
1563
|
+
await this.env.OAUTH_KV.delete(grantKey);
|
|
1564
|
+
}
|
|
1565
|
+
/**
|
|
1566
|
+
* Decodes a token and returns token data with decrypted props
|
|
1567
|
+
* @param token - The token
|
|
1568
|
+
* @returns Promise resolving to token data with decrypted props, or null if token is invalid
|
|
1569
|
+
*/
|
|
1570
|
+
async unwrapToken(token) {
|
|
1571
|
+
return await this.provider.unwrapToken(token, this.env);
|
|
1572
|
+
}
|
|
1574
1573
|
};
|
|
1574
|
+
/**
|
|
1575
|
+
* Default export of the OAuth provider
|
|
1576
|
+
* This allows users to import the library and use it directly as in the example
|
|
1577
|
+
*/
|
|
1575
1578
|
var oauth_provider_default = OAuthProvider;
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
};
|
|
1579
|
+
|
|
1580
|
+
//#endregion
|
|
1581
|
+
export { OAuthProvider, oauth_provider_default as default };
|