From 0a43a7dcd1555158a2631e0e7bf44716853bfb3a Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Fri, 10 Apr 2026 07:50:38 +0200 Subject: [PATCH] feat: register OIDC redirect URIs for provisioned tenant servers During tenant provisioning, adds /t/{slug}/oidc/callback to the Logto Traditional Web App's registered redirect URIs. This enables the server's OIDC login flow to work when accessed via Traefik routing. Also reads tradAppId from bootstrap JSON via LogtoConfig. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../cameleer/saas/identity/LogtoConfig.java | 8 +++- .../saas/identity/LogtoManagementClient.java | 44 +++++++++++++++++++ .../saas/vendor/VendorTenantService.java | 20 ++++++++- .../saas/vendor/VendorTenantServiceTest.java | 12 ++++- 4 files changed, 81 insertions(+), 3 deletions(-) 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 ---