@atproto/oauth-provider 0.9.2 → 0.10.0

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 (154) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/client/client-manager.d.ts.map +1 -1
  3. package/dist/client/client-manager.js +0 -7
  4. package/dist/client/client-manager.js.map +1 -1
  5. package/dist/client/client.js +6 -6
  6. package/dist/client/client.js.map +1 -1
  7. package/dist/device/device-manager.js +1 -1
  8. package/dist/device/device-manager.js.map +1 -1
  9. package/dist/dpop/dpop-manager.js +15 -15
  10. package/dist/dpop/dpop-manager.js.map +1 -1
  11. package/dist/errors/access-denied-error.d.ts +4 -7
  12. package/dist/errors/access-denied-error.d.ts.map +1 -1
  13. package/dist/errors/access-denied-error.js +4 -13
  14. package/dist/errors/access-denied-error.js.map +1 -1
  15. package/dist/errors/account-selection-required-error.d.ts +2 -2
  16. package/dist/errors/account-selection-required-error.d.ts.map +1 -1
  17. package/dist/errors/account-selection-required-error.js +2 -2
  18. package/dist/errors/account-selection-required-error.js.map +1 -1
  19. package/dist/errors/authorization-error.d.ts +10 -0
  20. package/dist/errors/authorization-error.d.ts.map +1 -0
  21. package/dist/errors/authorization-error.js +31 -0
  22. package/dist/errors/authorization-error.js.map +1 -0
  23. package/dist/errors/consent-required-error.d.ts +2 -2
  24. package/dist/errors/consent-required-error.d.ts.map +1 -1
  25. package/dist/errors/consent-required-error.js +2 -2
  26. package/dist/errors/consent-required-error.js.map +1 -1
  27. package/dist/errors/error-parser.d.ts.map +1 -1
  28. package/dist/errors/error-parser.js +2 -1
  29. package/dist/errors/error-parser.js.map +1 -1
  30. package/dist/errors/invalid-authorization-details-error.d.ts +2 -2
  31. package/dist/errors/invalid-authorization-details-error.d.ts.map +1 -1
  32. package/dist/errors/invalid-authorization-details-error.js +2 -2
  33. package/dist/errors/invalid-authorization-details-error.js.map +1 -1
  34. package/dist/errors/invalid-scope-error.d.ts +2 -2
  35. package/dist/errors/invalid-scope-error.d.ts.map +1 -1
  36. package/dist/errors/invalid-scope-error.js +2 -2
  37. package/dist/errors/invalid-scope-error.js.map +1 -1
  38. package/dist/errors/login-required-error.d.ts +2 -3
  39. package/dist/errors/login-required-error.d.ts.map +1 -1
  40. package/dist/errors/login-required-error.js +2 -7
  41. package/dist/errors/login-required-error.js.map +1 -1
  42. package/dist/lib/http/response.d.ts +4 -4
  43. package/dist/lib/http/response.d.ts.map +1 -1
  44. package/dist/lib/http/response.js +8 -7
  45. package/dist/lib/http/response.js.map +1 -1
  46. package/dist/lib/http/stream.d.ts +1 -0
  47. package/dist/lib/http/stream.d.ts.map +1 -1
  48. package/dist/lib/http/stream.js +6 -0
  49. package/dist/lib/http/stream.js.map +1 -1
  50. package/dist/lib/util/error.d.ts +2 -0
  51. package/dist/lib/util/error.d.ts.map +1 -0
  52. package/dist/lib/util/error.js +11 -0
  53. package/dist/lib/util/error.js.map +1 -0
  54. package/dist/lib/util/zod-error.d.ts +3 -1
  55. package/dist/lib/util/zod-error.d.ts.map +1 -1
  56. package/dist/lib/util/zod-error.js +20 -10
  57. package/dist/lib/util/zod-error.js.map +1 -1
  58. package/dist/metadata/build-metadata.d.ts +0 -1
  59. package/dist/metadata/build-metadata.d.ts.map +1 -1
  60. package/dist/metadata/build-metadata.js +6 -2
  61. package/dist/metadata/build-metadata.js.map +1 -1
  62. package/dist/oauth-errors.d.ts +1 -1
  63. package/dist/oauth-errors.d.ts.map +1 -1
  64. package/dist/oauth-errors.js +1 -1
  65. package/dist/oauth-errors.js.map +1 -1
  66. package/dist/oauth-hooks.d.ts +17 -3
  67. package/dist/oauth-hooks.d.ts.map +1 -1
  68. package/dist/oauth-hooks.js +7 -4
  69. package/dist/oauth-hooks.js.map +1 -1
  70. package/dist/oauth-provider.d.ts +1 -0
  71. package/dist/oauth-provider.d.ts.map +1 -1
  72. package/dist/oauth-provider.js +35 -49
  73. package/dist/oauth-provider.js.map +1 -1
  74. package/dist/oauth-verifier.d.ts +2 -1
  75. package/dist/oauth-verifier.d.ts.map +1 -1
  76. package/dist/oauth-verifier.js.map +1 -1
  77. package/dist/request/request-manager.d.ts +5 -5
  78. package/dist/request/request-manager.d.ts.map +1 -1
  79. package/dist/request/request-manager.js +63 -45
  80. package/dist/request/request-manager.js.map +1 -1
  81. package/dist/request/request-store.d.ts +6 -6
  82. package/dist/request/request-store.d.ts.map +1 -1
  83. package/dist/result/authorization-result-authorize-page.d.ts +2 -3
  84. package/dist/result/authorization-result-authorize-page.d.ts.map +1 -1
  85. package/dist/router/assets/send-authorization-page.js +3 -2
  86. package/dist/router/assets/send-authorization-page.js.map +1 -1
  87. package/dist/router/create-api-middleware.d.ts.map +1 -1
  88. package/dist/router/create-api-middleware.js +68 -48
  89. package/dist/router/create-api-middleware.js.map +1 -1
  90. package/dist/router/create-authorization-page-middleware.d.ts.map +1 -1
  91. package/dist/router/create-authorization-page-middleware.js +19 -17
  92. package/dist/router/create-authorization-page-middleware.js.map +1 -1
  93. package/dist/router/create-oauth-middleware.d.ts.map +1 -1
  94. package/dist/router/create-oauth-middleware.js +21 -18
  95. package/dist/router/create-oauth-middleware.js.map +1 -1
  96. package/dist/router/send-redirect.js +2 -2
  97. package/dist/router/send-redirect.js.map +1 -1
  98. package/dist/token/token-manager.js +1 -1
  99. package/dist/token/verify-token-claims.d.ts +1 -0
  100. package/dist/token/verify-token-claims.d.ts.map +1 -1
  101. package/dist/token/verify-token-claims.js.map +1 -1
  102. package/dist/types/authorization-response-error.d.ts +5 -0
  103. package/dist/types/authorization-response-error.d.ts.map +1 -0
  104. package/dist/types/authorization-response-error.js +21 -0
  105. package/dist/types/authorization-response-error.js.map +1 -0
  106. package/dist/types/par-response-error.d.ts +5 -0
  107. package/dist/types/par-response-error.d.ts.map +1 -0
  108. package/dist/types/par-response-error.js +22 -0
  109. package/dist/types/par-response-error.js.map +1 -0
  110. package/package.json +7 -6
  111. package/src/client/client-manager.ts +0 -8
  112. package/src/client/client.ts +6 -6
  113. package/src/device/device-manager.ts +1 -1
  114. package/src/dpop/dpop-manager.ts +16 -16
  115. package/src/errors/access-denied-error.ts +6 -33
  116. package/src/errors/account-selection-required-error.ts +2 -2
  117. package/src/errors/authorization-error.ts +45 -0
  118. package/src/errors/consent-required-error.ts +2 -2
  119. package/src/errors/error-parser.ts +2 -1
  120. package/src/errors/invalid-authorization-details-error.ts +2 -2
  121. package/src/errors/invalid-scope-error.ts +2 -2
  122. package/src/errors/login-required-error.ts +2 -12
  123. package/src/lib/http/response.ts +14 -13
  124. package/src/lib/http/stream.ts +6 -0
  125. package/src/lib/util/error.ts +7 -0
  126. package/src/lib/util/zod-error.ts +23 -11
  127. package/src/metadata/build-metadata.ts +8 -3
  128. package/src/oauth-errors.ts +1 -1
  129. package/src/oauth-hooks.ts +18 -2
  130. package/src/oauth-provider.ts +37 -58
  131. package/src/oauth-verifier.ts +3 -1
  132. package/src/request/request-manager.ts +76 -55
  133. package/src/request/request-store.ts +6 -6
  134. package/src/result/authorization-result-authorize-page.ts +2 -3
  135. package/src/router/assets/send-authorization-page.ts +3 -3
  136. package/src/router/create-api-middleware.ts +92 -64
  137. package/src/router/create-authorization-page-middleware.ts +19 -21
  138. package/src/router/create-oauth-middleware.ts +28 -27
  139. package/src/router/send-redirect.ts +2 -2
  140. package/src/token/token-manager.ts +1 -1
  141. package/src/token/verify-token-claims.ts +8 -0
  142. package/src/types/authorization-response-error.ts +27 -0
  143. package/src/types/par-response-error.ts +25 -0
  144. package/tsconfig.build.tsbuildinfo +1 -1
  145. package/dist/errors/invalid-parameters-error.d.ts +0 -6
  146. package/dist/errors/invalid-parameters-error.d.ts.map +0 -1
  147. package/dist/errors/invalid-parameters-error.js +0 -11
  148. package/dist/errors/invalid-parameters-error.js.map +0 -1
  149. package/dist/request/request-info.d.ts +0 -14
  150. package/dist/request/request-info.d.ts.map +0 -1
  151. package/dist/request/request-info.js +0 -3
  152. package/dist/request/request-info.js.map +0 -1
  153. package/src/errors/invalid-parameters-error.ts +0 -12
  154. package/src/request/request-info.ts +0 -14
@@ -20,16 +20,22 @@ import {
20
20
  import { signInDataSchema } from '../account/sign-in-data.js'
21
21
  import { signUpInputSchema } from '../account/sign-up-input.js'
22
22
  import { DeviceId, deviceIdSchema } from '../device/device-id.js'
23
- import { AccessDeniedError } from '../errors/access-denied-error.js'
24
- import { buildErrorPayload, buildErrorStatus } from '../errors/error-parser.js'
23
+ import { AuthorizationError } from '../errors/authorization-error.js'
24
+ import {
25
+ ErrorPayload,
26
+ buildErrorPayload,
27
+ buildErrorStatus,
28
+ } from '../errors/error-parser.js'
25
29
  import { InvalidRequestError } from '../errors/invalid-request-error.js'
26
30
  import { WWWAuthenticateError } from '../errors/www-authenticate-error.js'
27
31
  import {
32
+ JsonResponse,
28
33
  Middleware,
29
34
  RequestMetadata,
30
35
  Router,
31
36
  RouterCtx,
32
37
  SubCtx,
38
+ flushStream,
33
39
  jsonHandler,
34
40
  parseHttpRequest,
35
41
  subCtx,
@@ -84,7 +90,7 @@ export function createApiMiddleware<
84
90
  schema: verifyHandleSchema,
85
91
  async handler() {
86
92
  await server.accountManager.verifyHandleAvailability(this.input.handle)
87
- return { available: true }
93
+ return { json: { available: true } }
88
94
  },
89
95
  }),
90
96
  )
@@ -121,7 +127,8 @@ export function createApiMiddleware<
121
127
  requestUri: this.requestUri,
122
128
  })
123
129
 
124
- return { account, ephemeralToken }
130
+ const json = { account, ephemeralToken }
131
+ return { json }
125
132
  },
126
133
  }),
127
134
  )
@@ -173,7 +180,7 @@ export function createApiMiddleware<
173
180
  account.sub,
174
181
  )
175
182
 
176
- return {
183
+ const json = {
177
184
  account,
178
185
  ephemeralToken,
179
186
  consentRequired: server.checkConsentRequired(
@@ -181,9 +188,12 @@ export function createApiMiddleware<
181
188
  authorizedClients.get(clientId),
182
189
  ),
183
190
  }
191
+
192
+ return { json }
184
193
  }
185
194
 
186
- return { account, ephemeralToken }
195
+ const json = { account, ephemeralToken }
196
+ return { json }
187
197
  },
188
198
  }),
189
199
  )
@@ -205,7 +215,7 @@ export function createApiMiddleware<
205
215
  await server.accountManager.removeDeviceAccount(this.deviceId, sub)
206
216
  }
207
217
 
208
- return { success: true as const }
218
+ return { json: { success: true as const } }
209
219
  },
210
220
  }),
211
221
  )
@@ -222,7 +232,7 @@ export function createApiMiddleware<
222
232
  .strict(),
223
233
  async handler() {
224
234
  await server.accountManager.resetPasswordRequest(this.input)
225
- return { success: true }
235
+ return { json: { success: true } }
226
236
  },
227
237
  }),
228
238
  )
@@ -239,7 +249,7 @@ export function createApiMiddleware<
239
249
  .strict(),
240
250
  async handler() {
241
251
  await server.accountManager.resetPasswordConfirm(this.input)
242
- return { success: true }
252
+ return { json: { success: true } }
243
253
  },
244
254
  }),
245
255
  )
@@ -254,12 +264,14 @@ export function createApiMiddleware<
254
264
  this.deviceId,
255
265
  )
256
266
 
257
- return deviceAccounts.map(
267
+ const json = deviceAccounts.map(
258
268
  (deviceAccount): ActiveDeviceSession => ({
259
269
  account: deviceAccount.account,
260
270
  loginRequired: server.checkLoginRequired(deviceAccount),
261
271
  }),
262
272
  )
273
+
274
+ return { json }
263
275
  },
264
276
  }),
265
277
  )
@@ -289,7 +301,7 @@ export function createApiMiddleware<
289
301
  // expose the expiration date). This requires a change to the way
290
302
  // TokenInfo are stored (see TokenManager#isTokenExpired and
291
303
  // TokenManager#isTokenInactive).
292
- return tokenInfos.map(({ id, data }): ActiveOAuthSession => {
304
+ const json = tokenInfos.map(({ id, data }): ActiveOAuthSession => {
293
305
  return {
294
306
  tokenId: id,
295
307
 
@@ -302,6 +314,8 @@ export function createApiMiddleware<
302
314
  scope: data.parameters.scope,
303
315
  }
304
316
  })
317
+
318
+ return { json }
305
319
  },
306
320
  }),
307
321
  )
@@ -318,7 +332,7 @@ export function createApiMiddleware<
318
332
  account.sub,
319
333
  )
320
334
 
321
- return deviceAccounts.map(
335
+ const json = deviceAccounts.map(
322
336
  (accountSession): ActiveAccountSession => ({
323
337
  deviceId: accountSession.deviceId,
324
338
  deviceMetadata: {
@@ -331,6 +345,8 @@ export function createApiMiddleware<
331
345
  isCurrentDevice: accountSession.deviceId === this.deviceId,
332
346
  }),
333
347
  )
348
+
349
+ return { json }
334
350
  },
335
351
  }),
336
352
  )
@@ -350,7 +366,7 @@ export function createApiMiddleware<
350
366
  this.input.sub,
351
367
  )
352
368
 
353
- return { success: true }
369
+ return { json: { success: true } }
354
370
  },
355
371
  }),
356
372
  )
@@ -374,7 +390,7 @@ export function createApiMiddleware<
374
390
 
375
391
  await server.tokenManager.deleteToken(tokenInfo.id)
376
392
 
377
- return { success: true }
393
+ return { json: { success: true } }
378
394
  },
379
395
  }),
380
396
  )
@@ -382,8 +398,13 @@ export function createApiMiddleware<
382
398
  router.use(
383
399
  apiRoute({
384
400
  method: 'POST',
385
- endpoint: '/accept',
386
- schema: z.object({ sub: z.union([subSchema, signedJwtSchema]) }).strict(),
401
+ endpoint: '/consent',
402
+ schema: z
403
+ .object({
404
+ sub: z.union([subSchema, signedJwtSchema]),
405
+ scope: z.string().optional(),
406
+ })
407
+ .strict(),
387
408
  async handler(req, res) {
388
409
  if (!this.requestUri) {
389
410
  throw new InvalidRequestError(
@@ -391,7 +412,7 @@ export function createApiMiddleware<
391
412
  )
392
413
  }
393
414
 
394
- // Any AccessDeniedError caught in this block will result in a redirect
415
+ // Any AuthorizationError caught in this block will result in a redirect
395
416
  // to the client's redirect_uri with an error.
396
417
  try {
397
418
  const { clientId, parameters } = await server.requestManager.get(
@@ -400,7 +421,7 @@ export function createApiMiddleware<
400
421
  )
401
422
 
402
423
  // Any error thrown in this block will be transformed into an
403
- // AccessDeniedError.
424
+ // AuthorizationError.
404
425
  try {
405
426
  const { account, authorizedClients } = await authenticate.call(
406
427
  this,
@@ -416,6 +437,7 @@ export function createApiMiddleware<
416
437
  account,
417
438
  this.deviceId,
418
439
  this.deviceMetadata,
440
+ this.input.scope,
419
441
  )
420
442
 
421
443
  const clientData = authorizedClients.get(clientId)
@@ -436,13 +458,15 @@ export function createApiMiddleware<
436
458
 
437
459
  const url = buildRedirectUrl(server.issuer, parameters, { code })
438
460
 
439
- return { url }
461
+ return { json: { url } }
440
462
  } catch (err) {
441
463
  // Since we have access to the parameters, we can re-throw an
442
- // AccessDeniedError with the redirect_uri parameter.
443
- throw AccessDeniedError.from(parameters, err, 'server_error')
464
+ // AuthorizationError with the redirect_uri parameter.
465
+ throw AuthorizationError.from(parameters, err)
444
466
  }
445
467
  } catch (err) {
468
+ onError?.(req, res, err, 'Failed to consent authorization request')
469
+
446
470
  // If any error happened (unauthenticated, invalid request, etc.),
447
471
  // lets make sure the request can no longer be used.
448
472
  try {
@@ -451,20 +475,24 @@ export function createApiMiddleware<
451
475
  onError?.(req, res, err, 'Failed to delete request')
452
476
  }
453
477
 
454
- if (err instanceof AccessDeniedError && err.parameters.redirect_uri) {
455
- // Prefer logging the cause
456
- onError?.(req, res, err.cause ?? err, 'Authorization failed')
457
-
458
- const url = buildRedirectUrl(
459
- server.issuer,
460
- err.parameters,
461
- err.toJSON(),
462
- )
478
+ if (err instanceof AuthorizationError) {
479
+ try {
480
+ const url = buildRedirectUrl(
481
+ server.issuer,
482
+ err.parameters,
483
+ err.toJSON(),
484
+ )
463
485
 
464
- return { url }
486
+ return { json: { url } }
487
+ } catch {
488
+ // Unable to build redirect URL, ignore
489
+ }
465
490
  }
466
491
 
467
- throw err
492
+ // @NOTE Not re-throwing the error here, as the error was already
493
+ // handled by the `onError` callback, and apiRoute (`apiMiddleware`)
494
+ // would call `onError` again.
495
+ return buildErrorJsonResponse(err)
468
496
  }
469
497
  },
470
498
  }),
@@ -507,22 +535,25 @@ export function createApiMiddleware<
507
535
  error_description: 'The user rejected the request',
508
536
  })
509
537
 
510
- return { url }
538
+ return { json: { url } }
511
539
  } catch (err) {
512
- if (err instanceof AccessDeniedError && err.parameters.redirect_uri) {
513
- // Prefer logging the cause
514
- onError?.(req, res, err.cause ?? err, 'Authorization failed')
515
-
516
- const url = buildRedirectUrl(
517
- server.issuer,
518
- err.parameters,
519
- err.toJSON(),
520
- )
540
+ onError?.(req, res, err, 'Failed to reject authorization request')
541
+
542
+ if (err instanceof AuthorizationError) {
543
+ try {
544
+ const url = buildRedirectUrl(
545
+ server.issuer,
546
+ err.parameters,
547
+ err.toJSON(),
548
+ )
521
549
 
522
- return { url }
550
+ return { json: { url } }
551
+ } catch {
552
+ // Unable to build redirect URL, ignore
553
+ }
523
554
  }
524
555
 
525
- throw err
556
+ return buildErrorJsonResponse(err)
526
557
  } finally {
527
558
  await server.requestManager.delete(requestUri).catch((err) => {
528
559
  onError?.(req, res, err, 'Failed to delete request')
@@ -637,7 +668,7 @@ export function createApiMiddleware<
637
668
  this: ApiContext<RouteCtx<C>, InferValidation<S>>,
638
669
  req: Req,
639
670
  res: Res,
640
- ) => Awaitable<ApiEndpoints[E]['output']>
671
+ ) => Awaitable<JsonResponse<ErrorPayload | ApiEndpoints[E]['output']>>
641
672
  }): Middleware<C, Req, Res> {
642
673
  return createRoute(
643
674
  options.method,
@@ -659,12 +690,12 @@ export function createApiMiddleware<
659
690
  this: ApiContext<C, InferValidation<S>>,
660
691
  req: Req,
661
692
  res: Res,
662
- ) => unknown
693
+ ) => Awaitable<JsonResponse>
663
694
  }): Middleware<C, Req, Res> {
664
695
  const parseInput: (this: C, req: Req) => Promise<InferValidation<S>> =
665
696
  schema == null // No schema means endpoint doesn't accept any input
666
697
  ? async function (req) {
667
- req.resume() // Flush body
698
+ await flushStream(req)
668
699
  return undefined
669
700
  }
670
701
  : method === 'POST'
@@ -673,12 +704,7 @@ export function createApiMiddleware<
673
704
  return schema.parseAsync(body, { path: ['body'] })
674
705
  }
675
706
  : async function (req) {
676
- // @NOTE This should not be necessary with GET requests
677
- req.resume().once('error', (_err) => {
678
- // Ignore errors when flushing the request body
679
- // (e.g. client closed connection)
680
- })
681
-
707
+ await flushStream(req)
682
708
  const query = Object.fromEntries(this.url.searchParams)
683
709
  return schema.parseAsync(query, { path: ['query'] })
684
710
  }
@@ -726,30 +752,32 @@ export function createApiMiddleware<
726
752
  rotateDeviceCookies,
727
753
  )
728
754
 
729
- const context = subCtx(this, {
755
+ const context: ApiContext<C, InferValidation<S>> = subCtx(this, {
730
756
  input,
731
757
  requestUri,
732
758
  deviceId,
733
759
  deviceMetadata,
734
760
  })
735
761
 
736
- // Generate the API response
737
- const payload = await handler.call(context, req, res)
738
-
739
- return { payload, status: 200 }
762
+ return await handler.call(context, req, res)
740
763
  } catch (err) {
741
- onError?.(req, res, err, 'Failed to handle API request')
742
-
743
- // @TODO Rework the API error responses (relying on codes)
744
- const payload = buildErrorPayload(err)
745
- const status = buildErrorStatus(err)
764
+ onError?.(req, res, err, `Failed to handle API request`)
746
765
 
747
- return { payload, status }
766
+ // Make sore to always return a JSON response
767
+ return buildErrorJsonResponse(err)
748
768
  }
749
769
  })
750
770
  }
751
771
  }
752
772
 
773
+ function buildErrorJsonResponse(err: unknown) {
774
+ // @TODO Rework the API error responses (relying on codes)
775
+ const json = buildErrorPayload(err)
776
+ const status = buildErrorStatus(err)
777
+
778
+ return { json, status }
779
+ }
780
+
753
781
  function buildRedirectUrl(
754
782
  iss: string,
755
783
  parameters: OAuthAuthorizationRequestParameters,
@@ -3,7 +3,7 @@ import {
3
3
  oauthAuthorizationRequestQuerySchema,
4
4
  oauthClientCredentialsSchema,
5
5
  } from '@atproto/oauth-types'
6
- import { AccessDeniedError } from '../errors/access-denied-error.js'
6
+ import { AuthorizationError } from '../errors/authorization-error.js'
7
7
  import { InvalidRequestError } from '../errors/invalid-request-error.js'
8
8
  import {
9
9
  Middleware,
@@ -15,8 +15,8 @@ import {
15
15
  validateOrigin,
16
16
  validateReferrer,
17
17
  } from '../lib/http/index.js'
18
+ import { formatError } from '../lib/util/error.js'
18
19
  import type { Awaitable } from '../lib/util/type.js'
19
- import { extractZodErrorMessage } from '../lib/util/zod-error.js'
20
20
  import type { OAuthProvider } from '../oauth-provider.js'
21
21
  import { requestUriSchema } from '../request/request-uri.js'
22
22
  import { AuthorizationResultRedirect } from '../result/authorization-result-redirect.js'
@@ -62,7 +62,7 @@ export function createAuthorizationPageMiddleware<
62
62
 
63
63
  const clientCredentials = await oauthClientCredentialsSchema
64
64
  .parseAsync(query, { path: ['query'] })
65
- .catch(throwInvalidRequest)
65
+ .catch((err) => throwInvalidRequest(err, 'Invalid client credentials'))
66
66
 
67
67
  if ('client_secret' in clientCredentials) {
68
68
  throw new InvalidRequestError('Client secret must not be provided')
@@ -70,7 +70,7 @@ export function createAuthorizationPageMiddleware<
70
70
 
71
71
  const authorizationRequest = await oauthAuthorizationRequestQuerySchema
72
72
  .parseAsync(query, { path: ['query'] })
73
- .catch(throwInvalidRequest)
73
+ .catch((err) => throwInvalidRequest(err, 'Invalid request parameters'))
74
74
 
75
75
  const deviceInfo = await server.deviceManager.load(req, res)
76
76
 
@@ -88,20 +88,21 @@ export function createAuthorizationPageMiddleware<
88
88
  return sendAuthorizePage(req, res, result)
89
89
  }
90
90
  } catch (err) {
91
- // If we have the "redirect_uri" parameter, we can redirect the user
92
- // to the client with an error.
93
- if (err instanceof AccessDeniedError && err.parameters.redirect_uri) {
94
- // Prefer logging the cause
95
- onError?.(req, res, err.cause ?? err, 'Authorization failed')
96
-
97
- return sendAuthorizeRedirect(res, {
98
- issuer: server.issuer,
99
- parameters: err.parameters,
100
- redirect: err.toJSON(),
101
- })
91
+ onError?.(req, res, err, 'Authorization request denied')
92
+
93
+ if (err instanceof AuthorizationError) {
94
+ try {
95
+ return sendAuthorizeRedirect(res, {
96
+ issuer: server.issuer,
97
+ parameters: err.parameters,
98
+ redirect: err.toJSON(),
99
+ })
100
+ } catch {
101
+ // If we fail to send the redirect, we fall back to sending an error
102
+ }
102
103
  }
103
104
 
104
- throw err
105
+ return sendErrorPage(req, res, err)
105
106
  }
106
107
  }),
107
108
  )
@@ -155,11 +156,8 @@ export function createAuthorizationPageMiddleware<
155
156
  }
156
157
  }
157
158
 
158
- function throwInvalidRequest(err: unknown): never {
159
- throw new InvalidRequestError(
160
- extractZodErrorMessage(err) ?? 'Input validation error',
161
- err,
162
- )
159
+ function throwInvalidRequest(err: unknown, prefix: string): never {
160
+ throw new InvalidRequestError(formatError(err, prefix), err)
163
161
  }
164
162
 
165
163
  function sendAuthorizeRedirect(
@@ -19,7 +19,8 @@ import {
19
19
  parseHttpRequest,
20
20
  staticJsonMiddleware,
21
21
  } from '../lib/http/index.js'
22
- import { extractZodErrorMessage } from '../lib/util/zod-error.js'
22
+ import { formatError } from '../lib/util/error.js'
23
+ import { OAuthError } from '../oauth-errors.js'
23
24
  import type { OAuthProvider } from '../oauth-provider.js'
24
25
  import type { MiddlewareOptions } from './middleware-options.js'
25
26
 
@@ -98,11 +99,13 @@ export function createOAuthMiddleware<
98
99
 
99
100
  const credentials = await oauthClientCredentialsSchema
100
101
  .parseAsync(payload, { path: ['body'] })
101
- .catch(throwInvalidClient)
102
+ .catch((err) => throwInvalidClient(err, 'Client credentials missing'))
102
103
 
103
104
  const authorizationRequest = await oauthAuthorizationRequestParSchema
104
105
  .parseAsync(payload, { path: ['body'] })
105
- .catch(throwInvalidRequest)
106
+ .catch((err) =>
107
+ throwInvalidRequest(err, 'Invalid authorization request'),
108
+ )
106
109
 
107
110
  const dpopProof = await server.checkDpopProof(
108
111
  req.method!,
@@ -135,11 +138,11 @@ export function createOAuthMiddleware<
135
138
 
136
139
  const clientCredentials = await oauthClientCredentialsSchema
137
140
  .parseAsync(payload, { path: ['body'] })
138
- .catch(throwInvalidGrant)
141
+ .catch((err) => throwInvalidGrant(err, 'Client credentials missing'))
139
142
 
140
143
  const tokenRequest = await oauthTokenRequestSchema
141
144
  .parseAsync(payload, { path: ['body'] })
142
- .catch(throwInvalidGrant)
145
+ .catch((err) => throwInvalidGrant(err, 'Invalid request payload'))
143
146
 
144
147
  const dpopProof = await server.checkDpopProof(
145
148
  req.method!,
@@ -165,11 +168,11 @@ export function createOAuthMiddleware<
165
168
 
166
169
  const credentials = await oauthClientCredentialsSchema
167
170
  .parseAsync(payload, { path: ['body'] })
168
- .catch(throwInvalidRequest)
171
+ .catch((err) => throwInvalidRequest(err, 'Client credentials missing'))
169
172
 
170
173
  const tokenIdentification = await oauthTokenIdentificationSchema
171
174
  .parseAsync(payload, { path: ['body'] })
172
- .catch(throwInvalidRequest)
175
+ .catch((err) => throwInvalidRequest(err, 'Invalid request payload'))
173
176
 
174
177
  const dpopProof = await server.checkDpopProof(
175
178
  req.method!,
@@ -214,10 +217,17 @@ export function createOAuthMiddleware<
214
217
  res.appendHeader('Access-Control-Expose-Headers', name)
215
218
  }
216
219
 
217
- const payload = await buildOAuthResponse.call(this, req, res)
218
- return { payload, status }
220
+ const json = await buildOAuthResponse.call(this, req, res)
221
+ return { json, status }
219
222
  } catch (err) {
220
- onError?.(req, res, err, 'OAuth request error')
223
+ onError?.(
224
+ req,
225
+ res,
226
+ err,
227
+ err instanceof OAuthError
228
+ ? `OAuth "${err.error}" error`
229
+ : 'Unexpected error',
230
+ )
221
231
 
222
232
  if (!res.headersSent && err instanceof WWWAuthenticateError) {
223
233
  const name = 'WWW-Authenticate'
@@ -226,31 +236,22 @@ export function createOAuthMiddleware<
226
236
  }
227
237
 
228
238
  const status = buildErrorStatus(err)
229
- const payload = buildErrorPayload(err)
239
+ const json = buildErrorPayload(err)
230
240
 
231
- return { payload, status }
241
+ return { json, status }
232
242
  }
233
243
  })
234
244
  }
235
245
  }
236
246
 
237
- function throwInvalidGrant(err: unknown): never {
238
- throw new InvalidGrantError(
239
- extractZodErrorMessage(err) ?? 'Invalid grant',
240
- err,
241
- )
247
+ function throwInvalidGrant(err: unknown, prefix: string): never {
248
+ throw new InvalidGrantError(formatError(err, prefix), err)
242
249
  }
243
250
 
244
- function throwInvalidClient(err: unknown): never {
245
- throw new InvalidClientError(
246
- extractZodErrorMessage(err) ?? 'Client authentication failed',
247
- err,
248
- )
251
+ function throwInvalidClient(err: unknown, prefix: string): never {
252
+ throw new InvalidClientError(formatError(err, prefix), err)
249
253
  }
250
254
 
251
- function throwInvalidRequest(err: unknown): never {
252
- throw new InvalidRequestError(
253
- extractZodErrorMessage(err) ?? 'Input validation error',
254
- err,
255
- )
255
+ function throwInvalidRequest(err: unknown, prefix: string): never {
256
+ throw new InvalidRequestError(formatError(err, prefix), err)
256
257
  }
@@ -3,7 +3,7 @@ import {
3
3
  OAuthAuthorizationRequestParameters,
4
4
  OAuthResponseMode,
5
5
  } from '@atproto/oauth-types'
6
- import { AccessDeniedError } from '../errors/access-denied-error.js'
6
+ import { AuthorizationError } from '../errors/authorization-error.js'
7
7
  import { html, js } from '../lib/html/index.js'
8
8
  import { sendWebPage } from '../lib/send-web-page.js'
9
9
  import { AuthorizationRedirectParameters } from '../result/authorization-redirect-parameters.js'
@@ -37,7 +37,7 @@ export function buildRedirectUri(
37
37
  const uri = parameters.redirect_uri
38
38
  if (uri) return uri
39
39
 
40
- throw new AccessDeniedError(parameters, 'No redirect_uri', 'invalid_request')
40
+ throw new AuthorizationError(parameters, 'No redirect_uri', 'invalid_request')
41
41
  }
42
42
 
43
43
  export function buildRedirectMode(
@@ -299,7 +299,7 @@ export class TokenManager {
299
299
  // @TODO Add another store method that atomically consumes the refresh token
300
300
  // with a lock.
301
301
  const tokenInfo = await this.findByRefreshToken(token).catch((err) => {
302
- throw InvalidTokenError.from(err, `Invalid refresh token`)
302
+ throw InvalidGrantError.from(err, `Invalid refresh token`)
303
303
  })
304
304
 
305
305
  if (!tokenInfo) {
@@ -10,6 +10,14 @@ import { TokenId } from './token-id.js'
10
10
  const BEARER = 'Bearer' satisfies OAuthTokenType
11
11
  const DPOP = 'DPoP' satisfies OAuthTokenType
12
12
 
13
+ export type {
14
+ DpopProof,
15
+ OAuthAccessToken,
16
+ OAuthTokenType,
17
+ SignedTokenPayload,
18
+ TokenId,
19
+ }
20
+
13
21
  export type VerifyTokenClaimsOptions = {
14
22
  /** One of these audience must be included in the token audience(s) */
15
23
  audience?: [string, ...string[]]
@@ -0,0 +1,27 @@
1
+ import { z } from 'zod'
2
+ import {
3
+ oauthAuthorizationResponseErrorSchema,
4
+ oidcAuthorizationResponseErrorSchema,
5
+ } from '@atproto/oauth-types'
6
+
7
+ export const authorizationResponseErrorSchema = z.union([
8
+ oauthAuthorizationResponseErrorSchema,
9
+ // OIDC authentication error response are not part of the ATproto flavoured
10
+ // OAuth but we allow them because they provide better feedback to the client
11
+ // (in particular when SSO is used).
12
+ oidcAuthorizationResponseErrorSchema,
13
+ // This error is defined by rfc9396 (not part of the OAuth 2.1 or OIDC). But
14
+ // since, in ATproto flavoured OAuth, client registration is a dynamic part of
15
+ // the authorization process, we allow it.
16
+ z.literal('invalid_authorization_details'),
17
+ ])
18
+
19
+ export type AuthorizationResponseError = z.infer<
20
+ typeof authorizationResponseErrorSchema
21
+ >
22
+
23
+ export function isAuthorizationResponseError<T>(
24
+ value: T,
25
+ ): value is T & AuthorizationResponseError {
26
+ return authorizationResponseErrorSchema.safeParse(value).success
27
+ }
@@ -0,0 +1,25 @@
1
+ import { z } from 'zod'
2
+ import { authorizationResponseErrorSchema } from './authorization-response-error.js'
3
+
4
+ // https://datatracker.ietf.org/doc/html/rfc9126#section-2.3-1
5
+ // > Since initial processing of the pushed authorization request does not
6
+ // > involve resource owner interaction, error codes related to user
7
+ // > interaction, such as "access_denied", are never returned.
8
+
9
+ export const parResponseErrorSchema = z.intersection(
10
+ authorizationResponseErrorSchema,
11
+ z.enum([
12
+ 'invalid_request',
13
+ 'unauthorized_client',
14
+ 'unsupported_response_type',
15
+ 'invalid_scope',
16
+ 'server_error',
17
+ 'temporarily_unavailable',
18
+ ]),
19
+ )
20
+
21
+ export type PARResponseError = z.infer<typeof parResponseErrorSchema>
22
+
23
+ export function isPARResponseError<T>(value: T): value is T & PARResponseError {
24
+ return parResponseErrorSchema.safeParse(value).success
25
+ }