From 94de4c2a5bf44c513893fc1a3c2fa82ce8e62a62 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Sun, 26 Apr 2026 13:48:29 +0200 Subject: [PATCH] feat: add MFA Management API methods to LogtoManagementClient Add 5 new methods for MFA operations via Logto Management API: - getUserMfaVerifications: list all MFA factors for a user - createTotpVerification: create TOTP MFA verification - createBackupCodes: generate backup codes - deleteMfaVerification: delete a specific MFA verification - deleteAllMfaVerifications: delete all MFA verifications (admin reset) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../saas/identity/LogtoManagementClient.java | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/src/main/java/net/siegeln/cameleer/saas/identity/LogtoManagementClient.java b/src/main/java/net/siegeln/cameleer/saas/identity/LogtoManagementClient.java index d91c563..79806fc 100644 --- a/src/main/java/net/siegeln/cameleer/saas/identity/LogtoManagementClient.java +++ b/src/main/java/net/siegeln/cameleer/saas/identity/LogtoManagementClient.java @@ -526,6 +526,84 @@ public class LogtoManagementClient { .toBodilessEntity(); } + // --- MFA Verification Management --- + + /** List all MFA verifications for a user. Returns a list of MFA factor objects. */ + @SuppressWarnings("unchecked") + public List> getUserMfaVerifications(String userId) { + if (!isAvailable()) return List.of(); + try { + var resp = restClient.get() + .uri(config.getLogtoEndpoint() + "/api/users/" + userId + "/mfa-verifications") + .header("Authorization", "Bearer " + getAccessToken()) + .retrieve() + .body(List.class); + return resp != null ? resp : List.of(); + } catch (Exception e) { + log.warn("Failed to get MFA verifications for user {}: {}", userId, e.getMessage()); + return List.of(); + } + } + + /** Create a TOTP MFA verification for a user. Returns the secret and QR code. */ + @SuppressWarnings("unchecked") + public Map createTotpVerification(String userId, String secret) { + if (!isAvailable()) return Map.of(); + try { + return (Map) restClient.post() + .uri(config.getLogtoEndpoint() + "/api/users/" + userId + "/mfa-verifications") + .header("Authorization", "Bearer " + getAccessToken()) + .contentType(MediaType.APPLICATION_JSON) + .body(Map.of("type", "Totp", "secret", secret)) + .retrieve() + .body(Map.class); + } catch (Exception e) { + log.warn("Failed to create TOTP verification for user {}: {}", userId, e.getMessage()); + return Map.of(); + } + } + + /** Generate backup codes for a user. Returns the list of codes. */ + @SuppressWarnings("unchecked") + public Map createBackupCodes(String userId) { + if (!isAvailable()) return Map.of(); + try { + return (Map) restClient.post() + .uri(config.getLogtoEndpoint() + "/api/users/" + userId + "/mfa-verifications") + .header("Authorization", "Bearer " + getAccessToken()) + .contentType(MediaType.APPLICATION_JSON) + .body(Map.of("type", "BackupCode")) + .retrieve() + .body(Map.class); + } catch (Exception e) { + log.warn("Failed to create backup codes for user {}: {}", userId, e.getMessage()); + return Map.of(); + } + } + + /** Delete a specific MFA verification for a user. */ + public void deleteMfaVerification(String userId, String verificationId) { + if (!isAvailable()) return; + try { + restClient.delete() + .uri(config.getLogtoEndpoint() + "/api/users/" + userId + "/mfa-verifications/" + verificationId) + .header("Authorization", "Bearer " + getAccessToken()) + .retrieve() + .toBodilessEntity(); + } catch (Exception e) { + log.warn("Failed to delete MFA verification {} for user {}: {}", verificationId, userId, e.getMessage()); + } + } + + /** Delete all MFA verifications for a user (used for admin MFA reset). */ + public void deleteAllMfaVerifications(String userId) { + List> verifications = getUserMfaVerifications(userId); + for (Map v : verifications) { + String id = String.valueOf(v.get("id")); + deleteMfaVerification(userId, id); + } + } + /** Update a user's profile fields (e.g. name). */ public void updateUserProfile(String userId, Map profile) { if (!isAvailable()) throw new IllegalStateException("Logto not configured");