From cc3d2dc111bf61f70b7411c8b08ca2fa477107cf Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Mon, 27 Apr 2026 14:44:20 +0200 Subject: [PATCH] refactor: delegate TenantPortalService MFA/password/passkey methods to AccountService Co-Authored-By: Claude Sonnet 4.6 --- .../saas/portal/TenantPortalService.java | 167 ++---------------- 1 file changed, 19 insertions(+), 148 deletions(-) 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 db3df94..ead647a 100644 --- a/src/main/java/net/siegeln/cameleer/saas/portal/TenantPortalService.java +++ b/src/main/java/net/siegeln/cameleer/saas/portal/TenantPortalService.java @@ -1,5 +1,6 @@ package net.siegeln.cameleer.saas.portal; +import net.siegeln.cameleer.saas.account.AccountService; import net.siegeln.cameleer.saas.config.TenantContext; import net.siegeln.cameleer.saas.identity.LogtoManagementClient; import net.siegeln.cameleer.saas.identity.ServerApiClient; @@ -15,10 +16,6 @@ import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; -import java.nio.ByteBuffer; -import java.security.SecureRandom; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.HashMap; @@ -39,6 +36,7 @@ public class TenantPortalService { private final TenantProvisioner tenantProvisioner; private final ProvisioningProperties provisioningProps; private final VendorTenantService vendorTenantService; + private final AccountService accountService; public TenantPortalService(TenantService tenantService, LicenseService licenseService, @@ -46,7 +44,8 @@ public class TenantPortalService { LogtoManagementClient logtoClient, TenantProvisioner tenantProvisioner, ProvisioningProperties provisioningProps, - @Lazy VendorTenantService vendorTenantService) { + @Lazy VendorTenantService vendorTenantService, + AccountService accountService) { this.tenantService = tenantService; this.licenseService = licenseService; this.serverApiClient = serverApiClient; @@ -54,6 +53,7 @@ public class TenantPortalService { this.tenantProvisioner = tenantProvisioner; this.provisioningProps = provisioningProps; this.vendorTenantService = vendorTenantService; + this.accountService = accountService; } // --- Inner records --- @@ -221,9 +221,7 @@ public class TenantPortalService { } public void changePassword(String userId, String newPassword) { - if (newPassword == null || newPassword.length() < 8) { - throw new IllegalArgumentException("Password must be at least 8 characters"); - } + accountService.validatePassword(newPassword); logtoClient.updateUserPassword(userId, newPassword); } @@ -293,69 +291,26 @@ public class TenantPortalService { // --- MFA methods --- public MfaStatusData getMfaStatus(String userId) { - var verifications = logtoClient.getUserMfaVerifications(userId); - boolean enrolled = verifications.stream() - .anyMatch(v -> "Totp".equals(String.valueOf(v.get("type")))); - boolean hasBackupCodes = verifications.stream() - .anyMatch(v -> "BackupCode".equals(String.valueOf(v.get("type")))); - long passkeyCount = verifications.stream() - .filter(v -> "WebAuthn".equals(String.valueOf(v.get("type")))) - .count(); - return new MfaStatusData(enrolled, hasBackupCodes, passkeyCount > 0, (int) passkeyCount); + var data = accountService.getMfaStatus(userId); + return new MfaStatusData(data.enrolled(), data.hasBackupCodes(), data.passkeyEnrolled(), data.passkeyCount()); } - @SuppressWarnings("unchecked") public MfaSetupData setupTotp(String userId) { - byte[] secretBytes = new byte[20]; - new SecureRandom().nextBytes(secretBytes); - String secret = base32Encode(secretBytes); - - var response = logtoClient.createTotpVerification(userId, secret); - String qrCode = ""; - if (response.containsKey("secretQrCode")) { - qrCode = String.valueOf(response.get("secretQrCode")); - } else if (response.containsKey("qrCode")) { - qrCode = String.valueOf(response.get("qrCode")); - } - return new MfaSetupData(secret, qrCode); + var data = accountService.setupTotp(userId); + return new MfaSetupData(data.secret(), data.secretQrCode()); } public boolean verifyTotpCode(String secret, String code) { - if (secret == null || code == null || code.length() != 6) { - return false; - } - long currentTimeStep = System.currentTimeMillis() / 30000; - // Allow +-1 step drift - for (long step = currentTimeStep - 1; step <= currentTimeStep + 1; step++) { - String computed = computeTotp(secret, step); - if (computed.equals(code)) { - return true; - } - } - return false; + return accountService.verifyTotpCode(secret, code); } - @SuppressWarnings("unchecked") public BackupCodesData generateBackupCodes(String userId) { - var response = logtoClient.createBackupCodes(userId); - List codes = List.of(); - if (response.containsKey("codes")) { - var rawCodes = response.get("codes"); - if (rawCodes instanceof List) { - codes = ((List) rawCodes).stream() - .map(String::valueOf) - .toList(); - } - } - return new BackupCodesData(codes); + var data = accountService.generateBackupCodes(userId); + return new BackupCodesData(data.codes()); } public void removeTotp(String userId) { - var verifications = logtoClient.getUserMfaVerifications(userId); - for (var v : verifications) { - String id = String.valueOf(v.get("id")); - logtoClient.deleteMfaVerification(userId, id); - } + accountService.removeMfa(userId); } public void resetTeamMemberMfa(String userId) { @@ -378,40 +333,22 @@ public class TenantPortalService { 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 - )) + return accountService.listPasskeys(userId).stream() + .map(p -> new PasskeyCredential(p.id(), p.name(), p.agent(), p.createdAt())) .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); + accountService.renamePasskey(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); + accountService.deletePasskey(userId, credentialId); } public void updateMfaMethodPreference(String userId, String preference) { - logtoClient.updateUserCustomData(userId, Map.of("mfa_method_preference", preference)); + accountService.setMfaMethodPreference(userId, preference); } public void updateTenantSettings(Map updates) { @@ -455,70 +392,4 @@ public class TenantPortalService { return new AuthSettingsData(mfaMode, passkeyEnabled, passkeyMode); } - // --- TOTP helpers --- - - private String computeTotp(String base32Secret, long timeStep) { - try { - byte[] key = base32Decode(base32Secret); - byte[] data = ByteBuffer.allocate(8).putLong(timeStep).array(); - - Mac mac = Mac.getInstance("HmacSHA1"); - mac.init(new SecretKeySpec(key, "HmacSHA1")); - byte[] hash = mac.doFinal(data); - - int offset = hash[hash.length - 1] & 0x0F; - int binary = ((hash[offset] & 0x7F) << 24) - | ((hash[offset + 1] & 0xFF) << 16) - | ((hash[offset + 2] & 0xFF) << 8) - | (hash[offset + 3] & 0xFF); - int otp = binary % 1_000_000; - return String.format("%06d", otp); - } catch (Exception e) { - log.error("TOTP computation failed", e); - return ""; - } - } - - private static final String BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; - - private String base32Encode(byte[] data) { - StringBuilder result = new StringBuilder(); - int buffer = 0; - int bitsLeft = 0; - for (byte b : data) { - buffer = (buffer << 8) | (b & 0xFF); - bitsLeft += 8; - while (bitsLeft >= 5) { - result.append(BASE32_ALPHABET.charAt((buffer >> (bitsLeft - 5)) & 0x1F)); - bitsLeft -= 5; - } - } - if (bitsLeft > 0) { - result.append(BASE32_ALPHABET.charAt((buffer << (5 - bitsLeft)) & 0x1F)); - } - return result.toString(); - } - - private byte[] base32Decode(String encoded) { - String upper = encoded.toUpperCase().replaceAll("[=\\s]", ""); - int[] output = new int[upper.length() * 5 / 8]; - int buffer = 0; - int bitsLeft = 0; - int index = 0; - for (char c : upper.toCharArray()) { - int val = BASE32_ALPHABET.indexOf(c); - if (val < 0) continue; - buffer = (buffer << 5) | val; - bitsLeft += 5; - if (bitsLeft >= 8) { - output[index++] = (buffer >> (bitsLeft - 8)) & 0xFF; - bitsLeft -= 8; - } - } - byte[] result = new byte[index]; - for (int i = 0; i < index; i++) { - result[i] = (byte) output[i]; - } - return result; - } }