refactor: delegate TenantPortalService MFA/password/passkey methods to AccountService
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
package net.siegeln.cameleer.saas.portal;
|
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.config.TenantContext;
|
||||||
import net.siegeln.cameleer.saas.identity.LogtoManagementClient;
|
import net.siegeln.cameleer.saas.identity.LogtoManagementClient;
|
||||||
import net.siegeln.cameleer.saas.identity.ServerApiClient;
|
import net.siegeln.cameleer.saas.identity.ServerApiClient;
|
||||||
@@ -15,10 +16,6 @@ import org.slf4j.LoggerFactory;
|
|||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.stereotype.Service;
|
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.Instant;
|
||||||
import java.time.temporal.ChronoUnit;
|
import java.time.temporal.ChronoUnit;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@@ -39,6 +36,7 @@ public class TenantPortalService {
|
|||||||
private final TenantProvisioner tenantProvisioner;
|
private final TenantProvisioner tenantProvisioner;
|
||||||
private final ProvisioningProperties provisioningProps;
|
private final ProvisioningProperties provisioningProps;
|
||||||
private final VendorTenantService vendorTenantService;
|
private final VendorTenantService vendorTenantService;
|
||||||
|
private final AccountService accountService;
|
||||||
|
|
||||||
public TenantPortalService(TenantService tenantService,
|
public TenantPortalService(TenantService tenantService,
|
||||||
LicenseService licenseService,
|
LicenseService licenseService,
|
||||||
@@ -46,7 +44,8 @@ public class TenantPortalService {
|
|||||||
LogtoManagementClient logtoClient,
|
LogtoManagementClient logtoClient,
|
||||||
TenantProvisioner tenantProvisioner,
|
TenantProvisioner tenantProvisioner,
|
||||||
ProvisioningProperties provisioningProps,
|
ProvisioningProperties provisioningProps,
|
||||||
@Lazy VendorTenantService vendorTenantService) {
|
@Lazy VendorTenantService vendorTenantService,
|
||||||
|
AccountService accountService) {
|
||||||
this.tenantService = tenantService;
|
this.tenantService = tenantService;
|
||||||
this.licenseService = licenseService;
|
this.licenseService = licenseService;
|
||||||
this.serverApiClient = serverApiClient;
|
this.serverApiClient = serverApiClient;
|
||||||
@@ -54,6 +53,7 @@ public class TenantPortalService {
|
|||||||
this.tenantProvisioner = tenantProvisioner;
|
this.tenantProvisioner = tenantProvisioner;
|
||||||
this.provisioningProps = provisioningProps;
|
this.provisioningProps = provisioningProps;
|
||||||
this.vendorTenantService = vendorTenantService;
|
this.vendorTenantService = vendorTenantService;
|
||||||
|
this.accountService = accountService;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Inner records ---
|
// --- Inner records ---
|
||||||
@@ -221,9 +221,7 @@ public class TenantPortalService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void changePassword(String userId, String newPassword) {
|
public void changePassword(String userId, String newPassword) {
|
||||||
if (newPassword == null || newPassword.length() < 8) {
|
accountService.validatePassword(newPassword);
|
||||||
throw new IllegalArgumentException("Password must be at least 8 characters");
|
|
||||||
}
|
|
||||||
logtoClient.updateUserPassword(userId, newPassword);
|
logtoClient.updateUserPassword(userId, newPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,69 +291,26 @@ public class TenantPortalService {
|
|||||||
// --- MFA methods ---
|
// --- MFA methods ---
|
||||||
|
|
||||||
public MfaStatusData getMfaStatus(String userId) {
|
public MfaStatusData getMfaStatus(String userId) {
|
||||||
var verifications = logtoClient.getUserMfaVerifications(userId);
|
var data = accountService.getMfaStatus(userId);
|
||||||
boolean enrolled = verifications.stream()
|
return new MfaStatusData(data.enrolled(), data.hasBackupCodes(), data.passkeyEnrolled(), data.passkeyCount());
|
||||||
.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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public MfaSetupData setupTotp(String userId) {
|
public MfaSetupData setupTotp(String userId) {
|
||||||
byte[] secretBytes = new byte[20];
|
var data = accountService.setupTotp(userId);
|
||||||
new SecureRandom().nextBytes(secretBytes);
|
return new MfaSetupData(data.secret(), data.secretQrCode());
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean verifyTotpCode(String secret, String code) {
|
public boolean verifyTotpCode(String secret, String code) {
|
||||||
if (secret == null || code == null || code.length() != 6) {
|
return accountService.verifyTotpCode(secret, code);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public BackupCodesData generateBackupCodes(String userId) {
|
public BackupCodesData generateBackupCodes(String userId) {
|
||||||
var response = logtoClient.createBackupCodes(userId);
|
var data = accountService.generateBackupCodes(userId);
|
||||||
List<String> codes = List.of();
|
return new BackupCodesData(data.codes());
|
||||||
if (response.containsKey("codes")) {
|
|
||||||
var rawCodes = response.get("codes");
|
|
||||||
if (rawCodes instanceof List) {
|
|
||||||
codes = ((List<Object>) rawCodes).stream()
|
|
||||||
.map(String::valueOf)
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new BackupCodesData(codes);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removeTotp(String userId) {
|
public void removeTotp(String userId) {
|
||||||
var verifications = logtoClient.getUserMfaVerifications(userId);
|
accountService.removeMfa(userId);
|
||||||
for (var v : verifications) {
|
|
||||||
String id = String.valueOf(v.get("id"));
|
|
||||||
logtoClient.deleteMfaVerification(userId, id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void resetTeamMemberMfa(String userId) {
|
public void resetTeamMemberMfa(String userId) {
|
||||||
@@ -378,40 +333,22 @@ public class TenantPortalService {
|
|||||||
|
|
||||||
public record PasskeyCredential(String id, String name, String agent, String createdAt) {}
|
public record PasskeyCredential(String id, String name, String agent, String createdAt) {}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public List<PasskeyCredential> listPasskeys(String userId) {
|
public List<PasskeyCredential> listPasskeys(String userId) {
|
||||||
return logtoClient.getWebAuthnCredentials(userId).stream()
|
return accountService.listPasskeys(userId).stream()
|
||||||
.map(v -> new PasskeyCredential(
|
.map(p -> new PasskeyCredential(p.id(), p.name(), p.agent(), p.createdAt()))
|
||||||
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();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void renamePasskey(String userId, String credentialId, String name) {
|
public void renamePasskey(String userId, String credentialId, String name) {
|
||||||
var credentials = logtoClient.getWebAuthnCredentials(userId);
|
accountService.renamePasskey(userId, credentialId, name);
|
||||||
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) {
|
public void deletePasskey(String userId, String credentialId) {
|
||||||
var credentials = logtoClient.getWebAuthnCredentials(userId);
|
accountService.deletePasskey(userId, credentialId);
|
||||||
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) {
|
public void updateMfaMethodPreference(String userId, String preference) {
|
||||||
logtoClient.updateUserCustomData(userId, Map.of("mfa_method_preference", preference));
|
accountService.setMfaMethodPreference(userId, preference);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateTenantSettings(Map<String, Object> updates) {
|
public void updateTenantSettings(Map<String, Object> updates) {
|
||||||
@@ -455,70 +392,4 @@ public class TenantPortalService {
|
|||||||
return new AuthSettingsData(mfaMode, passkeyEnabled, passkeyMode);
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user