fix: prevent MFA lockout and move enrollment to modal dialog
All checks were successful
CI / build (push) Successful in 1m58s
CI / docker (push) Successful in 1m47s

Three fixes for MFA enrollment and sign-in:

- Defer TOTP registration with Logto until after 6-digit code verification.
  Previously setupTotp() immediately registered the secret, so abandoning
  enrollment mid-way left MFA active without a working authenticator.
- Move entire MFA enrollment flow (QR code, verify, backup codes) into a
  Modal dialog instead of replacing the Card content inline.
- Fix sign-in MFA flow: submitMfa() no longer calls identifyUser() after
  TOTP verify — user is already identified, and passing the MFA
  verificationId to identification returned 422 ("method not activated").

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-27 16:25:15 +02:00
parent 9231a1fc60
commit a5c20830a7
4 changed files with 168 additions and 144 deletions

View File

@@ -62,7 +62,7 @@ public class AccountController {
@PostMapping("/mfa/totp/verify")
public Map<String, Boolean> verifyTotp(@AuthenticationPrincipal Jwt jwt,
@RequestBody TotpVerifyRequest request) {
boolean ok = accountService.verifyTotpCode(request.secret(), request.code());
boolean ok = accountService.verifyAndEnableTotp(jwt.getSubject(), request.secret(), request.code());
return Map.of("verified", ok);
}

View File

@@ -108,11 +108,22 @@ public class AccountService {
new SecureRandom().nextBytes(secretBytes);
String secret = base32Encode(secretBytes);
var result = logtoClient.createTotpVerification(userId, secret);
String qrCode = result.containsKey("secretQrCode")
? String.valueOf(result.get("secretQrCode"))
: String.valueOf(result.getOrDefault("qrCode", ""));
return new MfaSetupData(secret, qrCode);
// Build otpauth URI locally — do NOT register with Logto yet.
// The secret is only registered after the user verifies the 6-digit code.
var user = logtoClient.getUser(userId);
String email = user != null ? String.valueOf(user.getOrDefault("primaryEmail", "")) : "";
String label = email.isBlank() ? userId : email;
String otpauthUri = String.format(
"otpauth://totp/Cameleer:%s?secret=%s&issuer=Cameleer&algorithm=SHA1&digits=6&period=30",
label, secret);
return new MfaSetupData(secret, otpauthUri);
}
public boolean verifyAndEnableTotp(String userId, String secret, String code) {
if (!verifyTotpCode(secret, code)) return false;
logtoClient.createTotpVerification(userId, secret);
return true;
}
public boolean verifyTotpCode(String secret, String code) {