MFA Recovery codes: enforce recovery codes generation after setting up an MFA (#50343)

## What kind of change does this PR introduce?

Afters users set up an MFA, automatically generate recovery codes

## How to test

- On an account that doesn't have recovery codes generated yet, add a
new MFA
- When you finished verifying the MFA, it should automatically open the
recovery codes modal introduced in previous PRs

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Bug Fixes**
- Improved the two-factor authentication setup flow by checking the
latest recovery-code enrollment status before generating codes.
- Recovery codes are now generated and displayed after verification when
they are enabled but not yet enrolled.
- Loading indicators now reflect recovery-code status checks, providing
clearer feedback during setup.

- **Improvements**
- Updated the recovery-code confirmation message to explain how codes
can restore access after losing access to an MFA app and remind users to
store them securely.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Gildas Garcia authored and GitHub committed 2026-09-16 10:46:07 +02:00
1 parent ee3fbc4e61
commit c52fca1340
2 files changed
+36 -4

No files matched your search

@@ -1,5 +1,6 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { useQueryClient } from '@tanstack/react-query'
import { useFlag } from 'common'
import { useEffect, useState } from 'react'
import { useForm, type SubmitHandler } from 'react-hook-form'
import { toast } from 'sonner'
@@ -10,11 +11,14 @@ import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import { z } from 'zod'
import { RecoveryCodesModal } from './RecoveryCodesModal'
import InformationBox from '@/components/ui/InformationBox'
import { organizationKeys } from '@/data/organizations/keys'
import { useMfaChallengeAndVerifyMutation } from '@/data/profile/mfa-challenge-and-verify-mutation'
import { useMfaEnrollMutation } from '@/data/profile/mfa-enroll-mutation'
import { useMfaUnenrollMutation } from '@/data/profile/mfa-unenroll-mutation'
import { useRecoveryCodesGenerateMutation } from '@/data/recovery-codes/recovery-codes-generate-mutation'
import { useRecoveryCodesStatusQuery } from '@/data/recovery-codes/recovery-codes-status-query'
import { useLastVisitedOrganization } from '@/hooks/misc/useLastVisitedOrganization'
type TOTP = { qr_code: string; secret: string; uri: string }
@@ -26,6 +30,15 @@ interface AddNewFactorModalProps {
export const AddNewFactorModal = ({ visible, onClose }: AddNewFactorModalProps) => {
const { data, mutate: enroll, isPending: isEnrolling, reset } = useMfaEnrollMutation()
const enableAuthRecoveryCodes = useFlag('enableAuthRecoveryCodes')
const { isPending: isRecoveryCodesStatusPending, refetch: refetchRecoveryCodesStatus } =
useRecoveryCodesStatusQuery({
enabled: enableAuthRecoveryCodes,
})
const recoveryCodesGenerateMutation = useRecoveryCodesGenerateMutation()
const [isRecoveryCodesModalOpen, setIsRecoveryCodesModalOpen] = useState<boolean>(false)
useEffect(() => {
if (!visible) reset()
@@ -35,7 +48,7 @@ export const AddNewFactorModal = ({ visible, onClose }: AddNewFactorModalProps)
<>
<FirstStep
visible={visible && !Boolean(data)}
isEnrolling={isEnrolling}
isEnrolling={isEnrolling || (enableAuthRecoveryCodes && isRecoveryCodesStatusPending)}
enroll={enroll}
reset={reset}
onClose={onClose}
@@ -44,8 +57,24 @@ export const AddNewFactorModal = ({ visible, onClose }: AddNewFactorModalProps)
visible={visible && Boolean(data)}
factorName={data?.friendly_name ?? ''}
factor={data as Extract<typeof data, { type: 'totp' }>}
isLoading={isEnrolling}
onClose={onClose}
isLoading={isEnrolling || (enableAuthRecoveryCodes && isRecoveryCodesStatusPending)}
onClose={async () => {
if (enableAuthRecoveryCodes) {
const { data: currentRecoveryCodesStatus } = await refetchRecoveryCodesStatus()
const shouldGenerateRecoveryCodes = currentRecoveryCodesStatus?.status === 'unenrolled'
if (shouldGenerateRecoveryCodes) {
recoveryCodesGenerateMutation.mutate({})
setIsRecoveryCodesModalOpen(true)
}
}
onClose()
}}
/>
<RecoveryCodesModal
open={isRecoveryCodesModalOpen}
onOpenChange={(open) => setIsRecoveryCodesModalOpen(open)}
mutation={recoveryCodesGenerateMutation}
/>
</>
)
@@ -132,7 +132,10 @@ const GenerateRecoveryCodesModalContent = ({
if (status === 'success') {
return (
<div className="flex flex-col gap-4">
<p>Save your recovery codes somewhere safe.</p>
<p>
Recovery codes allow you to recover your account in case you lost access to your MFA apps.
Save them somewhere safe.
</p>
<pre className="relative bg-muted rounded-md py-2 px-4">
<code className="flex gap-2 flex-wrap justify-between">
{codes?.map((code) => (