@frontegg/redux-store 7.131.0-alpha.1 → 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.
@@ -3,7 +3,7 @@ import { createRandomString, generateCodeChallenge, GTMEventAction, isSsoEnforce
3
3
  import { HOSTED_LOGIN_VERIFIER_KEY } from '../../../constants';
4
4
  import { isEnforceSsoOnTenantAccessEnabled } from '@frontegg/rest-api';
5
5
  import { getBaseNameWithoutSlashSuffix, getSearchParam, isOauthCallbackRoute, TENANT_ID_PARAM_KEY } from '../helpers';
6
- import { getUri, getUrlDerivedStepUp, isSteppedUp, redirectByStepUpUrl, restoreTabTenantUnlessSsoCallback } from '../../helpers';
6
+ import { getUri, getUrlDerivedStepUp, isSteppedUp, isOnLoginCodeCallbackRoute, redirectByStepUpUrl, restoreTabTenantUnlessSsoCallback } from '../../helpers';
7
7
  import { SHOULD_STEP_UP_KEY } from '../../StepUpState/consts';
8
8
  import { FronteggNativeModule } from '../../../toolkit';
9
9
  export default ((store, api, sharedActions) => {
@@ -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,12 +27,16 @@ 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 without SSO enforcement
31
+ if (isOnLoginCodeCallbackRoute(store)) {
32
+ throw error;
33
+ }
30
34
  // the tenant requires SSO: hosted login for that tenant, not the plain one the caller falls back to
31
35
  if (await actions.__redirectToSsoIfRequired(error)) {
32
36
  return;
33
37
  }
34
38
  const tenantId = (_store$auth$user = store.auth.user) == null ? void 0 : _store$auth$user.tenantId;
35
- 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))) {
36
40
  throw error;
37
41
  }
38
42
  // one retry for the tenant of the current token (a cold reload has no user yet: no retry)
@@ -33,7 +33,12 @@ 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?: boolean;
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
41
+ */: boolean;
37
42
  firstTime?: boolean;
38
43
  loginDirectAction?: import("../interfaces").LoginDirectAction;
39
44
  }) => Promise<void>;
@@ -77,10 +82,6 @@ declare const _default: (store: FronteggState, api: RestApi, sharedActions: Shar
77
82
  resetBreachedPassword: (payload: IForgotPassword) => Promise<void>;
78
83
  refreshTokenForSocialLogins: () => Promise<void>;
79
84
  __refreshToken: () => Promise<void>;
80
- __redirectToSsoForTenant: ({ tenantId, address }: {
81
- tenantId: string;
82
- address: string;
83
- }) => Promise<void>;
84
85
  __redirectToSsoIfRequired: (e: unknown) => Promise<boolean>;
85
86
  };
86
87
  export default _default;
@@ -25,15 +25,15 @@ 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';
33
33
  import { FronteggNativeModule, isEntitlementsDeeplyEqual } from '../../../toolkit';
34
34
  import { REQUEST_NAME, UserVerifiedOriginTypes } from '../../interfaces';
35
35
  import { authStrategyLoginStepMap, defaultOnRedirectTo } from '../consts';
36
- import { isMfaRequired, isOnAuthPageRoute, isOnOidcCallbackRoute, isResetPasswordRequired, restoreTabTenantUnlessSsoCallback, setAfterAuthRedirectUrlForStepUp } from '../../helpers';
36
+ import { isMfaRequired, isOnAuthPageRoute, isOnLoginCodeCallbackRoute, isResetPasswordRequired, restoreTabTenantUnlessSsoCallback, setAfterAuthRedirectUrlForStepUp } from '../../helpers';
37
37
  import { MFAStep } from '../../MfaState/interfaces';
38
38
  import { SamlVendors } from '../../SSOState/interfaces';
39
39
  import { DEFAULT_RETRY_CONFIG } from '../../../constants';
@@ -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,13 +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
- if (!isEnforceSsoOnTenantAccessEnabled(store.root.appName) || !isSsoEnforcementError(e)) {
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))) {
133
147
  return false;
134
148
  }
135
149
  if (await __redirectToSsoIfRequired(e)) {
136
150
  return true;
137
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
+ }
138
156
  const currentTenantId = (_store$auth$user = store.auth.user) == null ? void 0 : _store$auth$user.tenantId;
139
157
  if (isSsoEnforcementRetry || !currentTenantId) {
140
158
  return false;
@@ -245,9 +263,10 @@ export default ((store, api, sharedActions) => {
245
263
  });
246
264
  }
247
265
  } catch (e) {
248
- // a refused identity refresh has already used up the refresh token: no retry, only the SSO redirect, except on
249
- // the OIDC callback, where the page itself completes the OIDC login
250
- if (isEnforceSsoOnTenantAccessEnabled(store.root.appName) && !isOnOidcCallbackRoute(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))) {
251
270
  return;
252
271
  }
253
272
  contextHolder.setAccessToken(null);
@@ -260,9 +279,7 @@ export default ((store, api, sharedActions) => {
260
279
  };
261
280
 
262
281
  /** @private */
263
- const __refreshTokenWithNativeTokens = async ({
264
- isSsoEnforcementRetry = false
265
- } = {}) => {
282
+ const __refreshTokenWithNativeTokens = async () => {
266
283
  try {
267
284
  const authApi = api.auth;
268
285
  const response = await refreshTokenWithRetry(async () => {
@@ -276,13 +293,6 @@ export default ((store, api, sharedActions) => {
276
293
  isAuthenticated: true
277
294
  });
278
295
  } catch (e) {
279
- // native tokens can't name a tenant: the retry relies on the tab tenant only
280
- const retry = () => __refreshTokenWithNativeTokens({
281
- isSsoEnforcementRetry: true
282
- });
283
- if (await __handleSsoEnforcementRefreshError(e, isSsoEnforcementRetry, retry)) {
284
- return;
285
- }
286
296
  contextHolder.setAccessToken(null);
287
297
  contextHolder.setUser(null);
288
298
  actions.setAuthState({
@@ -294,7 +304,7 @@ export default ((store, api, sharedActions) => {
294
304
 
295
305
  /** @protected */
296
306
  const __refreshToken = async () => {
297
- restoreTabTenantUnlessSsoCallback(store);
307
+ await restoreTabTenantUnlessSsoCallback(store);
298
308
  if (FronteggNativeModule.isGetTokensAvailable()) {
299
309
  await __refreshTokenWithNativeTokens();
300
310
  return;
@@ -582,13 +592,7 @@ export default ((store, api, sharedActions) => {
582
592
  redirectToFullPage(address);
583
593
  return;
584
594
  }
585
- let ssoRedirectUrl = address;
586
- if (idpType === SamlVendors.Oidc && !ssoRedirectUrl.includes('redirect_uri')) {
587
- const {
588
- oidcRedirectUrl
589
- } = store.auth.routes;
590
- ssoRedirectUrl += `&redirect_uri=${window.location.origin}${oidcRedirectUrl}`;
591
- }
595
+ const ssoRedirectUrl = idpType === SamlVendors.Oidc ? withOidcRedirectUri(address, store.auth.routes.oidcRedirectUrl) : address;
592
596
  if (FronteggNativeModule.isLoginWithSSOAvailable()) {
593
597
  var _ref4;
594
598
  FronteggNativeModule.loginWithSSO((_ref4 = email != null ? email : username) != null ? _ref4 : '');
@@ -807,11 +811,13 @@ export default ((store, api, sharedActions) => {
807
811
  });
808
812
  }
809
813
  };
810
- const clearLogoutLocalState = ({
814
+ const clearLogoutLocalState = async ({
811
815
  keepHostedOAuthState = false
812
816
  } = {}) => {
813
817
  if (contextHolder.isSessionPerTenantEnabled()) {
814
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);
815
821
  }
816
822
  // Step-up re-login goes through silentLogout but still needs the hosted OAuth state to finish the client flow.
817
823
  if (!keepHostedOAuthState) {
@@ -832,7 +838,7 @@ export default ((store, api, sharedActions) => {
832
838
  } catch {
833
839
  /* empty */
834
840
  }
835
- clearLogoutLocalState();
841
+ await clearLogoutLocalState();
836
842
  actions.resetAuthState();
837
843
  await actions.requestAuthorize(true);
838
844
  payload == null ? void 0 : payload();
@@ -845,7 +851,7 @@ export default ((store, api, sharedActions) => {
845
851
  /* empty */
846
852
  }
847
853
  const isStepUpInProgress = !!((_window2 = window) != null && _window2.localStorage.getItem(SHOULD_STEP_UP_KEY));
848
- clearLogoutLocalState({
854
+ await clearLogoutLocalState({
849
855
  keepHostedOAuthState: isStepUpInProgress
850
856
  });
851
857
  setTimeout(() => {
@@ -1693,7 +1699,6 @@ export default ((store, api, sharedActions) => {
1693
1699
  refreshTokenForSocialLogins,
1694
1700
  // protected
1695
1701
  __refreshToken,
1696
- __redirectToSsoForTenant,
1697
1702
  __redirectToSsoIfRequired
1698
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));
1699
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));
@@ -94,12 +94,11 @@ export default ((store, api, sharedActions) => {
94
94
  }
95
95
  callback == null ? void 0 : callback(true);
96
96
  } catch (e) {
97
- callback == null ? void 0 : callback(false);
98
-
99
97
  // the session's tenant requires SSO: its SSO login steps the user up, instead of logging them out
100
98
  if (await actions.__redirectToSsoIfRequired(e)) {
101
99
  return;
102
100
  }
101
+ callback == null ? void 0 : callback(false);
103
102
  await __handleStepUpError(e);
104
103
  }
105
104
  };
@@ -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
@@ -9,15 +9,16 @@ export declare const isResetPasswordRequired: (user: ILoginResponse, appName: st
9
9
  export declare const mapMetaDataObjectToActions: (obj: any, path?: string[]) => CommitChangeDto[];
10
10
  export declare const getUri: (urlStrategy: FronteggState["root"]["urlStrategy"]) => string;
11
11
  /**
12
- * On the OIDC callback the page still has to finish the OIDC login with its code: a refresh refused before that is not
13
- * a reason to go back to the IdP, which would send the user straight back here, forever.
12
+ * On the OIDC or hosted login callback the page still has to finish the login with its code: a refresh refused before
13
+ * that is not a reason to go to SSO again. On the OIDC callback the IdP would send the user straight back here, forever;
14
+ * on the hosted callback a new hosted login would replace the PKCE verifier the code exchange needs.
14
15
  */
15
- export declare const isOnOidcCallbackRoute: (store: FronteggState) => boolean;
16
+ export declare const isOnLoginCodeCallbackRoute: (store: FronteggState) => boolean;
16
17
  /**
17
18
  * After a switch refused for SSO the tab points at the SSO tenant. Its SSO login comes back on a callback route and
18
19
  * keeps it; any other page load means the user didn't finish that login, so the tab goes back to its tenant.
19
20
  */
20
- export declare const restoreTabTenantUnlessSsoCallback: (store: FronteggState) => void;
21
+ export declare const restoreTabTenantUnlessSsoCallback: (store: FronteggState) => Promise<void>;
21
22
  export declare const prepareIdentifiersForSignup: (payload: ISignUpUser) => ISignUpUser;
22
23
  /**
23
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
@@ -2,7 +2,7 @@ import _objectWithoutPropertiesLoose from "@babel/runtime/helpers/esm/objectWith
2
2
  import _extends from "@babel/runtime/helpers/esm/extends";
3
3
  const _excluded = ["phoneNumber", "email", "username"],
4
4
  _excluded2 = ["authenticatedUrl"];
5
- import { ContextHolder, restoreTabTenantBeforeSsoSwitch, isEnforceSsoOnTenantAccessEnabled } from '@frontegg/rest-api';
5
+ import { ContextHolder, restoreTabTenantBeforeSsoSwitch } from '@frontegg/rest-api';
6
6
  import { defaultFronteggRoutes } from './LoginState/consts';
7
7
  export * from './Entitlements/helpers';
8
8
  export * from './LoginState/helpers';
@@ -67,24 +67,27 @@ const getCurrentRoutes = urlStrategy => {
67
67
  };
68
68
 
69
69
  /**
70
- * On the OIDC callback the page still has to finish the OIDC login with its code: a refresh refused before that is not
71
- * a reason to go back to the IdP, which would send the user straight back here, forever.
70
+ * On the OIDC or hosted login callback the page still has to finish the login with its code: a refresh refused before
71
+ * that is not a reason to go to SSO again. On the OIDC callback the IdP would send the user straight back here, forever;
72
+ * on the hosted callback a new hosted login would replace the PKCE verifier the code exchange needs.
72
73
  */
73
- export const isOnOidcCallbackRoute = store => {
74
+ export const isOnLoginCodeCallbackRoute = store => {
74
75
  var _store$root$urlStrate;
75
76
  const {
76
- oidcRedirectUrl
77
+ oidcRedirectUrl,
78
+ hostedLoginRedirectUrl
77
79
  } = _extends({}, defaultFronteggRoutes, store.auth.routes);
78
- return !!oidcRedirectUrl && getCurrentRoutes((_store$root$urlStrate = store.root.urlStrategy) != null ? _store$root$urlStrate : 'path').some(currentRoute => currentRoute.endsWith(oidcRedirectUrl));
80
+ const currentRoutes = getCurrentRoutes((_store$root$urlStrate = store.root.urlStrategy) != null ? _store$root$urlStrate : 'path');
81
+ return [oidcRedirectUrl, hostedLoginRedirectUrl].some(route => !!route && currentRoutes.some(currentRoute => currentRoute.endsWith(route)));
79
82
  };
80
83
 
81
84
  /**
82
85
  * After a switch refused for SSO the tab points at the SSO tenant. Its SSO login comes back on a callback route and
83
86
  * keeps it; any other page load means the user didn't finish that login, so the tab goes back to its tenant.
84
87
  */
85
- export const restoreTabTenantUnlessSsoCallback = store => {
88
+ export const restoreTabTenantUnlessSsoCallback = async store => {
86
89
  var _store$root$urlStrate2;
87
- if (!isEnforceSsoOnTenantAccessEnabled(store.root.appName) || !ContextHolder.for(store.root.appName).isSessionPerTenantEnabled()) {
90
+ if (!ContextHolder.for(store.root.appName).isSessionPerTenantEnabled()) {
88
91
  return;
89
92
  }
90
93
  const {
@@ -94,9 +97,13 @@ export const restoreTabTenantUnlessSsoCallback = store => {
94
97
  } = _extends({}, defaultFronteggRoutes, store.auth.routes);
95
98
  // matched like login matches its callback pages: endsWith, so a basename before the route still counts
96
99
  const currentRoutes = getCurrentRoutes((_store$root$urlStrate2 = store.root.urlStrategy) != null ? _store$root$urlStrate2 : 'path');
97
- 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)));
98
105
  if (!isSsoCallback) {
99
- restoreTabTenantBeforeSsoSwitch();
106
+ await restoreTabTenantBeforeSsoSwitch(store.root.appName);
100
107
  }
101
108
  };
102
109
  export const prepareIdentifiersForSignup = payload => {
package/index.js CHANGED
@@ -1,4 +1,4 @@
1
- /** @license Frontegg v7.131.0-alpha.1
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,12 +34,16 @@ 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 without SSO enforcement
38
+ if ((0, _helpers3.isOnLoginCodeCallbackRoute)(store)) {
39
+ throw error;
40
+ }
37
41
  // the tenant requires SSO: hosted login for that tenant, not the plain one the caller falls back to
38
42
  if (await actions.__redirectToSsoIfRequired(error)) {
39
43
  return;
40
44
  }
41
45
  const tenantId = (_store$auth$user = store.auth.user) == null ? void 0 : _store$auth$user.tenantId;
42
- 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))) {
43
47
  throw error;
44
48
  }
45
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,13 +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
- if (!(0, _restApi.isEnforceSsoOnTenantAccessEnabled)(store.root.appName) || !(0, _helpers.isSsoEnforcementError)(e)) {
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))) {
140
154
  return false;
141
155
  }
142
156
  if (await __redirectToSsoIfRequired(e)) {
143
157
  return true;
144
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
+ }
145
163
  const currentTenantId = (_store$auth$user = store.auth.user) == null ? void 0 : _store$auth$user.tenantId;
146
164
  if (isSsoEnforcementRetry || !currentTenantId) {
147
165
  return false;
@@ -252,9 +270,10 @@ var _default = (store, api, sharedActions) => {
252
270
  });
253
271
  }
254
272
  } catch (e) {
255
- // a refused identity refresh has already used up the refresh token: no retry, only the SSO redirect, except on
256
- // the OIDC callback, where the page itself completes the OIDC login
257
- if ((0, _restApi.isEnforceSsoOnTenantAccessEnabled)(store.root.appName) && !(0, _helpers3.isOnOidcCallbackRoute)(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))) {
258
277
  return;
259
278
  }
260
279
  contextHolder.setAccessToken(null);
@@ -267,9 +286,7 @@ var _default = (store, api, sharedActions) => {
267
286
  };
268
287
 
269
288
  /** @private */
270
- const __refreshTokenWithNativeTokens = async ({
271
- isSsoEnforcementRetry = false
272
- } = {}) => {
289
+ const __refreshTokenWithNativeTokens = async () => {
273
290
  try {
274
291
  const authApi = api.auth;
275
292
  const response = await (0, _helpers.refreshTokenWithRetry)(async () => {
@@ -283,13 +300,6 @@ var _default = (store, api, sharedActions) => {
283
300
  isAuthenticated: true
284
301
  });
285
302
  } catch (e) {
286
- // native tokens can't name a tenant: the retry relies on the tab tenant only
287
- const retry = () => __refreshTokenWithNativeTokens({
288
- isSsoEnforcementRetry: true
289
- });
290
- if (await __handleSsoEnforcementRefreshError(e, isSsoEnforcementRetry, retry)) {
291
- return;
292
- }
293
303
  contextHolder.setAccessToken(null);
294
304
  contextHolder.setUser(null);
295
305
  actions.setAuthState({
@@ -301,7 +311,7 @@ var _default = (store, api, sharedActions) => {
301
311
 
302
312
  /** @protected */
303
313
  const __refreshToken = async () => {
304
- (0, _helpers3.restoreTabTenantUnlessSsoCallback)(store);
314
+ await (0, _helpers3.restoreTabTenantUnlessSsoCallback)(store);
305
315
  if (_toolkit.FronteggNativeModule.isGetTokensAvailable()) {
306
316
  await __refreshTokenWithNativeTokens();
307
317
  return;
@@ -589,13 +599,7 @@ var _default = (store, api, sharedActions) => {
589
599
  redirectToFullPage(address);
590
600
  return;
591
601
  }
592
- let ssoRedirectUrl = address;
593
- if (idpType === _interfaces4.SamlVendors.Oidc && !ssoRedirectUrl.includes('redirect_uri')) {
594
- const {
595
- oidcRedirectUrl
596
- } = store.auth.routes;
597
- ssoRedirectUrl += `&redirect_uri=${window.location.origin}${oidcRedirectUrl}`;
598
- }
602
+ const ssoRedirectUrl = idpType === _interfaces4.SamlVendors.Oidc ? (0, _helpers2.withOidcRedirectUri)(address, store.auth.routes.oidcRedirectUrl) : address;
599
603
  if (_toolkit.FronteggNativeModule.isLoginWithSSOAvailable()) {
600
604
  var _ref4;
601
605
  _toolkit.FronteggNativeModule.loginWithSSO((_ref4 = email != null ? email : username) != null ? _ref4 : '');
@@ -814,11 +818,13 @@ var _default = (store, api, sharedActions) => {
814
818
  });
815
819
  }
816
820
  };
817
- const clearLogoutLocalState = ({
821
+ const clearLogoutLocalState = async ({
818
822
  keepHostedOAuthState = false
819
823
  } = {}) => {
820
824
  if (contextHolder.isSessionPerTenantEnabled()) {
821
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);
822
828
  }
823
829
  // Step-up re-login goes through silentLogout but still needs the hosted OAuth state to finish the client flow.
824
830
  if (!keepHostedOAuthState) {
@@ -839,7 +845,7 @@ var _default = (store, api, sharedActions) => {
839
845
  } catch {
840
846
  /* empty */
841
847
  }
842
- clearLogoutLocalState();
848
+ await clearLogoutLocalState();
843
849
  actions.resetAuthState();
844
850
  await actions.requestAuthorize(true);
845
851
  payload == null ? void 0 : payload();
@@ -852,7 +858,7 @@ var _default = (store, api, sharedActions) => {
852
858
  /* empty */
853
859
  }
854
860
  const isStepUpInProgress = !!((_window2 = window) != null && _window2.localStorage.getItem(_consts.SHOULD_STEP_UP_KEY));
855
- clearLogoutLocalState({
861
+ await clearLogoutLocalState({
856
862
  keepHostedOAuthState: isStepUpInProgress
857
863
  });
858
864
  setTimeout(() => {
@@ -1700,7 +1706,6 @@ var _default = (store, api, sharedActions) => {
1700
1706
  refreshTokenForSocialLogins,
1701
1707
  // protected
1702
1708
  __refreshToken,
1703
- __redirectToSsoForTenant,
1704
1709
  __redirectToSsoIfRequired
1705
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));
1706
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));
@@ -100,12 +100,11 @@ var _default = (store, api, sharedActions) => {
100
100
  }
101
101
  callback == null ? void 0 : callback(true);
102
102
  } catch (e) {
103
- callback == null ? void 0 : callback(false);
104
-
105
103
  // the session's tenant requires SSO: its SSO login steps the user up, instead of logging them out
106
104
  if (await actions.__redirectToSsoIfRequired(e)) {
107
105
  return;
108
106
  }
107
+ callback == null ? void 0 : callback(false);
109
108
  await __handleStepUpError(e);
110
109
  }
111
110
  };
@@ -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
  }
@@ -9,13 +9,13 @@ var _exportNames = {
9
9
  isResetPasswordRequired: true,
10
10
  mapMetaDataObjectToActions: true,
11
11
  getUri: true,
12
- isOnOidcCallbackRoute: true,
12
+ isOnLoginCodeCallbackRoute: true,
13
13
  restoreTabTenantUnlessSsoCallback: true,
14
14
  prepareIdentifiersForSignup: true,
15
15
  isOnAuthPageRoute: true,
16
16
  isAuthRoute: true
17
17
  };
18
- exports.restoreTabTenantUnlessSsoCallback = exports.prepareIdentifiersForSignup = exports.mapMetaDataObjectToActions = exports.isResetPasswordRequired = exports.isOnOidcCallbackRoute = exports.isOnAuthPageRoute = exports.isMfaRequired = exports.isAuthRoute = exports.getUri = void 0;
18
+ exports.restoreTabTenantUnlessSsoCallback = exports.prepareIdentifiersForSignup = exports.mapMetaDataObjectToActions = exports.isResetPasswordRequired = exports.isOnLoginCodeCallbackRoute = exports.isOnAuthPageRoute = exports.isMfaRequired = exports.isAuthRoute = exports.getUri = void 0;
19
19
  var _objectWithoutPropertiesLoose2 = _interopRequireDefault(require("@babel/runtime/helpers/objectWithoutPropertiesLoose"));
20
20
  var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
21
21
  var _restApi = require("@frontegg/rest-api");
@@ -122,25 +122,28 @@ const getCurrentRoutes = urlStrategy => {
122
122
  };
123
123
 
124
124
  /**
125
- * On the OIDC callback the page still has to finish the OIDC login with its code: a refresh refused before that is not
126
- * a reason to go back to the IdP, which would send the user straight back here, forever.
125
+ * On the OIDC or hosted login callback the page still has to finish the login with its code: a refresh refused before
126
+ * that is not a reason to go to SSO again. On the OIDC callback the IdP would send the user straight back here, forever;
127
+ * on the hosted callback a new hosted login would replace the PKCE verifier the code exchange needs.
127
128
  */
128
- const isOnOidcCallbackRoute = store => {
129
+ const isOnLoginCodeCallbackRoute = store => {
129
130
  var _store$root$urlStrate;
130
131
  const {
131
- oidcRedirectUrl
132
+ oidcRedirectUrl,
133
+ hostedLoginRedirectUrl
132
134
  } = (0, _extends2.default)({}, _consts.defaultFronteggRoutes, store.auth.routes);
133
- return !!oidcRedirectUrl && getCurrentRoutes((_store$root$urlStrate = store.root.urlStrategy) != null ? _store$root$urlStrate : 'path').some(currentRoute => currentRoute.endsWith(oidcRedirectUrl));
135
+ const currentRoutes = getCurrentRoutes((_store$root$urlStrate = store.root.urlStrategy) != null ? _store$root$urlStrate : 'path');
136
+ return [oidcRedirectUrl, hostedLoginRedirectUrl].some(route => !!route && currentRoutes.some(currentRoute => currentRoute.endsWith(route)));
134
137
  };
135
138
 
136
139
  /**
137
140
  * After a switch refused for SSO the tab points at the SSO tenant. Its SSO login comes back on a callback route and
138
141
  * keeps it; any other page load means the user didn't finish that login, so the tab goes back to its tenant.
139
142
  */
140
- exports.isOnOidcCallbackRoute = isOnOidcCallbackRoute;
141
- const restoreTabTenantUnlessSsoCallback = store => {
143
+ exports.isOnLoginCodeCallbackRoute = isOnLoginCodeCallbackRoute;
144
+ const restoreTabTenantUnlessSsoCallback = async store => {
142
145
  var _store$root$urlStrate2;
143
- if (!(0, _restApi.isEnforceSsoOnTenantAccessEnabled)(store.root.appName) || !_restApi.ContextHolder.for(store.root.appName).isSessionPerTenantEnabled()) {
146
+ if (!_restApi.ContextHolder.for(store.root.appName).isSessionPerTenantEnabled()) {
144
147
  return;
145
148
  }
146
149
  const {
@@ -150,9 +153,13 @@ const restoreTabTenantUnlessSsoCallback = store => {
150
153
  } = (0, _extends2.default)({}, _consts.defaultFronteggRoutes, store.auth.routes);
151
154
  // matched like login matches its callback pages: endsWith, so a basename before the route still counts
152
155
  const currentRoutes = getCurrentRoutes((_store$root$urlStrate2 = store.root.urlStrategy) != null ? _store$root$urlStrate2 : 'path');
153
- 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)));
154
161
  if (!isSsoCallback) {
155
- (0, _restApi.restoreTabTenantBeforeSsoSwitch)();
162
+ await (0, _restApi.restoreTabTenantBeforeSsoSwitch)(store.root.appName);
156
163
  }
157
164
  };
158
165
  exports.restoreTabTenantUnlessSsoCallback = restoreTabTenantUnlessSsoCallback;
package/node/index.js CHANGED
@@ -1,4 +1,4 @@
1
- /** @license Frontegg v7.131.0-alpha.1
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.1",
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.1",
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",