@atproto/oauth-provider 0.2.1 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (169) hide show
  1. package/CHANGELOG.md +36 -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/assets-middleware.ts +4 -2
  127. package/src/client/client-manager.ts +163 -161
  128. package/src/client/client-utils.ts +7 -12
  129. package/src/client/client.ts +112 -3
  130. package/src/constants.ts +0 -2
  131. package/src/errors/access-denied-error.ts +10 -4
  132. package/src/errors/account-selection-required-error.ts +2 -2
  133. package/src/errors/consent-required-error.ts +2 -2
  134. package/src/errors/invalid-authorization-details-error.ts +2 -2
  135. package/src/errors/invalid-client-id-error.ts +15 -4
  136. package/src/errors/invalid-client-metadata-error.ts +15 -3
  137. package/src/errors/invalid-parameters-error.ts +2 -2
  138. package/src/errors/invalid-scope-error.ts +15 -0
  139. package/src/errors/login-required-error.ts +2 -2
  140. package/src/lib/html/html.ts +14 -12
  141. package/src/lib/http/parser.ts +21 -8
  142. package/src/lib/http/request.ts +1 -23
  143. package/src/lib/http/stream.ts +29 -60
  144. package/src/lib/util/authorization-header.ts +5 -2
  145. package/src/lib/util/hostname.ts +9 -5
  146. package/src/metadata/build-metadata.ts +3 -1
  147. package/src/oauth-errors.ts +1 -0
  148. package/src/oauth-hooks.ts +3 -3
  149. package/src/oauth-provider.ts +368 -269
  150. package/src/oauth-verifier.ts +2 -2
  151. package/src/output/build-authorize-data.ts +2 -2
  152. package/src/output/send-authorize-redirect.ts +7 -6
  153. package/src/request/request-data.ts +2 -2
  154. package/src/request/request-info.ts +2 -2
  155. package/src/request/request-manager.ts +129 -103
  156. package/src/signer/signer.ts +24 -25
  157. package/src/token/token-data.ts +3 -3
  158. package/src/token/token-manager.ts +141 -99
  159. package/src/token/verify-token-claims.ts +3 -3
  160. package/dist/request/types.d.ts +0 -328
  161. package/dist/request/types.d.ts.map +0 -1
  162. package/dist/request/types.js +0 -27
  163. package/dist/request/types.js.map +0 -1
  164. package/dist/token/types.d.ts +0 -250
  165. package/dist/token/types.d.ts.map +0 -1
  166. package/dist/token/types.js +0 -36
  167. package/dist/token/types.js.map +0 -1
  168. package/src/request/types.ts +0 -48
  169. 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
  }