@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.
- package/auth/LoginState/actions/hostedLoginAuthorize.actions.js +7 -3
- package/auth/LoginState/actions/index.d.ts +6 -5
- package/auth/LoginState/actions/index.js +44 -39
- package/auth/LoginState/helpers.d.ts +2 -0
- package/auth/LoginState/helpers.js +3 -0
- package/auth/StepUpState/actions/generateStepUpSession.actions.js +1 -2
- package/auth/TenantsState/actions.js +5 -2
- package/auth/helpers.d.ts +5 -4
- package/auth/helpers.js +17 -10
- package/index.js +1 -1
- package/mocks/auth-mocks/loginActions.mocks.d.ts +0 -4
- package/node/auth/LoginState/actions/hostedLoginAuthorize.actions.js +6 -2
- package/node/auth/LoginState/actions/index.js +41 -36
- package/node/auth/LoginState/helpers.js +5 -1
- package/node/auth/StepUpState/actions/generateStepUpSession.actions.js +1 -2
- package/node/auth/TenantsState/actions.js +4 -1
- package/node/auth/helpers.js +19 -12
- package/node/index.js +1 -1
- package/package.json +2 -2
|
@@ -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 (!
|
|
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
|
|
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,
|
|
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
|
-
}
|
|
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
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
249
|
-
// the
|
|
250
|
-
|
|
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
|
-
|
|
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
|
|
13
|
-
* a reason to go
|
|
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
|
|
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
|
|
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
|
|
71
|
-
* a reason to go
|
|
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
|
|
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
|
-
|
|
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 (!
|
|
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
|
-
|
|
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
|
@@ -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,
|
|
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
|
-
}
|
|
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
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
256
|
-
// the
|
|
257
|
-
|
|
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
|
-
|
|
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
|
}
|
package/node/auth/helpers.js
CHANGED
|
@@ -9,13 +9,13 @@ var _exportNames = {
|
|
|
9
9
|
isResetPasswordRequired: true,
|
|
10
10
|
mapMetaDataObjectToActions: true,
|
|
11
11
|
getUri: true,
|
|
12
|
-
|
|
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.
|
|
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
|
|
126
|
-
* a reason to go
|
|
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
|
|
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
|
-
|
|
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.
|
|
141
|
-
const restoreTabTenantUnlessSsoCallback = store => {
|
|
143
|
+
exports.isOnLoginCodeCallbackRoute = isOnLoginCodeCallbackRoute;
|
|
144
|
+
const restoreTabTenantUnlessSsoCallback = async store => {
|
|
142
145
|
var _store$root$urlStrate2;
|
|
143
|
-
if (!
|
|
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
|
-
|
|
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
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontegg/redux-store",
|
|
3
|
-
"version": "7.131.0
|
|
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
|
|
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",
|