@frontegg/redux-store 7.130.0 → 7.131.0-alpha.1

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.
Files changed (37) hide show
  1. package/auth/ActivateAccountState/actions.js +6 -0
  2. package/auth/LoginState/actions/hostedLoginAuthorize.actions.d.ts +3 -1
  3. package/auth/LoginState/actions/hostedLoginAuthorize.actions.js +29 -6
  4. package/auth/LoginState/actions/index.d.ts +8 -1
  5. package/auth/LoginState/actions/index.js +172 -11
  6. package/auth/LoginState/actions/mfaWithAuthenticator.actions.js +3 -0
  7. package/auth/LoginState/actions/mfaWithEmailCode.actions.js +3 -0
  8. package/auth/LoginState/actions/mfaWithSMS.actions.js +3 -0
  9. package/auth/LoginState/actions/mfaWithWebAuthn.actions.js +3 -0
  10. package/auth/MfaState/actions.js +3 -0
  11. package/auth/SocialLoginState/actions.js +3 -0
  12. package/auth/StepUpState/actions/generateStepUpSession.actions.js +5 -0
  13. package/auth/StepUpState/helpers.d.ts +2 -1
  14. package/auth/StepUpState/helpers.js +3 -2
  15. package/auth/TenantsState/actions.js +8 -1
  16. package/auth/helpers.d.ts +15 -0
  17. package/auth/helpers.js +60 -2
  18. package/helpers/handlers.d.ts +4 -1
  19. package/helpers/handlers.js +4 -1
  20. package/index.js +1 -1
  21. package/mocks/auth-mocks/loginActions.mocks.d.ts +8 -1
  22. package/node/auth/ActivateAccountState/actions.js +6 -0
  23. package/node/auth/LoginState/actions/hostedLoginAuthorize.actions.js +27 -4
  24. package/node/auth/LoginState/actions/index.js +169 -8
  25. package/node/auth/LoginState/actions/mfaWithAuthenticator.actions.js +3 -0
  26. package/node/auth/LoginState/actions/mfaWithEmailCode.actions.js +3 -0
  27. package/node/auth/LoginState/actions/mfaWithSMS.actions.js +3 -0
  28. package/node/auth/LoginState/actions/mfaWithWebAuthn.actions.js +3 -0
  29. package/node/auth/MfaState/actions.js +3 -0
  30. package/node/auth/SocialLoginState/actions.js +3 -0
  31. package/node/auth/StepUpState/actions/generateStepUpSession.actions.js +5 -0
  32. package/node/auth/StepUpState/helpers.js +3 -2
  33. package/node/auth/TenantsState/actions.js +7 -0
  34. package/node/auth/helpers.js +66 -2
  35. package/node/helpers/handlers.js +5 -1
  36. package/node/index.js +1 -1
  37. package/package.json +2 -2
@@ -151,6 +151,9 @@ export default ((store, api, sharedActions) => {
151
151
  });
152
152
  callback == null ? void 0 : callback(true);
153
153
  } catch (e) {
154
+ if (await actions.__redirectToSsoIfRequired(e)) {
155
+ return;
156
+ }
154
157
  setActivateState({
155
158
  loading: false,
156
159
  error: errorHandler(e)
@@ -188,6 +191,9 @@ export default ((store, api, sharedActions) => {
188
191
  });
189
192
  callback == null ? void 0 : callback(true);
190
193
  } catch (e) {
194
+ if (await actions.__redirectToSsoIfRequired(e)) {
195
+ return;
196
+ }
191
197
  setActivateState({
192
198
  submitting: false,
193
199
  loading: false,
@@ -8,7 +8,9 @@ declare const _default: (store: FronteggState, api: RestApi, sharedActions: Shar
8
8
  firstTime?: boolean;
9
9
  loginDirectAction?: LoginDirectAction;
10
10
  }) => Promise<void>;
11
- __requestHostedLoginAuthorize: (additionalParams?: Record<string, string>) => Promise<void>;
11
+ __requestHostedLoginAuthorize: (additionalParams?: Record<string, string>, { forceTenantId }?: {
12
+ forceTenantId?: boolean;
13
+ }) => Promise<void>;
12
14
  __requestHostedLoginSilentAuthorize: () => Promise<void>;
13
15
  handleHostedLoginCallback: (payload: HostedLoginCallback) => Promise<void>;
14
16
  };
@@ -1,8 +1,9 @@
1
1
  import _extends from "@babel/runtime/helpers/esm/extends";
2
- import { createRandomString, generateCodeChallenge, GTMEventAction, refreshTokenWithRetry, reportGTMEvent } from '../../../helpers';
2
+ import { createRandomString, generateCodeChallenge, GTMEventAction, isSsoEnforcementError, refreshTokenWithRetry, reportGTMEvent } from '../../../helpers';
3
3
  import { HOSTED_LOGIN_VERIFIER_KEY } from '../../../constants';
4
+ import { isEnforceSsoOnTenantAccessEnabled } from '@frontegg/rest-api';
4
5
  import { getBaseNameWithoutSlashSuffix, getSearchParam, isOauthCallbackRoute, TENANT_ID_PARAM_KEY } from '../helpers';
5
- import { getUri, getUrlDerivedStepUp, isSteppedUp, redirectByStepUpUrl } from '../../helpers';
6
+ import { getUri, getUrlDerivedStepUp, isSteppedUp, redirectByStepUpUrl, restoreTabTenantUnlessSsoCallback } from '../../helpers';
6
7
  import { SHOULD_STEP_UP_KEY } from '../../StepUpState/consts';
7
8
  import { FronteggNativeModule } from '../../../toolkit';
8
9
  export default ((store, api, sharedActions) => {
@@ -14,6 +15,7 @@ export default ((store, api, sharedActions) => {
14
15
  const __requestHostedLoginSilentAuthorize = async () => {
15
16
  var _response;
16
17
  let response;
18
+ restoreTabTenantUnlessSsoCallback(store);
17
19
  if (FronteggNativeModule.isGetTokensAvailable()) {
18
20
  const authApi = api.auth;
19
21
  response = await refreshTokenWithRetry(async () => {
@@ -21,7 +23,21 @@ export default ((store, api, sharedActions) => {
21
23
  return await authApi.silentOAuthRefreshTokenV3(tokens);
22
24
  });
23
25
  } else {
24
- response = await api.auth.silentOAuthRefreshTokenV2();
26
+ try {
27
+ response = await api.auth.silentOAuthRefreshTokenV2();
28
+ } catch (error) {
29
+ var _store$auth$user, _error$errorContext;
30
+ // the tenant requires SSO: hosted login for that tenant, not the plain one the caller falls back to
31
+ if (await actions.__redirectToSsoIfRequired(error)) {
32
+ return;
33
+ }
34
+ 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) {
36
+ throw error;
37
+ }
38
+ // one retry for the tenant of the current token (a cold reload has no user yet: no retry)
39
+ response = await api.auth.silentOAuthRefreshTokenV2(tenantId);
40
+ }
25
41
  }
26
42
  if ((_response = response) != null && _response.user) {
27
43
  await actions.afterAuthenticationStateUpdate(response, {
@@ -32,7 +48,14 @@ export default ((store, api, sharedActions) => {
32
48
  throw new Error(`couldn't refresh user token with oauth service`);
33
49
  }
34
50
  };
35
- const requestHostedLoginAuthorize = async additionalParams => {
51
+
52
+ /**
53
+ * @param options.forceTenantId the tenantId in additionalParams wins over the resolved and the page's tenant
54
+ * (sending the user to that tenant's SSO); other callers keep their custom login tenant
55
+ */
56
+ const requestHostedLoginAuthorize = async (additionalParams, {
57
+ forceTenantId = false
58
+ } = {}) => {
36
59
  var _store$auth$customLog;
37
60
  const {
38
61
  routes,
@@ -79,7 +102,7 @@ export default ((store, api, sharedActions) => {
79
102
  code_challenge_method: 'S256',
80
103
  nonce
81
104
  }, additionalParams);
82
- if (context.tenantResolver) {
105
+ if (context.tenantResolver && !forceTenantId) {
83
106
  var _context$tenantResolv;
84
107
  const resolvedTenantResult = await ((_context$tenantResolv = context.tenantResolver) == null ? void 0 : _context$tenantResolv.call(context));
85
108
  if (resolvedTenantResult != null && resolvedTenantResult.tenant) {
@@ -87,7 +110,7 @@ export default ((store, api, sharedActions) => {
87
110
  }
88
111
  }
89
112
  const tenantId = getSearchParam(TENANT_ID_PARAM_KEY);
90
- if (tenantId) {
113
+ if (tenantId && !forceTenantId) {
91
114
  params['tenantId'] = tenantId;
92
115
  }
93
116
  if (FronteggNativeModule.isAvailable('closeWindow')) {
@@ -37,7 +37,9 @@ declare const _default: (store: FronteggState, api: RestApi, sharedActions: Shar
37
37
  firstTime?: boolean;
38
38
  loginDirectAction?: import("../interfaces").LoginDirectAction;
39
39
  }) => Promise<void>;
40
- __requestHostedLoginAuthorize: (additionalParams?: Record<string, string>) => Promise<void>;
40
+ __requestHostedLoginAuthorize: (additionalParams?: Record<string, string>, { forceTenantId }?: {
41
+ forceTenantId?: boolean;
42
+ }) => Promise<void>;
41
43
  __requestHostedLoginSilentAuthorize: () => Promise<void>;
42
44
  handleHostedLoginCallback: (payload: import("../interfaces").HostedLoginCallback) => Promise<void>;
43
45
  setLoginState: (state: Partial<LoginState>) => void;
@@ -75,5 +77,10 @@ declare const _default: (store: FronteggState, api: RestApi, sharedActions: Shar
75
77
  resetBreachedPassword: (payload: IForgotPassword) => Promise<void>;
76
78
  refreshTokenForSocialLogins: () => Promise<void>;
77
79
  __refreshToken: () => Promise<void>;
80
+ __redirectToSsoForTenant: ({ tenantId, address }: {
81
+ tenantId: string;
82
+ address: string;
83
+ }) => Promise<void>;
84
+ __redirectToSsoIfRequired: (e: unknown) => Promise<boolean>;
78
85
  };
79
86
  export default _default;
@@ -23,17 +23,17 @@ import mfaWithEmailCodeActions from './mfaWithEmailCode.actions';
23
23
  import mfaWithSMSActions from './mfaWithSMS.actions';
24
24
  import mfaWithWebAuthnActions from './mfaWithWebAuthn.actions';
25
25
  import { LoginFlow, LoginStep } from '../interfaces';
26
- import { base64urlDecode, deepResetState, delay, errorHandler, errorTraceId, GTMEventAction, publicKeyCredentialToJSON, refreshTokenWithRetry, reportGTMEvent, retryIfNeeded, withRetryConfig } from '../../../helpers';
26
+ import { base64urlDecode, deepResetState, delay, errorHandler, errorTraceId, GTMEventAction, isSsoEnforcementError, 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, PreLoginAddressType } from '@frontegg/rest-api';
29
+ import { AuthStrategyEnum, ContextHolder, isEnforceSsoOnTenantAccessEnabled, FeatureFlags, 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, isResetPasswordRequired } from '../../helpers';
36
+ import { isMfaRequired, isOnAuthPageRoute, isOnOidcCallbackRoute, 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';
@@ -43,8 +43,114 @@ export default ((store, api, sharedActions) => {
43
43
  const actions = sharedActions;
44
44
  const contextHolder = ContextHolder.for(store.root.appName);
45
45
 
46
+ /**
47
+ * Sends the user to log in through the tenant's SSO and back to the current page afterwards.
48
+ * @protected
49
+ */
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
52
+ let ssoRedirectInFlight = false;
53
+ const __redirectToSsoForTenant = async ({
54
+ tenantId,
55
+ address
56
+ }) => {
57
+ if (ssoRedirectInFlight) {
58
+ return;
59
+ }
60
+ ssoRedirectInFlight = true;
61
+ try {
62
+ await __redirectToSsoForTenantNow({
63
+ tenantId,
64
+ address
65
+ });
66
+ } finally {
67
+ ssoRedirectInFlight = false;
68
+ }
69
+ };
70
+ const __redirectToSsoForTenantNow = async ({
71
+ tenantId,
72
+ address
73
+ }) => {
74
+ var _store$root$urlStrate;
75
+ // the login box shows login's "redirecting to SSO" step instead of the screen that was refused (its spinner and
76
+ // a previous error)
77
+ setLoginState({
78
+ step: LoginStep.redirectToSSO,
79
+ loading: false,
80
+ error: undefined
81
+ });
82
+ // on an auth page (login, tenant chooser, social / magic link / SSO callback) keep the destination the login
83
+ // already saved: the page itself is not one to come back to
84
+ const urlStrategy = (_store$root$urlStrate = store.root.urlStrategy) != null ? _store$root$urlStrate : 'path';
85
+ const isOnAuthPage = isOnAuthPageRoute(urlStrategy, store.auth.routes);
86
+ if (!isOnAuthPage) {
87
+ setAfterAuthRedirectUrlForStepUp(urlStrategy);
88
+ }
89
+ if (store.auth.hostedLoginBox) {
90
+ var _window$localStorage;
91
+ // as loginWithRedirect does, but the SSO tenant wins over the custom login tenant
92
+ (_window$localStorage = window.localStorage) == null ? void 0 : _window$localStorage.removeItem(SHOULD_STEP_UP_KEY);
93
+ await actions.__requestHostedLoginAuthorize({
94
+ prompt: 'login',
95
+ tenantId
96
+ }, {
97
+ forceTenantId: true
98
+ });
99
+ return;
100
+ }
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);
107
+ };
108
+
109
+ /**
110
+ * Sends the user to the tenant's SSO when a login or tenant switch is refused because that tenant requires it
111
+ * (e.g. an invitation into a tenant whose SSO covers the user's email). The SSO login lands in that tenant.
112
+ * @returns true when redirected, so the caller doesn't show the error
113
+ * @protected
114
+ */
115
+ const __redirectToSsoIfRequired = async e => {
116
+ var _e$errorContext;
117
+ if (!isEnforceSsoOnTenantAccessEnabled(store.root.appName) || !isSsoEnforcementError(e) || !((_e$errorContext = e.errorContext) != null && _e$errorContext.ssoRequired)) {
118
+ return false;
119
+ }
120
+ await __redirectToSsoForTenant(e.errorContext);
121
+ return true;
122
+ };
123
+ /**
124
+ * Handles a hosted (OAuth) token refresh refused because the tenant requires SSO.
125
+ * ssoRequired: true sends the user to SSO, like login. ssoRequired: false renews once more for the tenant of the
126
+ * current token, which keeps the session: OAuth doesn't use up the refresh token on this refusal.
127
+ * @returns true when handled, false when the caller should log out
128
+ * @private
129
+ */
130
+ const __handleSsoEnforcementRefreshError = async (e, isSsoEnforcementRetry, refreshForTenant) => {
131
+ var _store$auth$user;
132
+ if (!isEnforceSsoOnTenantAccessEnabled(store.root.appName) || !isSsoEnforcementError(e)) {
133
+ return false;
134
+ }
135
+ if (await __redirectToSsoIfRequired(e)) {
136
+ return true;
137
+ }
138
+ const currentTenantId = (_store$auth$user = store.auth.user) == null ? void 0 : _store$auth$user.tenantId;
139
+ if (isSsoEnforcementRetry || !currentTenantId) {
140
+ return false;
141
+ }
142
+ if (contextHolder.isSessionPerTenantEnabled()) {
143
+ setTabTenantInSessionStorage(currentTenantId);
144
+ }
145
+ await refreshForTenant(currentTenantId);
146
+ return true;
147
+ };
148
+
46
149
  /** @private */
47
- const __refreshTokenHosted = async () => {
150
+ const __refreshTokenHosted = async ({
151
+ tenantId,
152
+ isSsoEnforcementRetry = false
153
+ } = {}) => {
48
154
  const user = store.auth.user;
49
155
  if (!(user != null && user.refreshToken)) {
50
156
  contextHolder.setAccessToken(null);
@@ -56,7 +162,7 @@ export default ((store, api, sharedActions) => {
56
162
  return;
57
163
  }
58
164
  try {
59
- const tabTenantId = getTabTenantFromSessionStorage(store.root.appName);
165
+ const tabTenantId = tenantId != null ? tenantId : getTabTenantFromSessionStorage(store.root.appName);
60
166
  const body = _extends({
61
167
  grant_type: 'refresh_token',
62
168
  refresh_token: user == null ? void 0 : user.refreshToken
@@ -71,6 +177,13 @@ export default ((store, api, sharedActions) => {
71
177
  isAuthenticated: true
72
178
  });
73
179
  } catch (e) {
180
+ const retry = tenantId => __refreshTokenHosted({
181
+ tenantId,
182
+ isSsoEnforcementRetry: true
183
+ });
184
+ if (await __handleSsoEnforcementRefreshError(e, isSsoEnforcementRetry, retry)) {
185
+ return;
186
+ }
74
187
  contextHolder.setAccessToken(null);
75
188
  contextHolder.setUser(null);
76
189
  actions.setAuthState({
@@ -132,6 +245,11 @@ export default ((store, api, sharedActions) => {
132
245
  });
133
246
  }
134
247
  } 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))) {
251
+ return;
252
+ }
135
253
  contextHolder.setAccessToken(null);
136
254
  contextHolder.setUser(null);
137
255
  actions.setAuthState({
@@ -142,7 +260,9 @@ export default ((store, api, sharedActions) => {
142
260
  };
143
261
 
144
262
  /** @private */
145
- const __refreshTokenWithNativeTokens = async () => {
263
+ const __refreshTokenWithNativeTokens = async ({
264
+ isSsoEnforcementRetry = false
265
+ } = {}) => {
146
266
  try {
147
267
  const authApi = api.auth;
148
268
  const response = await refreshTokenWithRetry(async () => {
@@ -156,6 +276,13 @@ export default ((store, api, sharedActions) => {
156
276
  isAuthenticated: true
157
277
  });
158
278
  } 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
+ }
159
286
  contextHolder.setAccessToken(null);
160
287
  contextHolder.setUser(null);
161
288
  actions.setAuthState({
@@ -167,6 +294,7 @@ export default ((store, api, sharedActions) => {
167
294
 
168
295
  /** @protected */
169
296
  const __refreshToken = async () => {
297
+ restoreTabTenantUnlessSsoCallback(store);
170
298
  if (FronteggNativeModule.isGetTokensAvailable()) {
171
299
  await __refreshTokenWithNativeTokens();
172
300
  return;
@@ -189,9 +317,9 @@ export default ((store, api, sharedActions) => {
189
317
  * @private
190
318
  */
191
319
  const __handleUnnecessaryEntitlementsUpdate = async updatedUser => {
192
- var _store$auth$user;
320
+ var _store$auth$user2;
193
321
  // @ts-ignore TODO: fix entitlements state type
194
- const oldEntitlements = (_store$auth$user = store.auth.user) == null ? void 0 : _store$auth$user.entitlements;
322
+ const oldEntitlements = (_store$auth$user2 = store.auth.user) == null ? void 0 : _store$auth$user2.entitlements;
195
323
  if (isEntitlementsDeeplyEqual(oldEntitlements, updatedUser == null ? void 0 : updatedUser.entitlements)) {
196
324
  // set the previous entitlements object
197
325
  return _extends({}, updatedUser, {
@@ -668,6 +796,9 @@ export default ((store, api, sharedActions) => {
668
796
  } catch (e) {
669
797
  contextHolder.setAccessToken(null);
670
798
  contextHolder.setUser(null);
799
+ if (await __redirectToSsoIfRequired(e)) {
800
+ return;
801
+ }
671
802
  callback == null ? void 0 : callback(false, e);
672
803
  setLoginState({
673
804
  email,
@@ -963,6 +1094,9 @@ export default ((store, api, sharedActions) => {
963
1094
  callback == null ? void 0 : callback(true);
964
1095
  }
965
1096
  } catch (e) {
1097
+ if (await __redirectToSsoIfRequired(e)) {
1098
+ return;
1099
+ }
966
1100
  setLoginState({
967
1101
  error: errorHandler(e, 'Failed to authenticate')
968
1102
  });
@@ -1099,6 +1233,9 @@ export default ((store, api, sharedActions) => {
1099
1233
  }
1100
1234
  // callback?.(true);
1101
1235
  } catch (e) {
1236
+ if (await __redirectToSsoIfRequired(e)) {
1237
+ return;
1238
+ }
1102
1239
  setLoginState({
1103
1240
  error: errorHandler(e)
1104
1241
  });
@@ -1259,6 +1396,9 @@ export default ((store, api, sharedActions) => {
1259
1396
  });
1260
1397
  callback == null ? void 0 : callback(true);
1261
1398
  } catch (e) {
1399
+ if (await __redirectToSsoIfRequired(e)) {
1400
+ return;
1401
+ }
1262
1402
  setLoginState({
1263
1403
  loading: false,
1264
1404
  error: errorHandler(e)
@@ -1282,6 +1422,9 @@ export default ((store, api, sharedActions) => {
1282
1422
  });
1283
1423
  callback == null ? void 0 : callback(true);
1284
1424
  } catch (e) {
1425
+ if (await __redirectToSsoIfRequired(e)) {
1426
+ return;
1427
+ }
1285
1428
  setLoginState({
1286
1429
  loading: false,
1287
1430
  error: errorHandler(e)
@@ -1332,6 +1475,9 @@ export default ((store, api, sharedActions) => {
1332
1475
  });
1333
1476
  callback == null ? void 0 : callback(true);
1334
1477
  } catch (e) {
1478
+ if (await __redirectToSsoIfRequired(e)) {
1479
+ return;
1480
+ }
1335
1481
  setLoginState({
1336
1482
  loading: false,
1337
1483
  error: errorHandler(e)
@@ -1388,7 +1534,10 @@ export default ((store, api, sharedActions) => {
1388
1534
  };
1389
1535
 
1390
1536
  /** @private */
1391
- const __refreshTokenHostedSocialLogins = async () => {
1537
+ const __refreshTokenHostedSocialLogins = async ({
1538
+ tenantId,
1539
+ isSsoEnforcementRetry = false
1540
+ } = {}) => {
1392
1541
  const {
1393
1542
  user: currentUser
1394
1543
  } = store.auth;
@@ -1402,7 +1551,7 @@ export default ((store, api, sharedActions) => {
1402
1551
  return;
1403
1552
  }
1404
1553
  try {
1405
- const tabTenantId = getTabTenantFromSessionStorage(store.root.appName);
1554
+ const tabTenantId = tenantId != null ? tenantId : getTabTenantFromSessionStorage(store.root.appName);
1406
1555
  const body = _extends({
1407
1556
  grant_type: 'refresh_token',
1408
1557
  refresh_token: currentUser == null ? void 0 : currentUser.refreshToken
@@ -1417,6 +1566,13 @@ export default ((store, api, sharedActions) => {
1417
1566
  isAuthenticated: true
1418
1567
  });
1419
1568
  } catch (e) {
1569
+ const retry = tenantId => __refreshTokenHostedSocialLogins({
1570
+ tenantId,
1571
+ isSsoEnforcementRetry: true
1572
+ });
1573
+ if (await __handleSsoEnforcementRefreshError(e, isSsoEnforcementRetry, retry)) {
1574
+ return;
1575
+ }
1420
1576
  contextHolder.setAccessToken(null);
1421
1577
  contextHolder.setUser(null);
1422
1578
  actions.setAuthState({
@@ -1487,6 +1643,9 @@ export default ((store, api, sharedActions) => {
1487
1643
  }
1488
1644
  }
1489
1645
  } catch (e) {
1646
+ if (await __redirectToSsoIfRequired(e)) {
1647
+ return;
1648
+ }
1490
1649
  contextHolder.setAccessToken(null);
1491
1650
  contextHolder.setUser(null);
1492
1651
  actions.setAuthState({
@@ -1533,6 +1692,8 @@ export default ((store, api, sharedActions) => {
1533
1692
  resetBreachedPassword,
1534
1693
  refreshTokenForSocialLogins,
1535
1694
  // protected
1536
- __refreshToken
1695
+ __refreshToken,
1696
+ __redirectToSsoForTenant,
1697
+ __redirectToSsoIfRequired
1537
1698
  }, 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));
1538
1699
  });
@@ -145,6 +145,9 @@ export default ((store, api, sharedActions) => {
145
145
  }
146
146
  return await __postLoginMfaAuthenticator(isAuthenticated, user, callback);
147
147
  } catch (e) {
148
+ if (await actions.__redirectToSsoIfRequired(e)) {
149
+ return;
150
+ }
148
151
  setLoadingAction({
149
152
  loading: false,
150
153
  error: errorHandler(e)
@@ -68,6 +68,9 @@ export default ((store, api, sharedActions) => {
68
68
  });
69
69
  callback == null ? void 0 : callback(true);
70
70
  } catch (e) {
71
+ if (await actions.__redirectToSsoIfRequired(e)) {
72
+ return;
73
+ }
71
74
  setLoadingAction({
72
75
  loading: false,
73
76
  error: errorHandler(e)
@@ -73,6 +73,9 @@ export default ((store, api, sharedActions) => {
73
73
  });
74
74
  callback == null ? void 0 : callback(true);
75
75
  } catch (e) {
76
+ if (await actions.__redirectToSsoIfRequired(e)) {
77
+ return;
78
+ }
76
79
  setLoadingAction({
77
80
  loading: false,
78
81
  error: errorHandler(e)
@@ -87,6 +87,9 @@ export default ((store, api, sharedActions) => {
87
87
  });
88
88
  callback == null ? void 0 : callback(true);
89
89
  } catch (e) {
90
+ if (await actions.__redirectToSsoIfRequired(e)) {
91
+ return;
92
+ }
90
93
  setLoadingAction({
91
94
  loading: false,
92
95
  error: errorHandler(e)
@@ -169,6 +169,9 @@ export default ((store, api, sharedActions) => {
169
169
  }
170
170
  callback == null ? void 0 : callback(mfaState.recoveryCode);
171
171
  } catch (e) {
172
+ if (await actions.__redirectToSsoIfRequired(e)) {
173
+ return;
174
+ }
172
175
  setMfaState({
173
176
  loading: false,
174
177
  error: errorHandler(e)
@@ -157,6 +157,9 @@ export default ((store, api, sharedActions) => {
157
157
  loading: false
158
158
  });
159
159
  } catch (e) {
160
+ if (await actions.__redirectToSsoIfRequired(e)) {
161
+ return;
162
+ }
160
163
  setSocialLoginsState({
161
164
  loading: false,
162
165
  error: errorHandler(e, 'Failed to authenticate'),
@@ -95,6 +95,11 @@ export default ((store, api, sharedActions) => {
95
95
  callback == null ? void 0 : callback(true);
96
96
  } catch (e) {
97
97
  callback == null ? void 0 : callback(false);
98
+
99
+ // the session's tenant requires SSO: its SSO login steps the user up, instead of logging them out
100
+ if (await actions.__redirectToSsoIfRequired(e)) {
101
+ return;
102
+ }
98
103
  await __handleStepUpError(e);
99
104
  }
100
105
  };
@@ -22,8 +22,9 @@ export interface UrlDerivedStepUp {
22
22
  export declare const getUrlDerivedStepUp: () => UrlDerivedStepUp;
23
23
  /**
24
24
  * Set the url and query params in the local storage FRONTEGG_AFTER_AUTH_REDIRECT_URL value
25
+ * @param urlStrategy 'hash' saves the route in the hash, which is the page the app shows
25
26
  */
26
- export declare function setAfterAuthRedirectUrlForStepUp(): void;
27
+ export declare function setAfterAuthRedirectUrlForStepUp(urlStrategy?: 'path' | 'hash'): void;
27
28
  /**
28
29
  * Redirects to the step up url with the max age param and set the redirect url in the local storage
29
30
  * The redirect url will be used after the step up flow is done
@@ -51,9 +51,10 @@ export const getUrlDerivedStepUp = () => {
51
51
 
52
52
  /**
53
53
  * Set the url and query params in the local storage FRONTEGG_AFTER_AUTH_REDIRECT_URL value
54
+ * @param urlStrategy 'hash' saves the route in the hash, which is the page the app shows
54
55
  */
55
- export function setAfterAuthRedirectUrlForStepUp() {
56
- const encodedRedirectUrl = window.location.pathname + window.location.search;
56
+ export function setAfterAuthRedirectUrlForStepUp(urlStrategy = 'path') {
57
+ const encodedRedirectUrl = urlStrategy === 'hash' ? window.location.hash.substring(1) || '/' : window.location.pathname + window.location.search;
57
58
  window.localStorage.setItem(FRONTEGG_AFTER_AUTH_REDIRECT_URL, encodedRedirectUrl);
58
59
  }
59
60
 
@@ -1,6 +1,6 @@
1
1
  import { initialState } from './state';
2
2
  import { deepResetState } from '../../helpers';
3
- import { getCurrentUserTenantsFunction } from '@frontegg/rest-api';
3
+ import { ContextHolder, getCurrentUserTenantsFunction, setTabTenantForSsoSwitch } from '@frontegg/rest-api';
4
4
  export default ((store, api, sharedActions) => {
5
5
  const actions = sharedActions;
6
6
  const setTenantsState = state => {
@@ -51,6 +51,13 @@ export default ((store, api, sharedActions) => {
51
51
  }) : actions.setAuthState({
52
52
  isLoading: false
53
53
  });
54
+ if (await actions.__redirectToSsoIfRequired(e)) {
55
+ // the tab comes back from SSO in the target tenant, not the one the failed switch restored
56
+ if (ContextHolder.for(store.root.appName).isSessionPerTenantEnabled()) {
57
+ setTabTenantForSsoSwitch(tenantId);
58
+ }
59
+ return;
60
+ }
54
61
  callback == null ? void 0 : callback(false, e);
55
62
  }
56
63
  };
package/auth/helpers.d.ts CHANGED
@@ -8,5 +8,20 @@ export declare const isMfaRequired: (user: ILoginResponse, appName: string) => b
8
8
  export declare const isResetPasswordRequired: (user: ILoginResponse, appName: string) => boolean;
9
9
  export declare const mapMetaDataObjectToActions: (obj: any, path?: string[]) => CommitChangeDto[];
10
10
  export declare const getUri: (urlStrategy: FronteggState["root"]["urlStrategy"]) => string;
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.
14
+ */
15
+ export declare const isOnOidcCallbackRoute: (store: FronteggState) => boolean;
16
+ /**
17
+ * After a switch refused for SSO the tab points at the SSO tenant. Its SSO login comes back on a callback route and
18
+ * keeps it; any other page load means the user didn't finish that login, so the tab goes back to its tenant.
19
+ */
20
+ export declare const restoreTabTenantUnlessSsoCallback: (store: FronteggState) => void;
11
21
  export declare const prepareIdentifiersForSignup: (payload: ISignUpUser) => ISignUpUser;
22
+ /**
23
+ * Whether the current route is an auth page (login, tenant chooser, social / magic link / SSO callback), not one to come
24
+ * back to. Unlike isAuthRoute: after-login / sign-up destinations don't count, and endsWith lets a basename prefix it.
25
+ */
26
+ export declare const isOnAuthPageRoute: (urlStrategy: FronteggState["root"]["urlStrategy"], routes?: Partial<AuthPageRoutes>) => boolean;
12
27
  export declare const isAuthRoute: (path: string, routes?: Partial<AuthPageRoutes>) => boolean;