feat: Add two-factor authentication via TOTP

Adds TOTP setup with QR code, login challenge, recovery codes and
trusted devices that let a browser skip the second factor for 30
days. Admins can reset another user's second factor by confirming
with their own password.
This commit is contained in:
Daniel Hiller
2026-08-07 20:11:55 +02:00
parent 36aa732fec
commit 2e4904f77d
74 changed files with 4173 additions and 4 deletions
@@ -4,6 +4,7 @@
*/
import ActionTypes from '../../constants/ActionTypes';
import AccessTokenSteps from '../../constants/AccessTokenSteps';
const initialState = {
data: {
@@ -20,6 +21,11 @@ const initialState = {
isCancelling: false,
isLanguageUpdating: false,
},
totpForm: {
isSubmitting: false,
isCancelling: false,
error: null,
},
};
// eslint-disable-next-line default-param-last
@@ -38,6 +44,9 @@ export default (state = initialState, { type, payload }) => {
case ActionTypes.TERMS_ACCEPT__SUCCESS:
case ActionTypes.TERMS_CANCEL__SUCCESS:
case ActionTypes.TERMS_CANCEL__FAILURE:
case ActionTypes.TOTP_VERIFY__SUCCESS:
case ActionTypes.TOTP_CHALLENGE_CANCEL__SUCCESS:
case ActionTypes.TOTP_CHALLENGE_CANCEL__FAILURE:
return initialState;
case ActionTypes.AUTHENTICATE__FAILURE:
if (payload.terms) {
@@ -53,11 +62,49 @@ export default (state = initialState, { type, payload }) => {
};
}
if (payload.error && payload.error.step === AccessTokenSteps.VERIFY_TOTP) {
return {
...state,
data: initialState.data,
isSubmitting: false,
pendingToken: payload.error.pendingToken,
step: payload.error.step,
totpForm: initialState.totpForm,
};
}
return {
...state,
isSubmitting: false,
error: payload.error,
};
case ActionTypes.TOTP_VERIFY:
return {
...state,
totpForm: {
...state.totpForm,
isSubmitting: true,
error: null,
},
};
case ActionTypes.TOTP_VERIFY__FAILURE:
return {
...state,
totpForm: {
...state.totpForm,
isSubmitting: false,
error: payload.error,
},
};
case ActionTypes.TOTP_CHALLENGE_CANCEL:
return {
...state,
pendingToken: null,
totpForm: {
...state.totpForm,
isCancelling: true,
},
};
case ActionTypes.AUTHENTICATE_ERROR_CLEAR:
return {
...state,