@atproto/oauth-provider 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/account/account-store.d.ts +2 -2
  3. package/dist/assets/app/bundle-manifest.json +3 -3
  4. package/dist/assets/app/main.css +1 -1
  5. package/dist/assets/app/main.js +3 -3
  6. package/dist/assets/app/main.js.map +1 -1
  7. package/dist/assets/assets-middleware.d.ts.map +1 -1
  8. package/dist/assets/assets-middleware.js +4 -2
  9. package/dist/assets/assets-middleware.js.map +1 -1
  10. package/dist/client/client-manager.d.ts.map +1 -1
  11. package/dist/client/client-manager.js +127 -118
  12. package/dist/client/client-manager.js.map +1 -1
  13. package/dist/client/client-utils.d.ts +1 -2
  14. package/dist/client/client-utils.d.ts.map +1 -1
  15. package/dist/client/client-utils.js +3 -12
  16. package/dist/client/client-utils.js.map +1 -1
  17. package/dist/client/client.d.ts +8 -3
  18. package/dist/client/client.d.ts.map +1 -1
  19. package/dist/client/client.js +70 -1
  20. package/dist/client/client.js.map +1 -1
  21. package/dist/constants.d.ts +0 -1
  22. package/dist/constants.d.ts.map +1 -1
  23. package/dist/constants.js +1 -2
  24. package/dist/constants.js.map +1 -1
  25. package/dist/errors/access-denied-error.d.ts +4 -4
  26. package/dist/errors/access-denied-error.d.ts.map +1 -1
  27. package/dist/errors/access-denied-error.js +2 -2
  28. package/dist/errors/access-denied-error.js.map +1 -1
  29. package/dist/errors/account-selection-required-error.d.ts +2 -2
  30. package/dist/errors/account-selection-required-error.d.ts.map +1 -1
  31. package/dist/errors/account-selection-required-error.js.map +1 -1
  32. package/dist/errors/consent-required-error.d.ts +2 -2
  33. package/dist/errors/consent-required-error.d.ts.map +1 -1
  34. package/dist/errors/consent-required-error.js.map +1 -1
  35. package/dist/errors/invalid-authorization-details-error.d.ts +2 -2
  36. package/dist/errors/invalid-authorization-details-error.d.ts.map +1 -1
  37. package/dist/errors/invalid-authorization-details-error.js.map +1 -1
  38. package/dist/errors/invalid-client-id-error.d.ts +1 -1
  39. package/dist/errors/invalid-client-id-error.d.ts.map +1 -1
  40. package/dist/errors/invalid-client-id-error.js +12 -6
  41. package/dist/errors/invalid-client-id-error.js.map +1 -1
  42. package/dist/errors/invalid-client-metadata-error.d.ts +1 -1
  43. package/dist/errors/invalid-client-metadata-error.d.ts.map +1 -1
  44. package/dist/errors/invalid-client-metadata-error.js +11 -3
  45. package/dist/errors/invalid-client-metadata-error.js.map +1 -1
  46. package/dist/errors/invalid-parameters-error.d.ts +2 -2
  47. package/dist/errors/invalid-parameters-error.d.ts.map +1 -1
  48. package/dist/errors/invalid-parameters-error.js.map +1 -1
  49. package/dist/errors/invalid-scope-error.d.ts +9 -0
  50. package/dist/errors/invalid-scope-error.d.ts.map +1 -0
  51. package/dist/errors/invalid-scope-error.js +14 -0
  52. package/dist/errors/invalid-scope-error.js.map +1 -0
  53. package/dist/errors/login-required-error.d.ts +2 -2
  54. package/dist/errors/login-required-error.d.ts.map +1 -1
  55. package/dist/errors/login-required-error.js.map +1 -1
  56. package/dist/lib/html/html.d.ts +1 -1
  57. package/dist/lib/html/html.d.ts.map +1 -1
  58. package/dist/lib/html/html.js +14 -11
  59. package/dist/lib/html/html.js.map +1 -1
  60. package/dist/lib/http/parser.d.ts +9 -2
  61. package/dist/lib/http/parser.d.ts.map +1 -1
  62. package/dist/lib/http/parser.js +15 -7
  63. package/dist/lib/http/parser.js.map +1 -1
  64. package/dist/lib/http/request.d.ts +0 -23
  65. package/dist/lib/http/request.d.ts.map +1 -1
  66. package/dist/lib/http/request.js +1 -11
  67. package/dist/lib/http/request.js.map +1 -1
  68. package/dist/lib/http/stream.d.ts +28 -6
  69. package/dist/lib/http/stream.d.ts.map +1 -1
  70. package/dist/lib/http/stream.js +21 -32
  71. package/dist/lib/http/stream.js.map +1 -1
  72. package/dist/lib/util/authorization-header.d.ts.map +1 -1
  73. package/dist/lib/util/authorization-header.js +1 -1
  74. package/dist/lib/util/authorization-header.js.map +1 -1
  75. package/dist/lib/util/hostname.d.ts +3 -2
  76. package/dist/lib/util/hostname.d.ts.map +1 -1
  77. package/dist/lib/util/hostname.js +12 -8
  78. package/dist/lib/util/hostname.js.map +1 -1
  79. package/dist/metadata/build-metadata.d.ts.map +1 -1
  80. package/dist/metadata/build-metadata.js +2 -1
  81. package/dist/metadata/build-metadata.js.map +1 -1
  82. package/dist/oauth-errors.d.ts +1 -0
  83. package/dist/oauth-errors.d.ts.map +1 -1
  84. package/dist/oauth-errors.js +3 -1
  85. package/dist/oauth-errors.js.map +1 -1
  86. package/dist/oauth-hooks.d.ts +3 -3
  87. package/dist/oauth-hooks.d.ts.map +1 -1
  88. package/dist/oauth-provider.d.ts +20 -22
  89. package/dist/oauth-provider.d.ts.map +1 -1
  90. package/dist/oauth-provider.js +234 -176
  91. package/dist/oauth-provider.js.map +1 -1
  92. package/dist/oauth-verifier.d.ts +2 -2
  93. package/dist/oauth-verifier.d.ts.map +1 -1
  94. package/dist/oauth-verifier.js.map +1 -1
  95. package/dist/output/build-authorize-data.d.ts +2 -2
  96. package/dist/output/build-authorize-data.d.ts.map +1 -1
  97. package/dist/output/send-authorize-redirect.d.ts +2 -4
  98. package/dist/output/send-authorize-redirect.d.ts.map +1 -1
  99. package/dist/output/send-authorize-redirect.js +5 -2
  100. package/dist/output/send-authorize-redirect.js.map +1 -1
  101. package/dist/request/request-data.d.ts +2 -2
  102. package/dist/request/request-data.d.ts.map +1 -1
  103. package/dist/request/request-info.d.ts +2 -2
  104. package/dist/request/request-info.d.ts.map +1 -1
  105. package/dist/request/request-manager.d.ts +4 -4
  106. package/dist/request/request-manager.d.ts.map +1 -1
  107. package/dist/request/request-manager.js +94 -60
  108. package/dist/request/request-manager.js.map +1 -1
  109. package/dist/signer/signed-token-payload.d.ts +122 -122
  110. package/dist/signer/signer.d.ts +41 -40
  111. package/dist/signer/signer.d.ts.map +1 -1
  112. package/dist/signer/signer.js +13 -15
  113. package/dist/signer/signer.js.map +1 -1
  114. package/dist/token/token-claims.d.ts +121 -121
  115. package/dist/token/token-data.d.ts +3 -3
  116. package/dist/token/token-data.d.ts.map +1 -1
  117. package/dist/token/token-manager.d.ts +4 -5
  118. package/dist/token/token-manager.d.ts.map +1 -1
  119. package/dist/token/token-manager.js +96 -72
  120. package/dist/token/token-manager.js.map +1 -1
  121. package/dist/token/verify-token-claims.d.ts +3 -3
  122. package/dist/token/verify-token-claims.d.ts.map +1 -1
  123. package/dist/token/verify-token-claims.js.map +1 -1
  124. package/package.json +7 -6
  125. package/src/assets/app/components/sign-in-form.tsx +31 -2
  126. package/src/assets/app/components/url-viewer.tsx +3 -3
  127. package/src/assets/assets-middleware.ts +4 -2
  128. package/src/client/client-manager.ts +163 -161
  129. package/src/client/client-utils.ts +7 -12
  130. package/src/client/client.ts +112 -3
  131. package/src/constants.ts +0 -2
  132. package/src/errors/access-denied-error.ts +10 -4
  133. package/src/errors/account-selection-required-error.ts +2 -2
  134. package/src/errors/consent-required-error.ts +2 -2
  135. package/src/errors/invalid-authorization-details-error.ts +2 -2
  136. package/src/errors/invalid-client-id-error.ts +15 -4
  137. package/src/errors/invalid-client-metadata-error.ts +15 -3
  138. package/src/errors/invalid-parameters-error.ts +2 -2
  139. package/src/errors/invalid-scope-error.ts +15 -0
  140. package/src/errors/login-required-error.ts +2 -2
  141. package/src/lib/html/html.ts +14 -12
  142. package/src/lib/http/parser.ts +21 -8
  143. package/src/lib/http/request.ts +1 -23
  144. package/src/lib/http/stream.ts +29 -60
  145. package/src/lib/util/authorization-header.ts +5 -2
  146. package/src/lib/util/hostname.ts +9 -5
  147. package/src/metadata/build-metadata.ts +3 -1
  148. package/src/oauth-errors.ts +1 -0
  149. package/src/oauth-hooks.ts +3 -3
  150. package/src/oauth-provider.ts +368 -269
  151. package/src/oauth-verifier.ts +2 -2
  152. package/src/output/build-authorize-data.ts +2 -2
  153. package/src/output/send-authorize-redirect.ts +7 -6
  154. package/src/request/request-data.ts +2 -2
  155. package/src/request/request-info.ts +2 -2
  156. package/src/request/request-manager.ts +129 -103
  157. package/src/signer/signer.ts +24 -25
  158. package/src/token/token-data.ts +3 -3
  159. package/src/token/token-manager.ts +141 -99
  160. package/src/token/verify-token-claims.ts +3 -3
  161. package/dist/request/types.d.ts +0 -328
  162. package/dist/request/types.d.ts.map +0 -1
  163. package/dist/request/types.js +0 -27
  164. package/dist/request/types.js.map +0 -1
  165. package/dist/token/types.d.ts +0 -250
  166. package/dist/token/types.d.ts.map +0 -1
  167. package/dist/token/types.js +0 -36
  168. package/dist/token/types.js.map +0 -1
  169. package/src/request/types.ts +0 -48
  170. package/src/token/types.ts +0 -86
@@ -3,16 +3,31 @@ import { SimpleStore } from '@atproto-labs/simple-store'
3
3
  import { SimpleStoreMemory } from '@atproto-labs/simple-store-memory'
4
4
  import { Jwks, Keyset } from '@atproto/jwk'
5
5
  import {
6
- AccessToken,
7
6
  CLIENT_ASSERTION_TYPE_JWT_BEARER,
8
- OAuthAuthenticationRequestParameters,
7
+ OAuthAccessToken,
8
+ OAuthAuthorizationCodeGrantTokenRequest,
9
+ OAuthAuthorizationRequestJar,
10
+ OAuthAuthorizationRequestPar,
11
+ OAuthAuthorizationRequestParameters,
12
+ OAuthAuthorizationRequestQuery,
9
13
  OAuthAuthorizationServerMetadata,
10
- OAuthClientIdentification,
14
+ OAuthClientCredentials,
15
+ OAuthClientCredentialsNone,
11
16
  OAuthClientMetadata,
17
+ OAuthIntrospectionResponse,
18
+ OAuthParResponse,
19
+ OAuthRefreshTokenGrantTokenRequest,
20
+ OAuthTokenIdentification,
21
+ OAuthTokenRequest,
12
22
  OAuthTokenResponse,
13
23
  OAuthTokenType,
14
24
  atprotoLoopbackClientMetadata,
15
- oauthAuthenticationRequestParametersSchema,
25
+ oauthAuthorizationRequestParSchema,
26
+ oauthAuthorizationRequestParametersSchema,
27
+ oauthAuthorizationRequestQuerySchema,
28
+ oauthClientCredentialsSchema,
29
+ oauthTokenIdentificationSchema,
30
+ oauthTokenRequestSchema,
16
31
  } from '@atproto/oauth-types'
17
32
  import { Redis, type RedisOptions } from 'ioredis'
18
33
  import z, { ZodError } from 'zod'
@@ -58,6 +73,7 @@ import {
58
73
  Router,
59
74
  ServerResponse,
60
75
  combineMiddlewares,
76
+ parseHttpRequest,
61
77
  setupCsrfToken,
62
78
  staticJsonHandler,
63
79
  validateCsrfToken,
@@ -65,7 +81,6 @@ import {
65
81
  validateFetchMode,
66
82
  validateFetchSite,
67
83
  validateReferer,
68
- validateRequestPayload,
69
84
  validateSameOrigin,
70
85
  writeJson,
71
86
  } from './lib/http/index.js'
@@ -86,33 +101,16 @@ import {
86
101
  sendAuthorizeRedirect,
87
102
  } from './output/send-authorize-redirect.js'
88
103
  import { ReplayStore, ifReplayStore } from './replay/replay-store.js'
104
+ import { codeSchema } from './request/code.js'
89
105
  import { RequestInfo } from './request/request-info.js'
90
106
  import { RequestManager } from './request/request-manager.js'
91
107
  import { RequestStoreMemory } from './request/request-store-memory.js'
92
108
  import { RequestStoreRedis } from './request/request-store-redis.js'
93
109
  import { RequestStore, ifRequestStore } from './request/request-store.js'
94
110
  import { RequestUri, requestUriSchema } from './request/request-uri.js'
95
- import {
96
- AuthorizationRequestJar,
97
- AuthorizationRequestQuery,
98
- PushedAuthorizationRequest,
99
- authorizationRequestQuerySchema,
100
- pushedAuthorizationRequestSchema,
101
- } from './request/types.js'
102
111
  import { isTokenId } from './token/token-id.js'
103
112
  import { TokenManager } from './token/token-manager.js'
104
113
  import { TokenStore, asTokenStore } from './token/token-store.js'
105
- import {
106
- CodeGrantRequest,
107
- Introspect,
108
- IntrospectionResponse,
109
- RefreshGrantRequest,
110
- Revoke,
111
- TokenRequest,
112
- introspectSchema,
113
- revokeSchema,
114
- tokenRequestSchema,
115
- } from './token/types.js'
116
114
  import { VerifyTokenClaimsOptions } from './token/verify-token-claims.js'
117
115
 
118
116
  export type OAuthProviderStore = Partial<
@@ -312,7 +310,7 @@ export class OAuthProvider extends OAuthVerifier {
312
310
 
313
311
  protected loginRequired(
314
312
  client: Client,
315
- parameters: OAuthAuthenticationRequestParameters,
313
+ parameters: OAuthAuthorizationRequestParameters,
316
314
  info: DeviceAccountInfo,
317
315
  ) {
318
316
  /** in seconds */
@@ -327,38 +325,57 @@ export class OAuthProvider extends OAuthVerifier {
327
325
  }
328
326
 
329
327
  protected async authenticateClient(
330
- client: Client,
331
- credentials: OAuthClientIdentification,
332
- ): Promise<ClientAuth> {
328
+ credentials: OAuthClientCredentials,
329
+ ): Promise<[Client, ClientAuth]> {
330
+ const client = await this.clientManager.getClient(credentials.client_id)
333
331
  const { clientAuth, nonce } = await client.verifyCredentials(credentials, {
334
332
  audience: this.issuer,
335
333
  })
336
334
 
335
+ if (
336
+ client.metadata.application_type === 'native' &&
337
+ clientAuth.method !== 'none'
338
+ ) {
339
+ // https://datatracker.ietf.org/doc/html/rfc8252#section-8.4
340
+ //
341
+ // > Except when using a mechanism like Dynamic Client Registration
342
+ // > [RFC7591] to provision per-instance secrets, native apps are
343
+ // > classified as public clients, as defined by Section 2.1 of OAuth 2.0
344
+ // > [RFC6749]; they MUST be registered with the authorization server as
345
+ // > such. Authorization servers MUST record the client type in the client
346
+ // > registration details in order to identify and process requests
347
+ // > accordingly.
348
+
349
+ throw new InvalidGrantError(
350
+ 'Native clients must authenticate using "none" method',
351
+ )
352
+ }
353
+
337
354
  if (nonce != null) {
338
355
  const unique = await this.replayManager.uniqueAuth(nonce, client.id)
339
356
  if (!unique) {
340
- throw new InvalidClientError(`${clientAuth.method} jti reused`)
357
+ throw new InvalidGrantError(`${clientAuth.method} jti reused`)
341
358
  }
342
359
  }
343
360
 
344
- return clientAuth
361
+ return [client, clientAuth]
345
362
  }
346
363
 
347
364
  protected async decodeJAR(
348
365
  client: Client,
349
- input: AuthorizationRequestJar,
366
+ input: OAuthAuthorizationRequestJar,
350
367
  ): Promise<
351
368
  | {
352
- payload: OAuthAuthenticationRequestParameters
369
+ payload: OAuthAuthorizationRequestParameters
353
370
  }
354
371
  | {
355
- payload: OAuthAuthenticationRequestParameters
372
+ payload: OAuthAuthorizationRequestParameters
356
373
  protectedHeader: { kid: string; alg: string }
357
374
  jkt: string
358
375
  }
359
376
  > {
360
377
  const result = await client.decodeRequestObject(input.request)
361
- const payload = oauthAuthenticationRequestParametersSchema.parse(
378
+ const payload = oauthAuthorizationRequestParametersSchema.parse(
362
379
  result.payload,
363
380
  )
364
381
 
@@ -405,17 +422,17 @@ export class OAuthProvider extends OAuthVerifier {
405
422
  * @see {@link https://datatracker.ietf.org/doc/html/rfc9126}
406
423
  */
407
424
  protected async pushedAuthorizationRequest(
408
- input: PushedAuthorizationRequest,
425
+ credentials: OAuthClientCredentials,
426
+ authorizationRequest: OAuthAuthorizationRequestPar,
409
427
  dpopJkt: null | string,
410
- ) {
428
+ ): Promise<OAuthParResponse> {
411
429
  try {
412
- const client = await this.clientManager.getClient(input.client_id)
413
- const clientAuth = await this.authenticateClient(client, input)
430
+ const [client, clientAuth] = await this.authenticateClient(credentials)
414
431
 
415
432
  const { payload: parameters } =
416
- 'request' in input // Handle JAR
417
- ? await this.decodeJAR(client, input)
418
- : { payload: input }
433
+ 'request' in authorizationRequest // Handle JAR
434
+ ? await this.decodeJAR(client, authorizationRequest)
435
+ : { payload: authorizationRequest }
419
436
 
420
437
  const { uri, expiresAt } =
421
438
  await this.requestManager.createAuthorizationRequest(
@@ -443,19 +460,21 @@ export class OAuthProvider extends OAuthVerifier {
443
460
  }
444
461
  }
445
462
 
446
- private async loadAuthorizationRequest(
463
+ private async processAuthorizationRequest(
447
464
  client: Client,
448
465
  deviceId: DeviceId,
449
- input: AuthorizationRequestQuery,
466
+ query: OAuthAuthorizationRequestQuery,
450
467
  ): Promise<RequestInfo> {
451
- // Load PAR
452
- if ('request_uri' in input) {
453
- return this.requestManager.get(input.request_uri, client.id, deviceId)
468
+ if ('request_uri' in query) {
469
+ const requestUri = await requestUriSchema
470
+ .parseAsync(query.request_uri, { path: ['query', 'request_uri'] })
471
+ .catch(throwInvalidRequest)
472
+
473
+ return this.requestManager.get(requestUri, client.id, deviceId)
454
474
  }
455
475
 
456
- // Handle JAR
457
- if ('request' in input) {
458
- const requestObject = await this.decodeJAR(client, input)
476
+ if ('request' in query) {
477
+ const requestObject = await this.decodeJAR(client, query)
459
478
 
460
479
  if ('protectedHeader' in requestObject && requestObject.protectedHeader) {
461
480
  // Allow using signed JAR during "/authorize" as client authentication.
@@ -488,7 +507,7 @@ export class OAuthProvider extends OAuthVerifier {
488
507
  return this.requestManager.createAuthorizationRequest(
489
508
  client,
490
509
  { method: 'none' },
491
- input,
510
+ query,
492
511
  deviceId,
493
512
  null,
494
513
  )
@@ -496,7 +515,7 @@ export class OAuthProvider extends OAuthVerifier {
496
515
 
497
516
  private async deleteRequest(
498
517
  uri: RequestUri,
499
- parameters: OAuthAuthenticationRequestParameters,
518
+ parameters: OAuthAuthorizationRequestParameters,
500
519
  ) {
501
520
  try {
502
521
  await this.requestManager.delete(uri)
@@ -505,107 +524,113 @@ export class OAuthProvider extends OAuthVerifier {
505
524
  }
506
525
  }
507
526
 
527
+ /**
528
+ * @see {@link https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-11#section-4.1.1}
529
+ */
508
530
  protected async authorize(
509
531
  deviceId: DeviceId,
510
- input: AuthorizationRequestQuery,
532
+ credentials: OAuthClientCredentialsNone,
533
+ query: OAuthAuthorizationRequestQuery,
511
534
  ): Promise<AuthorizationResultRedirect | AuthorizationResultAuthorize> {
512
535
  const { issuer } = this
513
- const client = await this.clientManager.getClient(input.client_id)
514
-
515
- try {
516
- const { uri, parameters, clientAuth } =
517
- await this.loadAuthorizationRequest(client, deviceId, input)
518
536
 
519
- try {
520
- const sessions = await this.getSessions(
521
- client,
522
- clientAuth,
523
- deviceId,
524
- parameters,
525
- )
526
-
527
- if (parameters.prompt === 'none') {
528
- const ssoSessions = sessions.filter((s) => s.matchesHint)
529
- if (ssoSessions.length > 1) {
530
- throw new AccountSelectionRequiredError(parameters)
531
- }
532
- if (ssoSessions.length < 1) {
533
- throw new LoginRequiredError(parameters)
537
+ // If there is a chance to redirect the user to the client, let's do
538
+ // it by wrapping the error in an AccessDeniedError.
539
+ const accessDeniedCatcher =
540
+ 'redirect_uri' in query
541
+ ? (err: unknown): never => {
542
+ // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-11#section-4.1.2.1
543
+ throw AccessDeniedError.from(query, err, 'invalid_request')
534
544
  }
545
+ : null
535
546
 
536
- const ssoSession = ssoSessions[0]!
537
- if (ssoSession.loginRequired) {
538
- throw new LoginRequiredError(parameters)
539
- }
540
- if (ssoSession.consentRequired) {
541
- throw new ConsentRequiredError(parameters)
542
- }
547
+ const client = await this.clientManager
548
+ .getClient(credentials.client_id)
549
+ .catch(accessDeniedCatcher)
543
550
 
544
- const code = await this.requestManager.setAuthorized(
545
- client,
546
- uri,
547
- deviceId,
548
- ssoSession.account,
549
- )
551
+ const { clientAuth, parameters, uri } =
552
+ await this.processAuthorizationRequest(client, deviceId, query).catch(
553
+ accessDeniedCatcher,
554
+ )
555
+
556
+ try {
557
+ const sessions = await this.getSessions(
558
+ client,
559
+ clientAuth,
560
+ deviceId,
561
+ parameters,
562
+ )
550
563
 
551
- return { issuer, client, parameters, redirect: { code } }
564
+ if (parameters.prompt === 'none') {
565
+ const ssoSessions = sessions.filter((s) => s.matchesHint)
566
+ if (ssoSessions.length > 1) {
567
+ throw new AccountSelectionRequiredError(parameters)
568
+ }
569
+ if (ssoSessions.length < 1) {
570
+ throw new LoginRequiredError(parameters)
552
571
  }
553
572
 
554
- // Automatic SSO when a did was provided
555
- if (parameters.prompt == null && parameters.login_hint != null) {
556
- const ssoSessions = sessions.filter((s) => s.matchesHint)
557
- if (ssoSessions.length === 1) {
558
- const ssoSession = ssoSessions[0]!
559
- if (!ssoSession.loginRequired && !ssoSession.consentRequired) {
560
- const code = await this.requestManager.setAuthorized(
561
- client,
562
- uri,
563
- deviceId,
564
- ssoSession.account,
565
- )
566
-
567
- return { issuer, client, parameters, redirect: { code } }
568
- }
569
- }
573
+ const ssoSession = ssoSessions[0]!
574
+ if (ssoSession.loginRequired) {
575
+ throw new LoginRequiredError(parameters)
576
+ }
577
+ if (ssoSession.consentRequired) {
578
+ throw new ConsentRequiredError(parameters)
570
579
  }
571
580
 
572
- return {
573
- issuer,
581
+ const code = await this.requestManager.setAuthorized(
574
582
  client,
575
- parameters,
576
- authorize: {
577
- uri,
578
- sessions,
579
- scopeDetails: parameters.scope
580
- ?.split(/\s+/)
581
- .filter(Boolean)
582
- .sort((a, b) => a.localeCompare(b))
583
- .map((scope) => ({
584
- scope,
585
- // @TODO Allow to customize the scope descriptions (e.g.
586
- // using a hook)
587
- description: undefined,
588
- })),
589
- },
590
- }
591
- } catch (err) {
592
- await this.deleteRequest(uri, parameters)
583
+ uri,
584
+ deviceId,
585
+ ssoSession.account,
586
+ )
593
587
 
594
- // Transform into an AccessDeniedError to allow redirecting the user
595
- // to the client with the error details.
596
- throw AccessDeniedError.from(parameters, err)
588
+ return { issuer, client, parameters, redirect: { code } }
597
589
  }
598
- } catch (err) {
599
- if (err instanceof AccessDeniedError) {
600
- return {
601
- issuer,
602
- client,
603
- parameters: err.parameters,
604
- redirect: err.toJSON(),
590
+
591
+ // Automatic SSO when a did was provided
592
+ if (parameters.prompt == null && parameters.login_hint != null) {
593
+ const ssoSessions = sessions.filter((s) => s.matchesHint)
594
+ if (ssoSessions.length === 1) {
595
+ const ssoSession = ssoSessions[0]!
596
+ if (!ssoSession.loginRequired && !ssoSession.consentRequired) {
597
+ const code = await this.requestManager.setAuthorized(
598
+ client,
599
+ uri,
600
+ deviceId,
601
+ ssoSession.account,
602
+ )
603
+
604
+ return { issuer, client, parameters, redirect: { code } }
605
+ }
605
606
  }
606
607
  }
607
608
 
608
- throw err
609
+ return {
610
+ issuer,
611
+ client,
612
+ parameters,
613
+ authorize: {
614
+ uri,
615
+ sessions,
616
+ scopeDetails: parameters.scope
617
+ ?.split(/\s+/)
618
+ .filter(Boolean)
619
+ .sort((a, b) => a.localeCompare(b))
620
+ .map((scope) => ({
621
+ scope,
622
+ // @TODO Allow to customize the scope descriptions (e.g.
623
+ // using a hook)
624
+ description: undefined,
625
+ })),
626
+ },
627
+ }
628
+ } catch (err) {
629
+ await this.deleteRequest(uri, parameters)
630
+
631
+ // Not using accessDeniedCatcher here because "parameters" will most
632
+ // likely contain the redirect_uri (using the client default).
633
+ throw AccessDeniedError.from(parameters, err)
609
634
  }
610
635
  }
611
636
 
@@ -613,7 +638,7 @@ export class OAuthProvider extends OAuthVerifier {
613
638
  client: Client,
614
639
  clientAuth: ClientAuth,
615
640
  deviceId: DeviceId,
616
- parameters: OAuthAuthenticationRequestParameters,
641
+ parameters: OAuthAuthorizationRequestParameters,
617
642
  ): Promise<
618
643
  {
619
644
  account: Account
@@ -702,52 +727,42 @@ export class OAuthProvider extends OAuthVerifier {
702
727
  const { issuer } = this
703
728
  const client = await this.clientManager.getClient(clientId)
704
729
 
705
- try {
706
- const { parameters, clientAuth } = await this.requestManager.get(
707
- uri,
708
- clientId,
709
- deviceId,
710
- )
711
-
712
- try {
713
- const { account, info } = await this.accountManager.get(deviceId, sub)
730
+ const { parameters, clientAuth } = await this.requestManager.get(
731
+ uri,
732
+ clientId,
733
+ deviceId,
734
+ )
714
735
 
715
- // The user is trying to authorize without a fresh login
716
- if (this.loginRequired(client, parameters, info)) {
717
- throw new LoginRequiredError(
718
- parameters,
719
- 'Account authentication required.',
720
- )
721
- }
736
+ try {
737
+ const { account, info } = await this.accountManager.get(deviceId, sub)
722
738
 
723
- const code = await this.requestManager.setAuthorized(
724
- client,
725
- uri,
726
- deviceId,
727
- account,
739
+ // The user is trying to authorize without a fresh login
740
+ if (this.loginRequired(client, parameters, info)) {
741
+ throw new LoginRequiredError(
742
+ parameters,
743
+ 'Account authentication required.',
728
744
  )
745
+ }
729
746
 
730
- await this.accountManager.addAuthorizedClient(
731
- deviceId,
732
- account,
733
- client,
734
- clientAuth,
735
- )
747
+ const code = await this.requestManager.setAuthorized(
748
+ client,
749
+ uri,
750
+ deviceId,
751
+ account,
752
+ )
736
753
 
737
- return { issuer, client, parameters, redirect: { code } }
738
- } catch (err) {
739
- await this.deleteRequest(uri, parameters)
754
+ await this.accountManager.addAuthorizedClient(
755
+ deviceId,
756
+ account,
757
+ client,
758
+ clientAuth,
759
+ )
740
760
 
741
- // throw AccessDeniedError.from(parameters, err)
742
- throw err
743
- }
761
+ return { issuer, parameters, redirect: { code } }
744
762
  } catch (err) {
745
- if (err instanceof AccessDeniedError) {
746
- const { parameters } = err
747
- return { issuer, client, parameters, redirect: err.toJSON() }
748
- }
763
+ await this.deleteRequest(uri, parameters)
749
764
 
750
- throw err
765
+ throw AccessDeniedError.from(parameters, err)
751
766
  }
752
767
  }
753
768
 
@@ -756,69 +771,69 @@ export class OAuthProvider extends OAuthVerifier {
756
771
  uri: RequestUri,
757
772
  clientId: ClientId,
758
773
  ): Promise<AuthorizationResultRedirect> {
759
- try {
760
- const { parameters } = await this.requestManager.get(
761
- uri,
762
- clientId,
763
- deviceId,
764
- )
774
+ const { parameters } = await this.requestManager.get(
775
+ uri,
776
+ clientId,
777
+ deviceId,
778
+ )
765
779
 
766
- await this.deleteRequest(uri, parameters)
780
+ await this.deleteRequest(uri, parameters)
767
781
 
768
- // Trigger redirect (see catch block)
769
- throw new AccessDeniedError(parameters, 'Access denied')
770
- } catch (err) {
771
- if (err instanceof AccessDeniedError) {
772
- return {
773
- issuer: this.issuer,
774
- client: await this.clientManager.getClient(clientId),
775
- parameters: err.parameters,
776
- redirect: err.toJSON(),
777
- }
778
- }
779
-
780
- throw err
782
+ return {
783
+ issuer: this.issuer,
784
+ parameters: parameters,
785
+ redirect: {
786
+ error: 'access_denied',
787
+ error_description: 'Access denied',
788
+ },
781
789
  }
782
790
  }
783
791
 
784
792
  protected async token(
785
- input: TokenRequest,
793
+ credentials: OAuthClientCredentials,
794
+ request: OAuthTokenRequest,
786
795
  dpopJkt: null | string,
787
796
  ): Promise<OAuthTokenResponse> {
788
- const client = await this.clientManager.getClient(input.client_id)
789
- const clientAuth = await this.authenticateClient(client, input)
797
+ const [client, clientAuth] = await this.authenticateClient(credentials)
798
+
799
+ if (!this.metadata.grant_types_supported?.includes(request.grant_type)) {
800
+ throw new InvalidGrantError(
801
+ `Grant type "${request.grant_type}" is not supported by the server`,
802
+ )
803
+ }
790
804
 
791
- if (!client.metadata.grant_types.includes(input.grant_type)) {
805
+ if (!client.metadata.grant_types.includes(request.grant_type)) {
792
806
  throw new InvalidGrantError(
793
- `"${input.grant_type}" grant type is not allowed for this client`,
807
+ `"${request.grant_type}" grant type is not allowed for this client`,
794
808
  )
795
809
  }
796
810
 
797
- if (input.grant_type === 'authorization_code') {
798
- return this.codeGrant(client, clientAuth, input, dpopJkt)
811
+ if (request.grant_type === 'authorization_code') {
812
+ return this.codeGrant(client, clientAuth, request, dpopJkt)
799
813
  }
800
814
 
801
- if (input.grant_type === 'refresh_token') {
802
- return this.refreshTokenGrant(client, clientAuth, input, dpopJkt)
815
+ if (request.grant_type === 'refresh_token') {
816
+ return this.refreshTokenGrant(client, clientAuth, request, dpopJkt)
803
817
  }
804
818
 
805
819
  throw new InvalidGrantError(
806
- // @ts-expect-error: fool proof
807
- `Grant type "${input.grant_type}" not supported`,
820
+ `Grant type "${request.grant_type}" not supported`,
808
821
  )
809
822
  }
810
823
 
811
824
  protected async codeGrant(
812
825
  client: Client,
813
826
  clientAuth: ClientAuth,
814
- input: CodeGrantRequest,
827
+ input: OAuthAuthorizationCodeGrantTokenRequest,
815
828
  dpopJkt: null | string,
816
829
  ): Promise<OAuthTokenResponse> {
817
830
  try {
831
+ const code = codeSchema.parse(input.code)
832
+
818
833
  const { sub, deviceId, parameters } = await this.requestManager.findCode(
819
834
  client,
820
835
  clientAuth,
821
- input.code,
836
+ code,
822
837
  )
823
838
 
824
839
  // the following check prevents re-use of PKCE challenges, enforcing the
@@ -874,7 +889,7 @@ export class OAuthProvider extends OAuthVerifier {
874
889
  async refreshTokenGrant(
875
890
  client: Client,
876
891
  clientAuth: ClientAuth,
877
- input: RefreshGrantRequest,
892
+ input: OAuthRefreshTokenGrantTokenRequest,
878
893
  dpopJkt: null | string,
879
894
  ): Promise<OAuthTokenResponse> {
880
895
  return this.tokenManager.refresh(client, clientAuth, input, dpopJkt)
@@ -883,20 +898,20 @@ export class OAuthProvider extends OAuthVerifier {
883
898
  /**
884
899
  * @see {@link https://datatracker.ietf.org/doc/html/rfc7009#section-2.1 rfc7009}
885
900
  */
886
- protected async revoke(input: Revoke) {
901
+ protected async revoke({ token }: OAuthTokenIdentification) {
887
902
  // @TODO this should also remove the account-device association (or, at
888
903
  // least, mark it as expired)
889
- await this.tokenManager.revoke(input.token)
904
+ await this.tokenManager.revoke(token)
890
905
  }
891
906
 
892
907
  /**
893
908
  * @see {@link https://datatracker.ietf.org/doc/html/rfc7662#section-2.1 rfc7662}
894
909
  */
895
910
  protected async introspect(
896
- input: Introspect,
897
- ): Promise<IntrospectionResponse> {
898
- const client = await this.clientManager.getClient(input.client_id)
899
- const clientAuth = await this.authenticateClient(client, input)
911
+ credentials: OAuthClientCredentials,
912
+ { token }: OAuthTokenIdentification,
913
+ ): Promise<OAuthIntrospectionResponse> {
914
+ const [client, clientAuth] = await this.authenticateClient(credentials)
900
915
 
901
916
  // RFC7662 states the following:
902
917
  //
@@ -915,7 +930,7 @@ export class OAuthProvider extends OAuthVerifier {
915
930
  const tokenInfo = await this.tokenManager.clientTokenInfo(
916
931
  client,
917
932
  clientAuth,
918
- input.token,
933
+ token,
919
934
  )
920
935
 
921
936
  return {
@@ -946,7 +961,7 @@ export class OAuthProvider extends OAuthVerifier {
946
961
 
947
962
  protected override async authenticateToken(
948
963
  tokenType: OAuthTokenType,
949
- token: AccessToken,
964
+ token: OAuthAccessToken,
950
965
  dpopJkt: string | null,
951
966
  verifyOptions?: VerifyTokenClaimsOptions,
952
967
  ) {
@@ -981,12 +996,7 @@ export class OAuthProvider extends OAuthVerifier {
981
996
  T = void,
982
997
  Req extends IncomingMessage = IncomingMessage,
983
998
  Res extends ServerResponse = ServerResponse,
984
- >({
985
- onError = process.env['NODE_ENV'] === 'development'
986
- ? (req, res, err, msg): void =>
987
- console.error(`OAuthProvider error (${msg}):`, err)
988
- : undefined,
989
- }: RouterOptions<Req, Res> = {}) {
999
+ >(options?: RouterOptions<Req, Res>) {
990
1000
  const deviceManager = new DeviceManager(this.deviceStore)
991
1001
  const outputManager = new OutputManager(this.customization)
992
1002
 
@@ -999,6 +1009,12 @@ export class OAuthProvider extends OAuthVerifier {
999
1009
  // Utils
1000
1010
 
1001
1011
  const csrfCookie = (uri: RequestUri) => `csrf-${uri}`
1012
+ const onError =
1013
+ options?.onError ??
1014
+ (process.env['NODE_ENV'] === 'development'
1015
+ ? (req, res, err, msg): void =>
1016
+ console.error(`OAuthProvider error (${msg}):`, err)
1017
+ : undefined)
1002
1018
 
1003
1019
  /**
1004
1020
  * Creates a middleware that will serve static JSON content.
@@ -1059,7 +1075,7 @@ export class OAuthProvider extends OAuthVerifier {
1059
1075
  // OAuthError are used to build expected responses, so we don't log
1060
1076
  // them as errors.
1061
1077
  if (!(err instanceof OAuthError) || err.statusCode >= 500) {
1062
- await onError?.(req, res, err, 'Unexpected error')
1078
+ onError?.(req, res, err, 'Unexpected error')
1063
1079
  }
1064
1080
  }
1065
1081
  }
@@ -1086,7 +1102,7 @@ export class OAuthProvider extends OAuthVerifier {
1086
1102
  throw new Error('Navigation handler did not send a response')
1087
1103
  }
1088
1104
  } catch (err) {
1089
- await onError?.(
1105
+ onError?.(
1090
1106
  req,
1091
1107
  res,
1092
1108
  err,
@@ -1099,6 +1115,30 @@ export class OAuthProvider extends OAuthVerifier {
1099
1115
  }
1100
1116
  }
1101
1117
 
1118
+ /**
1119
+ * Provides a better UX when a request is denied by redirecting to the
1120
+ * client with the error details. This will also log any error that caused
1121
+ * the access to be denied (such as system errors).
1122
+ */
1123
+ const accessDeniedToRedirectCatcher = (
1124
+ req: Req,
1125
+ res: Res,
1126
+ err: unknown,
1127
+ ): AuthorizationResultRedirect => {
1128
+ if (err instanceof AccessDeniedError && err.parameters.redirect_uri) {
1129
+ const { cause } = err
1130
+ if (cause) onError?.(req, res, cause, 'Access denied')
1131
+
1132
+ return {
1133
+ issuer: server.issuer,
1134
+ parameters: err.parameters,
1135
+ redirect: err.toJSON(),
1136
+ }
1137
+ }
1138
+
1139
+ throw err
1140
+ }
1141
+
1102
1142
  //- Public OAuth endpoints
1103
1143
 
1104
1144
  router.get(
@@ -1139,10 +1179,15 @@ export class OAuthProvider extends OAuthVerifier {
1139
1179
  router.post(
1140
1180
  '/oauth/par',
1141
1181
  jsonHandler(async function (req, _res) {
1142
- const input = await validateRequest(
1143
- req,
1144
- pushedAuthorizationRequestSchema,
1145
- )
1182
+ const payload = await parseHttpRequest(req, ['json', 'urlencoded'])
1183
+
1184
+ const credentials = await oauthClientCredentialsSchema
1185
+ .parseAsync(payload, { path: ['body'] })
1186
+ .catch(throwInvalidRequest)
1187
+
1188
+ const authorizationRequest = await oauthAuthorizationRequestParSchema
1189
+ .parseAsync(payload, { path: ['body'] })
1190
+ .catch(throwInvalidRequest)
1146
1191
 
1147
1192
  const dpopJkt = await server.checkDpopProof(
1148
1193
  req.headers['dpop'],
@@ -1150,7 +1195,11 @@ export class OAuthProvider extends OAuthVerifier {
1150
1195
  this.url,
1151
1196
  )
1152
1197
 
1153
- return server.pushedAuthorizationRequest(input, dpopJkt)
1198
+ return server.pushedAuthorizationRequest(
1199
+ credentials,
1200
+ authorizationRequest,
1201
+ dpopJkt,
1202
+ )
1154
1203
  }, 201),
1155
1204
  )
1156
1205
 
@@ -1166,7 +1215,15 @@ export class OAuthProvider extends OAuthVerifier {
1166
1215
  router.post(
1167
1216
  '/oauth/token',
1168
1217
  jsonHandler(async function (req, _res) {
1169
- const input = await validateRequest(req, tokenRequestSchema)
1218
+ const payload = await parseHttpRequest(req, ['json', 'urlencoded'])
1219
+
1220
+ const credentials = await oauthClientCredentialsSchema
1221
+ .parseAsync(payload, { path: ['body'] })
1222
+ .catch(throwInvalidClient)
1223
+
1224
+ const tokenRequest = await oauthTokenRequestSchema
1225
+ .parseAsync(payload, { path: ['body'] })
1226
+ .catch(throwInvalidGrant)
1170
1227
 
1171
1228
  const dpopJkt = await server.checkDpopProof(
1172
1229
  req.headers['dpop'],
@@ -1174,7 +1231,7 @@ export class OAuthProvider extends OAuthVerifier {
1174
1231
  this.url,
1175
1232
  )
1176
1233
 
1177
- return server.token(input, dpopJkt)
1234
+ return server.token(credentials, tokenRequest, dpopJkt)
1178
1235
  }),
1179
1236
  )
1180
1237
 
@@ -1182,10 +1239,14 @@ export class OAuthProvider extends OAuthVerifier {
1182
1239
  router.post(
1183
1240
  '/oauth/revoke',
1184
1241
  jsonHandler(async function (req, res) {
1185
- const input = await validateRequest(req, revokeSchema)
1242
+ const payload = await parseHttpRequest(req, ['json', 'urlencoded'])
1243
+
1244
+ const tokenIdentification = await oauthTokenIdentificationSchema
1245
+ .parseAsync(payload, { path: ['body'] })
1246
+ .catch(throwInvalidRequest)
1186
1247
 
1187
1248
  try {
1188
- await server.revoke(input)
1249
+ await server.revoke(tokenIdentification)
1189
1250
  } catch (err) {
1190
1251
  onError?.(req, res, err, 'Failed to revoke token')
1191
1252
  }
@@ -1197,10 +1258,13 @@ export class OAuthProvider extends OAuthVerifier {
1197
1258
  '/oauth/revoke',
1198
1259
  navigationHandler(async function (req, res) {
1199
1260
  const query = Object.fromEntries(this.url.searchParams)
1200
- const input = revokeSchema.parse(query, { path: ['query'] })
1261
+
1262
+ const tokenIdentification = await oauthTokenIdentificationSchema
1263
+ .parseAsync(query, { path: ['query'] })
1264
+ .catch(throwInvalidRequest)
1201
1265
 
1202
1266
  try {
1203
- await server.revoke(input)
1267
+ await server.revoke(tokenIdentification)
1204
1268
  } catch (err) {
1205
1269
  onError?.(req, res, err, 'Failed to revoke token')
1206
1270
  }
@@ -1217,8 +1281,17 @@ export class OAuthProvider extends OAuthVerifier {
1217
1281
  router.post(
1218
1282
  '/oauth/introspect',
1219
1283
  jsonHandler(async function (req, _res) {
1220
- const input = await validateRequest(req, introspectSchema)
1221
- return server.introspect(input)
1284
+ const payload = await parseHttpRequest(req, ['json', 'urlencoded'])
1285
+
1286
+ const credentials = await oauthClientCredentialsSchema
1287
+ .parseAsync(payload, { path: ['body'] })
1288
+ .catch(throwInvalidRequest)
1289
+
1290
+ const tokenIdentification = await oauthTokenIdentificationSchema
1291
+ .parseAsync(payload, { path: ['body'] })
1292
+ .catch(throwInvalidRequest)
1293
+
1294
+ return server.introspect(credentials, tokenIdentification)
1222
1295
  }),
1223
1296
  )
1224
1297
 
@@ -1232,12 +1305,24 @@ export class OAuthProvider extends OAuthVerifier {
1232
1305
  validateFetchSite(req, res, ['cross-site', 'none'])
1233
1306
 
1234
1307
  const query = Object.fromEntries(this.url.searchParams)
1235
- const input = await authorizationRequestQuerySchema.parseAsync(query, {
1236
- path: ['query'],
1237
- })
1308
+
1309
+ const credentials = await oauthClientCredentialsSchema
1310
+ .parseAsync(query, { path: ['body'] })
1311
+ .catch(throwInvalidRequest)
1312
+
1313
+ if ('client_secret' in credentials) {
1314
+ throw new InvalidRequestError('Client secret must not be provided')
1315
+ }
1316
+
1317
+ const authorizationRequest = await oauthAuthorizationRequestQuerySchema
1318
+ .parseAsync(query, { path: ['query'] })
1319
+ .catch(throwInvalidRequest)
1238
1320
 
1239
1321
  const { deviceId } = await deviceManager.load(req, res)
1240
- const data = await server.authorize(deviceId, input)
1322
+
1323
+ const data = await server
1324
+ .authorize(deviceId, credentials, authorizationRequest)
1325
+ .catch((err) => accessDeniedToRedirectCatcher(req, res, err))
1241
1326
 
1242
1327
  switch (true) {
1243
1328
  case 'redirect' in data: {
@@ -1270,7 +1355,10 @@ export class OAuthProvider extends OAuthVerifier {
1270
1355
  validateFetchSite(req, res, ['same-origin'])
1271
1356
  validateSameOrigin(req, res, issuerOrigin)
1272
1357
 
1273
- const input = await validateRequest(req, signInPayloadSchema)
1358
+ const payload = await parseHttpRequest(req, ['json'])
1359
+ const input = await signInPayloadSchema.parseAsync(payload, {
1360
+ path: ['body'],
1361
+ })
1274
1362
 
1275
1363
  validateReferer(req, res, {
1276
1364
  origin: issuerOrigin,
@@ -1338,12 +1426,14 @@ export class OAuthProvider extends OAuthVerifier {
1338
1426
 
1339
1427
  const { deviceId } = await deviceManager.load(req, res)
1340
1428
 
1341
- const data = await server.acceptRequest(
1342
- deviceId,
1343
- input.request_uri,
1344
- input.client_id,
1345
- input.account_sub,
1346
- )
1429
+ const data = await server
1430
+ .acceptRequest(
1431
+ deviceId,
1432
+ input.request_uri,
1433
+ input.client_id,
1434
+ input.account_sub,
1435
+ )
1436
+ .catch((err) => accessDeniedToRedirectCatcher(req, res, err))
1347
1437
 
1348
1438
  return await sendAuthorizeRedirect(res, data)
1349
1439
  }),
@@ -1392,11 +1482,9 @@ export class OAuthProvider extends OAuthVerifier {
1392
1482
 
1393
1483
  const { deviceId } = await deviceManager.load(req, res)
1394
1484
 
1395
- const data = await server.rejectRequest(
1396
- deviceId,
1397
- input.request_uri,
1398
- input.client_id,
1399
- )
1485
+ const data = await server
1486
+ .rejectRequest(deviceId, input.request_uri, input.client_id)
1487
+ .catch((err) => accessDeniedToRedirectCatcher(req, res, err))
1400
1488
 
1401
1489
  return await sendAuthorizeRedirect(res, data)
1402
1490
  }),
@@ -1406,25 +1494,36 @@ export class OAuthProvider extends OAuthVerifier {
1406
1494
  }
1407
1495
  }
1408
1496
 
1409
- async function validateRequest<S extends z.ZodTypeAny>(
1410
- req: IncomingMessage,
1411
- schema: S,
1412
- ): Promise<z.TypeOf<S>> {
1413
- try {
1414
- return await validateRequestPayload(req, schema)
1415
- } catch (err) {
1416
- if (err instanceof ZodError) {
1417
- const issue = err.issues[0]
1418
- if (issue?.path.length) {
1419
- // "part" will typically be
1420
- const [part, ...path] = issue.path
1421
- throw new InvalidRequestError(
1422
- `Validation of ${part}'s "${path.join('.')}" with error: ${issue.message}`,
1423
- err,
1424
- )
1425
- }
1426
- }
1497
+ function throwInvalidGrant(err: unknown): never {
1498
+ throw new InvalidGrantError(
1499
+ extractZodErrorMessage(err) || 'Invalid grant',
1500
+ err,
1501
+ )
1502
+ }
1427
1503
 
1428
- throw new InvalidRequestError('Input validation error', err)
1504
+ function throwInvalidClient(err: unknown): never {
1505
+ throw new InvalidClientError(
1506
+ extractZodErrorMessage(err) || 'Client authentication failed',
1507
+ err,
1508
+ )
1509
+ }
1510
+
1511
+ function throwInvalidRequest(err: unknown): never {
1512
+ throw new InvalidRequestError(
1513
+ extractZodErrorMessage(err) || 'Input validation error',
1514
+ err,
1515
+ )
1516
+ }
1517
+
1518
+ function extractZodErrorMessage(err: unknown): string | undefined {
1519
+ if (err instanceof ZodError) {
1520
+ const issue = err.issues[0]
1521
+ if (issue?.path.length) {
1522
+ // "part" will typically be "body" or "query"
1523
+ const [part, ...path] = issue.path
1524
+ return `Validation of "${path.join('.')}" ${part} parameter failed: ${issue.message}`
1525
+ }
1429
1526
  }
1527
+
1528
+ return undefined
1430
1529
  }