feat: add passkey management and auth settings to TenantPortalService

This commit is contained in:
hsiegeln
2026-04-27 08:45:49 +02:00
parent 40daca36a0
commit 5bf94c6d4e

View File

@@ -24,6 +24,7 @@ import java.time.temporal.ChronoUnit;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.UUID; import java.util.UUID;
@Service @Service
@@ -77,7 +78,7 @@ public class TenantPortalService {
String serverEndpoint, Instant createdAt 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) {} public record MfaSetupData(String secret, String secretQrCode) {}
@@ -297,7 +298,10 @@ public class TenantPortalService {
.anyMatch(v -> "Totp".equals(String.valueOf(v.get("type")))); .anyMatch(v -> "Totp".equals(String.valueOf(v.get("type"))));
boolean hasBackupCodes = verifications.stream() boolean hasBackupCodes = verifications.stream()
.anyMatch(v -> "BackupCode".equals(String.valueOf(v.get("type")))); .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") @SuppressWarnings("unchecked")
@@ -370,18 +374,87 @@ public class TenantPortalService {
logtoClient.deleteAllMfaVerifications(userId); logtoClient.deleteAllMfaVerifications(userId);
} }
// --- Passkey methods ---
public record PasskeyCredential(String id, String name, String agent, String createdAt) {}
@SuppressWarnings("unchecked")
public List<PasskeyCredential> 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<String, Object> updates) { public void updateTenantSettings(Map<String, Object> updates) {
TenantEntity tenant = resolveTenant(); TenantEntity tenant = resolveTenant();
Map<String, Object> settings = new HashMap<>( Map<String, Object> settings = new HashMap<>(
tenant.getSettings() != null ? tenant.getSettings() : Map.of()); tenant.getSettings() != null ? tenant.getSettings() : Map.of());
// Only allow known keys
if (updates.containsKey("mfaRequired")) { if (updates.containsKey("mfaRequired")) {
settings.put("mfaRequired", Boolean.TRUE.equals(updates.get("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); tenant.setSettings(settings);
tenantService.save(tenant); tenantService.save(tenant);
} }
public record AuthSettingsData(String mfaMode, boolean passkeyEnabled, String passkeyMode) {}
public AuthSettingsData getAuthSettings() {
TenantEntity tenant = resolveTenant();
Map<String, Object> 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 --- // --- TOTP helpers ---
private String computeTotp(String base32Secret, long timeStep) { private String computeTotp(String base32Secret, long timeStep) {