diff --git a/src/main/java/net/siegeln/cameleer/saas/identity/LogtoConfig.java b/src/main/java/net/siegeln/cameleer/saas/identity/LogtoConfig.java index 9de49d2..f0bf73c 100644 --- a/src/main/java/net/siegeln/cameleer/saas/identity/LogtoConfig.java +++ b/src/main/java/net/siegeln/cameleer/saas/identity/LogtoConfig.java @@ -28,11 +28,13 @@ public class LogtoConfig { @Value("${cameleer.identity.server-endpoint:http://cameleer3-server:8081}") private String serverEndpoint; + private String tradAppId; + @PostConstruct public void init() { if (isConfigured()) return; - // Fall back to bootstrap file for M2M credentials + // Fall back to bootstrap file for M2M credentials + trad app ID try { File file = new File(BOOTSTRAP_FILE); if (file.exists()) { @@ -43,6 +45,9 @@ public class LogtoConfig { if ((m2mClientSecret == null || m2mClientSecret.isEmpty()) && node.has("m2mClientSecret")) { m2mClientSecret = node.get("m2mClientSecret").asText(); } + if (node.has("tradAppId")) { + tradAppId = node.get("tradAppId").asText(); + } log.info("Loaded M2M credentials from bootstrap file"); } } catch (Exception e) { @@ -54,6 +59,7 @@ public class LogtoConfig { public String getM2mClientId() { return m2mClientId; } public String getM2mClientSecret() { return m2mClientSecret; } public String getServerEndpoint() { return serverEndpoint; } + public String getTradAppId() { return tradAppId; } public boolean isConfigured() { return logtoEndpoint != null && !logtoEndpoint.isEmpty() 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 11be18f..e617f92 100644 --- a/src/main/java/net/siegeln/cameleer/saas/identity/LogtoManagementClient.java +++ b/src/main/java/net/siegeln/cameleer/saas/identity/LogtoManagementClient.java @@ -75,6 +75,50 @@ public class LogtoManagementClient { .toBodilessEntity(); } + /** Add redirect URIs to a Logto application (for OIDC callback registration). */ + @SuppressWarnings("unchecked") + public void addAppRedirectUris(String appId, List redirectUris, List postLogoutUris) { + if (!isAvailable() || appId == null) return; + try { + String token = getAccessToken(); + // GET current app config + var app = (Map) restClient.get() + .uri(config.getLogtoEndpoint() + "/api/applications/" + appId) + .header("Authorization", "Bearer " + token) + .retrieve() + .body(Map.class); + if (app == null) return; + + var metadata = (Map) app.get("oidcClientMetadata"); + if (metadata == null) return; + + // Merge new URIs with existing + var existingRedirects = new ArrayList<>((List) metadata.getOrDefault("redirectUris", List.of())); + var existingPostLogout = new ArrayList<>((List) metadata.getOrDefault("postLogoutRedirectUris", List.of())); + for (String uri : redirectUris) { + if (!existingRedirects.contains(uri)) existingRedirects.add(uri); + } + for (String uri : postLogoutUris) { + if (!existingPostLogout.contains(uri)) existingPostLogout.add(uri); + } + + // PATCH app with updated URIs + restClient.patch() + .uri(config.getLogtoEndpoint() + "/api/applications/" + appId) + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + .body(Map.of("oidcClientMetadata", Map.of( + "redirectUris", existingRedirects, + "postLogoutRedirectUris", existingPostLogout + ))) + .retrieve() + .toBodilessEntity(); + log.info("Updated redirect URIs for app {}: added {}", appId, redirectUris); + } catch (Exception e) { + log.warn("Failed to update redirect URIs for app {}: {}", appId, e.getMessage()); + } + } + public List> getUserOrganizations(String userId) { if (!isAvailable()) return List.of(); diff --git a/src/main/java/net/siegeln/cameleer/saas/vendor/VendorTenantService.java b/src/main/java/net/siegeln/cameleer/saas/vendor/VendorTenantService.java index 8007e3c..d7f0c99 100644 --- a/src/main/java/net/siegeln/cameleer/saas/vendor/VendorTenantService.java +++ b/src/main/java/net/siegeln/cameleer/saas/vendor/VendorTenantService.java @@ -2,8 +2,10 @@ package net.siegeln.cameleer.saas.vendor; import net.siegeln.cameleer.saas.audit.AuditAction; import net.siegeln.cameleer.saas.audit.AuditService; +import net.siegeln.cameleer.saas.identity.LogtoConfig; import net.siegeln.cameleer.saas.identity.LogtoManagementClient; import net.siegeln.cameleer.saas.identity.ServerApiClient; +import net.siegeln.cameleer.saas.provisioning.ProvisioningProperties; import net.siegeln.cameleer.saas.identity.ServerApiClient.ServerHealthResponse; import net.siegeln.cameleer.saas.license.LicenseEntity; import net.siegeln.cameleer.saas.license.LicenseService; @@ -38,7 +40,9 @@ public class VendorTenantService { private final TenantProvisioner tenantProvisioner; private final ServerApiClient serverApiClient; private final LogtoManagementClient logtoClient; + private final LogtoConfig logtoConfig; private final AuditService auditService; + private final ProvisioningProperties provisioningProps; public VendorTenantService(TenantService tenantService, TenantRepository tenantRepository, @@ -46,14 +50,18 @@ public class VendorTenantService { TenantProvisioner tenantProvisioner, ServerApiClient serverApiClient, LogtoManagementClient logtoClient, - AuditService auditService) { + LogtoConfig logtoConfig, + AuditService auditService, + ProvisioningProperties provisioningProps) { this.tenantService = tenantService; this.tenantRepository = tenantRepository; this.licenseService = licenseService; this.tenantProvisioner = tenantProvisioner; this.serverApiClient = serverApiClient; this.logtoClient = logtoClient; + this.logtoConfig = logtoConfig; this.auditService = auditService; + this.provisioningProps = provisioningProps; } @Transactional @@ -86,6 +94,16 @@ public class VendorTenantService { } catch (Exception e) { log.warn("Failed to add vendor to org for tenant {}: {}", tenant.getSlug(), e.getMessage()); } + + // Register OIDC redirect URIs for the tenant's server in the Traditional Web App + String tradAppId = logtoConfig.getTradAppId(); + if (tradAppId != null) { + String base = provisioningProps.publicProtocol() + "://" + provisioningProps.publicHost(); + String slug = tenant.getSlug(); + logtoClient.addAppRedirectUris(tradAppId, + List.of(base + "/t/" + slug + "/oidc/callback"), + List.of(base + "/t/" + slug, base + "/t/" + slug + "/login?local")); + } } // 3. Generate license diff --git a/src/test/java/net/siegeln/cameleer/saas/vendor/VendorTenantServiceTest.java b/src/test/java/net/siegeln/cameleer/saas/vendor/VendorTenantServiceTest.java index c1b2220..e068f31 100644 --- a/src/test/java/net/siegeln/cameleer/saas/vendor/VendorTenantServiceTest.java +++ b/src/test/java/net/siegeln/cameleer/saas/vendor/VendorTenantServiceTest.java @@ -1,8 +1,10 @@ package net.siegeln.cameleer.saas.vendor; import net.siegeln.cameleer.saas.audit.AuditService; +import net.siegeln.cameleer.saas.identity.LogtoConfig; import net.siegeln.cameleer.saas.identity.LogtoManagementClient; import net.siegeln.cameleer.saas.identity.ServerApiClient; +import net.siegeln.cameleer.saas.provisioning.ProvisioningProperties; import net.siegeln.cameleer.saas.license.LicenseEntity; import net.siegeln.cameleer.saas.license.LicenseService; import net.siegeln.cameleer.saas.provisioning.ProvisionResult; @@ -53,6 +55,9 @@ class VendorTenantServiceTest { @Mock private LogtoManagementClient logtoClient; + @Mock + private LogtoConfig logtoConfig; + @Mock private AuditService auditService; @@ -60,9 +65,14 @@ class VendorTenantServiceTest { @BeforeEach void setUp() { + var provisioningProps = new ProvisioningProperties( + "img", "uiimg", "net", "traefik", "localhost", "https", + "jdbc:postgresql://pg:5432/db", "https://localhost/oidc", + "http://logto:3001/oidc/jwks", "https://localhost"); vendorTenantService = new VendorTenantService( tenantService, tenantRepository, licenseService, - tenantProvisioner, serverApiClient, logtoClient, auditService); + tenantProvisioner, serverApiClient, logtoClient, logtoConfig, + auditService, provisioningProps); } // --- Helpers ---