Two-Factor Auth

Two-Factor Authentication and Password Confirmation

password.confirm sends a user to /user/confirm-password unless they entered their password in the last password_timeout (three hours). The kit guards its security page with it, and Fortify 1,761 's confirmPassword option guards enabling 2FA. In the same shell as Resets and Verification, Grace turns on TOTP and signs in again; PHP computes the codes with pragmarx/google2fa, the library Fortify itself uses:

Confirming the password, enabling 2FA, and passing the challengeShell
mv grace.jar jar; A='Accept: application/json'; U=$B/user
curl -s $J -o /dev/null -w "$W" $B/settings/security
T=$(tok /user/confirm-password); H="X-CSRF-TOKEN: $T"
curl -s $J -o /dev/null -w "$W" -H "$H" -d "$G" $U/confirm-password
curl -s $J -o /dev/null -w "$W" -H "$H" -H "$A" -X POST $U/two-factor-authentication
S=$(curl -s $J -H "$A" $U/two-factor-secret-key | jq -r .secretKey); echo "$S"
C=$(php -r 'require "vendor/autoload.php";
  echo (new PragmaRX\Google2FA\Google2FA)->getCurrentOtp($argv[1]);' "$S")
curl -s $J -o /dev/null -w "$W" -H "$H" -H "$A" -d code=$C $U/confirmed-two-factor-authentication
RC=$(curl -s $J -H "$A" $U/two-factor-recovery-codes | jq -r '.[0]')
curl -s $J -o /dev/null -H "$H" -X POST $B/logout; T=$(tok /login)
curl -s $J -o /dev/null -w "$W" -d "_token=$T&email=grace@example.com&$G" $B/login
T=$(tok /two-factor-challenge)
curl -s $J -o /dev/null -w "$W" -d "_token=$T&code=$C" $B/two-factor-challenge
curl -s $J -o /dev/null -w "$W" -d "_token=$T&recovery_code=$RC" $B/two-factor-challenge
Output
302 http://127.0.0.1:8314/user/confirm-password
302 http://127.0.0.1:8314/settings/security
200
3KDUK2XXYBXW6UW3
200
302 http://127.0.0.1:8314/two-factor-challenge
302 http://127.0.0.1:8314/two-factor-challenge
302 http://127.0.0.1:8314/dashboard

Enabling stored an encrypted secret (shown as a QR code on the settings page) and eight recovery codes; only a valid code set two_factor_confirmed_at. The password alone then led only to the challenge. Replaying the setup code failed, because Fortify caches every accepted code; a recovery code works once and is then replaced.