@atproto/oauth-provider 0.9.2 → 0.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/client/client.js +6 -6
  3. package/dist/client/client.js.map +1 -1
  4. package/dist/device/device-manager.js +1 -1
  5. package/dist/device/device-manager.js.map +1 -1
  6. package/dist/dpop/dpop-manager.js +15 -15
  7. package/dist/dpop/dpop-manager.js.map +1 -1
  8. package/dist/errors/access-denied-error.d.ts +4 -7
  9. package/dist/errors/access-denied-error.d.ts.map +1 -1
  10. package/dist/errors/access-denied-error.js +4 -13
  11. package/dist/errors/access-denied-error.js.map +1 -1
  12. package/dist/errors/account-selection-required-error.d.ts +2 -2
  13. package/dist/errors/account-selection-required-error.d.ts.map +1 -1
  14. package/dist/errors/account-selection-required-error.js +2 -2
  15. package/dist/errors/account-selection-required-error.js.map +1 -1
  16. package/dist/errors/authorization-error.d.ts +10 -0
  17. package/dist/errors/authorization-error.d.ts.map +1 -0
  18. package/dist/errors/authorization-error.js +31 -0
  19. package/dist/errors/authorization-error.js.map +1 -0
  20. package/dist/errors/consent-required-error.d.ts +2 -2
  21. package/dist/errors/consent-required-error.d.ts.map +1 -1
  22. package/dist/errors/consent-required-error.js +2 -2
  23. package/dist/errors/consent-required-error.js.map +1 -1
  24. package/dist/errors/error-parser.d.ts.map +1 -1
  25. package/dist/errors/error-parser.js +2 -1
  26. package/dist/errors/error-parser.js.map +1 -1
  27. package/dist/errors/invalid-authorization-details-error.d.ts +2 -2
  28. package/dist/errors/invalid-authorization-details-error.d.ts.map +1 -1
  29. package/dist/errors/invalid-authorization-details-error.js +2 -2
  30. package/dist/errors/invalid-authorization-details-error.js.map +1 -1
  31. package/dist/errors/invalid-scope-error.d.ts +2 -2
  32. package/dist/errors/invalid-scope-error.d.ts.map +1 -1
  33. package/dist/errors/invalid-scope-error.js +2 -2
  34. package/dist/errors/invalid-scope-error.js.map +1 -1
  35. package/dist/errors/login-required-error.d.ts +2 -3
  36. package/dist/errors/login-required-error.d.ts.map +1 -1
  37. package/dist/errors/login-required-error.js +2 -7
  38. package/dist/errors/login-required-error.js.map +1 -1
  39. package/dist/lib/http/response.d.ts +4 -4
  40. package/dist/lib/http/response.d.ts.map +1 -1
  41. package/dist/lib/http/response.js +8 -7
  42. package/dist/lib/http/response.js.map +1 -1
  43. package/dist/lib/http/stream.d.ts +1 -0
  44. package/dist/lib/http/stream.d.ts.map +1 -1
  45. package/dist/lib/http/stream.js +6 -0
  46. package/dist/lib/http/stream.js.map +1 -1
  47. package/dist/lib/util/error.d.ts +2 -0
  48. package/dist/lib/util/error.d.ts.map +1 -0
  49. package/dist/lib/util/error.js +11 -0
  50. package/dist/lib/util/error.js.map +1 -0
  51. package/dist/lib/util/zod-error.d.ts +3 -1
  52. package/dist/lib/util/zod-error.d.ts.map +1 -1
  53. package/dist/lib/util/zod-error.js +20 -10
  54. package/dist/lib/util/zod-error.js.map +1 -1
  55. package/dist/oauth-errors.d.ts +1 -1
  56. package/dist/oauth-errors.d.ts.map +1 -1
  57. package/dist/oauth-errors.js +1 -1
  58. package/dist/oauth-errors.js.map +1 -1
  59. package/dist/oauth-hooks.d.ts +3 -2
  60. package/dist/oauth-hooks.d.ts.map +1 -1
  61. package/dist/oauth-hooks.js +4 -3
  62. package/dist/oauth-hooks.js.map +1 -1
  63. package/dist/oauth-provider.d.ts.map +1 -1
  64. package/dist/oauth-provider.js +18 -21
  65. package/dist/oauth-provider.js.map +1 -1
  66. package/dist/request/request-manager.d.ts.map +1 -1
  67. package/dist/request/request-manager.js +12 -12
  68. package/dist/request/request-manager.js.map +1 -1
  69. package/dist/router/create-api-middleware.d.ts.map +1 -1
  70. package/dist/router/create-api-middleware.js +60 -45
  71. package/dist/router/create-api-middleware.js.map +1 -1
  72. package/dist/router/create-authorization-page-middleware.d.ts.map +1 -1
  73. package/dist/router/create-authorization-page-middleware.js +19 -17
  74. package/dist/router/create-authorization-page-middleware.js.map +1 -1
  75. package/dist/router/create-oauth-middleware.d.ts.map +1 -1
  76. package/dist/router/create-oauth-middleware.js +21 -18
  77. package/dist/router/create-oauth-middleware.js.map +1 -1
  78. package/dist/router/send-redirect.js +2 -2
  79. package/dist/router/send-redirect.js.map +1 -1
  80. package/dist/token/token-manager.js +1 -1
  81. package/dist/types/authorization-response-error.d.ts +5 -0
  82. package/dist/types/authorization-response-error.d.ts.map +1 -0
  83. package/dist/types/authorization-response-error.js +21 -0
  84. package/dist/types/authorization-response-error.js.map +1 -0
  85. package/dist/types/par-response-error.d.ts +5 -0
  86. package/dist/types/par-response-error.d.ts.map +1 -0
  87. package/dist/types/par-response-error.js +22 -0
  88. package/dist/types/par-response-error.js.map +1 -0
  89. package/package.json +5 -5
  90. package/src/client/client.ts +6 -6
  91. package/src/device/device-manager.ts +1 -1
  92. package/src/dpop/dpop-manager.ts +16 -16
  93. package/src/errors/access-denied-error.ts +6 -33
  94. package/src/errors/account-selection-required-error.ts +2 -2
  95. package/src/errors/authorization-error.ts +45 -0
  96. package/src/errors/consent-required-error.ts +2 -2
  97. package/src/errors/error-parser.ts +2 -1
  98. package/src/errors/invalid-authorization-details-error.ts +2 -2
  99. package/src/errors/invalid-scope-error.ts +2 -2
  100. package/src/errors/login-required-error.ts +2 -12
  101. package/src/lib/http/response.ts +14 -13
  102. package/src/lib/http/stream.ts +6 -0
  103. package/src/lib/util/error.ts +7 -0
  104. package/src/lib/util/zod-error.ts +23 -11
  105. package/src/oauth-errors.ts +1 -1
  106. package/src/oauth-hooks.ts +3 -2
  107. package/src/oauth-provider.ts +18 -28
  108. package/src/request/request-manager.ts +12 -18
  109. package/src/router/create-api-middleware.ts +84 -62
  110. package/src/router/create-authorization-page-middleware.ts +19 -21
  111. package/src/router/create-oauth-middleware.ts +28 -27
  112. package/src/router/send-redirect.ts +2 -2
  113. package/src/token/token-manager.ts +1 -1
  114. package/src/types/authorization-response-error.ts +27 -0
  115. package/src/types/par-response-error.ts +25 -0
  116. package/tsconfig.build.tsbuildinfo +1 -1
  117. package/dist/errors/invalid-parameters-error.d.ts +0 -6
  118. package/dist/errors/invalid-parameters-error.d.ts.map +0 -1
  119. package/dist/errors/invalid-parameters-error.js +0 -11
  120. package/dist/errors/invalid-parameters-error.js.map +0 -1
  121. package/src/errors/invalid-parameters-error.ts +0 -12
@@ -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
  )
@@ -391,7 +407,7 @@ export function createApiMiddleware<
391
407
  )
392
408
  }
393
409
 
394
- // Any AccessDeniedError caught in this block will result in a redirect
410
+ // Any AuthorizationError caught in this block will result in a redirect
395
411
  // to the client's redirect_uri with an error.
396
412
  try {
397
413
  const { clientId, parameters } = await server.requestManager.get(
@@ -400,7 +416,7 @@ export function createApiMiddleware<
400
416
  )
401
417
 
402
418
  // Any error thrown in this block will be transformed into an
403
- // AccessDeniedError.
419
+ // AuthorizationError.
404
420
  try {
405
421
  const { account, authorizedClients } = await authenticate.call(
406
422
  this,
@@ -436,13 +452,15 @@ export function createApiMiddleware<
436
452
 
437
453
  const url = buildRedirectUrl(server.issuer, parameters, { code })
438
454
 
439
- return { url }
455
+ return { json: { url } }
440
456
  } catch (err) {
441
457
  // 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')
458
+ // AuthorizationError with the redirect_uri parameter.
459
+ throw AuthorizationError.from(parameters, err)
444
460
  }
445
461
  } catch (err) {
462
+ onError?.(req, res, err, 'Failed to accept authorization request')
463
+
446
464
  // If any error happened (unauthenticated, invalid request, etc.),
447
465
  // lets make sure the request can no longer be used.
448
466
  try {
@@ -451,20 +469,24 @@ export function createApiMiddleware<
451
469
  onError?.(req, res, err, 'Failed to delete request')
452
470
  }
453
471
 
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
- )
472
+ if (err instanceof AuthorizationError) {
473
+ try {
474
+ const url = buildRedirectUrl(
475
+ server.issuer,
476
+ err.parameters,
477
+ err.toJSON(),
478
+ )
463
479
 
464
- return { url }
480
+ return { json: { url } }
481
+ } catch {
482
+ // Unable to build redirect URL, ignore
483
+ }
465
484
  }
466
485
 
467
- throw err
486
+ // @NOTE Not re-throwing the error here, as the error was already
487
+ // handled by the `onError` callback, and apiRoute (`apiMiddleware`)
488
+ // would call `onError` again.
489
+ return buildErrorJsonResponse(err)
468
490
  }
469
491
  },
470
492
  }),
@@ -507,22 +529,25 @@ export function createApiMiddleware<
507
529
  error_description: 'The user rejected the request',
508
530
  })
509
531
 
510
- return { url }
532
+ return { json: { url } }
511
533
  } 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
- )
534
+ onError?.(req, res, err, 'Failed to reject authorization request')
535
+
536
+ if (err instanceof AuthorizationError) {
537
+ try {
538
+ const url = buildRedirectUrl(
539
+ server.issuer,
540
+ err.parameters,
541
+ err.toJSON(),
542
+ )
521
543
 
522
- return { url }
544
+ return { json: { url } }
545
+ } catch {
546
+ // Unable to build redirect URL, ignore
547
+ }
523
548
  }
524
549
 
525
- throw err
550
+ return buildErrorJsonResponse(err)
526
551
  } finally {
527
552
  await server.requestManager.delete(requestUri).catch((err) => {
528
553
  onError?.(req, res, err, 'Failed to delete request')
@@ -637,7 +662,7 @@ export function createApiMiddleware<
637
662
  this: ApiContext<RouteCtx<C>, InferValidation<S>>,
638
663
  req: Req,
639
664
  res: Res,
640
- ) => Awaitable<ApiEndpoints[E]['output']>
665
+ ) => Awaitable<JsonResponse<ErrorPayload | ApiEndpoints[E]['output']>>
641
666
  }): Middleware<C, Req, Res> {
642
667
  return createRoute(
643
668
  options.method,
@@ -659,12 +684,12 @@ export function createApiMiddleware<
659
684
  this: ApiContext<C, InferValidation<S>>,
660
685
  req: Req,
661
686
  res: Res,
662
- ) => unknown
687
+ ) => Awaitable<JsonResponse>
663
688
  }): Middleware<C, Req, Res> {
664
689
  const parseInput: (this: C, req: Req) => Promise<InferValidation<S>> =
665
690
  schema == null // No schema means endpoint doesn't accept any input
666
691
  ? async function (req) {
667
- req.resume() // Flush body
692
+ await flushStream(req)
668
693
  return undefined
669
694
  }
670
695
  : method === 'POST'
@@ -673,12 +698,7 @@ export function createApiMiddleware<
673
698
  return schema.parseAsync(body, { path: ['body'] })
674
699
  }
675
700
  : 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
-
701
+ await flushStream(req)
682
702
  const query = Object.fromEntries(this.url.searchParams)
683
703
  return schema.parseAsync(query, { path: ['query'] })
684
704
  }
@@ -726,30 +746,32 @@ export function createApiMiddleware<
726
746
  rotateDeviceCookies,
727
747
  )
728
748
 
729
- const context = subCtx(this, {
749
+ const context: ApiContext<C, InferValidation<S>> = subCtx(this, {
730
750
  input,
731
751
  requestUri,
732
752
  deviceId,
733
753
  deviceMetadata,
734
754
  })
735
755
 
736
- // Generate the API response
737
- const payload = await handler.call(context, req, res)
738
-
739
- return { payload, status: 200 }
756
+ return await handler.call(context, req, res)
740
757
  } 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)
758
+ onError?.(req, res, err, `Failed to handle API request`)
746
759
 
747
- return { payload, status }
760
+ // Make sore to always return a JSON response
761
+ return buildErrorJsonResponse(err)
748
762
  }
749
763
  })
750
764
  }
751
765
  }
752
766
 
767
+ function buildErrorJsonResponse(err: unknown) {
768
+ // @TODO Rework the API error responses (relying on codes)
769
+ const json = buildErrorPayload(err)
770
+ const status = buildErrorStatus(err)
771
+
772
+ return { json, status }
773
+ }
774
+
753
775
  function buildRedirectUrl(
754
776
  iss: string,
755
777
  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) {
@@ -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
+ }
@@ -1 +1 @@
1
- {"root":["./src/constants.ts","./src/index.ts","./src/oauth-client.ts","./src/oauth-dpop.ts","./src/oauth-errors.ts","./src/oauth-hooks.ts","./src/oauth-middleware.ts","./src/oauth-provider.ts","./src/oauth-store.ts","./src/oauth-verifier.ts","./src/access-token/access-token-mode.ts","./src/account/account-manager.ts","./src/account/account-store.ts","./src/account/sign-in-data.ts","./src/account/sign-up-input.ts","./src/client/client-auth.ts","./src/client/client-data.ts","./src/client/client-id.ts","./src/client/client-info.ts","./src/client/client-manager.ts","./src/client/client-store.ts","./src/client/client-utils.ts","./src/client/client.ts","./src/customization/branding.ts","./src/customization/build-customization-css.ts","./src/customization/build-customization-data.ts","./src/customization/colors.ts","./src/customization/customization.ts","./src/customization/links.ts","./src/device/device-data.ts","./src/device/device-id.ts","./src/device/device-manager.ts","./src/device/device-store.ts","./src/device/session-id.ts","./src/dpop/dpop-manager.ts","./src/dpop/dpop-nonce.ts","./src/dpop/dpop-proof.ts","./src/errors/access-denied-error.ts","./src/errors/account-selection-required-error.ts","./src/errors/consent-required-error.ts","./src/errors/error-parser.ts","./src/errors/handle-unavailable-error.ts","./src/errors/invalid-authorization-details-error.ts","./src/errors/invalid-client-error.ts","./src/errors/invalid-client-id-error.ts","./src/errors/invalid-client-metadata-error.ts","./src/errors/invalid-dpop-key-binding-error.ts","./src/errors/invalid-dpop-proof-error.ts","./src/errors/invalid-grant-error.ts","./src/errors/invalid-invite-code-error.ts","./src/errors/invalid-parameters-error.ts","./src/errors/invalid-redirect-uri-error.ts","./src/errors/invalid-request-error.ts","./src/errors/invalid-scope-error.ts","./src/errors/invalid-token-error.ts","./src/errors/login-required-error.ts","./src/errors/oauth-error.ts","./src/errors/second-authentication-factor-required-error.ts","./src/errors/unauthorized-client-error.ts","./src/errors/use-dpop-nonce-error.ts","./src/errors/www-authenticate-error.ts","./src/lib/hcaptcha.ts","./src/lib/redis.ts","./src/lib/send-web-page.ts","./src/lib/csp/index.ts","./src/lib/html/build-document.ts","./src/lib/html/escapers.ts","./src/lib/html/html.ts","./src/lib/html/hydration-data.ts","./src/lib/html/index.ts","./src/lib/html/tags.ts","./src/lib/html/util.ts","./src/lib/http/accept.ts","./src/lib/http/context.ts","./src/lib/http/headers.ts","./src/lib/http/index.ts","./src/lib/http/method.ts","./src/lib/http/middleware.ts","./src/lib/http/parser.ts","./src/lib/http/path.ts","./src/lib/http/request.ts","./src/lib/http/response.ts","./src/lib/http/route.ts","./src/lib/http/router.ts","./src/lib/http/security-headers.ts","./src/lib/http/stream.ts","./src/lib/http/types.ts","./src/lib/http/url.ts","./src/lib/util/authorization-header.ts","./src/lib/util/cast.ts","./src/lib/util/color.ts","./src/lib/util/crypto.ts","./src/lib/util/date.ts","./src/lib/util/function.ts","./src/lib/util/locale.ts","./src/lib/util/redirect-uri.ts","./src/lib/util/time.ts","./src/lib/util/type.ts","./src/lib/util/ui8.ts","./src/lib/util/well-known.ts","./src/lib/util/zod-error.ts","./src/metadata/build-metadata.ts","./src/oidc/sub.ts","./src/replay/replay-manager.ts","./src/replay/replay-store-memory.ts","./src/replay/replay-store-redis.ts","./src/replay/replay-store.ts","./src/request/code.ts","./src/request/request-data.ts","./src/request/request-id.ts","./src/request/request-info.ts","./src/request/request-manager.ts","./src/request/request-store.ts","./src/request/request-uri.ts","./src/result/authorization-redirect-parameters.ts","./src/result/authorization-result-authorize-page.ts","./src/result/authorization-result-redirect.ts","./src/router/create-account-page-middleware.ts","./src/router/create-api-middleware.ts","./src/router/create-authorization-page-middleware.ts","./src/router/create-oauth-middleware.ts","./src/router/error-handler.ts","./src/router/middleware-options.ts","./src/router/send-redirect.ts","./src/router/assets/assets-manifest.ts","./src/router/assets/assets.ts","./src/router/assets/csrf.ts","./src/router/assets/send-account-page.ts","./src/router/assets/send-authorization-page.ts","./src/router/assets/send-error-page.ts","./src/signer/api-token-payload.ts","./src/signer/signed-token-payload.ts","./src/signer/signer.ts","./src/token/refresh-token.ts","./src/token/token-data.ts","./src/token/token-id.ts","./src/token/token-manager.ts","./src/token/token-store.ts","./src/token/verify-token-claims.ts","./src/types/color-hue.ts","./src/types/email-otp.ts","./src/types/email.ts","./src/types/handle.ts","./src/types/invite-code.ts","./src/types/password.ts","./src/types/rgb-color.ts"],"version":"5.8.3"}
1
+ {"root":["./src/constants.ts","./src/index.ts","./src/oauth-client.ts","./src/oauth-dpop.ts","./src/oauth-errors.ts","./src/oauth-hooks.ts","./src/oauth-middleware.ts","./src/oauth-provider.ts","./src/oauth-store.ts","./src/oauth-verifier.ts","./src/access-token/access-token-mode.ts","./src/account/account-manager.ts","./src/account/account-store.ts","./src/account/sign-in-data.ts","./src/account/sign-up-input.ts","./src/client/client-auth.ts","./src/client/client-data.ts","./src/client/client-id.ts","./src/client/client-info.ts","./src/client/client-manager.ts","./src/client/client-store.ts","./src/client/client-utils.ts","./src/client/client.ts","./src/customization/branding.ts","./src/customization/build-customization-css.ts","./src/customization/build-customization-data.ts","./src/customization/colors.ts","./src/customization/customization.ts","./src/customization/links.ts","./src/device/device-data.ts","./src/device/device-id.ts","./src/device/device-manager.ts","./src/device/device-store.ts","./src/device/session-id.ts","./src/dpop/dpop-manager.ts","./src/dpop/dpop-nonce.ts","./src/dpop/dpop-proof.ts","./src/errors/access-denied-error.ts","./src/errors/account-selection-required-error.ts","./src/errors/authorization-error.ts","./src/errors/consent-required-error.ts","./src/errors/error-parser.ts","./src/errors/handle-unavailable-error.ts","./src/errors/invalid-authorization-details-error.ts","./src/errors/invalid-client-error.ts","./src/errors/invalid-client-id-error.ts","./src/errors/invalid-client-metadata-error.ts","./src/errors/invalid-dpop-key-binding-error.ts","./src/errors/invalid-dpop-proof-error.ts","./src/errors/invalid-grant-error.ts","./src/errors/invalid-invite-code-error.ts","./src/errors/invalid-redirect-uri-error.ts","./src/errors/invalid-request-error.ts","./src/errors/invalid-scope-error.ts","./src/errors/invalid-token-error.ts","./src/errors/login-required-error.ts","./src/errors/oauth-error.ts","./src/errors/second-authentication-factor-required-error.ts","./src/errors/unauthorized-client-error.ts","./src/errors/use-dpop-nonce-error.ts","./src/errors/www-authenticate-error.ts","./src/lib/hcaptcha.ts","./src/lib/redis.ts","./src/lib/send-web-page.ts","./src/lib/csp/index.ts","./src/lib/html/build-document.ts","./src/lib/html/escapers.ts","./src/lib/html/html.ts","./src/lib/html/hydration-data.ts","./src/lib/html/index.ts","./src/lib/html/tags.ts","./src/lib/html/util.ts","./src/lib/http/accept.ts","./src/lib/http/context.ts","./src/lib/http/headers.ts","./src/lib/http/index.ts","./src/lib/http/method.ts","./src/lib/http/middleware.ts","./src/lib/http/parser.ts","./src/lib/http/path.ts","./src/lib/http/request.ts","./src/lib/http/response.ts","./src/lib/http/route.ts","./src/lib/http/router.ts","./src/lib/http/security-headers.ts","./src/lib/http/stream.ts","./src/lib/http/types.ts","./src/lib/http/url.ts","./src/lib/util/authorization-header.ts","./src/lib/util/cast.ts","./src/lib/util/color.ts","./src/lib/util/crypto.ts","./src/lib/util/date.ts","./src/lib/util/error.ts","./src/lib/util/function.ts","./src/lib/util/locale.ts","./src/lib/util/redirect-uri.ts","./src/lib/util/time.ts","./src/lib/util/type.ts","./src/lib/util/ui8.ts","./src/lib/util/well-known.ts","./src/lib/util/zod-error.ts","./src/metadata/build-metadata.ts","./src/oidc/sub.ts","./src/replay/replay-manager.ts","./src/replay/replay-store-memory.ts","./src/replay/replay-store-redis.ts","./src/replay/replay-store.ts","./src/request/code.ts","./src/request/request-data.ts","./src/request/request-id.ts","./src/request/request-info.ts","./src/request/request-manager.ts","./src/request/request-store.ts","./src/request/request-uri.ts","./src/result/authorization-redirect-parameters.ts","./src/result/authorization-result-authorize-page.ts","./src/result/authorization-result-redirect.ts","./src/router/create-account-page-middleware.ts","./src/router/create-api-middleware.ts","./src/router/create-authorization-page-middleware.ts","./src/router/create-oauth-middleware.ts","./src/router/error-handler.ts","./src/router/middleware-options.ts","./src/router/send-redirect.ts","./src/router/assets/assets-manifest.ts","./src/router/assets/assets.ts","./src/router/assets/csrf.ts","./src/router/assets/send-account-page.ts","./src/router/assets/send-authorization-page.ts","./src/router/assets/send-error-page.ts","./src/signer/api-token-payload.ts","./src/signer/signed-token-payload.ts","./src/signer/signer.ts","./src/token/refresh-token.ts","./src/token/token-data.ts","./src/token/token-id.ts","./src/token/token-manager.ts","./src/token/token-store.ts","./src/token/verify-token-claims.ts","./src/types/authorization-response-error.ts","./src/types/color-hue.ts","./src/types/email-otp.ts","./src/types/email.ts","./src/types/handle.ts","./src/types/invite-code.ts","./src/types/par-response-error.ts","./src/types/password.ts","./src/types/rgb-color.ts"],"version":"5.8.3"}