@frontegg/redux-store 7.131.0-alpha.2 → 7.131.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.
@@ -15,7 +15,7 @@ export default ((store, api, sharedActions) => {
15
15
  const __requestHostedLoginSilentAuthorize = async () => {
16
16
  var _response;
17
17
  let response;
18
- restoreTabTenantUnlessSsoCallback(store);
18
+ await restoreTabTenantUnlessSsoCallback(store);
19
19
  if (FronteggNativeModule.isGetTokensAvailable()) {
20
20
  const authApi = api.auth;
21
21
  response = await refreshTokenWithRetry(async () => {
@@ -27,7 +27,7 @@ export default ((store, api, sharedActions) => {
27
27
  response = await api.auth.silentOAuthRefreshTokenV2();
28
28
  } catch (error) {
29
29
  var _store$auth$user, _error$errorContext;
30
- // on the hosted login callback the page finishes the login with its code: fail as with the flag off
30
+ // on the hosted login callback the page finishes the login with its code: fail as without SSO enforcement
31
31
  if (isOnLoginCodeCallbackRoute(store)) {
32
32
  throw error;
33
33
  }
@@ -36,7 +36,7 @@ export default ((store, api, sharedActions) => {
36
36
  return;
37
37
  }
38
38
  const tenantId = (_store$auth$user = store.auth.user) == null ? void 0 : _store$auth$user.tenantId;
39
- if (!isEnforceSsoOnTenantAccessEnabled(store.root.appName) || !isSsoEnforcementError(error) || ((_error$errorContext = error.errorContext) == null ? void 0 : _error$errorContext.ssoRequired) !== false || !tenantId) {
39
+ if (!isSsoEnforcementError(error) || ((_error$errorContext = error.errorContext) == null ? void 0 : _error$errorContext.ssoRequired) !== false || !tenantId || !(await isEnforceSsoOnTenantAccessEnabled(store.root.appName))) {
40
40
  throw error;
41
41
  }
42
42
  // one retry for the tenant of the current token (a cold reload has no user yet: no retry)
@@ -33,20 +33,11 @@ declare const _default: (store: FronteggState, api: RestApi, sharedActions: Shar
33
33
  requestHostedLoginAuthorize: (additionalParams?: Record<string, string>) => Promise<void>;
34
34
  requestHostedLoginAuthorizeV2: (payload?: {
35
35
  additionalParams?: Record<string, string>;
36
- shouldRedirectToLogin
37
- /**
38
- * Handles a hosted (OAuth) token refresh refused because the tenant requires SSO.
39
- * ssoRequired: true sends the user to SSO, like login. ssoRequired: false renews once more for the tenant of the
40
- * current token, which keeps the session: OAuth doesn't use up the refresh token on this refusal.
41
- * @returns true when handled, false when the caller should log out
42
- * @private
43
- */
44
- ? /**
45
- * Handles a hosted (OAuth) token refresh refused because the tenant requires SSO.
46
- * ssoRequired: true sends the user to SSO, like login. ssoRequired: false renews once more for the tenant of the
47
- * current token, which keeps the session: OAuth doesn't use up the refresh token on this refusal.
48
- * @returns true when handled, false when the caller should log out
49
- * @private
36
+ shouldRedirectToLogin? /**
37
+ * Sends the user to the tenant's SSO when a login or tenant switch is refused because that tenant requires it
38
+ * (e.g. an invitation into a tenant whose SSO covers the user's email). The SSO login lands in that tenant.
39
+ * @returns true when redirected, so the caller doesn't show the error
40
+ * @protected
50
41
  */: boolean;
51
42
  firstTime?: boolean;
52
43
  loginDirectAction?: import("../interfaces").LoginDirectAction;
@@ -91,10 +82,6 @@ declare const _default: (store: FronteggState, api: RestApi, sharedActions: Shar
91
82
  resetBreachedPassword: (payload: IForgotPassword) => Promise<void>;
92
83
  refreshTokenForSocialLogins: () => Promise<void>;
93
84
  __refreshToken: () => Promise<void>;
94
- __redirectToSsoForTenant: ({ tenantId, address }: {
95
- tenantId: string;
96
- address: string;
97
- }) => Promise<void>;
98
85
  __redirectToSsoIfRequired: (e: unknown) => Promise<boolean>;
99
86
  };
100
87
  export default _default;
@@ -25,8 +25,8 @@ import mfaWithWebAuthnActions from './mfaWithWebAuthn.actions';
25
25
  import { LoginFlow, LoginStep } from '../interfaces';
26
26
  import { base64urlDecode, deepResetState, delay, errorHandler, errorTraceId, GTMEventAction, isSsoEnforcementError, publicKeyCredentialToJSON, refreshTokenWithRetry, reportGTMEvent, retryIfNeeded, withRetryConfig } from '../../../helpers';
27
27
  import { initialState } from '../state';
28
- import { getSearchParam, isEmailPayload, isUsernamePayload, shouldShowPasswordRotationPromptFunc, TENANT_ID_PARAM_KEY } from '../helpers';
29
- import { AuthStrategyEnum, ContextHolder, isEnforceSsoOnTenantAccessEnabled, FeatureFlags, getTabTenantFromSessionStorage, removeTabTenantFromSessionStorage, setTabTenantInSessionStorage, WebAuthnDeviceType, PreLoginAddressType } from '@frontegg/rest-api';
28
+ import { getSearchParam, isEmailPayload, isUsernamePayload, shouldShowPasswordRotationPromptFunc, TENANT_ID_PARAM_KEY, withOidcRedirectUri } from '../helpers';
29
+ import { AuthStrategyEnum, ContextHolder, ENFORCE_SSO_ON_TENANT_ACCESS_FF, isEnforceSsoOnTenantAccessEnabled, FeatureFlags, clearTabTenantBeforeSsoSwitch, getTabTenantFromSessionStorage, removeTabTenantFromSessionStorage, setTabTenantInSessionStorage, 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';
@@ -48,8 +48,10 @@ export default ((store, api, sharedActions) => {
48
48
  * @protected
49
49
  */
50
50
  // as preLogin (takeLeading): an SSO redirect started while another is in flight is dropped; the first one leaves the
51
- // page, and a second hosted one would replace the PKCE verifier the first one's callback needs
51
+ // page, and a second hosted one would replace the PKCE verifier the first one's callback needs. Kept set after a
52
+ // redirect: the page is still there until the navigation commits.
52
53
  let ssoRedirectInFlight = false;
54
+ let ssoPageShowListenerRegistered = false;
53
55
  const __redirectToSsoForTenant = async ({
54
56
  tenantId,
55
57
  address
@@ -58,13 +60,25 @@ export default ((store, api, sharedActions) => {
58
60
  return;
59
61
  }
60
62
  ssoRedirectInFlight = true;
63
+ // Only an enabled SSO redirect needs bfcache recovery. Do not attach listeners to ordinary or preview stores.
64
+ if (!ssoPageShowListenerRegistered && typeof window !== 'undefined' && !store.root.previewMode) {
65
+ window.addEventListener('pageshow', event => {
66
+ const [enforceSsoOnTenantAccess] = FeatureFlags.getFeatureFlags([ENFORCE_SSO_ON_TENANT_ACCESS_FF], store.root.appName);
67
+ if (event.persisted && enforceSsoOnTenantAccess) {
68
+ ssoRedirectInFlight = false;
69
+ void restoreTabTenantUnlessSsoCallback(store);
70
+ }
71
+ });
72
+ ssoPageShowListenerRegistered = true;
73
+ }
61
74
  try {
62
75
  await __redirectToSsoForTenantNow({
63
76
  tenantId,
64
77
  address
65
78
  });
66
- } finally {
79
+ } catch (e) {
67
80
  ssoRedirectInFlight = false;
81
+ throw e;
68
82
  }
69
83
  };
70
84
  const __redirectToSsoForTenantNow = async ({
@@ -98,12 +112,9 @@ export default ((store, api, sharedActions) => {
98
112
  });
99
113
  return;
100
114
  }
101
- // as login does for an OIDC SSO page without redirect_uri; the refusal has no idpType, but an OIDC page asks for a code
102
- let ssoRedirectUrl = address;
103
- if (/[?&]response_type=code(&|$)/.test(address) && !address.includes('redirect_uri')) {
104
- ssoRedirectUrl += `&redirect_uri=${window.location.origin}${store.auth.routes.oidcRedirectUrl}`;
105
- }
106
- redirectToFullPage(ssoRedirectUrl);
115
+ // as login does for an OIDC SSO page; the refusal has no idpType, but an OIDC page asks for a code
116
+ const isOidcPage = /[?&]response_type=code(&|$)/.test(address);
117
+ redirectToFullPage(isOidcPage ? withOidcRedirectUri(address, store.auth.routes.oidcRedirectUrl) : address);
107
118
  };
108
119
 
109
120
  /**
@@ -114,7 +125,8 @@ export default ((store, api, sharedActions) => {
114
125
  */
115
126
  const __redirectToSsoIfRequired = async e => {
116
127
  var _e$errorContext;
117
- if (!isEnforceSsoOnTenantAccessEnabled(store.root.appName) || !isSsoEnforcementError(e) || !((_e$errorContext = e.errorContext) != null && _e$errorContext.ssoRequired)) {
128
+ // Ordinary failures keep their existing handling even while the flags request is pending.
129
+ if (!isSsoEnforcementError(e) || !((_e$errorContext = e.errorContext) != null && _e$errorContext.ssoRequired) || !(await isEnforceSsoOnTenantAccessEnabled(store.root.appName))) {
118
130
  return false;
119
131
  }
120
132
  await __redirectToSsoForTenant(e.errorContext);
@@ -128,14 +140,19 @@ export default ((store, api, sharedActions) => {
128
140
  * @private
129
141
  */
130
142
  const __handleSsoEnforcementRefreshError = async (e, isSsoEnforcementRetry, refreshForTenant) => {
131
- var _store$auth$user;
132
- // on a login callback the page finishes the login with its code: log out as with the flag off, no new SSO trip
133
- if (!isEnforceSsoOnTenantAccessEnabled(store.root.appName) || !isSsoEnforcementError(e) || isOnLoginCodeCallbackRoute(store)) {
143
+ var _e$errorContext2, _store$auth$user;
144
+ // on a login callback the page finishes the login with its code: log out as without SSO enforcement, no new SSO
145
+ // trip
146
+ if (!isSsoEnforcementError(e) || isOnLoginCodeCallbackRoute(store) || !(await isEnforceSsoOnTenantAccessEnabled(store.root.appName))) {
134
147
  return false;
135
148
  }
136
149
  if (await __redirectToSsoIfRequired(e)) {
137
150
  return true;
138
151
  }
152
+ // Only an explicit refusal without SSO permits recovery; missing context follows ordinary logout handling.
153
+ if (((_e$errorContext2 = e.errorContext) == null ? void 0 : _e$errorContext2.ssoRequired) !== false) {
154
+ return false;
155
+ }
139
156
  const currentTenantId = (_store$auth$user = store.auth.user) == null ? void 0 : _store$auth$user.tenantId;
140
157
  if (isSsoEnforcementRetry || !currentTenantId) {
141
158
  return false;
@@ -246,9 +263,10 @@ export default ((store, api, sharedActions) => {
246
263
  });
247
264
  }
248
265
  } catch (e) {
249
- // a refused identity refresh has already used up the refresh token: no retry, only the SSO redirect, except on
250
- // a login callback, where the page itself completes the login
251
- if (isEnforceSsoOnTenantAccessEnabled(store.root.appName) && !isOnLoginCodeCallbackRoute(store) && (await __redirectToSsoIfRequired(e))) {
266
+ // no retry: identity already refreshes for the session's own tenant when it can, so a refusal without SSO means
267
+ // there is none to keep. Only the SSO redirect, except on a login callback, where the page itself completes the
268
+ // login
269
+ if (!isOnLoginCodeCallbackRoute(store) && (await __redirectToSsoIfRequired(e))) {
252
270
  return;
253
271
  }
254
272
  contextHolder.setAccessToken(null);
@@ -261,9 +279,7 @@ export default ((store, api, sharedActions) => {
261
279
  };
262
280
 
263
281
  /** @private */
264
- const __refreshTokenWithNativeTokens = async ({
265
- isSsoEnforcementRetry = false
266
- } = {}) => {
282
+ const __refreshTokenWithNativeTokens = async () => {
267
283
  try {
268
284
  const authApi = api.auth;
269
285
  const response = await refreshTokenWithRetry(async () => {
@@ -277,13 +293,6 @@ export default ((store, api, sharedActions) => {
277
293
  isAuthenticated: true
278
294
  });
279
295
  } catch (e) {
280
- // native tokens can't name a tenant: the retry relies on the tab tenant only
281
- const retry = () => __refreshTokenWithNativeTokens({
282
- isSsoEnforcementRetry: true
283
- });
284
- if (await __handleSsoEnforcementRefreshError(e, isSsoEnforcementRetry, retry)) {
285
- return;
286
- }
287
296
  contextHolder.setAccessToken(null);
288
297
  contextHolder.setUser(null);
289
298
  actions.setAuthState({
@@ -295,7 +304,7 @@ export default ((store, api, sharedActions) => {
295
304
 
296
305
  /** @protected */
297
306
  const __refreshToken = async () => {
298
- restoreTabTenantUnlessSsoCallback(store);
307
+ await restoreTabTenantUnlessSsoCallback(store);
299
308
  if (FronteggNativeModule.isGetTokensAvailable()) {
300
309
  await __refreshTokenWithNativeTokens();
301
310
  return;
@@ -583,13 +592,7 @@ export default ((store, api, sharedActions) => {
583
592
  redirectToFullPage(address);
584
593
  return;
585
594
  }
586
- let ssoRedirectUrl = address;
587
- if (idpType === SamlVendors.Oidc && !ssoRedirectUrl.includes('redirect_uri')) {
588
- const {
589
- oidcRedirectUrl
590
- } = store.auth.routes;
591
- ssoRedirectUrl += `&redirect_uri=${window.location.origin}${oidcRedirectUrl}`;
592
- }
595
+ const ssoRedirectUrl = idpType === SamlVendors.Oidc ? withOidcRedirectUri(address, store.auth.routes.oidcRedirectUrl) : address;
593
596
  if (FronteggNativeModule.isLoginWithSSOAvailable()) {
594
597
  var _ref4;
595
598
  FronteggNativeModule.loginWithSSO((_ref4 = email != null ? email : username) != null ? _ref4 : '');
@@ -808,11 +811,13 @@ export default ((store, api, sharedActions) => {
808
811
  });
809
812
  }
810
813
  };
811
- const clearLogoutLocalState = ({
814
+ const clearLogoutLocalState = async ({
812
815
  keepHostedOAuthState = false
813
816
  } = {}) => {
814
817
  if (contextHolder.isSessionPerTenantEnabled()) {
815
818
  removeTabTenantFromSessionStorage();
819
+ // a pending SSO switch would put a tenant of the logged-out session back on the next page load
820
+ await clearTabTenantBeforeSsoSwitch(store.root.appName);
816
821
  }
817
822
  // Step-up re-login goes through silentLogout but still needs the hosted OAuth state to finish the client flow.
818
823
  if (!keepHostedOAuthState) {
@@ -833,7 +838,7 @@ export default ((store, api, sharedActions) => {
833
838
  } catch {
834
839
  /* empty */
835
840
  }
836
- clearLogoutLocalState();
841
+ await clearLogoutLocalState();
837
842
  actions.resetAuthState();
838
843
  await actions.requestAuthorize(true);
839
844
  payload == null ? void 0 : payload();
@@ -846,7 +851,7 @@ export default ((store, api, sharedActions) => {
846
851
  /* empty */
847
852
  }
848
853
  const isStepUpInProgress = !!((_window2 = window) != null && _window2.localStorage.getItem(SHOULD_STEP_UP_KEY));
849
- clearLogoutLocalState({
854
+ await clearLogoutLocalState({
850
855
  keepHostedOAuthState: isStepUpInProgress
851
856
  });
852
857
  setTimeout(() => {
@@ -1694,7 +1699,6 @@ export default ((store, api, sharedActions) => {
1694
1699
  refreshTokenForSocialLogins,
1695
1700
  // protected
1696
1701
  __refreshToken,
1697
- __redirectToSsoForTenant,
1698
1702
  __redirectToSsoIfRequired
1699
1703
  }, hostedLoginAuthorizeActions(store, api, sharedActions), afterAuthNavigationActions(store, api, sharedActions), handleVerifyMFAResponseActions(store, api, sharedActions), mfaRequiredStateActions(store, api, sharedActions), mfaWithAuthenticatorActions(store, api, sharedActions), mfaWithEmailCodeActions(store, api, sharedActions), mfaWithSMSActions(store, api, sharedActions), mfaWithWebAuthnActions(store, api, sharedActions));
1700
1704
  });
@@ -18,6 +18,8 @@ export declare const getPathAndSearchParamsFromUrl: (url: string, preventRedirec
18
18
  */
19
19
  export declare const getSearchParam: (key: string) => string | undefined;
20
20
  export declare const TENANT_ID_PARAM_KEY = "tenantId";
21
+ /** An OIDC SSO page without redirect_uri comes back to the OIDC callback, which completes the login with its code. */
22
+ export declare const withOidcRedirectUri: (address: string, oidcRedirectUrl?: string) => string;
21
23
  export declare const getNumberOfMfaDevices: (mfaDevices: UserMFADevicesResponse) => {
22
24
  numberOfWebAuthnDevices: number;
23
25
  numberOfPhoneDevices: number;
@@ -75,6 +75,9 @@ export const getSearchParam = key => {
75
75
  return param != null ? param : undefined;
76
76
  };
77
77
  export const TENANT_ID_PARAM_KEY = 'tenantId';
78
+
79
+ /** An OIDC SSO page without redirect_uri comes back to the OIDC callback, which completes the login with its code. */
80
+ export const withOidcRedirectUri = (address, oidcRedirectUrl) => address.includes('redirect_uri') ? address : `${address}&redirect_uri=${window.location.origin}${oidcRedirectUrl}`;
78
81
  export const getNumberOfMfaDevices = mfaDevices => {
79
82
  // Count platform / cross-platform separately — matches the MFA picker options.
80
83
  const numberOfWebAuthnDevices = Number(mfaDevices.webauthn.some(device => device.deviceType === WebAuthnDeviceType.Platform)) + Number(mfaDevices.webauthn.some(device => device.deviceType === WebAuthnDeviceType.CrossPlatform));
@@ -1,6 +1,6 @@
1
1
  import { initialState } from './state';
2
2
  import { deepResetState } from '../../helpers';
3
- import { ContextHolder, getCurrentUserTenantsFunction, setTabTenantForSsoSwitch } from '@frontegg/rest-api';
3
+ import { ContextHolder, getCurrentUserTenantsFunction, getTabTenantFromSessionStorage, setTabTenantForSsoSwitch } from '@frontegg/rest-api';
4
4
  export default ((store, api, sharedActions) => {
5
5
  const actions = sharedActions;
6
6
  const setTenantsState = state => {
@@ -46,6 +46,9 @@ export default ((store, api, sharedActions) => {
46
46
  });
47
47
  }
48
48
  } catch (e) {
49
+ // the tab tenant the switch was refused from, read before any await: a parallel switch that passes moves the tab
50
+ // while this one goes to SSO
51
+ const tabTenantId = getTabTenantFromSessionStorage(store.root.appName);
49
52
  silentReload ? actions.setTenantsState({
50
53
  switchingTenant: false
51
54
  }) : actions.setAuthState({
@@ -54,7 +57,7 @@ export default ((store, api, sharedActions) => {
54
57
  if (await actions.__redirectToSsoIfRequired(e)) {
55
58
  // the tab comes back from SSO in the target tenant, not the one the failed switch restored
56
59
  if (ContextHolder.for(store.root.appName).isSessionPerTenantEnabled()) {
57
- setTabTenantForSsoSwitch(tenantId);
60
+ setTabTenantForSsoSwitch(tenantId, tabTenantId);
58
61
  }
59
62
  return;
60
63
  }
package/auth/helpers.d.ts CHANGED
@@ -18,7 +18,7 @@ export declare const isOnLoginCodeCallbackRoute: (store: FronteggState) => boole
18
18
  * After a switch refused for SSO the tab points at the SSO tenant. Its SSO login comes back on a callback route and
19
19
  * keeps it; any other page load means the user didn't finish that login, so the tab goes back to its tenant.
20
20
  */
21
- export declare const restoreTabTenantUnlessSsoCallback: (store: FronteggState) => void;
21
+ export declare const restoreTabTenantUnlessSsoCallback: (store: FronteggState) => Promise<void>;
22
22
  export declare const prepareIdentifiersForSignup: (payload: ISignUpUser) => ISignUpUser;
23
23
  /**
24
24
  * Whether the current route is an auth page (login, tenant chooser, social / magic link / SSO callback), not one to come
package/auth/helpers.js CHANGED
@@ -85,10 +85,8 @@ export const isOnLoginCodeCallbackRoute = store => {
85
85
  * After a switch refused for SSO the tab points at the SSO tenant. Its SSO login comes back on a callback route and
86
86
  * keeps it; any other page load means the user didn't finish that login, so the tab goes back to its tenant.
87
87
  */
88
- export const restoreTabTenantUnlessSsoCallback = store => {
88
+ export const restoreTabTenantUnlessSsoCallback = async store => {
89
89
  var _store$root$urlStrate2;
90
- // Flags load in parallel with the first refresh. Recover an existing marker
91
- // before choosing that refresh's tenant, even if flags are not available yet.
92
90
  if (!ContextHolder.for(store.root.appName).isSessionPerTenantEnabled()) {
93
91
  return;
94
92
  }
@@ -99,9 +97,13 @@ export const restoreTabTenantUnlessSsoCallback = store => {
99
97
  } = _extends({}, defaultFronteggRoutes, store.auth.routes);
100
98
  // matched like login matches its callback pages: endsWith, so a basename before the route still counts
101
99
  const currentRoutes = getCurrentRoutes((_store$root$urlStrate2 = store.root.urlStrategy) != null ? _store$root$urlStrate2 : 'path');
102
- const isSsoCallback = [samlCallbackUrl, oidcRedirectUrl, hostedLoginRedirectUrl].some(route => !!route && currentRoutes.some(currentRoute => currentRoute.endsWith(route)));
100
+ // a refused login comes back on its callback too, not finished: SAML with samlerrors (as SamlLoginSuccess reads them),
101
+ // OIDC and hosted without a code (e.g. the IdP's error=access_denied), which their pages need to finish the login
102
+ const params = new URLSearchParams(window.location.search);
103
+ const callbackRoutes = [...(params.has('samlerrors') ? [] : [samlCallbackUrl]), ...(params.has('code') ? [oidcRedirectUrl, hostedLoginRedirectUrl] : [])];
104
+ const isSsoCallback = callbackRoutes.some(route => !!route && currentRoutes.some(currentRoute => currentRoute.endsWith(route)));
103
105
  if (!isSsoCallback) {
104
- restoreTabTenantBeforeSsoSwitch();
106
+ await restoreTabTenantBeforeSsoSwitch(store.root.appName);
105
107
  }
106
108
  };
107
109
  export const prepareIdentifiersForSignup = payload => {
package/index.js CHANGED
@@ -1,4 +1,4 @@
1
- /** @license Frontegg v7.131.0-alpha.2
1
+ /** @license Frontegg v7.131.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.
@@ -73,10 +73,6 @@ declare const _default: (store: FronteggState, api: RestApi, sharedActions: Shar
73
73
  resetBreachedPassword: (payload: import("dist/@frontegg/rest-api").IForgotPassword) => Promise<void>;
74
74
  refreshTokenForSocialLogins: () => Promise<void>;
75
75
  __refreshToken: () => Promise<void>;
76
- __redirectToSsoForTenant: ({ tenantId, address }: {
77
- tenantId: string;
78
- address: string;
79
- }) => Promise<void>;
80
76
  __redirectToSsoIfRequired: (e: unknown) => Promise<boolean>;
81
77
  };
82
78
  export default _default;
@@ -22,7 +22,7 @@ var _default = (store, api, sharedActions) => {
22
22
  const __requestHostedLoginSilentAuthorize = async () => {
23
23
  var _response;
24
24
  let response;
25
- (0, _helpers3.restoreTabTenantUnlessSsoCallback)(store);
25
+ await (0, _helpers3.restoreTabTenantUnlessSsoCallback)(store);
26
26
  if (_toolkit.FronteggNativeModule.isGetTokensAvailable()) {
27
27
  const authApi = api.auth;
28
28
  response = await (0, _helpers.refreshTokenWithRetry)(async () => {
@@ -34,7 +34,7 @@ var _default = (store, api, sharedActions) => {
34
34
  response = await api.auth.silentOAuthRefreshTokenV2();
35
35
  } catch (error) {
36
36
  var _store$auth$user, _error$errorContext;
37
- // on the hosted login callback the page finishes the login with its code: fail as with the flag off
37
+ // on the hosted login callback the page finishes the login with its code: fail as without SSO enforcement
38
38
  if ((0, _helpers3.isOnLoginCodeCallbackRoute)(store)) {
39
39
  throw error;
40
40
  }
@@ -43,7 +43,7 @@ var _default = (store, api, sharedActions) => {
43
43
  return;
44
44
  }
45
45
  const tenantId = (_store$auth$user = store.auth.user) == null ? void 0 : _store$auth$user.tenantId;
46
- if (!(0, _restApi.isEnforceSsoOnTenantAccessEnabled)(store.root.appName) || !(0, _helpers.isSsoEnforcementError)(error) || ((_error$errorContext = error.errorContext) == null ? void 0 : _error$errorContext.ssoRequired) !== false || !tenantId) {
46
+ if (!(0, _helpers.isSsoEnforcementError)(error) || ((_error$errorContext = error.errorContext) == null ? void 0 : _error$errorContext.ssoRequired) !== false || !tenantId || !(await (0, _restApi.isEnforceSsoOnTenantAccessEnabled)(store.root.appName))) {
47
47
  throw error;
48
48
  }
49
49
  // one retry for the tenant of the current token (a cold reload has no user yet: no retry)
@@ -55,8 +55,10 @@ var _default = (store, api, sharedActions) => {
55
55
  * @protected
56
56
  */
57
57
  // as preLogin (takeLeading): an SSO redirect started while another is in flight is dropped; the first one leaves the
58
- // page, and a second hosted one would replace the PKCE verifier the first one's callback needs
58
+ // page, and a second hosted one would replace the PKCE verifier the first one's callback needs. Kept set after a
59
+ // redirect: the page is still there until the navigation commits.
59
60
  let ssoRedirectInFlight = false;
61
+ let ssoPageShowListenerRegistered = false;
60
62
  const __redirectToSsoForTenant = async ({
61
63
  tenantId,
62
64
  address
@@ -65,13 +67,25 @@ var _default = (store, api, sharedActions) => {
65
67
  return;
66
68
  }
67
69
  ssoRedirectInFlight = true;
70
+ // Only an enabled SSO redirect needs bfcache recovery. Do not attach listeners to ordinary or preview stores.
71
+ if (!ssoPageShowListenerRegistered && typeof window !== 'undefined' && !store.root.previewMode) {
72
+ window.addEventListener('pageshow', event => {
73
+ const [enforceSsoOnTenantAccess] = _restApi.FeatureFlags.getFeatureFlags([_restApi.ENFORCE_SSO_ON_TENANT_ACCESS_FF], store.root.appName);
74
+ if (event.persisted && enforceSsoOnTenantAccess) {
75
+ ssoRedirectInFlight = false;
76
+ void (0, _helpers3.restoreTabTenantUnlessSsoCallback)(store);
77
+ }
78
+ });
79
+ ssoPageShowListenerRegistered = true;
80
+ }
68
81
  try {
69
82
  await __redirectToSsoForTenantNow({
70
83
  tenantId,
71
84
  address
72
85
  });
73
- } finally {
86
+ } catch (e) {
74
87
  ssoRedirectInFlight = false;
88
+ throw e;
75
89
  }
76
90
  };
77
91
  const __redirectToSsoForTenantNow = async ({
@@ -105,12 +119,9 @@ var _default = (store, api, sharedActions) => {
105
119
  });
106
120
  return;
107
121
  }
108
- // as login does for an OIDC SSO page without redirect_uri; the refusal has no idpType, but an OIDC page asks for a code
109
- let ssoRedirectUrl = address;
110
- if (/[?&]response_type=code(&|$)/.test(address) && !address.includes('redirect_uri')) {
111
- ssoRedirectUrl += `&redirect_uri=${window.location.origin}${store.auth.routes.oidcRedirectUrl}`;
112
- }
113
- redirectToFullPage(ssoRedirectUrl);
122
+ // as login does for an OIDC SSO page; the refusal has no idpType, but an OIDC page asks for a code
123
+ const isOidcPage = /[?&]response_type=code(&|$)/.test(address);
124
+ redirectToFullPage(isOidcPage ? (0, _helpers2.withOidcRedirectUri)(address, store.auth.routes.oidcRedirectUrl) : address);
114
125
  };
115
126
 
116
127
  /**
@@ -121,7 +132,8 @@ var _default = (store, api, sharedActions) => {
121
132
  */
122
133
  const __redirectToSsoIfRequired = async e => {
123
134
  var _e$errorContext;
124
- if (!(0, _restApi.isEnforceSsoOnTenantAccessEnabled)(store.root.appName) || !(0, _helpers.isSsoEnforcementError)(e) || !((_e$errorContext = e.errorContext) != null && _e$errorContext.ssoRequired)) {
135
+ // Ordinary failures keep their existing handling even while the flags request is pending.
136
+ if (!(0, _helpers.isSsoEnforcementError)(e) || !((_e$errorContext = e.errorContext) != null && _e$errorContext.ssoRequired) || !(await (0, _restApi.isEnforceSsoOnTenantAccessEnabled)(store.root.appName))) {
125
137
  return false;
126
138
  }
127
139
  await __redirectToSsoForTenant(e.errorContext);
@@ -135,14 +147,19 @@ var _default = (store, api, sharedActions) => {
135
147
  * @private
136
148
  */
137
149
  const __handleSsoEnforcementRefreshError = async (e, isSsoEnforcementRetry, refreshForTenant) => {
138
- var _store$auth$user;
139
- // on a login callback the page finishes the login with its code: log out as with the flag off, no new SSO trip
140
- if (!(0, _restApi.isEnforceSsoOnTenantAccessEnabled)(store.root.appName) || !(0, _helpers.isSsoEnforcementError)(e) || (0, _helpers3.isOnLoginCodeCallbackRoute)(store)) {
150
+ var _e$errorContext2, _store$auth$user;
151
+ // on a login callback the page finishes the login with its code: log out as without SSO enforcement, no new SSO
152
+ // trip
153
+ if (!(0, _helpers.isSsoEnforcementError)(e) || (0, _helpers3.isOnLoginCodeCallbackRoute)(store) || !(await (0, _restApi.isEnforceSsoOnTenantAccessEnabled)(store.root.appName))) {
141
154
  return false;
142
155
  }
143
156
  if (await __redirectToSsoIfRequired(e)) {
144
157
  return true;
145
158
  }
159
+ // Only an explicit refusal without SSO permits recovery; missing context follows ordinary logout handling.
160
+ if (((_e$errorContext2 = e.errorContext) == null ? void 0 : _e$errorContext2.ssoRequired) !== false) {
161
+ return false;
162
+ }
146
163
  const currentTenantId = (_store$auth$user = store.auth.user) == null ? void 0 : _store$auth$user.tenantId;
147
164
  if (isSsoEnforcementRetry || !currentTenantId) {
148
165
  return false;
@@ -253,9 +270,10 @@ var _default = (store, api, sharedActions) => {
253
270
  });
254
271
  }
255
272
  } catch (e) {
256
- // a refused identity refresh has already used up the refresh token: no retry, only the SSO redirect, except on
257
- // a login callback, where the page itself completes the login
258
- if ((0, _restApi.isEnforceSsoOnTenantAccessEnabled)(store.root.appName) && !(0, _helpers3.isOnLoginCodeCallbackRoute)(store) && (await __redirectToSsoIfRequired(e))) {
273
+ // no retry: identity already refreshes for the session's own tenant when it can, so a refusal without SSO means
274
+ // there is none to keep. Only the SSO redirect, except on a login callback, where the page itself completes the
275
+ // login
276
+ if (!(0, _helpers3.isOnLoginCodeCallbackRoute)(store) && (await __redirectToSsoIfRequired(e))) {
259
277
  return;
260
278
  }
261
279
  contextHolder.setAccessToken(null);
@@ -268,9 +286,7 @@ var _default = (store, api, sharedActions) => {
268
286
  };
269
287
 
270
288
  /** @private */
271
- const __refreshTokenWithNativeTokens = async ({
272
- isSsoEnforcementRetry = false
273
- } = {}) => {
289
+ const __refreshTokenWithNativeTokens = async () => {
274
290
  try {
275
291
  const authApi = api.auth;
276
292
  const response = await (0, _helpers.refreshTokenWithRetry)(async () => {
@@ -284,13 +300,6 @@ var _default = (store, api, sharedActions) => {
284
300
  isAuthenticated: true
285
301
  });
286
302
  } catch (e) {
287
- // native tokens can't name a tenant: the retry relies on the tab tenant only
288
- const retry = () => __refreshTokenWithNativeTokens({
289
- isSsoEnforcementRetry: true
290
- });
291
- if (await __handleSsoEnforcementRefreshError(e, isSsoEnforcementRetry, retry)) {
292
- return;
293
- }
294
303
  contextHolder.setAccessToken(null);
295
304
  contextHolder.setUser(null);
296
305
  actions.setAuthState({
@@ -302,7 +311,7 @@ var _default = (store, api, sharedActions) => {
302
311
 
303
312
  /** @protected */
304
313
  const __refreshToken = async () => {
305
- (0, _helpers3.restoreTabTenantUnlessSsoCallback)(store);
314
+ await (0, _helpers3.restoreTabTenantUnlessSsoCallback)(store);
306
315
  if (_toolkit.FronteggNativeModule.isGetTokensAvailable()) {
307
316
  await __refreshTokenWithNativeTokens();
308
317
  return;
@@ -590,13 +599,7 @@ var _default = (store, api, sharedActions) => {
590
599
  redirectToFullPage(address);
591
600
  return;
592
601
  }
593
- let ssoRedirectUrl = address;
594
- if (idpType === _interfaces4.SamlVendors.Oidc && !ssoRedirectUrl.includes('redirect_uri')) {
595
- const {
596
- oidcRedirectUrl
597
- } = store.auth.routes;
598
- ssoRedirectUrl += `&redirect_uri=${window.location.origin}${oidcRedirectUrl}`;
599
- }
602
+ const ssoRedirectUrl = idpType === _interfaces4.SamlVendors.Oidc ? (0, _helpers2.withOidcRedirectUri)(address, store.auth.routes.oidcRedirectUrl) : address;
600
603
  if (_toolkit.FronteggNativeModule.isLoginWithSSOAvailable()) {
601
604
  var _ref4;
602
605
  _toolkit.FronteggNativeModule.loginWithSSO((_ref4 = email != null ? email : username) != null ? _ref4 : '');
@@ -815,11 +818,13 @@ var _default = (store, api, sharedActions) => {
815
818
  });
816
819
  }
817
820
  };
818
- const clearLogoutLocalState = ({
821
+ const clearLogoutLocalState = async ({
819
822
  keepHostedOAuthState = false
820
823
  } = {}) => {
821
824
  if (contextHolder.isSessionPerTenantEnabled()) {
822
825
  (0, _restApi.removeTabTenantFromSessionStorage)();
826
+ // a pending SSO switch would put a tenant of the logged-out session back on the next page load
827
+ await (0, _restApi.clearTabTenantBeforeSsoSwitch)(store.root.appName);
823
828
  }
824
829
  // Step-up re-login goes through silentLogout but still needs the hosted OAuth state to finish the client flow.
825
830
  if (!keepHostedOAuthState) {
@@ -840,7 +845,7 @@ var _default = (store, api, sharedActions) => {
840
845
  } catch {
841
846
  /* empty */
842
847
  }
843
- clearLogoutLocalState();
848
+ await clearLogoutLocalState();
844
849
  actions.resetAuthState();
845
850
  await actions.requestAuthorize(true);
846
851
  payload == null ? void 0 : payload();
@@ -853,7 +858,7 @@ var _default = (store, api, sharedActions) => {
853
858
  /* empty */
854
859
  }
855
860
  const isStepUpInProgress = !!((_window2 = window) != null && _window2.localStorage.getItem(_consts.SHOULD_STEP_UP_KEY));
856
- clearLogoutLocalState({
861
+ await clearLogoutLocalState({
857
862
  keepHostedOAuthState: isStepUpInProgress
858
863
  });
859
864
  setTimeout(() => {
@@ -1701,7 +1706,6 @@ var _default = (store, api, sharedActions) => {
1701
1706
  refreshTokenForSocialLogins,
1702
1707
  // protected
1703
1708
  __refreshToken,
1704
- __redirectToSsoForTenant,
1705
1709
  __redirectToSsoIfRequired
1706
1710
  }, (0, _hostedLoginAuthorize.default)(store, api, sharedActions), (0, _afterAuthNavigation.default)(store, api, sharedActions), (0, _handleVerifyMFAResponse.default)(store, api, sharedActions), (0, _mfaRequiredState.default)(store, api, sharedActions), (0, _mfaWithAuthenticator.default)(store, api, sharedActions), (0, _mfaWithEmailCode.default)(store, api, sharedActions), (0, _mfaWithSMS.default)(store, api, sharedActions), (0, _mfaWithWebAuthn.default)(store, api, sharedActions));
1707
1711
  };
@@ -7,7 +7,7 @@ exports.isAbsoluteUrl = exports.getSearchParamsFromUrl = exports.getSearchParam
7
7
  exports.isEmailPayload = isEmailPayload;
8
8
  exports.isOauthCallbackRoute = void 0;
9
9
  exports.isUsernamePayload = isUsernamePayload;
10
- exports.shouldShowPasswordRotationPromptFunc = void 0;
10
+ exports.withOidcRedirectUri = exports.shouldShowPasswordRotationPromptFunc = void 0;
11
11
  var _restApi = require("@frontegg/rest-api");
12
12
  var _interfaces = require("../MfaState/interfaces");
13
13
  const isAbsoluteUrl = path => {
@@ -90,7 +90,11 @@ const getSearchParam = key => {
90
90
  };
91
91
  exports.getSearchParam = getSearchParam;
92
92
  const TENANT_ID_PARAM_KEY = 'tenantId';
93
+
94
+ /** An OIDC SSO page without redirect_uri comes back to the OIDC callback, which completes the login with its code. */
93
95
  exports.TENANT_ID_PARAM_KEY = TENANT_ID_PARAM_KEY;
96
+ const withOidcRedirectUri = (address, oidcRedirectUrl) => address.includes('redirect_uri') ? address : `${address}&redirect_uri=${window.location.origin}${oidcRedirectUrl}`;
97
+ exports.withOidcRedirectUri = withOidcRedirectUri;
94
98
  const getNumberOfMfaDevices = mfaDevices => {
95
99
  // Count platform / cross-platform separately — matches the MFA picker options.
96
100
  const numberOfWebAuthnDevices = Number(mfaDevices.webauthn.some(device => device.deviceType === _restApi.WebAuthnDeviceType.Platform)) + Number(mfaDevices.webauthn.some(device => device.deviceType === _restApi.WebAuthnDeviceType.CrossPlatform));
@@ -52,6 +52,9 @@ var _default = (store, api, sharedActions) => {
52
52
  });
53
53
  }
54
54
  } catch (e) {
55
+ // the tab tenant the switch was refused from, read before any await: a parallel switch that passes moves the tab
56
+ // while this one goes to SSO
57
+ const tabTenantId = (0, _restApi.getTabTenantFromSessionStorage)(store.root.appName);
55
58
  silentReload ? actions.setTenantsState({
56
59
  switchingTenant: false
57
60
  }) : actions.setAuthState({
@@ -60,7 +63,7 @@ var _default = (store, api, sharedActions) => {
60
63
  if (await actions.__redirectToSsoIfRequired(e)) {
61
64
  // the tab comes back from SSO in the target tenant, not the one the failed switch restored
62
65
  if (_restApi.ContextHolder.for(store.root.appName).isSessionPerTenantEnabled()) {
63
- (0, _restApi.setTabTenantForSsoSwitch)(tenantId);
66
+ (0, _restApi.setTabTenantForSsoSwitch)(tenantId, tabTenantId);
64
67
  }
65
68
  return;
66
69
  }
@@ -141,10 +141,8 @@ const isOnLoginCodeCallbackRoute = store => {
141
141
  * keeps it; any other page load means the user didn't finish that login, so the tab goes back to its tenant.
142
142
  */
143
143
  exports.isOnLoginCodeCallbackRoute = isOnLoginCodeCallbackRoute;
144
- const restoreTabTenantUnlessSsoCallback = store => {
144
+ const restoreTabTenantUnlessSsoCallback = async store => {
145
145
  var _store$root$urlStrate2;
146
- // Flags load in parallel with the first refresh. Recover an existing marker
147
- // before choosing that refresh's tenant, even if flags are not available yet.
148
146
  if (!_restApi.ContextHolder.for(store.root.appName).isSessionPerTenantEnabled()) {
149
147
  return;
150
148
  }
@@ -155,9 +153,13 @@ const restoreTabTenantUnlessSsoCallback = store => {
155
153
  } = (0, _extends2.default)({}, _consts.defaultFronteggRoutes, store.auth.routes);
156
154
  // matched like login matches its callback pages: endsWith, so a basename before the route still counts
157
155
  const currentRoutes = getCurrentRoutes((_store$root$urlStrate2 = store.root.urlStrategy) != null ? _store$root$urlStrate2 : 'path');
158
- const isSsoCallback = [samlCallbackUrl, oidcRedirectUrl, hostedLoginRedirectUrl].some(route => !!route && currentRoutes.some(currentRoute => currentRoute.endsWith(route)));
156
+ // a refused login comes back on its callback too, not finished: SAML with samlerrors (as SamlLoginSuccess reads them),
157
+ // OIDC and hosted without a code (e.g. the IdP's error=access_denied), which their pages need to finish the login
158
+ const params = new URLSearchParams(window.location.search);
159
+ const callbackRoutes = [...(params.has('samlerrors') ? [] : [samlCallbackUrl]), ...(params.has('code') ? [oidcRedirectUrl, hostedLoginRedirectUrl] : [])];
160
+ const isSsoCallback = callbackRoutes.some(route => !!route && currentRoutes.some(currentRoute => currentRoute.endsWith(route)));
159
161
  if (!isSsoCallback) {
160
- (0, _restApi.restoreTabTenantBeforeSsoSwitch)();
162
+ await (0, _restApi.restoreTabTenantBeforeSsoSwitch)(store.root.appName);
161
163
  }
162
164
  };
163
165
  exports.restoreTabTenantUnlessSsoCallback = restoreTabTenantUnlessSsoCallback;
package/node/index.js CHANGED
@@ -1,4 +1,4 @@
1
- /** @license Frontegg v7.131.0-alpha.2
1
+ /** @license Frontegg v7.131.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.131.0-alpha.2",
3
+ "version": "7.131.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.131.0-alpha.2",
10
+ "@frontegg/rest-api": "7.131.0",
11
11
  "fast-deep-equal": "3.1.3",
12
12
  "get-value": "^3.0.1",
13
13
  "proxy-compare": "^3.0.0",