From 7ee29856267be3d3f157b16c71918cb154ec60f2 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Fri, 10 Apr 2026 08:37:01 +0200 Subject: [PATCH] feat: push OIDC config to provisioned server for SSO login After provisioning a server, pushes Logto Traditional Web App credentials (client ID + secret) via the server's OIDC admin API. This enables SSO: users authenticated via Logto can access the server dashboard without a separate login. Reads tradAppSecret from bootstrap JSON via LogtoConfig. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../cameleer/saas/identity/LogtoConfig.java | 7 +++++- .../saas/vendor/VendorTenantService.java | 23 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) 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 f0bf73c..49f33e0 100644 --- a/src/main/java/net/siegeln/cameleer/saas/identity/LogtoConfig.java +++ b/src/main/java/net/siegeln/cameleer/saas/identity/LogtoConfig.java @@ -29,12 +29,13 @@ public class LogtoConfig { private String serverEndpoint; private String tradAppId; + private String tradAppSecret; @PostConstruct public void init() { if (isConfigured()) return; - // Fall back to bootstrap file for M2M credentials + trad app ID + // Fall back to bootstrap file for M2M credentials + trad app try { File file = new File(BOOTSTRAP_FILE); if (file.exists()) { @@ -48,6 +49,9 @@ public class LogtoConfig { if (node.has("tradAppId")) { tradAppId = node.get("tradAppId").asText(); } + if (node.has("tradAppSecret")) { + tradAppSecret = node.get("tradAppSecret").asText(); + } log.info("Loaded M2M credentials from bootstrap file"); } } catch (Exception e) { @@ -60,6 +64,7 @@ public class LogtoConfig { public String getM2mClientSecret() { return m2mClientSecret; } public String getServerEndpoint() { return serverEndpoint; } public String getTradAppId() { return tradAppId; } + public String getTradAppSecret() { return tradAppSecret; } public boolean isConfigured() { return logtoEndpoint != null && !logtoEndpoint.isEmpty() 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 d7f0c99..4e5aa03 100644 --- a/src/main/java/net/siegeln/cameleer/saas/vendor/VendorTenantService.java +++ b/src/main/java/net/siegeln/cameleer/saas/vendor/VendorTenantService.java @@ -25,6 +25,7 @@ import org.springframework.transaction.annotation.Transactional; import java.time.Duration; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.UUID; @@ -127,6 +128,28 @@ public class VendorTenantService { } catch (Exception e) { log.warn("License push failed for tenant {}: {}", tenant.getSlug(), e.getMessage()); } + + // Configure OIDC on the provisioned server (SSO via Logto) + if (logtoConfig.getTradAppId() != null && logtoConfig.getTradAppSecret() != null) { + try { + String publicBase = provisioningProps.publicProtocol() + "://" + provisioningProps.publicHost(); + serverApiClient.pushOidcConfig(result.serverEndpoint(), Map.of( + "enabled", true, + "issuerUri", publicBase + "/oidc", + "clientId", logtoConfig.getTradAppId(), + "clientSecret", logtoConfig.getTradAppSecret(), + "autoSignup", true, + "defaultRoles", List.of("VIEWER"), + "displayNameClaim", "name", + "rolesClaim", "roles", + "audience", "https://api.cameleer.local", + "additionalScopes", List.of() + )); + log.info("Pushed OIDC config to server for tenant {}", tenant.getSlug()); + } catch (Exception e) { + log.warn("OIDC config push failed for tenant {}: {}", tenant.getSlug(), e.getMessage()); + } + } } else { tenant.setProvisionError(result.error()); tenant.setStatus(TenantStatus.PROVISIONING);