@frontegg/redux-store 7.121.0 → 7.122.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/mfaRequiredState.actions.js +1 -0
- package/auth/LoginState/actions/mfaWithAuthenticator.actions.js +2 -0
- package/auth/LoginState/actions/mfaWithEmailCode.actions.js +2 -0
- package/auth/LoginState/actions/mfaWithSMS.actions.js +2 -0
- package/auth/LoginState/actions/mfaWithWebAuthn.actions.js +7 -0
- package/auth/LoginState/helpers/persistPreferredMfaMethod.d.ts +12 -0
- package/auth/LoginState/helpers/persistPreferredMfaMethod.js +20 -0
- package/auth/LoginState/helpers.js +3 -2
- package/auth/LoginState/preferredMfaMethod.d.ts +53 -0
- package/auth/LoginState/preferredMfaMethod.js +151 -0
- package/auth/MfaState/actions.js +4 -2
- package/auth/MfaState/interfaces.d.ts +2 -0
- package/auth/index.d.ts +1 -0
- package/auth/index.js +1 -0
- package/index.js +1 -1
- package/node/auth/LoginState/actions/mfaRequiredState.actions.js +1 -0
- package/node/auth/LoginState/actions/mfaWithAuthenticator.actions.js +2 -0
- package/node/auth/LoginState/actions/mfaWithEmailCode.actions.js +2 -0
- package/node/auth/LoginState/actions/mfaWithSMS.actions.js +2 -0
- package/node/auth/LoginState/actions/mfaWithWebAuthn.actions.js +7 -0
- package/node/auth/LoginState/helpers/persistPreferredMfaMethod.js +27 -0
- package/node/auth/LoginState/helpers.js +2 -1
- package/node/auth/LoginState/preferredMfaMethod.js +166 -0
- package/node/auth/MfaState/actions.js +4 -2
- package/node/auth/index.js +12 -0
- package/node/index.js +1 -1
- package/package.json +2 -2
|
@@ -35,6 +35,7 @@ export default ((store, api, _sharedActions) => {
|
|
|
35
35
|
Object.assign(mfaState, {
|
|
36
36
|
step: getMfaStepForNotEnrolledUsers(user.mfaStrategies),
|
|
37
37
|
qrCode: user.qrCode,
|
|
38
|
+
secret: user.secret,
|
|
38
39
|
recoveryCode: user.recoveryCode,
|
|
39
40
|
loading: false,
|
|
40
41
|
mfaToken: user.mfaToken,
|
|
@@ -4,6 +4,7 @@ const _excluded = ["callback"];
|
|
|
4
4
|
import { LoginFlow, LoginStep } from '../interfaces';
|
|
5
5
|
import { errorHandler, GTMEventAction, reportGTMEvent } from '../../../helpers';
|
|
6
6
|
import { isResetPasswordRequired, shouldShowPasswordRotationPromptFunc } from '../../helpers';
|
|
7
|
+
import { persistPreferredMfaMethod } from '../helpers/persistPreferredMfaMethod';
|
|
7
8
|
export default ((store, api, sharedActions) => {
|
|
8
9
|
const actions = sharedActions;
|
|
9
10
|
|
|
@@ -113,6 +114,7 @@ export default ((store, api, sharedActions) => {
|
|
|
113
114
|
tenants = [],
|
|
114
115
|
activeTenant
|
|
115
116
|
} = await api.auth.loginWithMfaV2(loginWithMfaPayload);
|
|
117
|
+
persistPreferredMfaMethod(store, 'authenticatorApp');
|
|
116
118
|
const isAuthenticated = !!user.accessToken;
|
|
117
119
|
const additionalUpdate = await __buildPostAuthStateUpdate(tenants, isAuthenticated, isStepUp);
|
|
118
120
|
await actions.afterAuthenticationStateUpdate({
|
|
@@ -3,6 +3,7 @@ const _excluded = ["callback"],
|
|
|
3
3
|
_excluded2 = ["callback"];
|
|
4
4
|
import { MFAStep } from '../../MfaState/interfaces';
|
|
5
5
|
import { errorHandler } from '../../../helpers';
|
|
6
|
+
import { persistPreferredMfaMethod } from '../helpers/persistPreferredMfaMethod';
|
|
6
7
|
export default ((store, api, sharedActions) => {
|
|
7
8
|
const actions = sharedActions;
|
|
8
9
|
/**
|
|
@@ -59,6 +60,7 @@ export default ((store, api, sharedActions) => {
|
|
|
59
60
|
});
|
|
60
61
|
try {
|
|
61
62
|
const data = await api.auth.verifyMFAEmailCodeV2(payload);
|
|
63
|
+
persistPreferredMfaMethod(store, 'email');
|
|
62
64
|
await actions.handleVerifyMFAResponse(data);
|
|
63
65
|
setLoadingAction({
|
|
64
66
|
loading: false,
|
|
@@ -3,6 +3,7 @@ const _excluded = ["callback", "deviceId"],
|
|
|
3
3
|
_excluded2 = ["callback", "deviceId"];
|
|
4
4
|
import { MFAStep } from '../../MfaState/interfaces';
|
|
5
5
|
import { errorHandler } from '../../../helpers';
|
|
6
|
+
import { persistPreferredMfaMethod } from '../helpers/persistPreferredMfaMethod';
|
|
6
7
|
export default ((store, api, sharedActions) => {
|
|
7
8
|
const actions = sharedActions;
|
|
8
9
|
|
|
@@ -64,6 +65,7 @@ export default ((store, api, sharedActions) => {
|
|
|
64
65
|
});
|
|
65
66
|
try {
|
|
66
67
|
const data = await api.auth.verifyMFASMSForLoginV2(deviceId, payload);
|
|
68
|
+
persistPreferredMfaMethod(store, 'sms');
|
|
67
69
|
await actions.handleVerifyMFAResponse(data, isStepUp);
|
|
68
70
|
setLoadingAction({
|
|
69
71
|
loading: false,
|
|
@@ -3,6 +3,8 @@ import _objectWithoutPropertiesLoose from "@babel/runtime/helpers/esm/objectWith
|
|
|
3
3
|
const _excluded = ["callback", "deviceId"],
|
|
4
4
|
_excluded2 = ["callback", "deviceId"];
|
|
5
5
|
import { base64urlDecode, errorHandler, publicKeyCredentialToJSON } from '../../../helpers';
|
|
6
|
+
import { findWebAuthnDeviceTypeById, getPreferredMfaMethodFromWebAuthnDeviceType } from '../preferredMfaMethod';
|
|
7
|
+
import { persistPreferredMfaMethod } from '../helpers/persistPreferredMfaMethod';
|
|
6
8
|
export default ((store, api, sharedActions) => {
|
|
7
9
|
const actions = sharedActions;
|
|
8
10
|
|
|
@@ -73,6 +75,11 @@ export default ((store, api, sharedActions) => {
|
|
|
73
75
|
const data = await api.auth.verifyMFAWebAuthnForLoginV2(deviceId, _extends({}, payload, {
|
|
74
76
|
options: publicKey
|
|
75
77
|
}));
|
|
78
|
+
const mfaDevices = isStepUp ? store.auth.stepUpState.mfaDevices : store.auth.mfaState.mfaDevices;
|
|
79
|
+
const deviceType = findWebAuthnDeviceTypeById(mfaDevices, deviceId);
|
|
80
|
+
if (deviceType) {
|
|
81
|
+
persistPreferredMfaMethod(store, getPreferredMfaMethodFromWebAuthnDeviceType(deviceType));
|
|
82
|
+
}
|
|
76
83
|
await actions.handleVerifyMFAResponse(data, isStepUp);
|
|
77
84
|
setLoadingAction({
|
|
78
85
|
loading: false,
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { FronteggState } from '../../../interfaces';
|
|
2
|
+
import { PreferredMfaMethod } from '../preferredMfaMethod';
|
|
3
|
+
/**
|
|
4
|
+
* Resolve a stable application key for preferred-MFA session storage.
|
|
5
|
+
* Prefers appId (multi-app), then clientId, then appName.
|
|
6
|
+
*/
|
|
7
|
+
export declare const getPreferredMfaAppKey: (store: FronteggState) => string;
|
|
8
|
+
/**
|
|
9
|
+
* Persist preferred MFA method after a successful challenge.
|
|
10
|
+
* Always writes; themeOptions.rememberLastMfaFactor only gates whether it is applied.
|
|
11
|
+
*/
|
|
12
|
+
export declare const persistPreferredMfaMethod: (store: FronteggState, method: PreferredMfaMethod) => void;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ContextHolder } from '@frontegg/rest-api';
|
|
2
|
+
import { setPreferredMfaMethod } from '../preferredMfaMethod';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Resolve a stable application key for preferred-MFA session storage.
|
|
6
|
+
* Prefers appId (multi-app), then clientId, then appName.
|
|
7
|
+
*/
|
|
8
|
+
export const getPreferredMfaAppKey = store => {
|
|
9
|
+
const appName = store.root.appName;
|
|
10
|
+
const context = ContextHolder.for(appName).getContext();
|
|
11
|
+
return context.appId || context.clientId || appName || 'default';
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Persist preferred MFA method after a successful challenge.
|
|
16
|
+
* Always writes; themeOptions.rememberLastMfaFactor only gates whether it is applied.
|
|
17
|
+
*/
|
|
18
|
+
export const persistPreferredMfaMethod = (store, method) => {
|
|
19
|
+
setPreferredMfaMethod(getPreferredMfaAppKey(store), method);
|
|
20
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ContextHolder, MFAStrategyEnum } from '@frontegg/rest-api';
|
|
1
|
+
import { ContextHolder, MFAStrategyEnum, WebAuthnDeviceType } from '@frontegg/rest-api';
|
|
2
2
|
import { MFAStep } from '../MfaState/interfaces';
|
|
3
3
|
export const isAbsoluteUrl = path => {
|
|
4
4
|
try {
|
|
@@ -76,7 +76,8 @@ export const getSearchParam = key => {
|
|
|
76
76
|
};
|
|
77
77
|
export const TENANT_ID_PARAM_KEY = 'tenantId';
|
|
78
78
|
export const getNumberOfMfaDevices = mfaDevices => {
|
|
79
|
-
|
|
79
|
+
// Count platform / cross-platform separately — matches the MFA picker options.
|
|
80
|
+
const numberOfWebAuthnDevices = Number(mfaDevices.webauthn.some(device => device.deviceType === WebAuthnDeviceType.Platform)) + Number(mfaDevices.webauthn.some(device => device.deviceType === WebAuthnDeviceType.CrossPlatform));
|
|
80
81
|
const numberOfPhoneDevices = +Boolean(mfaDevices.phones.length);
|
|
81
82
|
const numberOfAuthenticators = +Boolean(mfaDevices.authenticators.length);
|
|
82
83
|
const numberOfEmails = +Boolean(mfaDevices.emails.length);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { UserMFADevicesResponse, WebAuthnDeviceType } from '@frontegg/rest-api';
|
|
2
|
+
import { MFAStep } from '../MfaState/interfaces';
|
|
3
|
+
export type PreferredMfaMethod = 'sms' | 'email' | 'authenticatorApp' | 'webauthnPlatform' | 'webauthnCrossPlatform';
|
|
4
|
+
export type PreferredMfaResolution = {
|
|
5
|
+
type: 'step';
|
|
6
|
+
step: MFAStep;
|
|
7
|
+
} | {
|
|
8
|
+
type: 'webauthn';
|
|
9
|
+
deviceType: WebAuthnDeviceType;
|
|
10
|
+
} | {
|
|
11
|
+
type: 'none';
|
|
12
|
+
};
|
|
13
|
+
export declare const getPreferredMfaStorageKey: (appKey: string) => string;
|
|
14
|
+
export declare const isPreferredMfaMethod: (value: unknown) => value is PreferredMfaMethod;
|
|
15
|
+
/**
|
|
16
|
+
* Persist the last successful MFA method for this browser session, keyed by application.
|
|
17
|
+
*/
|
|
18
|
+
export declare const setPreferredMfaMethod: (appKey: string, method: PreferredMfaMethod) => void;
|
|
19
|
+
/**
|
|
20
|
+
* Read the remembered MFA method for this browser session, if any.
|
|
21
|
+
*/
|
|
22
|
+
export declare const getPreferredMfaMethod: (appKey: string) => PreferredMfaMethod | null;
|
|
23
|
+
export declare const clearPreferredMfaMethod: (appKey: string) => void;
|
|
24
|
+
/**
|
|
25
|
+
* Map a stored preferred MFA method to a concrete MFA step / WebAuthn action,
|
|
26
|
+
* only when that method is still enrolled and available.
|
|
27
|
+
*/
|
|
28
|
+
export declare const resolvePreferredMfa: (preferred: PreferredMfaMethod | null | undefined, mfaDevices?: UserMFADevicesResponse) => PreferredMfaResolution;
|
|
29
|
+
export declare const getPreferredMfaMethodFromWebAuthnDeviceType: (deviceType: WebAuthnDeviceType) => PreferredMfaMethod;
|
|
30
|
+
export declare const findWebAuthnDeviceTypeById: (mfaDevices: UserMFADevicesResponse | undefined, deviceId: string) => WebAuthnDeviceType | undefined;
|
|
31
|
+
export type PreferredMfaApplyAction = {
|
|
32
|
+
action: 'setStep';
|
|
33
|
+
step: MFAStep;
|
|
34
|
+
} | {
|
|
35
|
+
action: 'webauthn';
|
|
36
|
+
deviceId: string;
|
|
37
|
+
} | {
|
|
38
|
+
action: 'none';
|
|
39
|
+
};
|
|
40
|
+
export interface ApplyPreferredMfaFactorInput {
|
|
41
|
+
rememberLastMfaFactor: boolean;
|
|
42
|
+
alreadyApplied: boolean;
|
|
43
|
+
step: MFAStep;
|
|
44
|
+
preferred: PreferredMfaMethod | null;
|
|
45
|
+
mfaDevices: UserMFADevicesResponse | undefined;
|
|
46
|
+
mfaWebAuthnPlatformDeviceId?: string;
|
|
47
|
+
mfaWebAuthnCrossPlatformDeviceId?: string;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Pure decision for whether / how to apply a stored preferred MFA factor.
|
|
51
|
+
* Used by the login-box MFAFlow when themeOptions.rememberLastMfaFactor is on.
|
|
52
|
+
*/
|
|
53
|
+
export declare const getPreferredMfaApplyAction: ({ rememberLastMfaFactor, alreadyApplied, step, preferred, mfaDevices, mfaWebAuthnPlatformDeviceId, mfaWebAuthnCrossPlatformDeviceId, }: ApplyPreferredMfaFactorInput) => PreferredMfaApplyAction;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { WebAuthnDeviceType } from '@frontegg/rest-api';
|
|
2
|
+
import { MFAStep } from '../MfaState/interfaces';
|
|
3
|
+
const STORAGE_KEY_PREFIX = 'frontegg-preferred-mfa-method';
|
|
4
|
+
const PREFERRED_MFA_METHODS = new Set(['sms', 'email', 'authenticatorApp', 'webauthnPlatform', 'webauthnCrossPlatform']);
|
|
5
|
+
export const getPreferredMfaStorageKey = appKey => `${STORAGE_KEY_PREFIX}:${appKey}`;
|
|
6
|
+
export const isPreferredMfaMethod = value => typeof value === 'string' && PREFERRED_MFA_METHODS.has(value);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Persist the last successful MFA method for this browser session, keyed by application.
|
|
10
|
+
*/
|
|
11
|
+
export const setPreferredMfaMethod = (appKey, method) => {
|
|
12
|
+
if (typeof window === 'undefined') {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
window.sessionStorage.setItem(getPreferredMfaStorageKey(appKey), method);
|
|
17
|
+
} catch {
|
|
18
|
+
// sessionStorage may be unavailable (private mode / quota) — ignore
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Read the remembered MFA method for this browser session, if any.
|
|
24
|
+
*/
|
|
25
|
+
export const getPreferredMfaMethod = appKey => {
|
|
26
|
+
if (typeof window === 'undefined') {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const value = window.sessionStorage.getItem(getPreferredMfaStorageKey(appKey));
|
|
31
|
+
return isPreferredMfaMethod(value) ? value : null;
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
export const clearPreferredMfaMethod = appKey => {
|
|
37
|
+
if (typeof window === 'undefined') {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
window.sessionStorage.removeItem(getPreferredMfaStorageKey(appKey));
|
|
42
|
+
} catch {
|
|
43
|
+
// ignore
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const hasPhoneDevices = mfaDevices => mfaDevices.phones.length > 0;
|
|
47
|
+
const hasEmails = mfaDevices => mfaDevices.emails.length > 0;
|
|
48
|
+
const hasAuthenticators = mfaDevices => mfaDevices.authenticators.length > 0;
|
|
49
|
+
const hasWebAuthnDevice = (mfaDevices, deviceType) => mfaDevices.webauthn.some(device => device.deviceType === deviceType);
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Map a stored preferred MFA method to a concrete MFA step / WebAuthn action,
|
|
53
|
+
* only when that method is still enrolled and available.
|
|
54
|
+
*/
|
|
55
|
+
export const resolvePreferredMfa = (preferred, mfaDevices) => {
|
|
56
|
+
if (!preferred || !mfaDevices) {
|
|
57
|
+
return {
|
|
58
|
+
type: 'none'
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
switch (preferred) {
|
|
62
|
+
case 'sms':
|
|
63
|
+
return hasPhoneDevices(mfaDevices) ? {
|
|
64
|
+
type: 'step',
|
|
65
|
+
step: MFAStep.smsVerifyCode
|
|
66
|
+
} : {
|
|
67
|
+
type: 'none'
|
|
68
|
+
};
|
|
69
|
+
case 'email':
|
|
70
|
+
return hasEmails(mfaDevices) ? {
|
|
71
|
+
type: 'step',
|
|
72
|
+
step: MFAStep.emailVerifyCode
|
|
73
|
+
} : {
|
|
74
|
+
type: 'none'
|
|
75
|
+
};
|
|
76
|
+
case 'authenticatorApp':
|
|
77
|
+
return hasAuthenticators(mfaDevices) ? {
|
|
78
|
+
type: 'step',
|
|
79
|
+
step: MFAStep.authenticatorApp
|
|
80
|
+
} : {
|
|
81
|
+
type: 'none'
|
|
82
|
+
};
|
|
83
|
+
case 'webauthnPlatform':
|
|
84
|
+
return hasWebAuthnDevice(mfaDevices, WebAuthnDeviceType.Platform) ? {
|
|
85
|
+
type: 'webauthn',
|
|
86
|
+
deviceType: WebAuthnDeviceType.Platform
|
|
87
|
+
} : {
|
|
88
|
+
type: 'none'
|
|
89
|
+
};
|
|
90
|
+
case 'webauthnCrossPlatform':
|
|
91
|
+
return hasWebAuthnDevice(mfaDevices, WebAuthnDeviceType.CrossPlatform) ? {
|
|
92
|
+
type: 'webauthn',
|
|
93
|
+
deviceType: WebAuthnDeviceType.CrossPlatform
|
|
94
|
+
} : {
|
|
95
|
+
type: 'none'
|
|
96
|
+
};
|
|
97
|
+
default:
|
|
98
|
+
{
|
|
99
|
+
const _exhaustive = preferred;
|
|
100
|
+
return _exhaustive;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
export const getPreferredMfaMethodFromWebAuthnDeviceType = deviceType => {
|
|
105
|
+
return deviceType === WebAuthnDeviceType.Platform ? 'webauthnPlatform' : 'webauthnCrossPlatform';
|
|
106
|
+
};
|
|
107
|
+
export const findWebAuthnDeviceTypeById = (mfaDevices, deviceId) => {
|
|
108
|
+
var _mfaDevices$webauthn$;
|
|
109
|
+
return mfaDevices == null ? void 0 : (_mfaDevices$webauthn$ = mfaDevices.webauthn.find(device => device.id === deviceId)) == null ? void 0 : _mfaDevices$webauthn$.deviceType;
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* Pure decision for whether / how to apply a stored preferred MFA factor.
|
|
113
|
+
* Used by the login-box MFAFlow when themeOptions.rememberLastMfaFactor is on.
|
|
114
|
+
*/
|
|
115
|
+
export const getPreferredMfaApplyAction = ({
|
|
116
|
+
rememberLastMfaFactor,
|
|
117
|
+
alreadyApplied,
|
|
118
|
+
step,
|
|
119
|
+
preferred,
|
|
120
|
+
mfaDevices,
|
|
121
|
+
mfaWebAuthnPlatformDeviceId,
|
|
122
|
+
mfaWebAuthnCrossPlatformDeviceId
|
|
123
|
+
}) => {
|
|
124
|
+
if (!rememberLastMfaFactor || alreadyApplied || step !== MFAStep.verify) {
|
|
125
|
+
return {
|
|
126
|
+
action: 'none'
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
const resolution = resolvePreferredMfa(preferred, mfaDevices);
|
|
130
|
+
if (resolution.type === 'none') {
|
|
131
|
+
return {
|
|
132
|
+
action: 'none'
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (resolution.type === 'step') {
|
|
136
|
+
return {
|
|
137
|
+
action: 'setStep',
|
|
138
|
+
step: resolution.step
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
const deviceId = resolution.deviceType === WebAuthnDeviceType.Platform ? mfaWebAuthnPlatformDeviceId : mfaWebAuthnCrossPlatformDeviceId;
|
|
142
|
+
if (!deviceId) {
|
|
143
|
+
return {
|
|
144
|
+
action: 'none'
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
action: 'webauthn',
|
|
149
|
+
deviceId
|
|
150
|
+
};
|
|
151
|
+
};
|
package/auth/MfaState/actions.js
CHANGED
|
@@ -81,12 +81,14 @@ export default ((store, api, sharedActions) => {
|
|
|
81
81
|
});
|
|
82
82
|
try {
|
|
83
83
|
const {
|
|
84
|
-
qrCode
|
|
84
|
+
qrCode,
|
|
85
|
+
secret
|
|
85
86
|
} = await api.auth.enrollMfa();
|
|
86
87
|
setMfaState({
|
|
87
88
|
saving: false,
|
|
88
89
|
error: undefined,
|
|
89
|
-
qrCode
|
|
90
|
+
qrCode,
|
|
91
|
+
secret
|
|
90
92
|
});
|
|
91
93
|
} catch (e) {
|
|
92
94
|
setMfaState({
|
|
@@ -17,6 +17,8 @@ export interface MFAState {
|
|
|
17
17
|
error?: any;
|
|
18
18
|
recoveryCode?: string;
|
|
19
19
|
qrCode?: string | null;
|
|
20
|
+
/** TOTP setup key for manual authenticator enrollment (same secret as in qrCode). */
|
|
21
|
+
secret?: string | null;
|
|
20
22
|
mfaToken?: string;
|
|
21
23
|
otcToken?: string;
|
|
22
24
|
phoneNumber?: string;
|
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/preferredMfaMethod';
|
|
80
81
|
export * from './LoginState/helpers/tenantSelect';
|
|
81
82
|
export declare const createAuthState: (_overrideState?: DeepPartial<AuthState>) => AuthState;
|
|
82
83
|
export declare const buildAuthActions: (store: FronteggState, api: RestApi, actions: FronteggActions, snapshotAuthState: AuthState) => [AuthActions, AuthStateActions];
|
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/preferredMfaMethod';
|
|
84
85
|
export * from './LoginState/helpers/tenantSelect';
|
|
85
86
|
export const createAuthState = _overrideState => {
|
|
86
87
|
const _ref = _overrideState != null ? _overrideState : {},
|
package/index.js
CHANGED
|
@@ -42,6 +42,7 @@ var _default = (store, api, _sharedActions) => {
|
|
|
42
42
|
Object.assign(mfaState, {
|
|
43
43
|
step: (0, _helpers.getMfaStepForNotEnrolledUsers)(user.mfaStrategies),
|
|
44
44
|
qrCode: user.qrCode,
|
|
45
|
+
secret: user.secret,
|
|
45
46
|
recoveryCode: user.recoveryCode,
|
|
46
47
|
loading: false,
|
|
47
48
|
mfaToken: user.mfaToken,
|
|
@@ -10,6 +10,7 @@ var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends")
|
|
|
10
10
|
var _interfaces = require("../interfaces");
|
|
11
11
|
var _helpers = require("../../../helpers");
|
|
12
12
|
var _helpers2 = require("../../helpers");
|
|
13
|
+
var _persistPreferredMfaMethod = require("../helpers/persistPreferredMfaMethod");
|
|
13
14
|
const _excluded = ["callback"];
|
|
14
15
|
var _default = (store, api, sharedActions) => {
|
|
15
16
|
const actions = sharedActions;
|
|
@@ -120,6 +121,7 @@ var _default = (store, api, sharedActions) => {
|
|
|
120
121
|
tenants = [],
|
|
121
122
|
activeTenant
|
|
122
123
|
} = await api.auth.loginWithMfaV2(loginWithMfaPayload);
|
|
124
|
+
(0, _persistPreferredMfaMethod.persistPreferredMfaMethod)(store, 'authenticatorApp');
|
|
123
125
|
const isAuthenticated = !!user.accessToken;
|
|
124
126
|
const additionalUpdate = await __buildPostAuthStateUpdate(tenants, isAuthenticated, isStepUp);
|
|
125
127
|
await actions.afterAuthenticationStateUpdate({
|
|
@@ -8,6 +8,7 @@ exports.default = void 0;
|
|
|
8
8
|
var _objectWithoutPropertiesLoose2 = _interopRequireDefault(require("@babel/runtime/helpers/objectWithoutPropertiesLoose"));
|
|
9
9
|
var _interfaces = require("../../MfaState/interfaces");
|
|
10
10
|
var _helpers = require("../../../helpers");
|
|
11
|
+
var _persistPreferredMfaMethod = require("../helpers/persistPreferredMfaMethod");
|
|
11
12
|
const _excluded = ["callback"],
|
|
12
13
|
_excluded2 = ["callback"];
|
|
13
14
|
var _default = (store, api, sharedActions) => {
|
|
@@ -66,6 +67,7 @@ var _default = (store, api, sharedActions) => {
|
|
|
66
67
|
});
|
|
67
68
|
try {
|
|
68
69
|
const data = await api.auth.verifyMFAEmailCodeV2(payload);
|
|
70
|
+
(0, _persistPreferredMfaMethod.persistPreferredMfaMethod)(store, 'email');
|
|
69
71
|
await actions.handleVerifyMFAResponse(data);
|
|
70
72
|
setLoadingAction({
|
|
71
73
|
loading: false,
|
|
@@ -8,6 +8,7 @@ exports.default = void 0;
|
|
|
8
8
|
var _objectWithoutPropertiesLoose2 = _interopRequireDefault(require("@babel/runtime/helpers/objectWithoutPropertiesLoose"));
|
|
9
9
|
var _interfaces = require("../../MfaState/interfaces");
|
|
10
10
|
var _helpers = require("../../../helpers");
|
|
11
|
+
var _persistPreferredMfaMethod = require("../helpers/persistPreferredMfaMethod");
|
|
11
12
|
const _excluded = ["callback", "deviceId"],
|
|
12
13
|
_excluded2 = ["callback", "deviceId"];
|
|
13
14
|
var _default = (store, api, sharedActions) => {
|
|
@@ -71,6 +72,7 @@ var _default = (store, api, sharedActions) => {
|
|
|
71
72
|
});
|
|
72
73
|
try {
|
|
73
74
|
const data = await api.auth.verifyMFASMSForLoginV2(deviceId, payload);
|
|
75
|
+
(0, _persistPreferredMfaMethod.persistPreferredMfaMethod)(store, 'sms');
|
|
74
76
|
await actions.handleVerifyMFAResponse(data, isStepUp);
|
|
75
77
|
setLoadingAction({
|
|
76
78
|
loading: false,
|
|
@@ -8,6 +8,8 @@ exports.default = void 0;
|
|
|
8
8
|
var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
|
|
9
9
|
var _objectWithoutPropertiesLoose2 = _interopRequireDefault(require("@babel/runtime/helpers/objectWithoutPropertiesLoose"));
|
|
10
10
|
var _helpers = require("../../../helpers");
|
|
11
|
+
var _preferredMfaMethod = require("../preferredMfaMethod");
|
|
12
|
+
var _persistPreferredMfaMethod = require("../helpers/persistPreferredMfaMethod");
|
|
11
13
|
const _excluded = ["callback", "deviceId"],
|
|
12
14
|
_excluded2 = ["callback", "deviceId"];
|
|
13
15
|
var _default = (store, api, sharedActions) => {
|
|
@@ -80,6 +82,11 @@ var _default = (store, api, sharedActions) => {
|
|
|
80
82
|
const data = await api.auth.verifyMFAWebAuthnForLoginV2(deviceId, (0, _extends2.default)({}, payload, {
|
|
81
83
|
options: publicKey
|
|
82
84
|
}));
|
|
85
|
+
const mfaDevices = isStepUp ? store.auth.stepUpState.mfaDevices : store.auth.mfaState.mfaDevices;
|
|
86
|
+
const deviceType = (0, _preferredMfaMethod.findWebAuthnDeviceTypeById)(mfaDevices, deviceId);
|
|
87
|
+
if (deviceType) {
|
|
88
|
+
(0, _persistPreferredMfaMethod.persistPreferredMfaMethod)(store, (0, _preferredMfaMethod.getPreferredMfaMethodFromWebAuthnDeviceType)(deviceType));
|
|
89
|
+
}
|
|
83
90
|
await actions.handleVerifyMFAResponse(data, isStepUp);
|
|
84
91
|
setLoadingAction({
|
|
85
92
|
loading: false,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.persistPreferredMfaMethod = exports.getPreferredMfaAppKey = void 0;
|
|
7
|
+
var _restApi = require("@frontegg/rest-api");
|
|
8
|
+
var _preferredMfaMethod = require("../preferredMfaMethod");
|
|
9
|
+
/**
|
|
10
|
+
* Resolve a stable application key for preferred-MFA session storage.
|
|
11
|
+
* Prefers appId (multi-app), then clientId, then appName.
|
|
12
|
+
*/
|
|
13
|
+
const getPreferredMfaAppKey = store => {
|
|
14
|
+
const appName = store.root.appName;
|
|
15
|
+
const context = _restApi.ContextHolder.for(appName).getContext();
|
|
16
|
+
return context.appId || context.clientId || appName || 'default';
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Persist preferred MFA method after a successful challenge.
|
|
21
|
+
* Always writes; themeOptions.rememberLastMfaFactor only gates whether it is applied.
|
|
22
|
+
*/
|
|
23
|
+
exports.getPreferredMfaAppKey = getPreferredMfaAppKey;
|
|
24
|
+
const persistPreferredMfaMethod = (store, method) => {
|
|
25
|
+
(0, _preferredMfaMethod.setPreferredMfaMethod)(getPreferredMfaAppKey(store), method);
|
|
26
|
+
};
|
|
27
|
+
exports.persistPreferredMfaMethod = persistPreferredMfaMethod;
|
|
@@ -92,7 +92,8 @@ exports.getSearchParam = getSearchParam;
|
|
|
92
92
|
const TENANT_ID_PARAM_KEY = 'tenantId';
|
|
93
93
|
exports.TENANT_ID_PARAM_KEY = TENANT_ID_PARAM_KEY;
|
|
94
94
|
const getNumberOfMfaDevices = mfaDevices => {
|
|
95
|
-
|
|
95
|
+
// Count platform / cross-platform separately — matches the MFA picker options.
|
|
96
|
+
const numberOfWebAuthnDevices = Number(mfaDevices.webauthn.some(device => device.deviceType === _restApi.WebAuthnDeviceType.Platform)) + Number(mfaDevices.webauthn.some(device => device.deviceType === _restApi.WebAuthnDeviceType.CrossPlatform));
|
|
96
97
|
const numberOfPhoneDevices = +Boolean(mfaDevices.phones.length);
|
|
97
98
|
const numberOfAuthenticators = +Boolean(mfaDevices.authenticators.length);
|
|
98
99
|
const numberOfEmails = +Boolean(mfaDevices.emails.length);
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.setPreferredMfaMethod = exports.resolvePreferredMfa = exports.isPreferredMfaMethod = exports.getPreferredMfaStorageKey = exports.getPreferredMfaMethodFromWebAuthnDeviceType = exports.getPreferredMfaMethod = exports.getPreferredMfaApplyAction = exports.findWebAuthnDeviceTypeById = exports.clearPreferredMfaMethod = void 0;
|
|
7
|
+
var _restApi = require("@frontegg/rest-api");
|
|
8
|
+
var _interfaces = require("../MfaState/interfaces");
|
|
9
|
+
const STORAGE_KEY_PREFIX = 'frontegg-preferred-mfa-method';
|
|
10
|
+
const PREFERRED_MFA_METHODS = new Set(['sms', 'email', 'authenticatorApp', 'webauthnPlatform', 'webauthnCrossPlatform']);
|
|
11
|
+
const getPreferredMfaStorageKey = appKey => `${STORAGE_KEY_PREFIX}:${appKey}`;
|
|
12
|
+
exports.getPreferredMfaStorageKey = getPreferredMfaStorageKey;
|
|
13
|
+
const isPreferredMfaMethod = value => typeof value === 'string' && PREFERRED_MFA_METHODS.has(value);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Persist the last successful MFA method for this browser session, keyed by application.
|
|
17
|
+
*/
|
|
18
|
+
exports.isPreferredMfaMethod = isPreferredMfaMethod;
|
|
19
|
+
const setPreferredMfaMethod = (appKey, method) => {
|
|
20
|
+
if (typeof window === 'undefined') {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
window.sessionStorage.setItem(getPreferredMfaStorageKey(appKey), method);
|
|
25
|
+
} catch {
|
|
26
|
+
// sessionStorage may be unavailable (private mode / quota) — ignore
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Read the remembered MFA method for this browser session, if any.
|
|
32
|
+
*/
|
|
33
|
+
exports.setPreferredMfaMethod = setPreferredMfaMethod;
|
|
34
|
+
const getPreferredMfaMethod = appKey => {
|
|
35
|
+
if (typeof window === 'undefined') {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const value = window.sessionStorage.getItem(getPreferredMfaStorageKey(appKey));
|
|
40
|
+
return isPreferredMfaMethod(value) ? value : null;
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
exports.getPreferredMfaMethod = getPreferredMfaMethod;
|
|
46
|
+
const clearPreferredMfaMethod = appKey => {
|
|
47
|
+
if (typeof window === 'undefined') {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
window.sessionStorage.removeItem(getPreferredMfaStorageKey(appKey));
|
|
52
|
+
} catch {
|
|
53
|
+
// ignore
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
exports.clearPreferredMfaMethod = clearPreferredMfaMethod;
|
|
57
|
+
const hasPhoneDevices = mfaDevices => mfaDevices.phones.length > 0;
|
|
58
|
+
const hasEmails = mfaDevices => mfaDevices.emails.length > 0;
|
|
59
|
+
const hasAuthenticators = mfaDevices => mfaDevices.authenticators.length > 0;
|
|
60
|
+
const hasWebAuthnDevice = (mfaDevices, deviceType) => mfaDevices.webauthn.some(device => device.deviceType === deviceType);
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Map a stored preferred MFA method to a concrete MFA step / WebAuthn action,
|
|
64
|
+
* only when that method is still enrolled and available.
|
|
65
|
+
*/
|
|
66
|
+
const resolvePreferredMfa = (preferred, mfaDevices) => {
|
|
67
|
+
if (!preferred || !mfaDevices) {
|
|
68
|
+
return {
|
|
69
|
+
type: 'none'
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
switch (preferred) {
|
|
73
|
+
case 'sms':
|
|
74
|
+
return hasPhoneDevices(mfaDevices) ? {
|
|
75
|
+
type: 'step',
|
|
76
|
+
step: _interfaces.MFAStep.smsVerifyCode
|
|
77
|
+
} : {
|
|
78
|
+
type: 'none'
|
|
79
|
+
};
|
|
80
|
+
case 'email':
|
|
81
|
+
return hasEmails(mfaDevices) ? {
|
|
82
|
+
type: 'step',
|
|
83
|
+
step: _interfaces.MFAStep.emailVerifyCode
|
|
84
|
+
} : {
|
|
85
|
+
type: 'none'
|
|
86
|
+
};
|
|
87
|
+
case 'authenticatorApp':
|
|
88
|
+
return hasAuthenticators(mfaDevices) ? {
|
|
89
|
+
type: 'step',
|
|
90
|
+
step: _interfaces.MFAStep.authenticatorApp
|
|
91
|
+
} : {
|
|
92
|
+
type: 'none'
|
|
93
|
+
};
|
|
94
|
+
case 'webauthnPlatform':
|
|
95
|
+
return hasWebAuthnDevice(mfaDevices, _restApi.WebAuthnDeviceType.Platform) ? {
|
|
96
|
+
type: 'webauthn',
|
|
97
|
+
deviceType: _restApi.WebAuthnDeviceType.Platform
|
|
98
|
+
} : {
|
|
99
|
+
type: 'none'
|
|
100
|
+
};
|
|
101
|
+
case 'webauthnCrossPlatform':
|
|
102
|
+
return hasWebAuthnDevice(mfaDevices, _restApi.WebAuthnDeviceType.CrossPlatform) ? {
|
|
103
|
+
type: 'webauthn',
|
|
104
|
+
deviceType: _restApi.WebAuthnDeviceType.CrossPlatform
|
|
105
|
+
} : {
|
|
106
|
+
type: 'none'
|
|
107
|
+
};
|
|
108
|
+
default:
|
|
109
|
+
{
|
|
110
|
+
const _exhaustive = preferred;
|
|
111
|
+
return _exhaustive;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
exports.resolvePreferredMfa = resolvePreferredMfa;
|
|
116
|
+
const getPreferredMfaMethodFromWebAuthnDeviceType = deviceType => {
|
|
117
|
+
return deviceType === _restApi.WebAuthnDeviceType.Platform ? 'webauthnPlatform' : 'webauthnCrossPlatform';
|
|
118
|
+
};
|
|
119
|
+
exports.getPreferredMfaMethodFromWebAuthnDeviceType = getPreferredMfaMethodFromWebAuthnDeviceType;
|
|
120
|
+
const findWebAuthnDeviceTypeById = (mfaDevices, deviceId) => {
|
|
121
|
+
var _mfaDevices$webauthn$;
|
|
122
|
+
return mfaDevices == null ? void 0 : (_mfaDevices$webauthn$ = mfaDevices.webauthn.find(device => device.id === deviceId)) == null ? void 0 : _mfaDevices$webauthn$.deviceType;
|
|
123
|
+
};
|
|
124
|
+
exports.findWebAuthnDeviceTypeById = findWebAuthnDeviceTypeById;
|
|
125
|
+
/**
|
|
126
|
+
* Pure decision for whether / how to apply a stored preferred MFA factor.
|
|
127
|
+
* Used by the login-box MFAFlow when themeOptions.rememberLastMfaFactor is on.
|
|
128
|
+
*/
|
|
129
|
+
const getPreferredMfaApplyAction = ({
|
|
130
|
+
rememberLastMfaFactor,
|
|
131
|
+
alreadyApplied,
|
|
132
|
+
step,
|
|
133
|
+
preferred,
|
|
134
|
+
mfaDevices,
|
|
135
|
+
mfaWebAuthnPlatformDeviceId,
|
|
136
|
+
mfaWebAuthnCrossPlatformDeviceId
|
|
137
|
+
}) => {
|
|
138
|
+
if (!rememberLastMfaFactor || alreadyApplied || step !== _interfaces.MFAStep.verify) {
|
|
139
|
+
return {
|
|
140
|
+
action: 'none'
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
const resolution = resolvePreferredMfa(preferred, mfaDevices);
|
|
144
|
+
if (resolution.type === 'none') {
|
|
145
|
+
return {
|
|
146
|
+
action: 'none'
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
if (resolution.type === 'step') {
|
|
150
|
+
return {
|
|
151
|
+
action: 'setStep',
|
|
152
|
+
step: resolution.step
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
const deviceId = resolution.deviceType === _restApi.WebAuthnDeviceType.Platform ? mfaWebAuthnPlatformDeviceId : mfaWebAuthnCrossPlatformDeviceId;
|
|
156
|
+
if (!deviceId) {
|
|
157
|
+
return {
|
|
158
|
+
action: 'none'
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
action: 'webauthn',
|
|
163
|
+
deviceId
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
exports.getPreferredMfaApplyAction = getPreferredMfaApplyAction;
|
|
@@ -88,12 +88,14 @@ var _default = (store, api, sharedActions) => {
|
|
|
88
88
|
});
|
|
89
89
|
try {
|
|
90
90
|
const {
|
|
91
|
-
qrCode
|
|
91
|
+
qrCode,
|
|
92
|
+
secret
|
|
92
93
|
} = await api.auth.enrollMfa();
|
|
93
94
|
setMfaState({
|
|
94
95
|
saving: false,
|
|
95
96
|
error: undefined,
|
|
96
|
-
qrCode
|
|
97
|
+
qrCode,
|
|
98
|
+
secret
|
|
97
99
|
});
|
|
98
100
|
} catch (e) {
|
|
99
101
|
setMfaState({
|
package/node/auth/index.js
CHANGED
|
@@ -525,6 +525,18 @@ Object.keys(_oauthStorage).forEach(function (key) {
|
|
|
525
525
|
}
|
|
526
526
|
});
|
|
527
527
|
});
|
|
528
|
+
var _preferredMfaMethod = require("./LoginState/preferredMfaMethod");
|
|
529
|
+
Object.keys(_preferredMfaMethod).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] === _preferredMfaMethod[key]) return;
|
|
533
|
+
Object.defineProperty(exports, key, {
|
|
534
|
+
enumerable: true,
|
|
535
|
+
get: function () {
|
|
536
|
+
return _preferredMfaMethod[key];
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
});
|
|
528
540
|
var _tenantSelect = require("./LoginState/helpers/tenantSelect");
|
|
529
541
|
Object.keys(_tenantSelect).forEach(function (key) {
|
|
530
542
|
if (key === "default" || key === "__esModule") return;
|
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.122.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.122.0",
|
|
11
11
|
"fast-deep-equal": "3.1.3",
|
|
12
12
|
"get-value": "^3.0.1",
|
|
13
13
|
"proxy-compare": "^3.0.0",
|