@auth0/auth0-server-js 1.7.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -41,6 +41,7 @@ __export(index_exports, {
41
41
  ServerClient: () => ServerClient,
42
42
  ServerMfaClient: () => ServerMfaClient,
43
43
  ServerPasskeyClient: () => ServerPasskeyClient,
44
+ SessionExpiredError: () => SessionExpiredError,
44
45
  StartLinkUserError: () => StartLinkUserError,
45
46
  StatefulStateStore: () => StatefulStateStore,
46
47
  StatelessStateStore: () => StatelessStateStore,
@@ -99,14 +100,55 @@ var IssuerValidationError = class extends Error {
99
100
  this.name = "IssuerValidationError";
100
101
  }
101
102
  };
103
+ var SessionExpiredError = class extends Error {
104
+ code = "session_expired";
105
+ constructor(message) {
106
+ super(
107
+ message ?? "The session has expired because the upstream identity provider session ceiling was reached. The user needs to re-authenticate."
108
+ );
109
+ this.name = "SessionExpiredError";
110
+ }
111
+ };
102
112
 
103
113
  // src/state/utils.ts
114
+ var SESSION_EXPIRY_LEEWAY = 30;
115
+ var MAX_PLAUSIBLE_UNIX_SECONDS = 1e10;
116
+ function isPlausibleUnixSeconds(value) {
117
+ return typeof value === "number" && Number.isInteger(value) && value > 0 && value < MAX_PLAUSIBLE_UNIX_SECONDS;
118
+ }
119
+ function extractSessionExpiry(claims) {
120
+ const value = claims?.session_expiry;
121
+ return isPlausibleUnixSeconds(value) ? value : void 0;
122
+ }
123
+ function isSessionExpiryReached(sessionExpiresAt, nowSeconds) {
124
+ if (sessionExpiresAt === void 0) {
125
+ return false;
126
+ }
127
+ const now = nowSeconds ?? Math.floor(Date.now() / 1e3);
128
+ return now >= sessionExpiresAt - SESSION_EXPIRY_LEEWAY;
129
+ }
104
130
  var createUpdatedTokenSet = (audience, response) => ({
105
131
  audience,
106
132
  accessToken: response.accessToken,
107
133
  scope: response.scope,
108
134
  expiresAt: response.expiresAt
109
135
  });
136
+ function isSessionExpiryInPast(sessionExpiresAt, issuedAt) {
137
+ if (sessionExpiresAt === void 0) {
138
+ return false;
139
+ }
140
+ const reference = isPlausibleUnixSeconds(issuedAt) ? issuedAt : Math.floor(Date.now() / 1e3);
141
+ return sessionExpiresAt <= reference + SESSION_EXPIRY_LEEWAY;
142
+ }
143
+ function applySessionExpiryAtLogin(stateData, claims) {
144
+ const sessionExpiresAt = extractSessionExpiry(claims);
145
+ if (isSessionExpiryInPast(sessionExpiresAt, claims?.iat)) {
146
+ throw new SessionExpiredError(
147
+ "The upstream identity provider session_expiry is at or before the issued-at time; refusing to create an already-expired session."
148
+ );
149
+ }
150
+ return { ...stateData, sessionExpiresAt };
151
+ }
110
152
  function updateStateData(audience, stateData, tokenEndpointResponse, context) {
111
153
  if (stateData && tokenEndpointResponse.claims) {
112
154
  const newSub = tokenEndpointResponse.claims.sub;
@@ -208,7 +250,7 @@ function getTelemetryConfig(config) {
208
250
  return {
209
251
  enabled: true,
210
252
  name: config?.name ?? "@auth0/auth0-server-js",
211
- version: config?.version ?? "1.7.0"
253
+ version: config?.version ?? "1.8.0"
212
254
  };
213
255
  }
214
256
 
@@ -270,9 +312,12 @@ var ServerMfaClient = class {
270
312
  this.#options.stateStoreIdentifier,
271
313
  storeOptions
272
314
  );
273
- const updatedStateData = updateStateData(audience, existingStateData, tokenResponse, {
274
- domain: this.#options.domain
275
- });
315
+ const updatedStateData = applySessionExpiryAtLogin(
316
+ updateStateData(audience, existingStateData, tokenResponse, {
317
+ domain: this.#options.domain
318
+ }),
319
+ tokenResponse.claims
320
+ );
276
321
  await this.#options.stateStore.set(
277
322
  this.#options.stateStoreIdentifier,
278
323
  updatedStateData,
@@ -598,6 +643,7 @@ var ServerClient = class {
598
643
  *
599
644
  * @throws {MissingTransactionError} When no transaction was found.
600
645
  * @throws {TokenByCodeError} If there was an issue requesting the access token.
646
+ * @throws {SessionExpiredError} When the ID token's `session_expiry` is already in the past at login (the session is born expired); nothing is persisted.
601
647
  *
602
648
  * @returns A promise resolving to an object, containing the original appState (if present) and the authorizationDetails (when RAR was used).
603
649
  */
@@ -612,12 +658,15 @@ var ServerClient = class {
612
658
  // TransactionData.codeVerifier is optional only to accommodate magic-link transactions.
613
659
  codeVerifier: transactionData.codeVerifier
614
660
  });
661
+ await this.#transactionStore.delete(this.#transactionStoreIdentifier, storeOptions);
615
662
  const existingStateData = await this.#stateStore.get(this.#stateStoreIdentifier, storeOptions);
616
- const stateData = updateStateData(transactionData.audience ?? "default", existingStateData, tokenEndpointResponse, {
617
- domain
618
- });
663
+ const stateData = applySessionExpiryAtLogin(
664
+ updateStateData(transactionData.audience ?? "default", existingStateData, tokenEndpointResponse, {
665
+ domain
666
+ }),
667
+ tokenEndpointResponse.claims
668
+ );
619
669
  await this.#stateStore.set(this.#stateStoreIdentifier, stateData, true, storeOptions);
620
- await this.#transactionStore.delete(this.#transactionStoreIdentifier, storeOptions);
621
670
  return { appState: transactionData.appState, authorizationDetails: tokenEndpointResponse.authorizationDetails };
622
671
  }
623
672
  /**
@@ -627,6 +676,7 @@ var ServerClient = class {
627
676
  *
628
677
  * @throws {MissingSessionError} If there is no active session.
629
678
  * @throws {BuildLinkUserUrlError} If there was an issue when building the Authorization URL.
679
+ * @throws {SessionExpiredError} When the session's `session_expiry` ceiling has been reached; the session is cleared and re-authentication is required.
630
680
  *
631
681
  * @returns A promise resolving to a URL object, representing the URL to redirect the user-agent to to request authorization at Auth0.
632
682
  */
@@ -643,6 +693,10 @@ var ServerClient = class {
643
693
  throw new MissingSessionError("Session domain does not match the current domain.");
644
694
  }
645
695
  }
696
+ if (isSessionExpiryReached(stateData.sessionExpiresAt)) {
697
+ await this.#stateStore.delete(this.#stateStoreIdentifier, storeOptions);
698
+ throw new SessionExpiredError();
699
+ }
646
700
  const domain = this.#getSessionDomain(stateData);
647
701
  const authClient = this.#getAuthClient(domain);
648
702
  const { linkUserUrl, codeVerifier } = await authClient.buildLinkUserUrl({
@@ -686,6 +740,7 @@ var ServerClient = class {
686
740
  *
687
741
  * @throws {MissingSessionError} If there is no active session.
688
742
  * @throws {BuildUnlinkUserUrlError} If there was an issue when building the User Unlinking URL.
743
+ * @throws {SessionExpiredError} When the session's `session_expiry` ceiling has been reached; the session is cleared and re-authentication is required.
689
744
  *
690
745
  * @returns A promise resolving to a URL object, representing the URL to redirect the user-agent to to request authorization at Auth0.
691
746
  */
@@ -702,6 +757,10 @@ var ServerClient = class {
702
757
  throw new MissingSessionError("Session domain does not match the current domain.");
703
758
  }
704
759
  }
760
+ if (isSessionExpiryReached(stateData.sessionExpiresAt)) {
761
+ await this.#stateStore.delete(this.#stateStoreIdentifier, storeOptions);
762
+ throw new SessionExpiredError();
763
+ }
705
764
  const domain = this.#getSessionDomain(stateData);
706
765
  const authClient = this.#getAuthClient(domain);
707
766
  const { unlinkUserUrl, codeVerifier } = await authClient.buildUnlinkUserUrl({
@@ -746,6 +805,7 @@ var ServerClient = class {
746
805
  * @param storeOptions Optional options used to pass to the Transaction and State Store.
747
806
  *
748
807
  * @throws {BackchannelAuthenticationError} If there was an issue when doing backchannel authentication.
808
+ * @throws {SessionExpiredError} When the ID token's `session_expiry` is already in the past at login (the session is born expired); nothing is persisted.
749
809
  *
750
810
  * @returns A promise resolving to an object, containing the authorizationDetails (when RAR was used).
751
811
  */
@@ -762,11 +822,11 @@ var ServerClient = class {
762
822
  }
763
823
  });
764
824
  const existingStateData = await this.#stateStore.get(this.#stateStoreIdentifier, storeOptions);
765
- const stateData = updateStateData(
766
- this.#options.authorizationParams?.audience ?? "default",
767
- existingStateData,
768
- tokenEndpointResponse,
769
- { domain }
825
+ const stateData = applySessionExpiryAtLogin(
826
+ updateStateData(this.#options.authorizationParams?.audience ?? "default", existingStateData, tokenEndpointResponse, {
827
+ domain
828
+ }),
829
+ tokenEndpointResponse.claims
770
830
  );
771
831
  await this.#stateStore.set(this.#stateStoreIdentifier, stateData, true, storeOptions);
772
832
  return {
@@ -961,6 +1021,10 @@ var ServerClient = class {
961
1021
  return;
962
1022
  }
963
1023
  }
1024
+ if (isSessionExpiryReached(stateData.sessionExpiresAt)) {
1025
+ await this.#stateStore.delete(this.#stateStoreIdentifier, storeOptions);
1026
+ return;
1027
+ }
964
1028
  return stateData.user;
965
1029
  }
966
1030
  /**
@@ -977,6 +1041,10 @@ var ServerClient = class {
977
1041
  return;
978
1042
  }
979
1043
  }
1044
+ if (isSessionExpiryReached(stateData.sessionExpiresAt)) {
1045
+ await this.#stateStore.delete(this.#stateStoreIdentifier, storeOptions);
1046
+ return;
1047
+ }
980
1048
  const { internal, ...sessionData } = stateData;
981
1049
  return sessionData;
982
1050
  }
@@ -993,6 +1061,7 @@ var ServerClient = class {
993
1061
  * @param storeOptions Optional options used to pass to the Transaction and State Store.
994
1062
  *
995
1063
  * @throws {TokenByRefreshTokenError} If the refresh token was not found or there was an issue requesting the access token. When the cause is `mfa_required`, use `isMfaRequiredError(error)` to narrow the error and read `cause.mfa_token`.
1064
+ * @throws {SessionExpiredError} When the session's `session_expiry` ceiling has been reached; the session is cleared and no refresh is attempted — the user must re-authenticate.
996
1065
  *
997
1066
  * @returns The Token Set, containing the access token, as well as additional information.
998
1067
  */
@@ -1020,6 +1089,10 @@ var ServerClient = class {
1020
1089
  throw new MissingSessionError("Session domain does not match the current domain.");
1021
1090
  }
1022
1091
  }
1092
+ if (stateData && isSessionExpiryReached(stateData.sessionExpiresAt)) {
1093
+ await this.#stateStore.delete(this.#stateStoreIdentifier, resolvedStoreOptions);
1094
+ throw new SessionExpiredError();
1095
+ }
1023
1096
  const tokenSet = stateData?.tokenSets.find(
1024
1097
  (tokenSet2) => tokenSet2.audience === audience && (!scope || compareScopes(tokenSet2.scope, scope))
1025
1098
  );
@@ -1394,14 +1467,14 @@ var AbstractSessionStore = class extends AbstractStateStore {
1394
1467
  }
1395
1468
  /**
1396
1469
  * calculateMaxAge calculates the max age of the session based on createdAt and the rolling and absolute durations.
1470
+ * When sessionExpiresAt is provided, caps the maxAge to not exceed the time until that ceiling.
1397
1471
  */
1398
- calculateMaxAge(createdAt) {
1399
- if (!this.#rolling) {
1400
- return this.#absoluteDuration;
1401
- }
1472
+ calculateMaxAge(createdAt, sessionExpiresAt) {
1402
1473
  const now = Date.now() / 1e3 | 0;
1403
- const expiresAt = Math.min(now + this.#inactivityDuration, createdAt + this.#absoluteDuration);
1404
- const maxAge = expiresAt - now;
1474
+ let maxAge = this.#rolling ? Math.min(now + this.#inactivityDuration, createdAt + this.#absoluteDuration) - now : this.#absoluteDuration;
1475
+ if (sessionExpiresAt !== void 0) {
1476
+ maxAge = Math.min(maxAge, sessionExpiresAt - now);
1477
+ }
1405
1478
  return maxAge > 0 ? maxAge : 0;
1406
1479
  }
1407
1480
  };
@@ -1429,7 +1502,7 @@ var StatefulStateStore = class extends AbstractSessionStore {
1429
1502
  sessionId = generateId();
1430
1503
  }
1431
1504
  sessionId ??= generateId();
1432
- const maxAge = this.calculateMaxAge(stateData.internal.createdAt);
1505
+ const maxAge = this.calculateMaxAge(stateData.internal.createdAt, stateData.sessionExpiresAt);
1433
1506
  const cookieOpts = this.#getCookieOptions({
1434
1507
  maxAge
1435
1508
  });
@@ -1494,7 +1567,7 @@ var StatelessStateStore = class extends AbstractSessionStore {
1494
1567
  this.#cookieHandler = cookieHandler;
1495
1568
  }
1496
1569
  async set(identifier, stateData, removeIfExists, options) {
1497
- const maxAge = this.calculateMaxAge(stateData.internal.createdAt);
1570
+ const maxAge = this.calculateMaxAge(stateData.internal.createdAt, stateData.sessionExpiresAt);
1498
1571
  const cookieOpts = this.#getCookieOptions({
1499
1572
  maxAge
1500
1573
  });
@@ -1578,6 +1651,7 @@ var import_auth0_auth_js3 = require("@auth0/auth0-auth-js");
1578
1651
  ServerClient,
1579
1652
  ServerMfaClient,
1580
1653
  ServerPasskeyClient,
1654
+ SessionExpiredError,
1581
1655
  StartLinkUserError,
1582
1656
  StatefulStateStore,
1583
1657
  StatelessStateStore,