diff --git a/src/main/java/net/siegeln/cameleer/saas/portal/TenantPortalService.java b/src/main/java/net/siegeln/cameleer/saas/portal/TenantPortalService.java index 8cc6794..db3df94 100644 --- a/src/main/java/net/siegeln/cameleer/saas/portal/TenantPortalService.java +++ b/src/main/java/net/siegeln/cameleer/saas/portal/TenantPortalService.java @@ -24,6 +24,7 @@ import java.time.temporal.ChronoUnit; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.UUID; @Service @@ -77,7 +78,7 @@ public class TenantPortalService { String serverEndpoint, Instant createdAt ) {} - public record MfaStatusData(boolean enrolled, boolean hasBackupCodes) {} + public record MfaStatusData(boolean enrolled, boolean hasBackupCodes, boolean passkeyEnrolled, int passkeyCount) {} public record MfaSetupData(String secret, String secretQrCode) {} @@ -297,7 +298,10 @@ public class TenantPortalService { .anyMatch(v -> "Totp".equals(String.valueOf(v.get("type")))); boolean hasBackupCodes = verifications.stream() .anyMatch(v -> "BackupCode".equals(String.valueOf(v.get("type")))); - return new MfaStatusData(enrolled, hasBackupCodes); + long passkeyCount = verifications.stream() + .filter(v -> "WebAuthn".equals(String.valueOf(v.get("type")))) + .count(); + return new MfaStatusData(enrolled, hasBackupCodes, passkeyCount > 0, (int) passkeyCount); } @SuppressWarnings("unchecked") @@ -370,18 +374,87 @@ public class TenantPortalService { logtoClient.deleteAllMfaVerifications(userId); } + // --- Passkey methods --- + + public record PasskeyCredential(String id, String name, String agent, String createdAt) {} + + @SuppressWarnings("unchecked") + public List listPasskeys(String userId) { + return logtoClient.getWebAuthnCredentials(userId).stream() + .map(v -> new PasskeyCredential( + String.valueOf(v.get("id")), + v.get("name") != null ? String.valueOf(v.get("name")) : null, + v.get("agent") != null ? String.valueOf(v.get("agent")) : null, + v.get("createdAt") != null ? String.valueOf(v.get("createdAt")) : null + )) + .toList(); + } + + public void renamePasskey(String userId, String credentialId, String name) { + var credentials = logtoClient.getWebAuthnCredentials(userId); + boolean owns = credentials.stream() + .anyMatch(v -> credentialId.equals(String.valueOf(v.get("id")))); + if (!owns) { + throw new IllegalArgumentException("Credential not found"); + } + logtoClient.renameMfaVerification(userId, credentialId, name); + } + + public void deletePasskey(String userId, String credentialId) { + var credentials = logtoClient.getWebAuthnCredentials(userId); + boolean owns = credentials.stream() + .anyMatch(v -> credentialId.equals(String.valueOf(v.get("id")))); + if (!owns) { + throw new IllegalArgumentException("Credential not found"); + } + logtoClient.deleteMfaVerification(userId, credentialId); + } + + public void updateMfaMethodPreference(String userId, String preference) { + logtoClient.updateUserCustomData(userId, Map.of("mfa_method_preference", preference)); + } + public void updateTenantSettings(Map updates) { TenantEntity tenant = resolveTenant(); Map settings = new HashMap<>( tenant.getSettings() != null ? tenant.getSettings() : Map.of()); - // Only allow known keys if (updates.containsKey("mfaRequired")) { settings.put("mfaRequired", Boolean.TRUE.equals(updates.get("mfaRequired"))); } + if (updates.containsKey("mfaMode")) { + String mode = String.valueOf(updates.get("mfaMode")); + if (Set.of("off", "optional", "required").contains(mode)) { + settings.put("mfaMode", mode); + } + } + if (updates.containsKey("passkeyEnabled")) { + settings.put("passkeyEnabled", Boolean.TRUE.equals(updates.get("passkeyEnabled"))); + } + if (updates.containsKey("passkeyMode")) { + String mode = String.valueOf(updates.get("passkeyMode")); + if (Set.of("optional", "preferred", "required").contains(mode)) { + settings.put("passkeyMode", mode); + } + } tenant.setSettings(settings); tenantService.save(tenant); } + public record AuthSettingsData(String mfaMode, boolean passkeyEnabled, String passkeyMode) {} + + public AuthSettingsData getAuthSettings() { + TenantEntity tenant = resolveTenant(); + Map settings = tenant.getSettings() != null ? tenant.getSettings() : Map.of(); + String mfaMode = settings.containsKey("mfaMode") + ? String.valueOf(settings.get("mfaMode")) + : (Boolean.TRUE.equals(settings.get("mfaRequired")) ? "required" : "off"); + boolean passkeyEnabled = Boolean.TRUE.equals(settings.get("passkeyEnabled")); + String passkeyMode = settings.containsKey("passkeyMode") + ? String.valueOf(settings.get("passkeyMode")) + : "optional"; + return new AuthSettingsData(mfaMode, passkeyEnabled, passkeyMode); + } + // --- TOTP helpers --- private String computeTotp(String base32Secret, long timeStep) {