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