@atproto/oauth-provider 0.2.17 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (91) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/client/client-manager.d.ts.map +1 -1
  3. package/dist/client/client-manager.js +6 -8
  4. package/dist/client/client-manager.js.map +1 -1
  5. package/dist/device/device-data.d.ts +7 -8
  6. package/dist/device/device-data.d.ts.map +1 -1
  7. package/dist/device/device-data.js +3 -2
  8. package/dist/device/device-data.js.map +1 -1
  9. package/dist/device/device-manager.d.ts +104 -19
  10. package/dist/device/device-manager.d.ts.map +1 -1
  11. package/dist/device/device-manager.js +43 -30
  12. package/dist/device/device-manager.js.map +1 -1
  13. package/dist/lib/http/accept.d.ts +2 -1
  14. package/dist/lib/http/accept.d.ts.map +1 -1
  15. package/dist/lib/http/accept.js.map +1 -1
  16. package/dist/lib/http/method.d.ts +1 -1
  17. package/dist/lib/http/method.d.ts.map +1 -1
  18. package/dist/lib/http/request.d.ts +10 -1
  19. package/dist/lib/http/request.d.ts.map +1 -1
  20. package/dist/lib/http/request.js +38 -0
  21. package/dist/lib/http/request.js.map +1 -1
  22. package/dist/lib/http/response.d.ts +3 -2
  23. package/dist/lib/http/response.d.ts.map +1 -1
  24. package/dist/lib/http/response.js.map +1 -1
  25. package/dist/lib/http/route.d.ts +2 -1
  26. package/dist/lib/http/route.d.ts.map +1 -1
  27. package/dist/lib/http/route.js.map +1 -1
  28. package/dist/lib/http/router.d.ts +2 -1
  29. package/dist/lib/http/router.d.ts.map +1 -1
  30. package/dist/lib/http/router.js.map +1 -1
  31. package/dist/lib/http/stream.d.ts +1 -1
  32. package/dist/lib/http/stream.d.ts.map +1 -1
  33. package/dist/lib/http/types.d.ts +0 -1
  34. package/dist/lib/http/types.d.ts.map +1 -1
  35. package/dist/lib/util/function.d.ts +8 -0
  36. package/dist/lib/util/function.d.ts.map +1 -1
  37. package/dist/lib/util/function.js +1 -1
  38. package/dist/lib/util/function.js.map +1 -1
  39. package/dist/lib/util/time.d.ts +7 -1
  40. package/dist/lib/util/time.d.ts.map +1 -1
  41. package/dist/lib/util/time.js +23 -12
  42. package/dist/lib/util/time.js.map +1 -1
  43. package/dist/oauth-hooks.d.ts +56 -4
  44. package/dist/oauth-hooks.d.ts.map +1 -1
  45. package/dist/oauth-hooks.js +8 -0
  46. package/dist/oauth-hooks.js.map +1 -1
  47. package/dist/oauth-provider.d.ts +10 -7
  48. package/dist/oauth-provider.d.ts.map +1 -1
  49. package/dist/oauth-provider.js +24 -23
  50. package/dist/oauth-provider.js.map +1 -1
  51. package/dist/output/output-manager.d.ts +1 -1
  52. package/dist/output/output-manager.d.ts.map +1 -1
  53. package/dist/output/send-authorize-redirect.d.ts +1 -1
  54. package/dist/output/send-authorize-redirect.d.ts.map +1 -1
  55. package/dist/output/send-web-page.d.ts +1 -1
  56. package/dist/output/send-web-page.d.ts.map +1 -1
  57. package/dist/request/request-manager.d.ts +2 -1
  58. package/dist/request/request-manager.d.ts.map +1 -1
  59. package/dist/request/request-manager.js +9 -1
  60. package/dist/request/request-manager.js.map +1 -1
  61. package/dist/token/token-manager.d.ts +3 -2
  62. package/dist/token/token-manager.d.ts.map +1 -1
  63. package/dist/token/token-manager.js +39 -24
  64. package/dist/token/token-manager.js.map +1 -1
  65. package/package.json +2 -4
  66. package/src/client/client-manager.ts +9 -11
  67. package/src/device/device-data.ts +3 -2
  68. package/src/device/device-manager.ts +93 -75
  69. package/src/lib/http/accept.ts +2 -6
  70. package/src/lib/http/method.ts +1 -1
  71. package/src/lib/http/request.ts +64 -1
  72. package/src/lib/http/response.ts +3 -2
  73. package/src/lib/http/route.ts +2 -1
  74. package/src/lib/http/router.ts +2 -1
  75. package/src/lib/http/stream.ts +1 -1
  76. package/src/lib/http/types.ts +0 -1
  77. package/src/lib/util/function.ts +19 -2
  78. package/src/lib/util/time.ts +35 -18
  79. package/src/oauth-hooks.ts +73 -13
  80. package/src/oauth-provider.ts +69 -28
  81. package/src/output/output-manager.ts +1 -1
  82. package/src/output/send-authorize-redirect.ts +1 -1
  83. package/src/output/send-web-page.ts +1 -1
  84. package/src/request/request-manager.ts +13 -2
  85. package/src/token/token-manager.ts +52 -23
  86. package/tsconfig.backend.tsbuildinfo +1 -1
  87. package/dist/device/device-details.d.ts +0 -16
  88. package/dist/device/device-details.d.ts.map +0 -1
  89. package/dist/device/device-details.js +0 -34
  90. package/dist/device/device-details.js.map +0 -1
  91. package/src/device/device-details.ts +0 -42
@@ -11,21 +11,29 @@ import { ClientId } from './client/client-id.js'
11
11
  import { ClientInfo } from './client/client-info.js'
12
12
  import { Client } from './client/client.js'
13
13
  import { InvalidAuthorizationDetailsError } from './errors/invalid-authorization-details-error.js'
14
+ import { RequestMetadata } from './lib/http/request.js'
14
15
  import { Awaitable } from './lib/util/type.js'
16
+ import { AccessDeniedError, OAuthError } from './oauth-errors.js'
17
+ import { DeviceId } from './oauth-store.js'
15
18
 
16
19
  // Make sure all types needed to implement the OAuthHooks are exported
17
- export type {
18
- Account,
20
+ export {
21
+ AccessDeniedError,
22
+ type Account,
23
+ type Awaitable,
19
24
  Client,
20
- ClientAuth,
21
- ClientId,
22
- ClientInfo,
25
+ type ClientAuth,
26
+ type ClientId,
27
+ type ClientInfo,
28
+ type DeviceId,
23
29
  InvalidAuthorizationDetailsError,
24
- Jwks,
25
- OAuthAuthorizationDetails,
26
- OAuthAuthorizationRequestParameters,
27
- OAuthClientMetadata,
28
- OAuthTokenResponse,
30
+ type Jwks,
31
+ type OAuthAuthorizationDetails,
32
+ type OAuthAuthorizationRequestParameters,
33
+ type OAuthClientMetadata,
34
+ OAuthError,
35
+ type OAuthTokenResponse,
36
+ type RequestMetadata,
29
37
  }
30
38
 
31
39
  export type OAuthHooks = {
@@ -36,10 +44,10 @@ export type OAuthHooks = {
36
44
  * @throws {InvalidClientMetadataError} if the metadata is invalid
37
45
  * @see {@link InvalidClientMetadataError}
38
46
  */
39
- onClientInfo?: (
47
+ getClientInfo?: (
40
48
  clientId: ClientId,
41
49
  data: { metadata: OAuthClientMetadata; jwks?: Jwks },
42
- ) => Awaitable<void | undefined | Partial<ClientInfo>>
50
+ ) => Awaitable<undefined | Partial<ClientInfo>>
43
51
 
44
52
  /**
45
53
  * Allows enriching the authorization details with additional information
@@ -47,9 +55,61 @@ export type OAuthHooks = {
47
55
  *
48
56
  * @see {@link https://datatracker.ietf.org/doc/html/rfc9396 | RFC 9396}
49
57
  */
50
- onAuthorizationDetails?: (data: {
58
+ getAuthorizationDetails?: (data: {
51
59
  client: Client
60
+ clientAuth: ClientAuth
61
+ clientMetadata: RequestMetadata
52
62
  parameters: OAuthAuthorizationRequestParameters
53
63
  account: Account
54
64
  }) => Awaitable<undefined | OAuthAuthorizationDetails>
65
+
66
+ /**
67
+ * This hook is called when a client is authorized.
68
+ *
69
+ * @throws {AccessDeniedError} to deny the authorization request and redirect
70
+ * the user to the client with an OAuth error (other errors will result in an
71
+ * internal server error being displayed to the user)
72
+ *
73
+ * @note We use `deviceMetadata` instead of `clientMetadata` to make it clear
74
+ * that this metadata is from the user device, which might be different from
75
+ * the client metadata (because the OAuth client could live in a backend).
76
+ */
77
+ onAuthorized?: (data: {
78
+ client: Client
79
+ account: Account
80
+ parameters: OAuthAuthorizationRequestParameters
81
+ deviceId: DeviceId
82
+ deviceMetadata: RequestMetadata
83
+ }) => Awaitable<void>
84
+
85
+ /**
86
+ * This hook is called when an authorized client exchanges an authorization
87
+ * code for an access token.
88
+ *
89
+ * @throws {OAuthError} to cancel the token creation and revoke the session
90
+ */
91
+ onTokenCreated?: (data: {
92
+ client: Client
93
+ clientAuth: ClientAuth
94
+ clientMetadata: RequestMetadata
95
+ account: Account
96
+ parameters: OAuthAuthorizationRequestParameters
97
+ /** null when "password grant" used (in which case {@link onAuthorized} won't have been called) */
98
+ deviceId: null | DeviceId
99
+ }) => Awaitable<void>
100
+
101
+ /**
102
+ * This hook is called when an authorized client refreshes an access token.
103
+ *
104
+ * @throws {OAuthError} to cancel the token refresh and revoke the session
105
+ */
106
+ onTokenRefreshed?: (data: {
107
+ client: Client
108
+ clientAuth: ClientAuth
109
+ clientMetadata: RequestMetadata
110
+ account: Account
111
+ parameters: OAuthAuthorizationRequestParameters
112
+ /** null when "password grant" used (in which case {@link onAuthorized} won't have been called) */
113
+ deviceId: null | DeviceId
114
+ }) => Awaitable<void>
55
115
  }
@@ -1,3 +1,4 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http'
1
2
  import { mediaType } from '@hapi/accept'
2
3
  import createHttpError from 'http-errors'
3
4
  import type { Redis, RedisOptions } from 'ioredis'
@@ -54,7 +55,7 @@ import { ClientStore, ifClientStore } from './client/client-store.js'
54
55
  import { Client } from './client/client.js'
55
56
  import { AUTHENTICATION_MAX_AGE, TOKEN_MAX_AGE } from './constants.js'
56
57
  import { DeviceId } from './device/device-id.js'
57
- import { DeviceManager } from './device/device-manager.js'
58
+ import { DeviceManager, DeviceManagerOptions } from './device/device-manager.js'
58
59
  import { DeviceStore, asDeviceStore } from './device/device-store.js'
59
60
  import { AccessDeniedError } from './errors/access-denied-error.js'
60
61
  import { AccountSelectionRequiredError } from './errors/account-selection-required-error.js'
@@ -69,10 +70,8 @@ import { UnauthorizedClientError } from './errors/unauthorized-client-error.js'
69
70
  import { WWWAuthenticateError } from './errors/www-authenticate-error.js'
70
71
  import {
71
72
  Handler,
72
- IncomingMessage,
73
73
  Middleware,
74
74
  Router,
75
- ServerResponse,
76
75
  combineMiddlewares,
77
76
  parseHttpRequest,
78
77
  setupCsrfToken,
@@ -85,6 +84,7 @@ import {
85
84
  validateSameOrigin,
86
85
  writeJson,
87
86
  } from './lib/http/index.js'
87
+ import { RequestMetadata } from './lib/http/request.js'
88
88
  import { dateToEpoch, dateToRelativeSeconds } from './lib/util/date.js'
89
89
  import { Override } from './lib/util/type.js'
90
90
  import { CustomMetadata, buildMetadata } from './metadata/build-metadata.js'
@@ -134,7 +134,7 @@ export {
134
134
  export type RouterOptions<
135
135
  Req extends IncomingMessage = IncomingMessage,
136
136
  Res extends ServerResponse = ServerResponse,
137
- > = {
137
+ > = DeviceManagerOptions & {
138
138
  onError?: (req: Req, res: Res, err: unknown, message: string) => void
139
139
  }
140
140
 
@@ -529,9 +529,10 @@ export class OAuthProvider extends OAuthVerifier {
529
529
  * @see {@link https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-11#section-4.1.1}
530
530
  */
531
531
  protected async authorize(
532
- deviceId: DeviceId,
533
- credentials: OAuthClientCredentialsNone,
532
+ clientCredentials: OAuthClientCredentialsNone,
534
533
  query: OAuthAuthorizationRequestQuery,
534
+ deviceId: DeviceId,
535
+ deviceMetadata: RequestMetadata,
535
536
  ): Promise<AuthorizationResultRedirect | AuthorizationResultAuthorize> {
536
537
  const { issuer } = this
537
538
 
@@ -546,7 +547,7 @@ export class OAuthProvider extends OAuthVerifier {
546
547
  : null
547
548
 
548
549
  const client = await this.clientManager
549
- .getClient(credentials.client_id)
550
+ .getClient(clientCredentials.client_id)
550
551
  .catch(accessDeniedCatcher)
551
552
 
552
553
  const { clientAuth, parameters, uri } =
@@ -580,10 +581,11 @@ export class OAuthProvider extends OAuthVerifier {
580
581
  }
581
582
 
582
583
  const code = await this.requestManager.setAuthorized(
583
- client,
584
584
  uri,
585
- deviceId,
585
+ client,
586
586
  ssoSession.account,
587
+ deviceId,
588
+ deviceMetadata,
587
589
  )
588
590
 
589
591
  return { issuer, client, parameters, redirect: { code } }
@@ -596,10 +598,11 @@ export class OAuthProvider extends OAuthVerifier {
596
598
  const ssoSession = ssoSessions[0]!
597
599
  if (!ssoSession.loginRequired && !ssoSession.consentRequired) {
598
600
  const code = await this.requestManager.setAuthorized(
599
- client,
600
601
  uri,
601
- deviceId,
602
+ client,
602
603
  ssoSession.account,
604
+ deviceId,
605
+ deviceMetadata,
603
606
  )
604
607
 
605
608
  return { issuer, client, parameters, redirect: { code } }
@@ -720,10 +723,11 @@ export class OAuthProvider extends OAuthVerifier {
720
723
  }
721
724
 
722
725
  protected async acceptRequest(
723
- deviceId: DeviceId,
724
726
  uri: RequestUri,
725
727
  clientId: ClientId,
726
728
  sub: string,
729
+ deviceId: DeviceId,
730
+ deviceMetadata: RequestMetadata,
727
731
  ): Promise<AuthorizationResultRedirect> {
728
732
  const { issuer } = this
729
733
  const client = await this.clientManager.getClient(clientId)
@@ -746,10 +750,11 @@ export class OAuthProvider extends OAuthVerifier {
746
750
  }
747
751
 
748
752
  const code = await this.requestManager.setAuthorized(
749
- client,
750
753
  uri,
751
- deviceId,
754
+ client,
752
755
  account,
756
+ deviceId,
757
+ deviceMetadata,
753
758
  )
754
759
 
755
760
  await this.accountManager.addAuthorizedClient(
@@ -791,11 +796,13 @@ export class OAuthProvider extends OAuthVerifier {
791
796
  }
792
797
 
793
798
  protected async token(
794
- credentials: OAuthClientCredentials,
799
+ clientCredentials: OAuthClientCredentials,
800
+ clientMetadata: RequestMetadata,
795
801
  request: OAuthTokenRequest,
796
802
  dpopJkt: null | string,
797
803
  ): Promise<OAuthTokenResponse> {
798
- const [client, clientAuth] = await this.authenticateClient(credentials)
804
+ const [client, clientAuth] =
805
+ await this.authenticateClient(clientCredentials)
799
806
 
800
807
  if (!this.metadata.grant_types_supported?.includes(request.grant_type)) {
801
808
  throw new InvalidGrantError(
@@ -810,11 +817,23 @@ export class OAuthProvider extends OAuthVerifier {
810
817
  }
811
818
 
812
819
  if (request.grant_type === 'authorization_code') {
813
- return this.codeGrant(client, clientAuth, request, dpopJkt)
820
+ return this.codeGrant(
821
+ client,
822
+ clientAuth,
823
+ clientMetadata,
824
+ request,
825
+ dpopJkt,
826
+ )
814
827
  }
815
828
 
816
829
  if (request.grant_type === 'refresh_token') {
817
- return this.refreshTokenGrant(client, clientAuth, request, dpopJkt)
830
+ return this.refreshTokenGrant(
831
+ client,
832
+ clientAuth,
833
+ clientMetadata,
834
+ request,
835
+ dpopJkt,
836
+ )
818
837
  }
819
838
 
820
839
  throw new InvalidGrantError(
@@ -825,6 +844,7 @@ export class OAuthProvider extends OAuthVerifier {
825
844
  protected async codeGrant(
826
845
  client: Client,
827
846
  clientAuth: ClientAuth,
847
+ clientMetadata: RequestMetadata,
828
848
  input: OAuthAuthorizationCodeGrantTokenRequest,
829
849
  dpopJkt: null | string,
830
850
  ): Promise<OAuthTokenResponse> {
@@ -868,6 +888,7 @@ export class OAuthProvider extends OAuthVerifier {
868
888
  return await this.tokenManager.create(
869
889
  client,
870
890
  clientAuth,
891
+ clientMetadata,
871
892
  account,
872
893
  { id: deviceId, info },
873
894
  parameters,
@@ -890,10 +911,17 @@ export class OAuthProvider extends OAuthVerifier {
890
911
  async refreshTokenGrant(
891
912
  client: Client,
892
913
  clientAuth: ClientAuth,
914
+ clientMetadata: RequestMetadata,
893
915
  input: OAuthRefreshTokenGrantTokenRequest,
894
916
  dpopJkt: null | string,
895
917
  ): Promise<OAuthTokenResponse> {
896
- return this.tokenManager.refresh(client, clientAuth, input, dpopJkt)
918
+ return this.tokenManager.refresh(
919
+ client,
920
+ clientAuth,
921
+ clientMetadata,
922
+ input,
923
+ dpopJkt,
924
+ )
897
925
  }
898
926
 
899
927
  /**
@@ -998,7 +1026,7 @@ export class OAuthProvider extends OAuthVerifier {
998
1026
  Req extends IncomingMessage = IncomingMessage,
999
1027
  Res extends ServerResponse = ServerResponse,
1000
1028
  >(options?: RouterOptions<Req, Res>) {
1001
- const deviceManager = new DeviceManager(this.deviceStore)
1029
+ const deviceManager = new DeviceManager(this.deviceStore, options)
1002
1030
  const outputManager = new OutputManager(this.customization)
1003
1031
 
1004
1032
  // eslint-disable-next-line @typescript-eslint/no-this-alias
@@ -1224,7 +1252,9 @@ export class OAuthProvider extends OAuthVerifier {
1224
1252
  jsonHandler(async function (req, _res) {
1225
1253
  const payload = await parseHttpRequest(req, ['json', 'urlencoded'])
1226
1254
 
1227
- const credentials = await oauthClientCredentialsSchema
1255
+ const clientMetadata = await deviceManager.getRequestMetadata(req)
1256
+
1257
+ const clientCredentials = await oauthClientCredentialsSchema
1228
1258
  .parseAsync(payload, { path: ['body'] })
1229
1259
  .catch(throwInvalidClient)
1230
1260
 
@@ -1238,7 +1268,12 @@ export class OAuthProvider extends OAuthVerifier {
1238
1268
  this.url,
1239
1269
  )
1240
1270
 
1241
- return server.token(credentials, tokenRequest, dpopJkt)
1271
+ return server.token(
1272
+ clientCredentials,
1273
+ clientMetadata,
1274
+ tokenRequest,
1275
+ dpopJkt,
1276
+ )
1242
1277
  }),
1243
1278
  )
1244
1279
 
@@ -1313,11 +1348,11 @@ export class OAuthProvider extends OAuthVerifier {
1313
1348
 
1314
1349
  const query = Object.fromEntries(this.url.searchParams)
1315
1350
 
1316
- const credentials = await oauthClientCredentialsSchema
1351
+ const clientCredentials = await oauthClientCredentialsSchema
1317
1352
  .parseAsync(query, { path: ['body'] })
1318
1353
  .catch(throwInvalidRequest)
1319
1354
 
1320
- if ('client_secret' in credentials) {
1355
+ if ('client_secret' in clientCredentials) {
1321
1356
  throw new InvalidRequestError('Client secret must not be provided')
1322
1357
  }
1323
1358
 
@@ -1325,10 +1360,15 @@ export class OAuthProvider extends OAuthVerifier {
1325
1360
  .parseAsync(query, { path: ['query'] })
1326
1361
  .catch(throwInvalidRequest)
1327
1362
 
1328
- const { deviceId } = await deviceManager.load(req, res)
1363
+ const { deviceId, deviceMetadata } = await deviceManager.load(req, res)
1329
1364
 
1330
1365
  const data = await server
1331
- .authorize(deviceId, credentials, authorizationRequest)
1366
+ .authorize(
1367
+ clientCredentials,
1368
+ authorizationRequest,
1369
+ deviceId,
1370
+ deviceMetadata,
1371
+ )
1332
1372
  .catch((err) => accessDeniedToRedirectCatcher(req, res, err))
1333
1373
 
1334
1374
  switch (true) {
@@ -1431,14 +1471,15 @@ export class OAuthProvider extends OAuthVerifier {
1431
1471
  true,
1432
1472
  )
1433
1473
 
1434
- const { deviceId } = await deviceManager.load(req, res)
1474
+ const { deviceId, deviceMetadata } = await deviceManager.load(req, res)
1435
1475
 
1436
1476
  const data = await server
1437
1477
  .acceptRequest(
1438
- deviceId,
1439
1478
  input.request_uri,
1440
1479
  input.client_id,
1441
1480
  input.account_sub,
1481
+ deviceId,
1482
+ deviceMetadata,
1442
1483
  )
1443
1484
  .catch((err) => accessDeniedToRedirectCatcher(req, res, err))
1444
1485
 
@@ -1,4 +1,4 @@
1
- import { ServerResponse } from 'node:http'
1
+ import type { ServerResponse } from 'node:http'
2
2
  import { Asset } from '../assets/asset.js'
3
3
  import { getAsset } from '../assets/index.js'
4
4
  import { Html, cssCode, html } from '../lib/html/index.js'
@@ -1,4 +1,4 @@
1
- import { ServerResponse } from 'node:http'
1
+ import type { ServerResponse } from 'node:http'
2
2
  import {
3
3
  OAuthAuthorizationRequestParameters,
4
4
  OAuthTokenType,
@@ -1,5 +1,5 @@
1
1
  import { createHash } from 'node:crypto'
2
- import { ServerResponse } from 'node:http'
2
+ import type { ServerResponse } from 'node:http'
3
3
  import {
4
4
  AssetRef,
5
5
  BuildDocumentOptions,
@@ -20,6 +20,8 @@ import { InvalidGrantError } from '../errors/invalid-grant-error.js'
20
20
  import { InvalidParametersError } from '../errors/invalid-parameters-error.js'
21
21
  import { InvalidRequestError } from '../errors/invalid-request-error.js'
22
22
  import { InvalidScopeError } from '../errors/invalid-scope-error.js'
23
+ import { RequestMetadata } from '../lib/http/request.js'
24
+ import { callAsync } from '../lib/util/function.js'
23
25
  import { OAuthHooks } from '../oauth-hooks.js'
24
26
  import { Signer } from '../signer/signer.js'
25
27
  import { Code, generateCode } from './code.js'
@@ -369,10 +371,11 @@ export class RequestManager {
369
371
  }
370
372
 
371
373
  async setAuthorized(
372
- client: Client,
373
374
  uri: RequestUri,
374
- deviceId: DeviceId,
375
+ client: Client,
375
376
  account: Account,
377
+ deviceId: DeviceId,
378
+ deviceMetadata: RequestMetadata,
376
379
  ): Promise<Code> {
377
380
  const id = decodeRequestUri(uri)
378
381
 
@@ -413,6 +416,14 @@ export class RequestManager {
413
416
  expiresAt: new Date(Date.now() + AUTHORIZATION_INACTIVITY_TIMEOUT),
414
417
  })
415
418
 
419
+ await callAsync(this.hooks.onAuthorized, {
420
+ client,
421
+ account,
422
+ parameters: data.parameters,
423
+ deviceId,
424
+ deviceMetadata,
425
+ })
426
+
416
427
  return code
417
428
  } catch (err) {
418
429
  await this.store.deleteRequest(id)
@@ -29,6 +29,7 @@ import { InvalidDpopProofError } from '../errors/invalid-dpop-proof-error.js'
29
29
  import { InvalidGrantError } from '../errors/invalid-grant-error.js'
30
30
  import { InvalidRequestError } from '../errors/invalid-request-error.js'
31
31
  import { InvalidTokenError } from '../errors/invalid-token-error.js'
32
+ import { RequestMetadata } from '../lib/http/request.js'
32
33
  import { dateToEpoch, dateToRelativeSeconds } from '../lib/util/date.js'
33
34
  import { callAsync } from '../lib/util/function.js'
34
35
  import { OAuthHooks } from '../oauth-hooks.js'
@@ -82,6 +83,7 @@ export class TokenManager {
82
83
  async create(
83
84
  client: Client,
84
85
  clientAuth: ClientAuth,
86
+ clientMetadata: RequestMetadata,
85
87
  account: Account,
86
88
  device: null | { id: DeviceId; info: DeviceAccountInfo },
87
89
  parameters: OAuthAuthorizationRequestParameters,
@@ -207,13 +209,16 @@ export class TokenManager {
207
209
  const now = new Date()
208
210
  const expiresAt = this.createTokenExpiry(now)
209
211
 
210
- const authorizationDetails = this.hooks.onAuthorizationDetails
211
- ? await callAsync(this.hooks.onAuthorizationDetails, {
212
- client,
213
- parameters,
214
- account,
215
- })
216
- : undefined
212
+ const authorizationDetails = await callAsync(
213
+ this.hooks.getAuthorizationDetails,
214
+ {
215
+ client,
216
+ clientAuth,
217
+ clientMetadata,
218
+ parameters,
219
+ account,
220
+ },
221
+ )
217
222
 
218
223
  const tokenData: TokenData = {
219
224
  createdAt: now,
@@ -246,7 +251,7 @@ export class TokenManager {
246
251
  authorization_details: authorizationDetails,
247
252
  })
248
253
 
249
- return this.buildTokenResponse(
254
+ const response = await this.buildTokenResponse(
250
255
  client,
251
256
  accessToken,
252
257
  refreshToken,
@@ -255,6 +260,17 @@ export class TokenManager {
255
260
  account,
256
261
  authorizationDetails,
257
262
  )
263
+
264
+ await callAsync(this.hooks.onTokenCreated, {
265
+ client,
266
+ clientAuth,
267
+ clientMetadata,
268
+ account,
269
+ parameters,
270
+ deviceId: device ? device.id : null,
271
+ })
272
+
273
+ return response
258
274
  } catch (err) {
259
275
  // Just in case the token could not be issued, we delete it from the store
260
276
  await this.store.deleteToken(tokenId)
@@ -316,6 +332,7 @@ export class TokenManager {
316
332
  async refresh(
317
333
  client: Client,
318
334
  clientAuth: ClientAuth,
335
+ clientMetadata: RequestMetadata,
319
336
  input: OAuthRefreshTokenGrantTokenRequest,
320
337
  dpopJkt: null | string,
321
338
  ): Promise<OAuthTokenResponse> {
@@ -377,13 +394,16 @@ export class TokenManager {
377
394
  throw new InvalidGrantError(`Refresh token expired`)
378
395
  }
379
396
 
380
- const authorization_details = this.hooks.onAuthorizationDetails
381
- ? await callAsync(this.hooks.onAuthorizationDetails, {
382
- client,
383
- parameters,
384
- account,
385
- })
386
- : undefined
397
+ const authorizationDetails = await callAsync(
398
+ this.hooks.getAuthorizationDetails,
399
+ {
400
+ client,
401
+ clientAuth,
402
+ clientMetadata,
403
+ parameters,
404
+ account,
405
+ },
406
+ )
387
407
 
388
408
  const nextTokenId = await generateTokenId()
389
409
  const nextRefreshToken = await generateRefreshToken()
@@ -426,24 +446,33 @@ export class TokenManager {
426
446
  iat: now,
427
447
  jti: nextTokenId,
428
448
  cnf: parameters.dpop_jkt ? { jkt: parameters.dpop_jkt } : undefined,
429
- authorization_details,
449
+ authorization_details: authorizationDetails,
430
450
  })
431
451
 
432
- return this.buildTokenResponse(
452
+ const response = await this.buildTokenResponse(
433
453
  client,
434
454
  accessToken,
435
455
  nextRefreshToken,
436
456
  expiresAt,
437
457
  parameters,
438
458
  account,
439
- authorization_details,
459
+ authorizationDetails,
440
460
  )
461
+
462
+ await callAsync(this.hooks.onTokenRefreshed, {
463
+ client,
464
+ clientAuth,
465
+ clientMetadata,
466
+ account,
467
+ parameters,
468
+ deviceId: tokenInfo.data.deviceId,
469
+ })
470
+
471
+ return response
441
472
  } catch (err) {
442
- if (err instanceof InvalidRequestError) {
443
- // Consider the refresh token might be compromised if sanity checks
444
- // failed.
445
- await this.store.deleteToken(tokenInfo.id)
446
- }
473
+ // Just in case the token could not be refreshed, we delete it from the store
474
+ await this.store.deleteToken(tokenInfo.id)
475
+
447
476
  throw err
448
477
  }
449
478
  }
@@ -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-provider.ts","./src/oauth-store.ts","./src/oauth-verifier.ts","./src/access-token/access-token-type.ts","./src/account/account-manager.ts","./src/account/account-store.ts","./src/account/account.ts","./src/assets/asset.ts","./src/assets/assets-middleware.ts","./src/assets/index.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/device/device-data.ts","./src/device/device-details.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/errors/access-denied-error.ts","./src/errors/account-selection-required-error.ts","./src/errors/consent-required-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-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/redis.ts","./src/lib/html/build-document.ts","./src/lib/html/escapers.ts","./src/lib/html/html.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/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/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/crypto.ts","./src/lib/util/date.ts","./src/lib/util/function.ts","./src/lib/util/hostname.ts","./src/lib/util/redirect-uri.ts","./src/lib/util/time.ts","./src/lib/util/type.ts","./src/lib/util/well-known.ts","./src/metadata/build-metadata.ts","./src/oidc/sub.ts","./src/output/build-authorize-data.ts","./src/output/build-error-payload.ts","./src/output/customization.ts","./src/output/output-manager.ts","./src/output/send-authorize-redirect.ts","./src/output/send-web-page.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-memory.ts","./src/request/request-store-redis.ts","./src/request/request-store.ts","./src/request/request-uri.ts","./src/signer/signed-token-payload.ts","./src/signer/signer.ts","./src/token/refresh-token.ts","./src/token/token-claims.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"],"version":"5.6.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-provider.ts","./src/oauth-store.ts","./src/oauth-verifier.ts","./src/access-token/access-token-type.ts","./src/account/account-manager.ts","./src/account/account-store.ts","./src/account/account.ts","./src/assets/asset.ts","./src/assets/assets-middleware.ts","./src/assets/index.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/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/errors/access-denied-error.ts","./src/errors/account-selection-required-error.ts","./src/errors/consent-required-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-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/redis.ts","./src/lib/html/build-document.ts","./src/lib/html/escapers.ts","./src/lib/html/html.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/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/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/crypto.ts","./src/lib/util/date.ts","./src/lib/util/function.ts","./src/lib/util/hostname.ts","./src/lib/util/redirect-uri.ts","./src/lib/util/time.ts","./src/lib/util/type.ts","./src/lib/util/well-known.ts","./src/metadata/build-metadata.ts","./src/oidc/sub.ts","./src/output/build-authorize-data.ts","./src/output/build-error-payload.ts","./src/output/customization.ts","./src/output/output-manager.ts","./src/output/send-authorize-redirect.ts","./src/output/send-web-page.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-memory.ts","./src/request/request-store-redis.ts","./src/request/request-store.ts","./src/request/request-uri.ts","./src/signer/signed-token-payload.ts","./src/signer/signer.ts","./src/token/refresh-token.ts","./src/token/token-claims.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"],"version":"5.6.3"}
@@ -1,16 +0,0 @@
1
- import { IncomingMessage } from 'node:http';
2
- import { z } from 'zod';
3
- export declare const deviceDetailsSchema: z.ZodObject<{
4
- userAgent: z.ZodNullable<z.ZodString>;
5
- ipAddress: z.ZodString;
6
- }, "strip", z.ZodTypeAny, {
7
- userAgent: string | null;
8
- ipAddress: string;
9
- }, {
10
- userAgent: string | null;
11
- ipAddress: string;
12
- }>;
13
- export type DeviceDetails = z.infer<typeof deviceDetailsSchema>;
14
- export declare function extractDeviceDetails(req: IncomingMessage, trustProxy: boolean): DeviceDetails;
15
- export declare function extractIpAddress(req: IncomingMessage, trustProxy: boolean): string | undefined;
16
- //# sourceMappingURL=device-details.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"device-details.d.ts","sourceRoot":"","sources":["../../src/device/device-details.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAC3C,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB,eAAO,MAAM,mBAAmB;;;;;;;;;EAG9B,CAAA;AACF,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAA;AAE/D,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,eAAe,EACpB,UAAU,EAAE,OAAO,GAClB,aAAa,CASf;AAED,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,eAAe,EACpB,UAAU,EAAE,OAAO,GAClB,MAAM,GAAG,SAAS,CAepB"}
@@ -1,34 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.deviceDetailsSchema = void 0;
4
- exports.extractDeviceDetails = extractDeviceDetails;
5
- exports.extractIpAddress = extractIpAddress;
6
- const zod_1 = require("zod");
7
- exports.deviceDetailsSchema = zod_1.z.object({
8
- userAgent: zod_1.z.string().nullable(),
9
- ipAddress: zod_1.z.string(),
10
- });
11
- function extractDeviceDetails(req, trustProxy) {
12
- const userAgent = req.headers['user-agent'] || null;
13
- const ipAddress = extractIpAddress(req, trustProxy) || null;
14
- if (!ipAddress) {
15
- throw new Error('Could not determine IP address');
16
- }
17
- return { userAgent, ipAddress };
18
- }
19
- function extractIpAddress(req, trustProxy) {
20
- // Express app compatibility
21
- if ('ip' in req && typeof req.ip === 'string') {
22
- return req.ip;
23
- }
24
- if (trustProxy) {
25
- const forwardedFor = req.headers['x-forwarded-for'];
26
- if (typeof forwardedFor === 'string') {
27
- const firstForward = forwardedFor.split(',')[0].trim();
28
- if (firstForward)
29
- return firstForward;
30
- }
31
- }
32
- return req.socket.remoteAddress;
33
- }
34
- //# sourceMappingURL=device-details.js.map