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) <noreply@anthropic.com>
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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<String> redirectUris, List<String> postLogoutUris) {
|
||||
if (!isAvailable() || appId == null) return;
|
||||
try {
|
||||
String token = getAccessToken();
|
||||
// GET current app config
|
||||
var app = (Map<String, Object>) restClient.get()
|
||||
.uri(config.getLogtoEndpoint() + "/api/applications/" + appId)
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.retrieve()
|
||||
.body(Map.class);
|
||||
if (app == null) return;
|
||||
|
||||
var metadata = (Map<String, Object>) app.get("oidcClientMetadata");
|
||||
if (metadata == null) return;
|
||||
|
||||
// Merge new URIs with existing
|
||||
var existingRedirects = new ArrayList<>((List<String>) metadata.getOrDefault("redirectUris", List.of()));
|
||||
var existingPostLogout = new ArrayList<>((List<String>) 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<Map<String, String>> getUserOrganizations(String userId) {
|
||||
if (!isAvailable()) return List.of();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ---
|
||||
|
||||
Reference in New Issue
Block a user