@auth0/auth0-server-js 1.7.0 → 1.9.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
@@ -34,13 +34,14 @@ __export(index_exports, {
34
34
  MissingRequiredArgumentError: () => MissingRequiredArgumentError,
35
35
  MissingSessionError: () => MissingSessionError,
36
36
  MissingTransactionError: () => MissingTransactionError,
37
- OrganizationValidationError: () => import_auth0_auth_js3.OrganizationValidationError,
37
+ OrganizationValidationError: () => import_auth0_auth_js4.OrganizationValidationError,
38
38
  PasskeyChallengeError: () => import_auth0_auth_js3.PasskeyChallengeError,
39
39
  PasskeyGetTokenError: () => import_auth0_auth_js3.PasskeyGetTokenError,
40
40
  PasskeyRegisterError: () => import_auth0_auth_js3.PasskeyRegisterError,
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.9.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,
@@ -556,9 +601,16 @@ var ServerClient = class {
556
601
  }
557
602
  /**
558
603
  * Starts the interactive login process, and returns a URL to redirect the user-agent to to request authorization at Auth0.
604
+ *
605
+ * When `organization` is provided (per-login option, client-level default, or via
606
+ * `authorizationParams.organization`), it is forwarded to `/authorize` and remembered so the
607
+ * returned ID token's organization claim can be validated in `completeInteractiveLogin`.
608
+ *
559
609
  * @param options Optional options used to configure the interactive login process.
560
610
  * @param storeOptions Optional options used to pass to the Transaction and State Store.
561
611
  *
612
+ * @throws {OrganizationValidationError} If the resolved `organization` is blank.
613
+ * @throws {InvalidConfigurationError} If `invitation` is provided without an `organization`.
562
614
  * @throws {BuildAuthorizationUrlError} If there was an issue when building the Authorization URL.
563
615
  *
564
616
  * @returns A promise resolving to a URL object, representing the URL to redirect the user-agent to to request authorization at Auth0.
@@ -569,6 +621,16 @@ var ServerClient = class {
569
621
  throw new MissingRequiredArgumentError("authorizationParams.redirect_uri");
570
622
  }
571
623
  const scope = ensureOpenIdScope(options?.authorizationParams?.scope ?? this.#options.authorizationParams?.scope);
624
+ const perLoginAuthParamsOrganization = typeof options?.authorizationParams?.organization === "string" ? options.authorizationParams.organization : void 0;
625
+ const clientAuthParamsOrganization = typeof this.#options.authorizationParams?.organization === "string" ? this.#options.authorizationParams.organization : void 0;
626
+ const resolvedOrganization = options?.organization ?? perLoginAuthParamsOrganization ?? this.#options.organization ?? clientAuthParamsOrganization;
627
+ if (resolvedOrganization !== void 0 && !resolvedOrganization.trim()) {
628
+ throw new import_auth0_auth_js.OrganizationValidationError("organization must not be blank");
629
+ }
630
+ const hasInvitation = !!(options?.invitation || options?.authorizationParams?.invitation);
631
+ if (hasInvitation && !resolvedOrganization) {
632
+ throw new InvalidConfigurationError("organization is required when invitation is provided.");
633
+ }
572
634
  const domain = await this.#resolveDomain(storeOptions);
573
635
  const authClient = this.#getAuthClient(domain);
574
636
  const { codeVerifier, authorizationUrl } = await authClient.buildAuthorizationUrl({
@@ -576,7 +638,9 @@ var ServerClient = class {
576
638
  authorizationParams: {
577
639
  ...options?.authorizationParams,
578
640
  redirect_uri: redirectUri,
579
- scope
641
+ scope,
642
+ ...resolvedOrganization ? { organization: resolvedOrganization } : {},
643
+ ...options?.invitation ? { invitation: options.invitation } : {}
580
644
  }
581
645
  });
582
646
  const transactionState = {
@@ -584,6 +648,9 @@ var ServerClient = class {
584
648
  codeVerifier,
585
649
  domain
586
650
  };
651
+ if (resolvedOrganization) {
652
+ transactionState.organization = resolvedOrganization;
653
+ }
587
654
  if (options?.appState) {
588
655
  transactionState.appState = options.appState;
589
656
  }
@@ -598,6 +665,8 @@ var ServerClient = class {
598
665
  *
599
666
  * @throws {MissingTransactionError} When no transaction was found.
600
667
  * @throws {TokenByCodeError} If there was an issue requesting the access token.
668
+ * @throws {OrganizationValidationError} When an organization was requested at login and the returned ID token's organization claim is missing or does not match; nothing is persisted.
669
+ * @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
670
  *
602
671
  * @returns A promise resolving to an object, containing the original appState (if present) and the authorizationDetails (when RAR was used).
603
672
  */
@@ -610,14 +679,18 @@ var ServerClient = class {
610
679
  const authClient = this.#getAuthClient(domain);
611
680
  const tokenEndpointResponse = await authClient.getTokenByCode(url, {
612
681
  // TransactionData.codeVerifier is optional only to accommodate magic-link transactions.
613
- codeVerifier: transactionData.codeVerifier
682
+ codeVerifier: transactionData.codeVerifier,
683
+ organization: transactionData.organization
614
684
  });
685
+ await this.#transactionStore.delete(this.#transactionStoreIdentifier, storeOptions);
615
686
  const existingStateData = await this.#stateStore.get(this.#stateStoreIdentifier, storeOptions);
616
- const stateData = updateStateData(transactionData.audience ?? "default", existingStateData, tokenEndpointResponse, {
617
- domain
618
- });
687
+ const stateData = applySessionExpiryAtLogin(
688
+ updateStateData(transactionData.audience ?? "default", existingStateData, tokenEndpointResponse, {
689
+ domain
690
+ }),
691
+ tokenEndpointResponse.claims
692
+ );
619
693
  await this.#stateStore.set(this.#stateStoreIdentifier, stateData, true, storeOptions);
620
- await this.#transactionStore.delete(this.#transactionStoreIdentifier, storeOptions);
621
694
  return { appState: transactionData.appState, authorizationDetails: tokenEndpointResponse.authorizationDetails };
622
695
  }
623
696
  /**
@@ -627,6 +700,7 @@ var ServerClient = class {
627
700
  *
628
701
  * @throws {MissingSessionError} If there is no active session.
629
702
  * @throws {BuildLinkUserUrlError} If there was an issue when building the Authorization URL.
703
+ * @throws {SessionExpiredError} When the session's `session_expiry` ceiling has been reached; the session is cleared and re-authentication is required.
630
704
  *
631
705
  * @returns A promise resolving to a URL object, representing the URL to redirect the user-agent to to request authorization at Auth0.
632
706
  */
@@ -643,6 +717,10 @@ var ServerClient = class {
643
717
  throw new MissingSessionError("Session domain does not match the current domain.");
644
718
  }
645
719
  }
720
+ if (isSessionExpiryReached(stateData.sessionExpiresAt)) {
721
+ await this.#stateStore.delete(this.#stateStoreIdentifier, storeOptions);
722
+ throw new SessionExpiredError();
723
+ }
646
724
  const domain = this.#getSessionDomain(stateData);
647
725
  const authClient = this.#getAuthClient(domain);
648
726
  const { linkUserUrl, codeVerifier } = await authClient.buildLinkUserUrl({
@@ -686,6 +764,7 @@ var ServerClient = class {
686
764
  *
687
765
  * @throws {MissingSessionError} If there is no active session.
688
766
  * @throws {BuildUnlinkUserUrlError} If there was an issue when building the User Unlinking URL.
767
+ * @throws {SessionExpiredError} When the session's `session_expiry` ceiling has been reached; the session is cleared and re-authentication is required.
689
768
  *
690
769
  * @returns A promise resolving to a URL object, representing the URL to redirect the user-agent to to request authorization at Auth0.
691
770
  */
@@ -702,6 +781,10 @@ var ServerClient = class {
702
781
  throw new MissingSessionError("Session domain does not match the current domain.");
703
782
  }
704
783
  }
784
+ if (isSessionExpiryReached(stateData.sessionExpiresAt)) {
785
+ await this.#stateStore.delete(this.#stateStoreIdentifier, storeOptions);
786
+ throw new SessionExpiredError();
787
+ }
705
788
  const domain = this.#getSessionDomain(stateData);
706
789
  const authClient = this.#getAuthClient(domain);
707
790
  const { unlinkUserUrl, codeVerifier } = await authClient.buildUnlinkUserUrl({
@@ -746,6 +829,7 @@ var ServerClient = class {
746
829
  * @param storeOptions Optional options used to pass to the Transaction and State Store.
747
830
  *
748
831
  * @throws {BackchannelAuthenticationError} If there was an issue when doing backchannel authentication.
832
+ * @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
833
  *
750
834
  * @returns A promise resolving to an object, containing the authorizationDetails (when RAR was used).
751
835
  */
@@ -762,11 +846,11 @@ var ServerClient = class {
762
846
  }
763
847
  });
764
848
  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 }
849
+ const stateData = applySessionExpiryAtLogin(
850
+ updateStateData(this.#options.authorizationParams?.audience ?? "default", existingStateData, tokenEndpointResponse, {
851
+ domain
852
+ }),
853
+ tokenEndpointResponse.claims
770
854
  );
771
855
  await this.#stateStore.set(this.#stateStoreIdentifier, stateData, true, storeOptions);
772
856
  return {
@@ -961,6 +1045,10 @@ var ServerClient = class {
961
1045
  return;
962
1046
  }
963
1047
  }
1048
+ if (isSessionExpiryReached(stateData.sessionExpiresAt)) {
1049
+ await this.#stateStore.delete(this.#stateStoreIdentifier, storeOptions);
1050
+ return;
1051
+ }
964
1052
  return stateData.user;
965
1053
  }
966
1054
  /**
@@ -977,6 +1065,10 @@ var ServerClient = class {
977
1065
  return;
978
1066
  }
979
1067
  }
1068
+ if (isSessionExpiryReached(stateData.sessionExpiresAt)) {
1069
+ await this.#stateStore.delete(this.#stateStoreIdentifier, storeOptions);
1070
+ return;
1071
+ }
980
1072
  const { internal, ...sessionData } = stateData;
981
1073
  return sessionData;
982
1074
  }
@@ -993,6 +1085,7 @@ var ServerClient = class {
993
1085
  * @param storeOptions Optional options used to pass to the Transaction and State Store.
994
1086
  *
995
1087
  * @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`.
1088
+ * @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
1089
  *
997
1090
  * @returns The Token Set, containing the access token, as well as additional information.
998
1091
  */
@@ -1020,6 +1113,10 @@ var ServerClient = class {
1020
1113
  throw new MissingSessionError("Session domain does not match the current domain.");
1021
1114
  }
1022
1115
  }
1116
+ if (stateData && isSessionExpiryReached(stateData.sessionExpiresAt)) {
1117
+ await this.#stateStore.delete(this.#stateStoreIdentifier, resolvedStoreOptions);
1118
+ throw new SessionExpiredError();
1119
+ }
1023
1120
  const tokenSet = stateData?.tokenSets.find(
1024
1121
  (tokenSet2) => tokenSet2.audience === audience && (!scope || compareScopes(tokenSet2.scope, scope))
1025
1122
  );
@@ -1394,14 +1491,14 @@ var AbstractSessionStore = class extends AbstractStateStore {
1394
1491
  }
1395
1492
  /**
1396
1493
  * calculateMaxAge calculates the max age of the session based on createdAt and the rolling and absolute durations.
1494
+ * When sessionExpiresAt is provided, caps the maxAge to not exceed the time until that ceiling.
1397
1495
  */
1398
- calculateMaxAge(createdAt) {
1399
- if (!this.#rolling) {
1400
- return this.#absoluteDuration;
1401
- }
1496
+ calculateMaxAge(createdAt, sessionExpiresAt) {
1402
1497
  const now = Date.now() / 1e3 | 0;
1403
- const expiresAt = Math.min(now + this.#inactivityDuration, createdAt + this.#absoluteDuration);
1404
- const maxAge = expiresAt - now;
1498
+ let maxAge = this.#rolling ? Math.min(now + this.#inactivityDuration, createdAt + this.#absoluteDuration) - now : this.#absoluteDuration;
1499
+ if (sessionExpiresAt !== void 0) {
1500
+ maxAge = Math.min(maxAge, sessionExpiresAt - now);
1501
+ }
1405
1502
  return maxAge > 0 ? maxAge : 0;
1406
1503
  }
1407
1504
  };
@@ -1429,7 +1526,7 @@ var StatefulStateStore = class extends AbstractSessionStore {
1429
1526
  sessionId = generateId();
1430
1527
  }
1431
1528
  sessionId ??= generateId();
1432
- const maxAge = this.calculateMaxAge(stateData.internal.createdAt);
1529
+ const maxAge = this.calculateMaxAge(stateData.internal.createdAt, stateData.sessionExpiresAt);
1433
1530
  const cookieOpts = this.#getCookieOptions({
1434
1531
  maxAge
1435
1532
  });
@@ -1494,7 +1591,7 @@ var StatelessStateStore = class extends AbstractSessionStore {
1494
1591
  this.#cookieHandler = cookieHandler;
1495
1592
  }
1496
1593
  async set(identifier, stateData, removeIfExists, options) {
1497
- const maxAge = this.calculateMaxAge(stateData.internal.createdAt);
1594
+ const maxAge = this.calculateMaxAge(stateData.internal.createdAt, stateData.sessionExpiresAt);
1498
1595
  const cookieOpts = this.#getCookieOptions({
1499
1596
  maxAge
1500
1597
  });
@@ -1578,6 +1675,7 @@ var import_auth0_auth_js3 = require("@auth0/auth0-auth-js");
1578
1675
  ServerClient,
1579
1676
  ServerMfaClient,
1580
1677
  ServerPasskeyClient,
1678
+ SessionExpiredError,
1581
1679
  StartLinkUserError,
1582
1680
  StatefulStateStore,
1583
1681
  StatelessStateStore,