refactor: move passkey enrollment to sign-in UI via Experience API
All checks were successful
CI / build (push) Successful in 2m12s
CI / docker (push) Successful in 1m49s

Remove the SaaS backend proxy approach for passkey registration (Account
API binding, Management API proxy, password modal in PasskeySection).
Instead, offer passkey enrollment natively during sign-in via Logto's
Experience API — the correct architectural layer.

Sign-in flow: when Logto returns MFA enrollment available (422), show a
"Secure your account" screen with Register passkey / Set up later. Uses
Experience API WebAuthn registration endpoints. Works for all users
(SaaS and future server users) since the sign-in UI is shared.

PasskeySection in account settings now only manages existing passkeys
(list/rename/delete) and directs users to register during sign-in.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-27 18:33:46 +02:00
parent 4df6fc9e03
commit 18e6f32f90
8 changed files with 120 additions and 231 deletions

View File

@@ -103,17 +103,6 @@ public class AccountController {
return ResponseEntity.noContent().build();
}
// --- WebAuthn bind (proxied via Management API since Account API /my-account/ returns 401) ---
record WebAuthnBindRequest(String verificationRecordId, String identityVerificationId) {}
@PostMapping("/mfa/webauthn/bind")
public ResponseEntity<Void> bindWebAuthn(@AuthenticationPrincipal Jwt jwt,
@RequestBody WebAuthnBindRequest request) {
accountService.bindWebAuthnPasskey(jwt.getSubject(), request.verificationRecordId(), request.identityVerificationId());
return ResponseEntity.noContent().build();
}
// --- MFA Preference ---
@PostMapping("/mfa/method-preference")

View File

@@ -150,10 +150,6 @@ public class AccountService {
}
}
public void bindWebAuthnPasskey(String userId, String verificationRecordId, String identityVerificationId) {
logtoClient.bindWebAuthnMfa(userId, verificationRecordId, identityVerificationId);
}
// --- Passkeys ---
public List<PasskeyCredential> listPasskeys(String userId) {

View File

@@ -13,8 +13,8 @@ import java.util.List;
import java.util.Map;
/**
* Syncs Logto configuration on startup: enables the Account Center
* and ensures MFA factors (including WebAuthn) match the vendor policy.
* Syncs Logto configuration on startup: ensures MFA factors
* (including WebAuthn) match the vendor policy.
*/
@Component
public class LogtoStartupConfig {
@@ -32,7 +32,6 @@ public class LogtoStartupConfig {
@EventListener(ApplicationReadyEvent.class)
public void onStartup() {
logtoClient.enableAccountCenter();
syncMfaFactors();
}

View File

@@ -653,40 +653,6 @@ public class LogtoManagementClient {
}
}
/** Bind a WebAuthn passkey to a user via the Management API. */
public void bindWebAuthnMfa(String userId, String verificationRecordId, String identityVerificationId) {
if (!isAvailable()) throw new IllegalStateException("Logto not configured");
restClient.post()
.uri(config.getLogtoEndpoint() + "/api/users/" + userId + "/mfa-verifications")
.header("Authorization", "Bearer " + getAccessToken())
.contentType(MediaType.APPLICATION_JSON)
.body(Map.of("type", "WebAuthn", "verificationRecordId", verificationRecordId))
.retrieve()
.toBodilessEntity();
}
/** Enable the Account Center API for MFA management (WebAuthn passkey registration). */
public void enableAccountCenter() {
if (!isAvailable()) return;
try {
restClient.patch()
.uri(config.getLogtoEndpoint() + "/api/account-center")
.header("Authorization", "Bearer " + getAccessToken())
.contentType(MediaType.APPLICATION_JSON)
.body(Map.of(
"enabled", true,
"fields", Map.of(
"mfa", "Edit"
)
))
.retrieve()
.toBodilessEntity();
log.info("Account Center enabled for MFA management");
} catch (Exception e) {
log.warn("Failed to enable Account Center: {}", e.getMessage());
}
}
/** Update user custom data (partial merge). Used for mfa_method_preference. */
public void updateUserCustomData(String userId, Map<String, Object> customData) {
if (!isAvailable()) return;