@atproto/oauth-provider 0.19.7 → 0.19.8

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.
Files changed (165) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/errors/error-parser.js +5 -5
  3. package/dist/errors/error-parser.js.map +1 -1
  4. package/package.json +22 -18
  5. package/src/access-token/access-token-mode.ts +0 -4
  6. package/src/account/account-manager.ts +0 -591
  7. package/src/account/account-store.ts +0 -305
  8. package/src/account/sign-in-data.ts +0 -15
  9. package/src/account/sign-up-input.ts +0 -20
  10. package/src/client/client-auth.ts +0 -64
  11. package/src/client/client-data.ts +0 -9
  12. package/src/client/client-id.ts +0 -4
  13. package/src/client/client-info.ts +0 -13
  14. package/src/client/client-manager.ts +0 -772
  15. package/src/client/client-store.ts +0 -35
  16. package/src/client/client-utils.ts +0 -37
  17. package/src/client/client.ts +0 -394
  18. package/src/constants.ts +0 -80
  19. package/src/customization/branding.ts +0 -12
  20. package/src/customization/build-customization-css.ts +0 -55
  21. package/src/customization/build-customization-data.ts +0 -24
  22. package/src/customization/colors.ts +0 -48
  23. package/src/customization/customization.ts +0 -29
  24. package/src/customization/links.ts +0 -10
  25. package/src/device/device-data.ts +0 -11
  26. package/src/device/device-id.ts +0 -27
  27. package/src/device/device-manager.ts +0 -292
  28. package/src/device/device-store.ts +0 -31
  29. package/src/device/session-id.ts +0 -21
  30. package/src/dpop/dpop-manager.ts +0 -214
  31. package/src/dpop/dpop-nonce.ts +0 -121
  32. package/src/dpop/dpop-proof.ts +0 -6
  33. package/src/errors/access-denied-error.ts +0 -12
  34. package/src/errors/account-selection-required-error.ts +0 -12
  35. package/src/errors/authorization-error.ts +0 -45
  36. package/src/errors/consent-required-error.ts +0 -12
  37. package/src/errors/error-parser.ts +0 -147
  38. package/src/errors/handle-unavailable-error.ts +0 -23
  39. package/src/errors/invalid-authorization-details-error.ts +0 -27
  40. package/src/errors/invalid-client-error.ts +0 -20
  41. package/src/errors/invalid-client-id-error.ts +0 -31
  42. package/src/errors/invalid-client-metadata-error.ts +0 -68
  43. package/src/errors/invalid-credentials-error.ts +0 -29
  44. package/src/errors/invalid-dpop-key-binding-error.ts +0 -21
  45. package/src/errors/invalid-dpop-proof-error.ts +0 -13
  46. package/src/errors/invalid-grant-error.ts +0 -21
  47. package/src/errors/invalid-invite-code-error.ts +0 -10
  48. package/src/errors/invalid-redirect-uri-error.ts +0 -17
  49. package/src/errors/invalid-request-error.ts +0 -32
  50. package/src/errors/invalid-scope-error.ts +0 -15
  51. package/src/errors/invalid-token-error.ts +0 -60
  52. package/src/errors/login-required-error.ts +0 -12
  53. package/src/errors/oauth-error.ts +0 -28
  54. package/src/errors/second-authentication-factor-required-error.ts +0 -25
  55. package/src/errors/unauthorized-client-error.ts +0 -20
  56. package/src/errors/use-dpop-nonce-error.ts +0 -32
  57. package/src/errors/www-authenticate-error.ts +0 -64
  58. package/src/index.ts +0 -16
  59. package/src/lexicon/lexicon-data.ts +0 -11
  60. package/src/lexicon/lexicon-getter.ts +0 -55
  61. package/src/lexicon/lexicon-manager.ts +0 -116
  62. package/src/lexicon/lexicon-store.ts +0 -35
  63. package/src/lib/csp/index.ts +0 -101
  64. package/src/lib/hcaptcha.ts +0 -228
  65. package/src/lib/html/README.md +0 -9
  66. package/src/lib/html/build-document.ts +0 -151
  67. package/src/lib/html/escapers.ts +0 -66
  68. package/src/lib/html/html.ts +0 -56
  69. package/src/lib/html/hydration-data.ts +0 -19
  70. package/src/lib/html/index.ts +0 -5
  71. package/src/lib/html/tags.ts +0 -67
  72. package/src/lib/html/util.ts +0 -17
  73. package/src/lib/http/README.md +0 -11
  74. package/src/lib/http/accept.ts +0 -90
  75. package/src/lib/http/context.ts +0 -42
  76. package/src/lib/http/headers.ts +0 -15
  77. package/src/lib/http/index.ts +0 -10
  78. package/src/lib/http/method.ts +0 -18
  79. package/src/lib/http/middleware.ts +0 -169
  80. package/src/lib/http/parser.ts +0 -101
  81. package/src/lib/http/path.ts +0 -82
  82. package/src/lib/http/request.ts +0 -256
  83. package/src/lib/http/response.ts +0 -122
  84. package/src/lib/http/route.ts +0 -60
  85. package/src/lib/http/router.ts +0 -117
  86. package/src/lib/http/security-headers.ts +0 -100
  87. package/src/lib/http/stream.ts +0 -57
  88. package/src/lib/http/types.ts +0 -16
  89. package/src/lib/http/url.ts +0 -23
  90. package/src/lib/nsid.ts +0 -10
  91. package/src/lib/redis.ts +0 -25
  92. package/src/lib/util/authorization-header.ts +0 -28
  93. package/src/lib/util/cast.ts +0 -18
  94. package/src/lib/util/color.ts +0 -168
  95. package/src/lib/util/crypto.ts +0 -32
  96. package/src/lib/util/date.ts +0 -7
  97. package/src/lib/util/error.ts +0 -7
  98. package/src/lib/util/function.ts +0 -39
  99. package/src/lib/util/locale.ts +0 -12
  100. package/src/lib/util/object.ts +0 -23
  101. package/src/lib/util/redirect-uri.ts +0 -46
  102. package/src/lib/util/time.ts +0 -49
  103. package/src/lib/util/type.ts +0 -182
  104. package/src/lib/util/ui8.ts +0 -14
  105. package/src/lib/util/well-known.ts +0 -8
  106. package/src/lib/util/zod-error.ts +0 -26
  107. package/src/lib/write-form-redirect.ts +0 -58
  108. package/src/lib/write-html.ts +0 -70
  109. package/src/metadata/build-metadata.ts +0 -141
  110. package/src/oauth-client.ts +0 -3
  111. package/src/oauth-dpop.ts +0 -2
  112. package/src/oauth-errors.ts +0 -26
  113. package/src/oauth-hooks.ts +0 -519
  114. package/src/oauth-middleware.ts +0 -53
  115. package/src/oauth-provider.ts +0 -1110
  116. package/src/oauth-store.ts +0 -12
  117. package/src/oauth-verifier.ts +0 -260
  118. package/src/replay/replay-manager.ts +0 -51
  119. package/src/replay/replay-store-memory.ts +0 -36
  120. package/src/replay/replay-store-redis.ts +0 -30
  121. package/src/replay/replay-store.ts +0 -44
  122. package/src/request/code.ts +0 -23
  123. package/src/request/request-data.ts +0 -36
  124. package/src/request/request-id.ts +0 -22
  125. package/src/request/request-manager.ts +0 -521
  126. package/src/request/request-store.ts +0 -60
  127. package/src/request/request-uri.ts +0 -42
  128. package/src/result/authorization-redirect-parameters.ts +0 -24
  129. package/src/result/authorization-result-authorize-page.ts +0 -16
  130. package/src/result/authorization-result-redirect.ts +0 -8
  131. package/src/router/assets/assets-manifest.ts +0 -117
  132. package/src/router/assets/assets.ts +0 -124
  133. package/src/router/assets/csrf.ts +0 -79
  134. package/src/router/assets/send-account-page.ts +0 -23
  135. package/src/router/assets/send-authorization-page.ts +0 -42
  136. package/src/router/assets/send-cookie-error-page.ts +0 -21
  137. package/src/router/assets/send-error-page.ts +0 -25
  138. package/src/router/assets/send-redirect.ts +0 -140
  139. package/src/router/create-account-page-middleware.ts +0 -88
  140. package/src/router/create-api-middleware.ts +0 -1051
  141. package/src/router/create-authorization-page-middleware.ts +0 -281
  142. package/src/router/create-oauth-middleware.ts +0 -257
  143. package/src/router/error-handler.ts +0 -6
  144. package/src/router/middleware-options.ts +0 -9
  145. package/src/signer/access-token-payload.ts +0 -25
  146. package/src/signer/api-token-payload.ts +0 -18
  147. package/src/signer/signer.ts +0 -121
  148. package/src/token/refresh-token.ts +0 -30
  149. package/src/token/token-claims.ts +0 -22
  150. package/src/token/token-data.ts +0 -40
  151. package/src/token/token-id.ts +0 -25
  152. package/src/token/token-manager.ts +0 -427
  153. package/src/token/token-store.ts +0 -88
  154. package/src/types/authorization-response-error.ts +0 -27
  155. package/src/types/color-hue.ts +0 -3
  156. package/src/types/email-otp.ts +0 -3
  157. package/src/types/email.ts +0 -26
  158. package/src/types/handle.ts +0 -23
  159. package/src/types/invite-code.ts +0 -4
  160. package/src/types/par-response-error.ts +0 -25
  161. package/src/types/password.ts +0 -4
  162. package/src/types/rgb-color.ts +0 -21
  163. package/tsconfig.build.json +0 -8
  164. package/tsconfig.build.tsbuildinfo +0 -1
  165. package/tsconfig.json +0 -4
@@ -1,1051 +0,0 @@
1
- import type { IncomingMessage, ServerResponse } from 'node:http'
2
- import createHttpError from 'http-errors'
3
- import { z } from 'zod'
4
- import { Did, didSchema } from '@atproto/did'
5
- import { signedJwtSchema } from '@atproto/jwk'
6
- import {
7
- API_ENDPOINT_PREFIX,
8
- ActiveAccountSession,
9
- ActiveDeviceSession,
10
- ActiveOAuthSession,
11
- ApiEndpoints,
12
- ISODateString,
13
- } from '@atproto/oauth-provider-api'
14
- import {
15
- OAuthAuthorizationRequestParameters,
16
- OAuthRedirectUri,
17
- OAuthResponseMode,
18
- oauthRedirectUriSchema,
19
- oauthResponseModeSchema,
20
- oauthScopeSchema,
21
- } from '@atproto/oauth-types'
22
- import { signInDataSchema } from '../account/sign-in-data.js'
23
- import { signUpInputSchema } from '../account/sign-up-input.js'
24
- import { DeviceId, deviceIdSchema } from '../device/device-id.js'
25
- import { AuthorizationError } from '../errors/authorization-error.js'
26
- import {
27
- ErrorPayload,
28
- buildErrorPayload,
29
- buildErrorStatus,
30
- } from '../errors/error-parser.js'
31
- import { InvalidRequestError } from '../errors/invalid-request-error.js'
32
- import { WWWAuthenticateError } from '../errors/www-authenticate-error.js'
33
- import {
34
- JsonResponse,
35
- Middleware,
36
- RequestMetadata,
37
- Router,
38
- RouterCtx,
39
- SubCtx,
40
- flushStream,
41
- jsonHandler,
42
- parseHttpRequest,
43
- subCtx,
44
- validateFetchMode,
45
- validateFetchSite,
46
- validateOrigin,
47
- validateReferrer,
48
- } from '../lib/http/index.js'
49
- import { RouteCtx, createRoute } from '../lib/http/route.js'
50
- import { asArray } from '../lib/util/cast.js'
51
- import { localeSchema } from '../lib/util/locale.js'
52
- import type { Awaitable } from '../lib/util/type.js'
53
- import type { OAuthProvider } from '../oauth-provider.js'
54
- import { RequestUri, requestUriSchema } from '../request/request-uri.js'
55
- import { AuthorizationRedirectParameters } from '../result/authorization-redirect-parameters.js'
56
- import { tokenIdSchema } from '../token/token-id.js'
57
- import { emailOtpSchema } from '../types/email-otp.js'
58
- import { emailSchema } from '../types/email.js'
59
- import { handleSchema } from '../types/handle.js'
60
- import { newPasswordSchema, oldPasswordSchema } from '../types/password.js'
61
- import { validateCsrfToken } from './assets/csrf.js'
62
- import {
63
- ERROR_REDIRECT_KEYS,
64
- OAuthRedirectOptions,
65
- OAuthRedirectQueryParameter,
66
- SUCCESS_REDIRECT_KEYS,
67
- buildRedirectMode,
68
- buildRedirectParams,
69
- buildRedirectUri,
70
- } from './assets/send-redirect.js'
71
- import type { MiddlewareOptions } from './middleware-options.js'
72
-
73
- export function createApiMiddleware<
74
- Ctx extends object | void = void,
75
- Req extends IncomingMessage = IncomingMessage,
76
- Res extends ServerResponse = ServerResponse,
77
- >(
78
- server: OAuthProvider,
79
- { onError }: MiddlewareOptions<Req, Res>,
80
- ): Middleware<Ctx, Req, Res> {
81
- const issuerUrl = new URL(server.issuer)
82
- const issuerOrigin = issuerUrl.origin
83
- const router = new Router<Ctx, Req, Res>(issuerUrl)
84
-
85
- router.use(
86
- apiRoute({
87
- method: 'POST',
88
- endpoint: '/verify-handle-availability',
89
- schema: z.object({ handle: handleSchema }).strict(),
90
- async handler() {
91
- await server.accountManager.verifyHandleAvailability(this.input.handle)
92
- return { json: { available: true } }
93
- },
94
- }),
95
- )
96
-
97
- router.use(
98
- apiRoute({
99
- method: 'POST',
100
- endpoint: '/sign-up',
101
- schema: signUpInputSchema,
102
- rotateDeviceCookies: true,
103
- async handler() {
104
- const { deviceId, deviceMetadata, input, requestUri } = this
105
-
106
- const account = await server.accountManager.createAccount(
107
- deviceId,
108
- deviceMetadata,
109
- input,
110
- )
111
-
112
- // Remember when not in the context of a request by default
113
- const remember = requestUri == null
114
-
115
- // Only "remember" the newly created account if it was not created during an
116
- // OAuth flow.
117
- if (remember) {
118
- await server.accountManager.upsertDeviceAccount(deviceId, account.did)
119
- }
120
-
121
- const ephemeralToken = remember
122
- ? undefined
123
- : await server.signer.createEphemeralToken({
124
- sub: account.did,
125
- deviceId,
126
- requestUri: this.requestUri,
127
- })
128
-
129
- const json = { account, ephemeralToken }
130
- return { json }
131
- },
132
- }),
133
- )
134
-
135
- router.use(
136
- apiRoute({
137
- method: 'POST',
138
- endpoint: '/sign-in',
139
- schema: signInDataSchema.extend({ remember: z.boolean().optional() }),
140
- rotateDeviceCookies: true,
141
- async handler() {
142
- const { deviceId, deviceMetadata, requestUri } = this
143
-
144
- // Remember when not in the context of a request by default
145
- const { remember = requestUri == null, ...input } = this.input
146
-
147
- // Look up the client identifier associated with the pending OAuth
148
- // request, if any, so it can be surfaced to the sign-in hooks.
149
- const clientId = requestUri
150
- ? await server.requestManager.peekClientId(requestUri)
151
- : undefined
152
-
153
- const account = await server.accountManager.authenticateAccount(
154
- deviceId,
155
- deviceMetadata,
156
- input,
157
- clientId,
158
- )
159
-
160
- if (remember) {
161
- await server.accountManager.upsertDeviceAccount(deviceId, account.did)
162
- } else {
163
- // In case the user was already signed in, and signed in again, this
164
- // time without "remember me", let's sign them off of the device.
165
- await server.accountManager.removeDeviceAccount(deviceId, account.did)
166
- }
167
-
168
- const ephemeralToken = remember
169
- ? undefined
170
- : await server.signer.createEphemeralToken({
171
- sub: account.did,
172
- deviceId,
173
- requestUri,
174
- })
175
-
176
- const json = { account, ephemeralToken }
177
- return { json }
178
- },
179
- }),
180
- )
181
-
182
- router.use(
183
- apiRoute({
184
- method: 'POST',
185
- endpoint: '/sign-out',
186
- schema: z
187
- .object({
188
- did: z.union([didSchema, z.array(didSchema)]),
189
- })
190
- .strict(),
191
- rotateDeviceCookies: true,
192
- async handler() {
193
- const uniqueSubs = new Set(asArray(this.input.did))
194
-
195
- for (const sub of uniqueSubs) {
196
- await server.accountManager.removeDeviceAccount(this.deviceId, sub)
197
- }
198
-
199
- return { json: { success: true as const } }
200
- },
201
- }),
202
- )
203
-
204
- router.use(
205
- apiRoute({
206
- method: 'POST',
207
- endpoint: '/reset-password-request',
208
- schema: z
209
- .object({
210
- locale: localeSchema,
211
- email: emailSchema,
212
- })
213
- .strict(),
214
- async handler() {
215
- await server.accountManager.resetPasswordRequest(
216
- this.deviceId,
217
- this.deviceMetadata,
218
- this.input,
219
- )
220
- return { json: { success: true } }
221
- },
222
- }),
223
- )
224
-
225
- router.use(
226
- apiRoute({
227
- method: 'POST',
228
- endpoint: '/reset-password-confirm',
229
- schema: z
230
- .object({
231
- token: emailOtpSchema,
232
- password: newPasswordSchema,
233
- })
234
- .strict(),
235
- async handler() {
236
- await server.accountManager.resetPasswordConfirm(
237
- this.deviceId,
238
- this.deviceMetadata,
239
- this.input,
240
- )
241
- return { json: { success: true } }
242
- },
243
- }),
244
- )
245
-
246
- router.use(
247
- apiRoute({
248
- method: 'POST',
249
- endpoint: '/update-email-request',
250
- schema: z
251
- .object({
252
- did: didSchema,
253
- locale: localeSchema.optional(),
254
- })
255
- .strict(),
256
- async handler(req, res) {
257
- const { account } = await authenticate.call(this, req, res)
258
-
259
- const { tokenRequired } =
260
- await server.accountManager.updateEmailRequest(
261
- this.deviceId,
262
- this.deviceMetadata,
263
- this.input,
264
- account,
265
- )
266
-
267
- return { json: { tokenRequired } }
268
- },
269
- }),
270
- )
271
-
272
- router.use(
273
- apiRoute({
274
- method: 'POST',
275
- endpoint: '/update-email-confirm',
276
- schema: z
277
- .object({
278
- did: didSchema,
279
- email: emailSchema,
280
- token: emailOtpSchema.optional(),
281
- locale: localeSchema.optional(),
282
- })
283
- .strict(),
284
- async handler(req, res) {
285
- let { account } = await authenticate.call(this, req, res)
286
-
287
- account = await server.accountManager.updateEmailConfirm(
288
- this.deviceId,
289
- this.deviceMetadata,
290
- this.input,
291
- account,
292
- )
293
-
294
- return { json: { account } }
295
- },
296
- }),
297
- )
298
-
299
- router.use(
300
- apiRoute({
301
- method: 'POST',
302
- endpoint: '/verify-email-request',
303
- schema: z
304
- .object({
305
- did: didSchema,
306
- locale: localeSchema.optional(),
307
- })
308
- .strict(),
309
- async handler(req, res) {
310
- const { account } = await authenticate.call(this, req, res)
311
-
312
- await server.accountManager.verifyEmailRequest(
313
- this.deviceId,
314
- this.deviceMetadata,
315
- this.input,
316
- account,
317
- )
318
-
319
- return { json: { success: true } }
320
- },
321
- }),
322
- )
323
-
324
- router.use(
325
- apiRoute({
326
- method: 'POST',
327
- endpoint: '/verify-email-confirm',
328
- schema: z
329
- .object({
330
- did: didSchema,
331
- token: emailOtpSchema,
332
- email: emailSchema,
333
- })
334
- .strict(),
335
- async handler(req, res) {
336
- let { account } = await authenticate.call(this, req, res)
337
-
338
- account = await server.accountManager.verifyEmailConfirm(
339
- this.deviceId,
340
- this.deviceMetadata,
341
- this.input,
342
- account,
343
- )
344
-
345
- return { json: { account } }
346
- },
347
- }),
348
- )
349
-
350
- router.use(
351
- apiRoute({
352
- method: 'POST',
353
- endpoint: '/update-handle',
354
- schema: z
355
- .object({
356
- did: didSchema,
357
- handle: handleSchema,
358
- })
359
- .strict(),
360
- async handler(req, res) {
361
- let { account } = await authenticate.call(this, req, res)
362
-
363
- account = await server.accountManager.updateHandle(
364
- this.deviceId,
365
- this.deviceMetadata,
366
- this.input,
367
- account,
368
- )
369
-
370
- return { json: { account } }
371
- },
372
- }),
373
- )
374
-
375
- router.use(
376
- apiRoute({
377
- method: 'POST',
378
- endpoint: '/deactivate-account',
379
- schema: z.object({ did: didSchema }).strict(),
380
- async handler(req, res) {
381
- let { account } = await authenticate.call(this, req, res)
382
-
383
- if (!account.deactivated) {
384
- account = await server.accountManager.deactivateAccount(
385
- this.deviceId,
386
- this.deviceMetadata,
387
- account,
388
- )
389
- }
390
-
391
- return { json: { account } }
392
- },
393
- }),
394
- )
395
-
396
- router.use(
397
- apiRoute({
398
- method: 'POST',
399
- endpoint: '/reactivate-account',
400
- schema: z.object({ did: didSchema }).strict(),
401
- async handler(req, res) {
402
- let { account } = await authenticate.call(this, req, res)
403
-
404
- if (account.deactivated) {
405
- account = await server.accountManager.reactivateAccount(
406
- this.deviceId,
407
- this.deviceMetadata,
408
- account,
409
- )
410
- }
411
-
412
- return { json: { account } }
413
- },
414
- }),
415
- )
416
-
417
- router.use(
418
- apiRoute({
419
- method: 'POST',
420
- endpoint: '/delete-account-request',
421
- schema: z
422
- .object({ did: didSchema, locale: localeSchema.optional() })
423
- .strict(),
424
- async handler(req, res) {
425
- const { account } = await authenticate.call(this, req, res)
426
-
427
- await server.accountManager.deleteAccountRequest(
428
- this.deviceId,
429
- this.deviceMetadata,
430
- this.input,
431
- account,
432
- )
433
-
434
- return { json: { success: true } }
435
- },
436
- }),
437
- )
438
-
439
- router.use(
440
- apiRoute({
441
- method: 'POST',
442
- endpoint: '/delete-account-confirm',
443
- schema: z
444
- .object({
445
- did: didSchema,
446
- token: emailOtpSchema,
447
- password: oldPasswordSchema,
448
- })
449
- .strict(),
450
- async handler(req, res) {
451
- const { account } = await authenticate.call(this, req, res)
452
-
453
- await server.accountManager.deleteAccountConfirm(
454
- this.deviceId,
455
- this.deviceMetadata,
456
- this.input,
457
- account,
458
- )
459
-
460
- return { json: { success: true } }
461
- },
462
- }),
463
- )
464
-
465
- router.use(
466
- apiRoute({
467
- method: 'GET',
468
- endpoint: '/device-sessions',
469
- schema: undefined,
470
- async handler() {
471
- const deviceAccounts = await server.accountManager.listDeviceAccounts(
472
- this.deviceId,
473
- )
474
-
475
- const json = deviceAccounts.map(
476
- (deviceAccount): ActiveDeviceSession => ({
477
- account: deviceAccount.account,
478
- loginRequired: server.checkLoginRequired(deviceAccount),
479
- }),
480
- )
481
-
482
- return { json }
483
- },
484
- }),
485
- )
486
-
487
- router.use(
488
- apiRoute({
489
- method: 'GET',
490
- endpoint: '/oauth-sessions',
491
- schema: z.object({ did: didSchema }).strict(),
492
- async handler(req, res) {
493
- const { account } = await authenticate.call(this, req, res)
494
-
495
- const tokenInfos = await server.tokenManager.listAccountTokens(
496
- account.did,
497
- )
498
-
499
- const clientIds = tokenInfos.map((tokenInfo) => tokenInfo.data.clientId)
500
-
501
- const clients = await server.clientManager.loadClients(clientIds, {
502
- onError: (err, clientId) => {
503
- onError?.(req, res, err, `Failed to load client ${clientId}`)
504
- return undefined // metadata won't be available in the UI
505
- },
506
- })
507
-
508
- // @TODO: We should ideally filter sessions that are expired (or even
509
- // expose the expiration date). This requires a change to the way
510
- // TokenInfo are stored (see TokenManager#isTokenExpired and
511
- // TokenManager#isTokenInactive).
512
- const json = tokenInfos.map(({ id, data }): ActiveOAuthSession => {
513
- return {
514
- tokenId: id,
515
-
516
- createdAt: data.createdAt.toISOString() as ISODateString,
517
- updatedAt: data.updatedAt.toISOString() as ISODateString,
518
-
519
- clientId: data.clientId,
520
- clientMetadata: clients.get(data.clientId)?.metadata,
521
-
522
- scope: data.parameters.scope,
523
- }
524
- })
525
-
526
- return { json }
527
- },
528
- }),
529
- )
530
-
531
- router.use(
532
- apiRoute({
533
- method: 'GET',
534
- endpoint: '/account-sessions',
535
- schema: z.object({ did: didSchema }).strict(),
536
- async handler(req, res) {
537
- const { account } = await authenticate.call(this, req, res)
538
-
539
- const deviceAccounts = await server.accountManager.listAccountDevices(
540
- account.did,
541
- )
542
-
543
- const json = deviceAccounts.map(
544
- (accountSession): ActiveAccountSession => ({
545
- deviceId: accountSession.deviceId,
546
- deviceMetadata: {
547
- ipAddress: accountSession.deviceData.ipAddress,
548
- userAgent: accountSession.deviceData.userAgent,
549
- lastSeenAt:
550
- accountSession.deviceData.lastSeenAt.toISOString() as ISODateString,
551
- },
552
-
553
- isCurrentDevice: accountSession.deviceId === this.deviceId,
554
- }),
555
- )
556
-
557
- return { json }
558
- },
559
- }),
560
- )
561
-
562
- router.use(
563
- apiRoute({
564
- method: 'POST',
565
- endpoint: '/revoke-account-session',
566
- schema: z.object({ did: didSchema, deviceId: deviceIdSchema }).strict(),
567
- async handler() {
568
- // @NOTE This route is not authenticated. If a user is able to steal
569
- // another user's session cookie, we allow them to revoke the device
570
- // session.
571
-
572
- await server.accountManager.removeDeviceAccount(
573
- this.input.deviceId,
574
- this.input.did,
575
- )
576
-
577
- return { json: { success: true } }
578
- },
579
- }),
580
- )
581
-
582
- router.use(
583
- apiRoute({
584
- method: 'POST',
585
- endpoint: '/revoke-oauth-session',
586
- schema: z.object({ did: didSchema, tokenId: tokenIdSchema }).strict(),
587
- async handler(req, res) {
588
- const { account } = await authenticate.call(this, req, res)
589
-
590
- const tokenInfo = await server.tokenManager.getTokenInfo(
591
- this.input.tokenId,
592
- )
593
-
594
- if (!tokenInfo || tokenInfo.account.did !== account.did) {
595
- // report this as though the token was not found
596
- throw new InvalidRequestError(`Invalid token`)
597
- }
598
-
599
- await server.tokenManager.deleteToken(tokenInfo.id)
600
-
601
- return { json: { success: true } }
602
- },
603
- }),
604
- )
605
-
606
- router.use(
607
- apiRoute({
608
- method: 'POST',
609
- endpoint: '/consent',
610
- schema: z
611
- .object({ did: didSchema, scope: oauthScopeSchema.optional() })
612
- .strict(),
613
- async handler(req, res) {
614
- if (!this.requestUri) {
615
- throw new InvalidRequestError(
616
- 'This endpoint can only be used in the context of an OAuth request',
617
- )
618
- }
619
-
620
- // Any AuthorizationError caught in this block will result in a redirect
621
- // to the client's redirect_uri with an error.
622
- try {
623
- const { clientId, parameters } = await server.requestManager.get(
624
- this.requestUri,
625
- this.deviceId,
626
- )
627
-
628
- // Any error thrown in this block will be transformed into an
629
- // AuthorizationError.
630
- try {
631
- const { account, authorizedClients } = await authenticate.call(
632
- this,
633
- req,
634
- res,
635
- )
636
-
637
- const client = await server.clientManager.getClient(clientId)
638
-
639
- const code = await server.requestManager.setAuthorized(
640
- this.requestUri,
641
- client,
642
- account,
643
- this.deviceId,
644
- this.deviceMetadata,
645
- this.input.scope,
646
- )
647
-
648
- const clientData = authorizedClients.get(clientId)
649
- if (server.checkConsentRequired(parameters, clientData)) {
650
- const scopes = new Set(clientData?.authorizedScopes)
651
-
652
- // Add the newly accepted scopes to the authorized scopes
653
-
654
- // @NOTE `oauthScopeSchema` ensures that `scope` contains no
655
- // leading/trailing/duplicate spaces.
656
- for (const s of parameters.scope?.split(' ') ?? []) scopes.add(s)
657
-
658
- await server.accountManager.setAuthorizedClient(account, client, {
659
- ...clientData,
660
- authorizedScopes: [...scopes],
661
- })
662
- }
663
-
664
- const url = buildRedirectUrl(server.issuer, parameters, { code })
665
-
666
- return { json: { url } }
667
- } catch (err) {
668
- // Since we have access to the parameters, we can re-throw an
669
- // AuthorizationError with the redirect_uri parameter.
670
- throw AuthorizationError.from(parameters, err)
671
- }
672
- } catch (err) {
673
- onError?.(req, res, err, 'Failed to consent authorization request')
674
-
675
- // If any error happened (unauthenticated, invalid request, etc.),
676
- // lets make sure the request can no longer be used.
677
- try {
678
- await server.requestManager.delete(this.requestUri)
679
- } catch (err) {
680
- onError?.(req, res, err, 'Failed to delete request')
681
- }
682
-
683
- if (err instanceof AuthorizationError) {
684
- try {
685
- const url = buildRedirectUrl(
686
- server.issuer,
687
- err.parameters,
688
- err.toJSON(),
689
- )
690
-
691
- return { json: { url } }
692
- } catch {
693
- // Unable to build redirect URL, ignore
694
- }
695
- }
696
-
697
- // @NOTE Not re-throwing the error here, as the error was already
698
- // handled by the `onError` callback, and apiRoute (`apiMiddleware`)
699
- // would call `onError` again.
700
- return buildErrorJsonResponse(err)
701
- }
702
- },
703
- }),
704
- )
705
-
706
- router.use(
707
- apiRoute({
708
- method: 'POST',
709
- endpoint: '/reject',
710
- schema: z.object({}).strict(),
711
- rotateDeviceCookies: true,
712
- async handler(req, res) {
713
- const { requestUri } = this
714
- if (!requestUri) {
715
- throw new InvalidRequestError(
716
- 'This endpoint can only be used in the context of an OAuth request',
717
- )
718
- }
719
-
720
- // Once this endpoint is called, the request will definitely be
721
- // rejected.
722
- try {
723
- // No need to authenticate the user here as they are not authorizing a
724
- // particular account (CSRF protection is enough).
725
-
726
- // @NOTE that the client could *technically* trigger this endpoint while
727
- // the user is on the authorize page by forging the request (because the
728
- // client knows the RequestURI from PAR and has all the info needed to
729
- // forge the request, including CSRF). This cannot be used as DoS attack
730
- // as the request ID is not guessable and would only result in a bad UX
731
- // for misbehaving clients, only for the users of those clients.
732
-
733
- const { parameters } = await server.requestManager.get(
734
- requestUri,
735
- this.deviceId,
736
- )
737
-
738
- const url = buildRedirectUrl(server.issuer, parameters, {
739
- error: 'access_denied',
740
- error_description: 'The user rejected the request',
741
- })
742
-
743
- return { json: { url } }
744
- } catch (err) {
745
- onError?.(req, res, err, 'Failed to reject authorization request')
746
-
747
- if (err instanceof AuthorizationError) {
748
- try {
749
- const url = buildRedirectUrl(
750
- server.issuer,
751
- err.parameters,
752
- err.toJSON(),
753
- )
754
-
755
- return { json: { url } }
756
- } catch {
757
- // Unable to build redirect URL, ignore
758
- }
759
- }
760
-
761
- return buildErrorJsonResponse(err)
762
- } finally {
763
- await server.requestManager.delete(requestUri).catch((err) => {
764
- onError?.(req, res, err, 'Failed to delete request')
765
- })
766
- }
767
- },
768
- }),
769
- )
770
-
771
- return router.buildMiddleware()
772
-
773
- async function authenticate(
774
- this: ApiContext<void, { did: Did }>,
775
- req: Req,
776
- _res: Res,
777
- ) {
778
- if (req.headers.authorization?.startsWith('Bearer ')) {
779
- try {
780
- // If there is an authorization header, verify that the ephemeral token it
781
- // contains is a jwt bound to the right [sub, device, request].
782
- const bearer = req.headers.authorization.slice(7)
783
- const ephemeralToken = signedJwtSchema.parse(bearer)
784
- const { payload } =
785
- await server.signer.verifyEphemeralToken(ephemeralToken)
786
-
787
- if (
788
- payload.sub === this.input.did &&
789
- payload.deviceId === this.deviceId &&
790
- payload.requestUri === this.requestUri
791
- ) {
792
- return await server.accountManager.getAccount(payload.sub)
793
- }
794
- } catch (err) {
795
- throw new WWWAuthenticateError(
796
- 'unauthorized',
797
- `Invalid or expired bearer token`,
798
- { Bearer: {} },
799
- err,
800
- )
801
- }
802
- }
803
-
804
- try {
805
- // Ensures the "sub" has an active session on the device
806
- const deviceAccount = await server.accountManager.getDeviceAccount(
807
- this.deviceId,
808
- this.input.did,
809
- )
810
-
811
- // The session exists but was created too long ago
812
- if (server.checkLoginRequired(deviceAccount)) {
813
- throw new InvalidRequestError('Login required')
814
- }
815
-
816
- return deviceAccount
817
- } catch (err) {
818
- throw new WWWAuthenticateError(
819
- 'unauthorized',
820
- `User ${this.input.did} not authenticated on this device`,
821
- { Bearer: {} },
822
- err,
823
- )
824
- }
825
- }
826
-
827
- type ApiContext<T extends object | void, I = void> = SubCtx<
828
- T,
829
- {
830
- deviceId: DeviceId
831
- deviceMetadata: RequestMetadata
832
-
833
- /**
834
- * The parsed input data (json payload if "POST", query params if "GET").
835
- */
836
- input: I
837
-
838
- /**
839
- * When defined, the request originated from the authorize page.
840
- */
841
- requestUri?: RequestUri
842
- }
843
- >
844
-
845
- type InferValidation<S extends void | z.ZodTypeAny> = S extends z.ZodTypeAny
846
- ? z.infer<S>
847
- : void
848
-
849
- /**
850
- * The main purpose of this function is to ensure that the endpoint
851
- * implementation matches its type definition from {@link ApiEndpoints}.
852
- * @private
853
- */
854
- function apiRoute<
855
- C extends RouterCtx<Ctx>,
856
- M extends 'GET' | 'POST',
857
- E extends `/${string}` &
858
- // Extract all the endpoint path that match the method (allows for
859
- // auto-complete & better error reporting)
860
- {
861
- [E in keyof ApiEndpoints]: ApiEndpoints[E] extends { method: M }
862
- ? E
863
- : never
864
- }[keyof ApiEndpoints],
865
- S extends // A schema that validates the POST input or GET params
866
- ApiEndpoints[E] extends { method: 'POST'; input: infer I }
867
- ? z.ZodType<I, z.ZodTypeDef, unknown>
868
- : ApiEndpoints[E] extends { method: 'GET'; params: infer P }
869
- ? z.ZodType<P, z.ZodTypeDef, unknown>
870
- : void,
871
- >(options: {
872
- method: M
873
- endpoint: E
874
- schema: S
875
- rotateDeviceCookies?: boolean
876
- handler: (
877
- this: ApiContext<RouteCtx<C>, InferValidation<S>>,
878
- req: Req,
879
- res: Res,
880
- ) => Awaitable<JsonResponse<ErrorPayload | ApiEndpoints[E]['output']>>
881
- }): Middleware<C, Req, Res> {
882
- return createRoute(
883
- options.method,
884
- `${API_ENDPOINT_PREFIX}${options.endpoint}`,
885
- apiMiddleware(options),
886
- )
887
- }
888
-
889
- function apiMiddleware<C extends RouterCtx, S extends void | z.ZodTypeAny>({
890
- method,
891
- schema,
892
- rotateDeviceCookies,
893
- handler,
894
- }: {
895
- method: 'GET' | 'POST'
896
- schema: S
897
- rotateDeviceCookies?: boolean
898
- handler: (
899
- this: ApiContext<C, InferValidation<S>>,
900
- req: Req,
901
- res: Res,
902
- ) => Awaitable<JsonResponse>
903
- }): Middleware<C, Req, Res> {
904
- const parseInput: (this: C, req: Req) => Promise<InferValidation<S>> =
905
- schema == null // No schema means endpoint doesn't accept any input
906
- ? async function (req) {
907
- await flushStream(req)
908
- return undefined
909
- }
910
- : method === 'POST'
911
- ? async function (req) {
912
- const body = await parseHttpRequest(req, ['json'])
913
- return schema.parseAsync(body, { path: ['body'] })
914
- }
915
- : async function (req) {
916
- await flushStream(req)
917
- const query = Object.fromEntries(this.url.searchParams)
918
- return schema.parseAsync(query, { path: ['query'] })
919
- }
920
-
921
- return jsonHandler<C, Req, Res>(async function (req, res) {
922
- try {
923
- // Prevent caching of API routes
924
- res.setHeader('Cache-Control', 'no-store')
925
- res.setHeader('Pragma', 'no-cache')
926
-
927
- // Prevent CORS requests
928
- validateFetchMode(req, ['same-origin'])
929
- validateFetchSite(req, ['same-origin'])
930
- validateOrigin(req, issuerOrigin)
931
- const referrer = validateReferrer(req, { origin: issuerOrigin })
932
-
933
- // Ensure we are one the right page
934
- if (
935
- // trailing slashes are not allowed
936
- referrer.pathname !== '/oauth/authorize' &&
937
- referrer.pathname !== '/account' &&
938
- !referrer.pathname.startsWith(`/account/`)
939
- ) {
940
- throw createHttpError(400, `Invalid referrer ${referrer}`)
941
- }
942
-
943
- // Check if the request originated from the authorize page
944
- const requestUri =
945
- referrer.pathname === '/oauth/authorize'
946
- ? await requestUriSchema.parseAsync(
947
- referrer.searchParams.get('request_uri'),
948
- )
949
- : undefined
950
-
951
- // Validate CSRF token
952
- await validateCsrfToken(req, res)
953
-
954
- // Parse and validate the input data
955
- const input = await parseInput.call(this, req)
956
-
957
- // Load session data, rotating the session cookie if needed
958
- const { deviceId, deviceMetadata } = await server.deviceManager.load(
959
- req,
960
- res,
961
- rotateDeviceCookies,
962
- )
963
-
964
- const context: ApiContext<C, InferValidation<S>> = subCtx(this, {
965
- input,
966
- requestUri,
967
- deviceId,
968
- deviceMetadata,
969
- })
970
-
971
- return await handler.call(context, req, res)
972
- } catch (err) {
973
- onError?.(req, res, err, `Failed to handle API request`)
974
-
975
- // Make sore to always return a JSON response
976
- return buildErrorJsonResponse(err)
977
- }
978
- })
979
- }
980
- }
981
-
982
- function buildErrorJsonResponse(err: unknown) {
983
- // @TODO Rework the API error responses (relying on codes)
984
- const json = buildErrorPayload(err)
985
- const status = buildErrorStatus(err)
986
-
987
- return { json, status }
988
- }
989
-
990
- function buildRedirectUrl(
991
- iss: string,
992
- parameters: OAuthAuthorizationRequestParameters,
993
- redirect: AuthorizationRedirectParameters,
994
- ): string {
995
- const url = new URL('/oauth/authorize/redirect', iss)
996
-
997
- url.searchParams.set('redirect_mode', buildRedirectMode(parameters))
998
- url.searchParams.set('redirect_uri', buildRedirectUri(parameters))
999
-
1000
- for (const [key, value] of buildRedirectParams(iss, parameters, redirect)) {
1001
- url.searchParams.set(key, value)
1002
- }
1003
-
1004
- return url.href
1005
- }
1006
-
1007
- export function parseRedirectUrl(url: URL): OAuthRedirectOptions {
1008
- if (url.pathname !== '/oauth/authorize/redirect') {
1009
- throw new InvalidRequestError(
1010
- `Invalid redirect URL: ${url.pathname} is not a valid path`,
1011
- )
1012
- }
1013
-
1014
- const params: [OAuthRedirectQueryParameter, string][] = []
1015
-
1016
- const state = url.searchParams.get('state')
1017
- if (state) params.push(['state', state])
1018
-
1019
- const iss = url.searchParams.get('iss')
1020
- if (iss) params.push(['iss', iss])
1021
-
1022
- if (url.searchParams.has('code')) {
1023
- for (const key of SUCCESS_REDIRECT_KEYS) {
1024
- const value = url.searchParams.get(key)
1025
- if (value != null) params.push([key, value])
1026
- }
1027
- } else if (url.searchParams.has('error')) {
1028
- for (const key of ERROR_REDIRECT_KEYS) {
1029
- const value = url.searchParams.get(key)
1030
- if (value != null) params.push([key, value])
1031
- }
1032
- } else {
1033
- throw new InvalidRequestError(
1034
- 'Invalid redirect URL: neither code nor error found',
1035
- )
1036
- }
1037
-
1038
- try {
1039
- const mode: OAuthResponseMode = oauthResponseModeSchema.parse(
1040
- url.searchParams.get('redirect_mode'),
1041
- )
1042
-
1043
- const redirectUri: OAuthRedirectUri = oauthRedirectUriSchema.parse(
1044
- url.searchParams.get('redirect_uri'),
1045
- )
1046
-
1047
- return { mode, redirectUri, params }
1048
- } catch (err) {
1049
- throw InvalidRequestError.from(err, 'Invalid redirect URL')
1050
- }
1051
- }