From ca92b3ce7d2cf7beb8a0c9610190a2e9c4e0e7e0 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Mon, 6 Apr 2026 00:26:40 +0200 Subject: [PATCH] feat: add CAMELEER_OIDC_TLS_SKIP_VERIFY to bypass cert verification for OIDC Self-signed CA certs on the OIDC provider (e.g. Logto behind a reverse proxy) cause the login flow to fail because Java's truststore rejects the connection. This adds an opt-in env var that creates a trust-all SSLContext scoped to OIDC HTTP calls only (discovery, token exchange, JWKS fetch) without affecting system-wide TLS. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../app/security/InsecureTlsHelper.java | 71 +++++++++++++++++++ .../app/security/OidcTokenExchanger.java | 29 ++++++-- .../server/app/security/SecurityConfig.java | 14 +++- .../app/security/SecurityProperties.java | 3 + .../src/main/resources/application.yml | 1 + 5 files changed, 110 insertions(+), 8 deletions(-) create mode 100644 cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/InsecureTlsHelper.java diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/InsecureTlsHelper.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/InsecureTlsHelper.java new file mode 100644 index 00000000..4b8c8730 --- /dev/null +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/InsecureTlsHelper.java @@ -0,0 +1,71 @@ +package com.cameleer3.server.app.security; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; + +/** + * Provides trust-all TLS helpers for OIDC connections when + * {@code CAMELEER_OIDC_TLS_SKIP_VERIFY=true}. Package-private — only used + * by {@link OidcTokenExchanger} and {@link SecurityConfig}. + */ +final class InsecureTlsHelper { + + private static final Logger log = LoggerFactory.getLogger(InsecureTlsHelper.class); + + private static volatile SSLSocketFactory cachedFactory; + private static final HostnameVerifier ACCEPT_ALL = (hostname, session) -> true; + + private InsecureTlsHelper() {} + + static SSLSocketFactory socketFactory() { + if (cachedFactory == null) { + synchronized (InsecureTlsHelper.class) { + if (cachedFactory == null) { + try { + TrustManager[] trustAll = {new X509TrustManager() { + @Override public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } + @Override public void checkClientTrusted(X509Certificate[] certs, String authType) {} + @Override public void checkServerTrusted(X509Certificate[] certs, String authType) {} + }}; + SSLContext ctx = SSLContext.getInstance("TLS"); + ctx.init(null, trustAll, new SecureRandom()); + cachedFactory = ctx.getSocketFactory(); + log.warn("OIDC TLS certificate verification disabled (skip-verify) — do not use in production"); + } catch (Exception e) { + throw new IllegalStateException("Failed to create trust-all SSLContext", e); + } + } + } + } + return cachedFactory; + } + + static HostnameVerifier hostnameVerifier() { + return ACCEPT_ALL; + } + + /** + * Opens an {@link InputStream} from the given URL, applying trust-all TLS + * if the connection is HTTPS and {@code skipVerify} is true. + */ + static InputStream openStream(URL url, boolean skipVerify) throws Exception { + URLConnection conn = url.openConnection(); + if (skipVerify && conn instanceof HttpsURLConnection https) { + https.setSSLSocketFactory(socketFactory()); + https.setHostnameVerifier(hostnameVerifier()); + } + return conn.getInputStream(); + } +} diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/OidcTokenExchanger.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/OidcTokenExchanger.java index 61f47e21..bf9d2b49 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/OidcTokenExchanger.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/OidcTokenExchanger.java @@ -27,6 +27,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; +import com.nimbusds.jose.jwk.source.RemoteJWKSet; +import com.nimbusds.jose.util.DefaultResourceRetriever; + import java.io.InputStream; import java.net.URI; import java.net.URL; @@ -48,13 +51,16 @@ public class OidcTokenExchanger { private static final Logger log = LoggerFactory.getLogger(OidcTokenExchanger.class); private final OidcConfigRepository configRepository; + private final boolean tlsSkipVerify; private volatile String cachedIssuerUri; private volatile OIDCProviderMetadata providerMetadata; private volatile ConfigurableJWTProcessor jwtProcessor; - public OidcTokenExchanger(OidcConfigRepository configRepository) { + public OidcTokenExchanger(OidcConfigRepository configRepository, + SecurityProperties securityProperties) { this.configRepository = configRepository; + this.tlsSkipVerify = securityProperties.isOidcTlsSkipVerify(); } public record OidcUserInfo(String subject, String email, String name, List roles, String idToken) {} @@ -78,7 +84,12 @@ public class OidcTokenExchanger { new AuthorizationCodeGrant(new AuthorizationCode(code), new URI(redirectUri)) ); - TokenResponse tokenResponse = TokenResponse.parse(tokenRequest.toHTTPRequest().send()); + var httpRequest = tokenRequest.toHTTPRequest(); + if (tlsSkipVerify) { + httpRequest.setSSLSocketFactory(InsecureTlsHelper.socketFactory()); + httpRequest.setHostnameVerifier(InsecureTlsHelper.hostnameVerifier()); + } + TokenResponse tokenResponse = TokenResponse.parse(httpRequest.send()); if (!tokenResponse.indicatesSuccess()) { String error = tokenResponse.toErrorResponse().getErrorObject().getDescription(); @@ -191,7 +202,7 @@ public class OidcTokenExchanger { // .well-known/openid-configuration automatically, the user provides // the complete URL. URL discoveryUrl = new URI(issuerUri).toURL(); - try (InputStream in = discoveryUrl.openStream()) { + try (InputStream in = InsecureTlsHelper.openStream(discoveryUrl, tlsSkipVerify)) { JSONObject json = (JSONObject) new JSONParser(JSONParser.DEFAULT_PERMISSIVE_MODE) .parse(in); providerMetadata = OIDCProviderMetadata.parse(json); @@ -210,9 +221,15 @@ public class OidcTokenExchanger { synchronized (this) { if (jwtProcessor == null) { OIDCProviderMetadata metadata = getProviderMetadata(issuerUri); - JWKSource jwkSource = JWKSourceBuilder - .create(metadata.getJWKSetURI().toURL()) - .build(); + URL jwksUrl = metadata.getJWKSetURI().toURL(); + JWKSource jwkSource; + if (tlsSkipVerify) { + var retriever = new DefaultResourceRetriever(5000, 5000, 0, true, + InsecureTlsHelper.socketFactory()); + jwkSource = new RemoteJWKSet<>(jwksUrl, retriever); + } else { + jwkSource = JWKSourceBuilder.create(jwksUrl).build(); + } Set expectedAlgs = Set.of(JWSAlgorithm.RS256, JWSAlgorithm.ES256); JWSKeySelector keySelector = diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityConfig.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityConfig.java index e7349042..00954b14 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityConfig.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityConfig.java @@ -3,9 +3,12 @@ package com.cameleer3.server.app.security; import com.cameleer3.server.core.agent.AgentRegistryService; import com.cameleer3.server.core.security.JwtService; import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.jwk.source.JWKSource; import com.nimbusds.jose.jwk.source.JWKSourceBuilder; +import com.nimbusds.jose.jwk.source.RemoteJWKSet; import com.nimbusds.jose.proc.JWSVerificationKeySelector; import com.nimbusds.jose.proc.SecurityContext; +import com.nimbusds.jose.util.DefaultResourceRetriever; import com.nimbusds.jwt.proc.DefaultJWTProcessor; import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata; import net.minidev.json.JSONObject; @@ -169,7 +172,7 @@ public class SecurityConfig { : issuerUri + "/.well-known/openid-configuration"; URL url = new URI(discoveryUrl).toURL(); OIDCProviderMetadata metadata; - try (InputStream in = url.openStream()) { + try (InputStream in = InsecureTlsHelper.openStream(url, properties.isOidcTlsSkipVerify())) { JSONObject json = (JSONObject) new JSONParser(JSONParser.DEFAULT_PERMISSIVE_MODE).parse(in); metadata = OIDCProviderMetadata.parse(json); } @@ -177,7 +180,14 @@ public class SecurityConfig { } // Build decoder supporting ES384 (Logto default) and ES256, RS256 - var jwkSource = JWKSourceBuilder.create(jwksUri).build(); + JWKSource jwkSource; + if (properties.isOidcTlsSkipVerify()) { + var retriever = new DefaultResourceRetriever(5000, 5000, 0, true, + InsecureTlsHelper.socketFactory()); + jwkSource = new RemoteJWKSet<>(jwksUri, retriever); + } else { + jwkSource = JWKSourceBuilder.create(jwksUri).build(); + } Set algorithms = Set.of(JWSAlgorithm.ES384, JWSAlgorithm.ES256, JWSAlgorithm.RS256); var keySelector = new JWSVerificationKeySelector(algorithms, jwkSource); var processor = new DefaultJWTProcessor(); diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityProperties.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityProperties.java index 100525cd..7a9d6e98 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityProperties.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityProperties.java @@ -20,6 +20,7 @@ public class SecurityProperties { private String oidcIssuerUri; private String oidcJwkSetUri; private String oidcAudience; + private boolean oidcTlsSkipVerify; public long getAccessTokenExpiryMs() { return accessTokenExpiryMs; } public void setAccessTokenExpiryMs(long accessTokenExpiryMs) { this.accessTokenExpiryMs = accessTokenExpiryMs; } @@ -43,4 +44,6 @@ public class SecurityProperties { public void setOidcJwkSetUri(String oidcJwkSetUri) { this.oidcJwkSetUri = oidcJwkSetUri; } public String getOidcAudience() { return oidcAudience; } public void setOidcAudience(String oidcAudience) { this.oidcAudience = oidcAudience; } + public boolean isOidcTlsSkipVerify() { return oidcTlsSkipVerify; } + public void setOidcTlsSkipVerify(boolean oidcTlsSkipVerify) { this.oidcTlsSkipVerify = oidcTlsSkipVerify; } } diff --git a/cameleer3-server-app/src/main/resources/application.yml b/cameleer3-server-app/src/main/resources/application.yml index 3dfe2cc0..ae313df9 100644 --- a/cameleer3-server-app/src/main/resources/application.yml +++ b/cameleer3-server-app/src/main/resources/application.yml @@ -53,6 +53,7 @@ security: oidc-issuer-uri: ${CAMELEER_OIDC_ISSUER_URI:} oidc-jwk-set-uri: ${CAMELEER_OIDC_JWK_SET_URI:} oidc-audience: ${CAMELEER_OIDC_AUDIENCE:} + oidc-tls-skip-verify: ${CAMELEER_OIDC_TLS_SKIP_VERIFY:false} springdoc: