@frontegg/redux-store 7.118.0 → 7.120.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.
@@ -1,6 +1,8 @@
1
1
  import _extends from "@babel/runtime/helpers/esm/extends";
2
2
  import { getPathAndSearchParamsFromUrl, getRedirectUrl, isAbsoluteUrl } from '../helpers';
3
+ import { shouldPromptChooseTenant } from '../helpers/tenantSelect';
3
4
  import { FRONTEGG_AFTER_AUTH_REDIRECT_URL } from '../../../constants';
5
+ import { LoginStep } from '../interfaces';
4
6
  import { SHOULD_STEP_UP_KEY } from '../../StepUpState/consts';
5
7
  import { delay } from '../../../helpers';
6
8
  import { isSteppedUp } from '../../StepUpState/helpers';
@@ -146,17 +148,106 @@ export default ((store, api, sharedActions) => {
146
148
  });
147
149
  };
148
150
 
151
+ /**
152
+ * Choose-tenant gate — the single chokepoint all auth methods (password/MFA/SSO/passwordless/
153
+ * passkey) and interstitials converge on. The trigger is the `authOptions.tenantSelection.enabled`
154
+ * SDK option, so it fires identically for embedded and hosted integrators (in hosted mode
155
+ * oauth-service is responsible for setting the option). When the option is on, the LD flag is on,
156
+ * and the user has >1 switchable tenant, route to the ChooseTenant step instead of finalizing.
157
+ * The ChooseTenant confirm handler sets `tenantChoiceResolved` and re-enters
158
+ * afterAuthNavigation, which then falls through here. Runs only post-auth; no tenant data is read
159
+ * until the resolved-guard, the LD flag, and the option all pass.
160
+ */
161
+ const shouldRouteToChooseTenant = async () => {
162
+ var _store$auth$tenantSel, _store$auth$user, _store$auth$user$act, _store$auth$tenantsSt;
163
+ const loginState = store.auth.loginState;
164
+ // Guard first so re-entry after the choice is resolved finalizes without re-reading the flag.
165
+ if (loginState.tenantChoiceResolved) {
166
+ return false;
167
+ }
168
+ // Cheap option guard before any flag/tenant work — never act when the integrator didn't opt in.
169
+ const enabledByOption = ((_store$auth$tenantSel = store.auth.tenantSelection) == null ? void 0 : _store$auth$tenantSel.enabled) === true;
170
+ if (!enabledByOption) {
171
+ return false;
172
+ }
173
+ const [ldEnabled] = await actions.getFeatureFlags(['login-box-choose-tenant']);
174
+ if (!ldEnabled) {
175
+ return false; // feature off — never read tenant data
176
+ }
177
+
178
+ const isImpersonation = !!((_store$auth$user = store.auth.user) != null && (_store$auth$user$act = _store$auth$user.act) != null && _store$auth$user$act.sub);
179
+ if (isImpersonation) {
180
+ return false;
181
+ }
182
+ // SSO/social postLogin authenticates without populating tenantsState — load it here so the
183
+ // chooser also opens on those paths (password/MFA already have tenants from the login response).
184
+ if (!((_store$auth$tenantsSt = store.auth.tenantsState.tenants) != null && _store$auth$tenantsSt.length)) {
185
+ var _store$auth$tenantsSt2;
186
+ let loadFailed = false;
187
+ await actions.loadTenants({
188
+ callback: (_tenants, error) => loadFailed = !!error
189
+ });
190
+ // Do NOT fail-open: if the tenant set could not be loaded while the feature is on, route to the
191
+ // chooser (which shows a load error + retry / continue-with-current) instead of finalizing on
192
+ // the default tenant and silently skipping the required org choice.
193
+ if (loadFailed && !((_store$auth$tenantsSt2 = store.auth.tenantsState.tenants) != null && _store$auth$tenantsSt2.length)) {
194
+ return true;
195
+ }
196
+ }
197
+ const {
198
+ tenants
199
+ } = store.auth.tenantsState;
200
+ return shouldPromptChooseTenant({
201
+ ldEnabled,
202
+ enabledByOption,
203
+ tenants,
204
+ isImpersonation,
205
+ tenantChoiceResolved: loginState.tenantChoiceResolved
206
+ });
207
+ };
208
+
149
209
  /**
150
210
  * After auth navigation for login flow
151
211
  * Handling also step up scenario when user silently logout to continue to step up
152
212
  */
213
+
153
214
  const afterAuthNavigation = async payload => {
154
215
  var _window4, _payload$preventRedir;
216
+ // Once the choose-tenant step starts a switch the chooser owns navigation. Every re-entry here
217
+ // (the switch's token refresh, and — critically — a late or timed-out switch completing AFTER the
218
+ // user hit the escape hatch) must be a no-op, so navigation can't run twice or jump into the wrong
219
+ // org. Two latches guard it: `chooseTenantSwitching` covers the in-login window, but it lives in
220
+ // loginState and is wiped by the finalize's resetLoginState — so a late switch would slip through.
221
+ // `tenantsState.switchingTenant` stays set for the WHOLE switchTenant call and survives that reset,
222
+ // so it suppresses the late re-entry too. The confirm handler performs the ONE finalize via the
223
+ // one-shot `chooseTenantFinalizing` flag, which passes through both latches and is consumed here.
224
+ // (An in-app tenant switch never reaches this function — its refresh only calls afterAuthNavigation
225
+ // from a login/callback URL, not from an authenticated app page — so this cannot over-suppress.)
226
+ const {
227
+ chooseTenantSwitching,
228
+ chooseTenantFinalizing
229
+ } = store.auth.loginState;
230
+ const {
231
+ switchingTenant
232
+ } = store.auth.tenantsState;
233
+ if ((chooseTenantSwitching || switchingTenant) && !chooseTenantFinalizing) {
234
+ return;
235
+ }
236
+ if (chooseTenantFinalizing) {
237
+ actions.setLoginState({
238
+ chooseTenantFinalizing: false
239
+ }); // consume: later re-entries stay suppressed
240
+ }
241
+
155
242
  const {
156
243
  customLoginAuthenticatedUrl,
157
244
  stepUpUrl
158
245
  } = store.auth.routes;
159
246
 
247
+ // Step-up on the CURRENT tenant resolves before the org chooser: a user who needs both
248
+ // authenticates + steps up first, then picks an org. switchTenant handles any MFA the chosen
249
+ // org requires on its own, and the isSteppedUp guard below means an already-stepped-up user is
250
+ // never re-prompted.
160
251
  // login with magic code, try to step up, no other mfa, invalid max age, force_enroll -> logout, login with first factor, not-stepped up jwt -> navigate to step up
161
252
  const shouldStepUp = (_window4 = window) == null ? void 0 : _window4.localStorage.getItem(SHOULD_STEP_UP_KEY);
162
253
  const user = store.auth.user;
@@ -169,6 +260,20 @@ export default ((store, api, sharedActions) => {
169
260
  forceStepUpUrl: stepUpUrl
170
261
  });
171
262
  }
263
+ if (await shouldRouteToChooseTenant()) {
264
+ actions.setLoginState({
265
+ step: LoginStep.chooseTenant,
266
+ loading: false
267
+ });
268
+ // Mirror the other post-auth interstitials (e.g. promptPasskeys): navigate to the login URL
269
+ // so the ChooseTenant step actually renders even when afterAuthNavigation is invoked from an
270
+ // OAuth/social callback route.
271
+ store.auth.onRedirectTo(store.auth.routes.loginUrl, {
272
+ preserveQueryParams: true
273
+ });
274
+ return; // do NOT finalize; ChooseTenant confirm re-enters afterAuthNavigation
275
+ }
276
+
172
277
  let customLoginURL = customLoginAuthenticatedUrl;
173
278
  if (!customLoginAuthenticatedUrl) {
174
279
  var _store$auth$routes;
@@ -26,7 +26,7 @@ import { LoginFlow, LoginStep } from '../interfaces';
26
26
  import { base64urlDecode, deepResetState, delay, errorHandler, errorTraceId, GTMEventAction, publicKeyCredentialToJSON, refreshTokenWithRetry, reportGTMEvent, retryIfNeeded, withRetryConfig } from '../../../helpers';
27
27
  import { initialState } from '../state';
28
28
  import { getSearchParam, isEmailPayload, isUsernamePayload, shouldShowPasswordRotationPromptFunc, TENANT_ID_PARAM_KEY } from '../helpers';
29
- import { AuthStrategyEnum, ContextHolder, FeatureFlags, getTabTenantFromSessionStorage, removeTabTenantFromSessionStorage, WebAuthnDeviceType } from '@frontegg/rest-api';
29
+ import { AuthStrategyEnum, ContextHolder, FeatureFlags, getTabTenantFromSessionStorage, removeTabTenantFromSessionStorage, WebAuthnDeviceType, PreLoginAddressType } from '@frontegg/rest-api';
30
30
  import hostedLoginAuthorizeActions from './hostedLoginAuthorize.actions';
31
31
  import { clearHostedOAuthState } from '../oauthStorage';
32
32
  import { SHOULD_STEP_UP_KEY } from '../../StepUpState/consts';
@@ -410,9 +410,21 @@ export default ((store, api, sharedActions) => {
410
410
  }));
411
411
  const {
412
412
  address,
413
- idpType
413
+ idpType,
414
+ addressType
414
415
  } = preLoginResult != null ? preLoginResult : {};
415
416
  if (address) {
417
+ if (addressType === PreLoginAddressType.External) {
418
+ // Domain-based routing: navigate to the vendor legacy login URL immediately,
419
+ // skipping the SSO interstitial and the SSO/OIDC-specific handling.
420
+ setLoginState({
421
+ loading: false
422
+ });
423
+ onRedirectTo(address, {
424
+ refresh: true
425
+ });
426
+ return;
427
+ }
416
428
  let ssoRedirectUrl = address;
417
429
  if (idpType === SamlVendors.Oidc && !ssoRedirectUrl.includes('redirect_uri')) {
418
430
  const {
@@ -567,7 +579,10 @@ export default ((store, api, sharedActions) => {
567
579
  step: loginState.flow === LoginFlow.Login ? LoginStep.success : loginState.step,
568
580
  tenants,
569
581
  tenantsLoading: true,
570
- isBreachedPassword: user.isBreachedPassword
582
+ isBreachedPassword: user.isBreachedPassword,
583
+ // Preserve the choose-tenant guard across this post-login state rebuild so a
584
+ // resolved choice is not re-prompted (choose-organization).
585
+ tenantChoiceResolved: loginState.tenantChoiceResolved
571
586
  },
572
587
  isAuthenticated
573
588
  });
@@ -0,0 +1,20 @@
1
+ import { ITenantsResponseV2 } from '@frontegg/rest-api';
2
+ /**
3
+ * The set of tenants the user can actually switch into — the same set `switchActiveTenant`
4
+ * accepts server-side, so the chooser never lists a tenant that would be rejected at confirm time.
5
+ * Excludes: pending invites (invitationToken), locked/disabled tenants, and expired temporary access.
6
+ */
7
+ export declare function getSwitchableTenants(tenants?: ITenantsResponseV2[]): ITenantsResponseV2[];
8
+ /**
9
+ * The single gate for the post-auth "choose organization" dialog. The trigger is the
10
+ * `authOptions.tenantSelection.enabled` SDK option (`enabledByOption`) — the same signal for embedded
11
+ * and hosted (in hosted mode oauth-service is responsible for setting the option). The `>1` gate is
12
+ * over the SWITCHABLE set; undefined/empty tenants collapse to 0 and fall through (fail-safe).
13
+ */
14
+ export declare function shouldPromptChooseTenant(args: {
15
+ ldEnabled: boolean;
16
+ enabledByOption: boolean;
17
+ tenants?: ITenantsResponseV2[];
18
+ isImpersonation: boolean;
19
+ tenantChoiceResolved: boolean;
20
+ }): boolean;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * A switchable tenant membership carries fields returned by GET /me/tenants that are not
3
+ * part of the base ITenantsResponseV2 shape (pending invite / disabled / temporary access).
4
+ */
5
+
6
+ /**
7
+ * The set of tenants the user can actually switch into — the same set `switchActiveTenant`
8
+ * accepts server-side, so the chooser never lists a tenant that would be rejected at confirm time.
9
+ * Excludes: pending invites (invitationToken), locked/disabled tenants, and expired temporary access.
10
+ */
11
+ export function getSwitchableTenants(tenants) {
12
+ if (!tenants) {
13
+ return [];
14
+ }
15
+ const now = Date.now();
16
+ return tenants.filter(tenant => {
17
+ const t = tenant;
18
+ if (t.invitationToken) {
19
+ return false; // invited, not yet a member
20
+ }
21
+
22
+ if (t.isDisabled) {
23
+ return false; // locked/disabled tenant
24
+ }
25
+
26
+ if (t.temporaryExpirationDate && new Date(t.temporaryExpirationDate).getTime() < now) {
27
+ return false; // temporary access expired
28
+ }
29
+
30
+ return true;
31
+ });
32
+ }
33
+
34
+ /**
35
+ * The single gate for the post-auth "choose organization" dialog. The trigger is the
36
+ * `authOptions.tenantSelection.enabled` SDK option (`enabledByOption`) — the same signal for embedded
37
+ * and hosted (in hosted mode oauth-service is responsible for setting the option). The `>1` gate is
38
+ * over the SWITCHABLE set; undefined/empty tenants collapse to 0 and fall through (fail-safe).
39
+ */
40
+ export function shouldPromptChooseTenant(args) {
41
+ if (!args.ldEnabled) {
42
+ return false;
43
+ }
44
+ if (args.tenantChoiceResolved) {
45
+ return false;
46
+ }
47
+ if (args.isImpersonation) {
48
+ return false;
49
+ }
50
+ if (!args.enabledByOption) {
51
+ return false;
52
+ }
53
+ return getSwitchableTenants(args.tenants).length > 1;
54
+ }
@@ -19,7 +19,8 @@ export declare enum LoginStep {
19
19
  'breachedPasswordSuccess' = "breachedPasswordSuccess",
20
20
  'passwordRotationExpired' = "passwordRotationExpired",
21
21
  'passwordRotationNotification' = "passwordRotationNotification",
22
- 'magicLinkPostLoginSuccess' = "magicLinkPostLoginSuccess"
22
+ 'magicLinkPostLoginSuccess' = "magicLinkPostLoginSuccess",
23
+ 'chooseTenant' = "chooseTenant"
23
24
  }
24
25
  export declare enum LoginFlow {
25
26
  Login = "login",
@@ -56,6 +57,26 @@ export interface LoginState {
56
57
  passwordExpiresIn?: number;
57
58
  notificationPeriod?: number;
58
59
  userId?: string;
60
+ /**
61
+ * Guard for the post-auth "choose organization" gate (choose-organization feature).
62
+ * Set to true once the user has resolved the tenant choice (or the escape hatch was used),
63
+ * so re-entry into afterAuthNavigation finalizes without re-prompting.
64
+ */
65
+ tenantChoiceResolved: boolean;
66
+ /**
67
+ * True while the choose-organization step is performing switchTenant + showing its success
68
+ * interstitial. switchTenant refreshes the token, which re-enters afterAuthNavigation; this flag
69
+ * makes that re-entry a no-op so the chooser controls navigation itself (no flicker / lost success).
70
+ * Once set it latches on for the rest of the flow so a late/timed-out switch completion can't
71
+ * navigate after the user already finalized.
72
+ */
73
+ chooseTenantSwitching?: boolean;
74
+ /**
75
+ * One-shot flag the choose-organization confirm handler sets to let its single finalize navigation
76
+ * through the `chooseTenantSwitching` latch; afterAuthNavigation consumes it on the next call so
77
+ * subsequent (late switch) re-entries remain suppressed.
78
+ */
79
+ chooseTenantFinalizing?: boolean;
59
80
  }
60
81
  export interface HostedLoginCallback {
61
82
  code: string;
@@ -19,6 +19,7 @@ export let LoginStep;
19
19
  LoginStep["passwordRotationExpired"] = "passwordRotationExpired";
20
20
  LoginStep["passwordRotationNotification"] = "passwordRotationNotification";
21
21
  LoginStep["magicLinkPostLoginSuccess"] = "magicLinkPostLoginSuccess";
22
+ LoginStep["chooseTenant"] = "chooseTenant";
22
23
  })(LoginStep || (LoginStep = {}));
23
24
  export let LoginFlow;
24
25
  (function (LoginFlow) {
@@ -5,6 +5,9 @@ export const initialState = {
5
5
  step: LoginStep.preLogin,
6
6
  loading: false,
7
7
  email: '',
8
- tenants: []
8
+ tenants: [],
9
+ tenantChoiceResolved: false,
10
+ chooseTenantSwitching: false,
11
+ chooseTenantFinalizing: false
9
12
  };
10
13
  export default (overrideState => createProxy(initialState, overrideState));
@@ -13,6 +13,7 @@ export default ((store, api, sharedActions) => {
13
13
  const {
14
14
  tenantId,
15
15
  silentReload,
16
+ keepLoginState,
16
17
  callback
17
18
  } = payload;
18
19
  silentReload ? actions.setTenantsState({
@@ -24,7 +25,11 @@ export default ((store, api, sharedActions) => {
24
25
  await api.tenants.switchTenant({
25
26
  tenantId
26
27
  });
27
- if (silentReload) {
28
+
29
+ // keepLoginState skips the auth-state reset (which would clobber the in-flight loginState —
30
+ // step, choose-tenant guards). __refreshToken below re-establishes user/isAuthenticated, so the
31
+ // reset is only needed to clear stale state for the standard tab-switch use case.
32
+ if (silentReload && !keepLoginState) {
28
33
  actions.resetAuthState({
29
34
  isLoading: false,
30
35
  isAuthenticated: true,
@@ -10,4 +10,11 @@ export interface TenantsState {
10
10
  tenantTree: ISubTenantTree | null;
11
11
  activeTenant?: ITenantsResponseV2;
12
12
  }
13
- export type SwitchTenantOptions = WithCallback<ISwitchTenant>;
13
+ export type SwitchTenantOptions = WithCallback<ISwitchTenant & {
14
+ /**
15
+ * Client-only (not sent to the API). With `silentReload`, skip the auth-state reset so the
16
+ * in-flight login/loginState (step, choose-tenant guards) is preserved. Used by the login-box
17
+ * choose-organization step, which switches tenant mid-login and must keep rendering its own step.
18
+ */
19
+ keepLoginState?: boolean;
20
+ }>;
package/auth/index.d.ts CHANGED
@@ -77,6 +77,7 @@ export * from './UsersEmailsPolicyState/interfaces';
77
77
  export * from './interfaces';
78
78
  export { FORGOT_PASSWORD_ERROR_KEYS };
79
79
  export * from './LoginState/oauthStorage';
80
+ export * from './LoginState/helpers/tenantSelect';
80
81
  export declare const createAuthState: (_overrideState?: DeepPartial<AuthState>) => AuthState;
81
82
  export declare const buildAuthActions: (store: FronteggState, api: RestApi, actions: FronteggActions, snapshotAuthState: AuthState) => [AuthActions, AuthStateActions];
82
83
  export type AcceptInvitationActions = ReturnType<typeof buildAcceptInvitationActions>;
package/auth/index.js CHANGED
@@ -81,6 +81,7 @@ export * from './UsersEmailsPolicyState/interfaces';
81
81
  export * from './interfaces';
82
82
  export { FORGOT_PASSWORD_ERROR_KEYS };
83
83
  export * from './LoginState/oauthStorage';
84
+ export * from './LoginState/helpers/tenantSelect';
84
85
  export const createAuthState = _overrideState => {
85
86
  const _ref = _overrideState != null ? _overrideState : {},
86
87
  {
@@ -69,6 +69,13 @@ export interface AuthState extends Routes, PluginOptions {
69
69
  socialLoginOptions?: {
70
70
  promptConsent?: boolean;
71
71
  };
72
+ /**
73
+ * Post-login "choose your organization" step. Set via `authOptions.tenantSelection`; read by the
74
+ * afterAuthNavigation choose-tenant gate. Also gated by the `login-box-choose-tenant` feature flag.
75
+ */
76
+ tenantSelection?: {
77
+ enabled?: boolean;
78
+ };
72
79
  user?: User | null;
73
80
  isSSOAuth: boolean;
74
81
  ssoACS?: string;
@@ -44,6 +44,8 @@ export const publicKeyCredentialToJSON = pubKeyCred => {
44
44
  arr.push(publicKeyCredentialToJSON(i));
45
45
  }
46
46
  return arr;
47
+ } else if (typeof ArrayBuffer !== 'undefined' && ArrayBuffer.isView(pubKeyCred)) {
48
+ return base64urlEncode(pubKeyCred);
47
49
  } else if (pubKeyCred instanceof ArrayBuffer) {
48
50
  return base64urlEncode(pubKeyCred);
49
51
  } else if (pubKeyCred instanceof Object) {
package/index.js CHANGED
@@ -1,4 +1,4 @@
1
- /** @license Frontegg v7.118.0
1
+ /** @license Frontegg v7.120.0
2
2
  *
3
3
  * This source code is licensed under the MIT license found in the
4
4
  * LICENSE file in the root directory of this source tree.
@@ -7,7 +7,9 @@ Object.defineProperty(exports, "__esModule", {
7
7
  exports.default = void 0;
8
8
  var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
9
9
  var _helpers = require("../helpers");
10
+ var _tenantSelect = require("../helpers/tenantSelect");
10
11
  var _constants = require("../../../constants");
12
+ var _interfaces = require("../interfaces");
11
13
  var _consts = require("../../StepUpState/consts");
12
14
  var _helpers2 = require("../../../helpers");
13
15
  var _helpers3 = require("../../StepUpState/helpers");
@@ -153,17 +155,106 @@ var _default = (store, api, sharedActions) => {
153
155
  });
154
156
  };
155
157
 
158
+ /**
159
+ * Choose-tenant gate — the single chokepoint all auth methods (password/MFA/SSO/passwordless/
160
+ * passkey) and interstitials converge on. The trigger is the `authOptions.tenantSelection.enabled`
161
+ * SDK option, so it fires identically for embedded and hosted integrators (in hosted mode
162
+ * oauth-service is responsible for setting the option). When the option is on, the LD flag is on,
163
+ * and the user has >1 switchable tenant, route to the ChooseTenant step instead of finalizing.
164
+ * The ChooseTenant confirm handler sets `tenantChoiceResolved` and re-enters
165
+ * afterAuthNavigation, which then falls through here. Runs only post-auth; no tenant data is read
166
+ * until the resolved-guard, the LD flag, and the option all pass.
167
+ */
168
+ const shouldRouteToChooseTenant = async () => {
169
+ var _store$auth$tenantSel, _store$auth$user, _store$auth$user$act, _store$auth$tenantsSt;
170
+ const loginState = store.auth.loginState;
171
+ // Guard first so re-entry after the choice is resolved finalizes without re-reading the flag.
172
+ if (loginState.tenantChoiceResolved) {
173
+ return false;
174
+ }
175
+ // Cheap option guard before any flag/tenant work — never act when the integrator didn't opt in.
176
+ const enabledByOption = ((_store$auth$tenantSel = store.auth.tenantSelection) == null ? void 0 : _store$auth$tenantSel.enabled) === true;
177
+ if (!enabledByOption) {
178
+ return false;
179
+ }
180
+ const [ldEnabled] = await actions.getFeatureFlags(['login-box-choose-tenant']);
181
+ if (!ldEnabled) {
182
+ return false; // feature off — never read tenant data
183
+ }
184
+
185
+ const isImpersonation = !!((_store$auth$user = store.auth.user) != null && (_store$auth$user$act = _store$auth$user.act) != null && _store$auth$user$act.sub);
186
+ if (isImpersonation) {
187
+ return false;
188
+ }
189
+ // SSO/social postLogin authenticates without populating tenantsState — load it here so the
190
+ // chooser also opens on those paths (password/MFA already have tenants from the login response).
191
+ if (!((_store$auth$tenantsSt = store.auth.tenantsState.tenants) != null && _store$auth$tenantsSt.length)) {
192
+ var _store$auth$tenantsSt2;
193
+ let loadFailed = false;
194
+ await actions.loadTenants({
195
+ callback: (_tenants, error) => loadFailed = !!error
196
+ });
197
+ // Do NOT fail-open: if the tenant set could not be loaded while the feature is on, route to the
198
+ // chooser (which shows a load error + retry / continue-with-current) instead of finalizing on
199
+ // the default tenant and silently skipping the required org choice.
200
+ if (loadFailed && !((_store$auth$tenantsSt2 = store.auth.tenantsState.tenants) != null && _store$auth$tenantsSt2.length)) {
201
+ return true;
202
+ }
203
+ }
204
+ const {
205
+ tenants
206
+ } = store.auth.tenantsState;
207
+ return (0, _tenantSelect.shouldPromptChooseTenant)({
208
+ ldEnabled,
209
+ enabledByOption,
210
+ tenants,
211
+ isImpersonation,
212
+ tenantChoiceResolved: loginState.tenantChoiceResolved
213
+ });
214
+ };
215
+
156
216
  /**
157
217
  * After auth navigation for login flow
158
218
  * Handling also step up scenario when user silently logout to continue to step up
159
219
  */
220
+
160
221
  const afterAuthNavigation = async payload => {
161
222
  var _window4, _payload$preventRedir;
223
+ // Once the choose-tenant step starts a switch the chooser owns navigation. Every re-entry here
224
+ // (the switch's token refresh, and — critically — a late or timed-out switch completing AFTER the
225
+ // user hit the escape hatch) must be a no-op, so navigation can't run twice or jump into the wrong
226
+ // org. Two latches guard it: `chooseTenantSwitching` covers the in-login window, but it lives in
227
+ // loginState and is wiped by the finalize's resetLoginState — so a late switch would slip through.
228
+ // `tenantsState.switchingTenant` stays set for the WHOLE switchTenant call and survives that reset,
229
+ // so it suppresses the late re-entry too. The confirm handler performs the ONE finalize via the
230
+ // one-shot `chooseTenantFinalizing` flag, which passes through both latches and is consumed here.
231
+ // (An in-app tenant switch never reaches this function — its refresh only calls afterAuthNavigation
232
+ // from a login/callback URL, not from an authenticated app page — so this cannot over-suppress.)
233
+ const {
234
+ chooseTenantSwitching,
235
+ chooseTenantFinalizing
236
+ } = store.auth.loginState;
237
+ const {
238
+ switchingTenant
239
+ } = store.auth.tenantsState;
240
+ if ((chooseTenantSwitching || switchingTenant) && !chooseTenantFinalizing) {
241
+ return;
242
+ }
243
+ if (chooseTenantFinalizing) {
244
+ actions.setLoginState({
245
+ chooseTenantFinalizing: false
246
+ }); // consume: later re-entries stay suppressed
247
+ }
248
+
162
249
  const {
163
250
  customLoginAuthenticatedUrl,
164
251
  stepUpUrl
165
252
  } = store.auth.routes;
166
253
 
254
+ // Step-up on the CURRENT tenant resolves before the org chooser: a user who needs both
255
+ // authenticates + steps up first, then picks an org. switchTenant handles any MFA the chosen
256
+ // org requires on its own, and the isSteppedUp guard below means an already-stepped-up user is
257
+ // never re-prompted.
167
258
  // login with magic code, try to step up, no other mfa, invalid max age, force_enroll -> logout, login with first factor, not-stepped up jwt -> navigate to step up
168
259
  const shouldStepUp = (_window4 = window) == null ? void 0 : _window4.localStorage.getItem(_consts.SHOULD_STEP_UP_KEY);
169
260
  const user = store.auth.user;
@@ -176,6 +267,20 @@ var _default = (store, api, sharedActions) => {
176
267
  forceStepUpUrl: stepUpUrl
177
268
  });
178
269
  }
270
+ if (await shouldRouteToChooseTenant()) {
271
+ actions.setLoginState({
272
+ step: _interfaces.LoginStep.chooseTenant,
273
+ loading: false
274
+ });
275
+ // Mirror the other post-auth interstitials (e.g. promptPasskeys): navigate to the login URL
276
+ // so the ChooseTenant step actually renders even when afterAuthNavigation is invoked from an
277
+ // OAuth/social callback route.
278
+ store.auth.onRedirectTo(store.auth.routes.loginUrl, {
279
+ preserveQueryParams: true
280
+ });
281
+ return; // do NOT finalize; ChooseTenant confirm re-enters afterAuthNavigation
282
+ }
283
+
179
284
  let customLoginURL = customLoginAuthenticatedUrl;
180
285
  if (!customLoginAuthenticatedUrl) {
181
286
  var _store$auth$routes;
@@ -417,9 +417,21 @@ var _default = (store, api, sharedActions) => {
417
417
  }));
418
418
  const {
419
419
  address,
420
- idpType
420
+ idpType,
421
+ addressType
421
422
  } = preLoginResult != null ? preLoginResult : {};
422
423
  if (address) {
424
+ if (addressType === _restApi.PreLoginAddressType.External) {
425
+ // Domain-based routing: navigate to the vendor legacy login URL immediately,
426
+ // skipping the SSO interstitial and the SSO/OIDC-specific handling.
427
+ setLoginState({
428
+ loading: false
429
+ });
430
+ onRedirectTo(address, {
431
+ refresh: true
432
+ });
433
+ return;
434
+ }
423
435
  let ssoRedirectUrl = address;
424
436
  if (idpType === _interfaces4.SamlVendors.Oidc && !ssoRedirectUrl.includes('redirect_uri')) {
425
437
  const {
@@ -574,7 +586,10 @@ var _default = (store, api, sharedActions) => {
574
586
  step: loginState.flow === _interfaces.LoginFlow.Login ? _interfaces.LoginStep.success : loginState.step,
575
587
  tenants,
576
588
  tenantsLoading: true,
577
- isBreachedPassword: user.isBreachedPassword
589
+ isBreachedPassword: user.isBreachedPassword,
590
+ // Preserve the choose-tenant guard across this post-login state rebuild so a
591
+ // resolved choice is not re-prompted (choose-organization).
592
+ tenantChoiceResolved: loginState.tenantChoiceResolved
578
593
  },
579
594
  isAuthenticated
580
595
  });
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.getSwitchableTenants = getSwitchableTenants;
7
+ exports.shouldPromptChooseTenant = shouldPromptChooseTenant;
8
+ /**
9
+ * A switchable tenant membership carries fields returned by GET /me/tenants that are not
10
+ * part of the base ITenantsResponseV2 shape (pending invite / disabled / temporary access).
11
+ */
12
+
13
+ /**
14
+ * The set of tenants the user can actually switch into — the same set `switchActiveTenant`
15
+ * accepts server-side, so the chooser never lists a tenant that would be rejected at confirm time.
16
+ * Excludes: pending invites (invitationToken), locked/disabled tenants, and expired temporary access.
17
+ */
18
+ function getSwitchableTenants(tenants) {
19
+ if (!tenants) {
20
+ return [];
21
+ }
22
+ const now = Date.now();
23
+ return tenants.filter(tenant => {
24
+ const t = tenant;
25
+ if (t.invitationToken) {
26
+ return false; // invited, not yet a member
27
+ }
28
+
29
+ if (t.isDisabled) {
30
+ return false; // locked/disabled tenant
31
+ }
32
+
33
+ if (t.temporaryExpirationDate && new Date(t.temporaryExpirationDate).getTime() < now) {
34
+ return false; // temporary access expired
35
+ }
36
+
37
+ return true;
38
+ });
39
+ }
40
+
41
+ /**
42
+ * The single gate for the post-auth "choose organization" dialog. The trigger is the
43
+ * `authOptions.tenantSelection.enabled` SDK option (`enabledByOption`) — the same signal for embedded
44
+ * and hosted (in hosted mode oauth-service is responsible for setting the option). The `>1` gate is
45
+ * over the SWITCHABLE set; undefined/empty tenants collapse to 0 and fall through (fail-safe).
46
+ */
47
+ function shouldPromptChooseTenant(args) {
48
+ if (!args.ldEnabled) {
49
+ return false;
50
+ }
51
+ if (args.tenantChoiceResolved) {
52
+ return false;
53
+ }
54
+ if (args.isImpersonation) {
55
+ return false;
56
+ }
57
+ if (!args.enabledByOption) {
58
+ return false;
59
+ }
60
+ return getSwitchableTenants(args.tenants).length > 1;
61
+ }
@@ -32,6 +32,7 @@ exports.LoginStep = LoginStep;
32
32
  LoginStep["passwordRotationExpired"] = "passwordRotationExpired";
33
33
  LoginStep["passwordRotationNotification"] = "passwordRotationNotification";
34
34
  LoginStep["magicLinkPostLoginSuccess"] = "magicLinkPostLoginSuccess";
35
+ LoginStep["chooseTenant"] = "chooseTenant";
35
36
  })(LoginStep || (exports.LoginStep = LoginStep = {}));
36
37
  let LoginFlow;
37
38
  exports.LoginFlow = LoginFlow;
@@ -11,7 +11,10 @@ const initialState = {
11
11
  step: _interfaces.LoginStep.preLogin,
12
12
  loading: false,
13
13
  email: '',
14
- tenants: []
14
+ tenants: [],
15
+ tenantChoiceResolved: false,
16
+ chooseTenantSwitching: false,
17
+ chooseTenantFinalizing: false
15
18
  };
16
19
  exports.initialState = initialState;
17
20
  var _default = overrideState => (0, _proxy.createProxy)(initialState, overrideState);
@@ -19,6 +19,7 @@ var _default = (store, api, sharedActions) => {
19
19
  const {
20
20
  tenantId,
21
21
  silentReload,
22
+ keepLoginState,
22
23
  callback
23
24
  } = payload;
24
25
  silentReload ? actions.setTenantsState({
@@ -30,7 +31,11 @@ var _default = (store, api, sharedActions) => {
30
31
  await api.tenants.switchTenant({
31
32
  tenantId
32
33
  });
33
- if (silentReload) {
34
+
35
+ // keepLoginState skips the auth-state reset (which would clobber the in-flight loginState —
36
+ // step, choose-tenant guards). __refreshToken below re-establishes user/isAuthenticated, so the
37
+ // reset is only needed to clear stale state for the standard tab-switch use case.
38
+ if (silentReload && !keepLoginState) {
34
39
  actions.resetAuthState({
35
40
  isLoading: false,
36
41
  isAuthenticated: true,
@@ -525,6 +525,18 @@ Object.keys(_oauthStorage).forEach(function (key) {
525
525
  }
526
526
  });
527
527
  });
528
+ var _tenantSelect = require("./LoginState/helpers/tenantSelect");
529
+ Object.keys(_tenantSelect).forEach(function (key) {
530
+ if (key === "default" || key === "__esModule") return;
531
+ if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return;
532
+ if (key in exports && exports[key] === _tenantSelect[key]) return;
533
+ Object.defineProperty(exports, key, {
534
+ enumerable: true,
535
+ get: function () {
536
+ return _tenantSelect[key];
537
+ }
538
+ });
539
+ });
528
540
  const _excluded = ["routes"],
529
541
  _excluded2 = ["requestName"];
530
542
  const createAuthState = _overrideState => {
@@ -51,6 +51,8 @@ const publicKeyCredentialToJSON = pubKeyCred => {
51
51
  arr.push(publicKeyCredentialToJSON(i));
52
52
  }
53
53
  return arr;
54
+ } else if (typeof ArrayBuffer !== 'undefined' && ArrayBuffer.isView(pubKeyCred)) {
55
+ return (0, _encoders.base64urlEncode)(pubKeyCred);
54
56
  } else if (pubKeyCred instanceof ArrayBuffer) {
55
57
  return (0, _encoders.base64urlEncode)(pubKeyCred);
56
58
  } else if (pubKeyCred instanceof Object) {
package/node/index.js CHANGED
@@ -1,4 +1,4 @@
1
- /** @license Frontegg v7.118.0
1
+ /** @license Frontegg v7.120.0
2
2
  *
3
3
  * This source code is licensed under the MIT license found in the
4
4
  * LICENSE file in the root directory of this source tree.
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@frontegg/redux-store",
3
- "version": "7.118.0",
3
+ "version": "7.120.0",
4
4
  "main": "./node/index.js",
5
5
  "license": "MIT",
6
6
  "author": "Frontegg LTD",
7
7
  "dependencies": {
8
8
  "@babel/runtime": "^7.18.6",
9
9
  "@frontegg/entitlements-javascript-commons": "1.1.2",
10
- "@frontegg/rest-api": "7.118.0",
10
+ "@frontegg/rest-api": "7.120.0",
11
11
  "fast-deep-equal": "3.1.3",
12
12
  "get-value": "^3.0.1",
13
13
  "proxy-compare": "^3.0.0",