CasConfig.java
/*
* Copyright (c) 2007-2026 MetaSolutions AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.entrystore.rest.springboot.security;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apereo.cas.client.ssl.HttpsURLConnectionFactory;
import org.apereo.cas.client.validation.AbstractUrlBasedTicketValidator;
import org.apereo.cas.client.validation.Cas10TicketValidator;
import org.apereo.cas.client.validation.Cas20ServiceTicketValidator;
import org.apereo.cas.client.validation.Cas30ServiceTicketValidator;
import org.apereo.cas.client.validation.TicketValidator;
import org.entrystore.PrincipalManager;
import org.entrystore.impl.RepositoryManagerImpl;
import org.entrystore.rest.springboot.configuration.CasCustomConfiguration;
import org.entrystore.rest.springboot.configuration.ConditionalOnBooleanConfig;
import org.entrystore.rest.springboot.util.ErrorResponseWriter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.cas.ServiceProperties;
import org.springframework.security.cas.authentication.CasAuthenticationProvider;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.util.List;
import java.util.UUID;
@Slf4j
@Configuration
@RequiredArgsConstructor
@ConditionalOnBooleanConfig("entrystore.auth.cas.enabled")
public class CasConfig {
// Backchannel HTTP timeouts for CAS ticket validation — prevents thread pool exhaustion
// if the CAS server becomes slow or unresponsive during login.
private static final int CAS_CONNECT_TIMEOUT_MS = 5_000;
private static final int CAS_READ_TIMEOUT_MS = 10_000;
private static final HostnameVerifier TRUST_ALL_HOSTNAMES = (hostname, session) -> true;
private final CasCustomConfiguration casConfiguration;
private final ESUserDetailsService userDetailsService;
private final PrincipalManager principalManager;
private final RepositoryManagerImpl repositoryManager;
private final ErrorResponseWriter errorResponseWriter;
@Value("${entrystore.https.disable-verification:false}")
private boolean disableSslVerification;
@Bean
public ServiceProperties casServiceProperties() {
ServiceProperties sp = new ServiceProperties();
String baseUrl = repositoryManager.getRepositoryURL().toExternalForm();
if (!baseUrl.endsWith("/")) {
baseUrl += "/";
}
sp.setService(baseUrl + "auth/cas");
sp.setSendRenew(false);
return sp;
}
@Bean
public TicketValidator casTicketValidator() {
String casServerUrl = casConfiguration.server().url();
AbstractUrlBasedTicketValidator validator = switch (casConfiguration.version()) {
case CAS1 -> new Cas10TicketValidator(casServerUrl);
case CAS2 -> new Cas20ServiceTicketValidator(casServerUrl);
case CAS3 -> new Cas30ServiceTicketValidator(casServerUrl);
};
// Built once at bean init so a missing TLS provider fails Spring startup instead of
// escaping the lambda as HTTP 500 (IllegalStateException is not an AuthenticationException,
// so it would bypass the CAS failure handler).
final SSLSocketFactory trustAllSocketFactory;
if (disableSslVerification) {
log.warn("SSL verification is DISABLED for CAS backchannel validation (entrystore.https.disable-verification=true). " +
"This is insecure and should only be used in development environments with self-signed certificates.");
trustAllSocketFactory = createTrustAllSocketFactory();
} else {
trustAllSocketFactory = null;
}
var defaultFactory = new HttpsURLConnectionFactory();
validator.setURLConnectionFactory(urlConnection -> {
var conn = defaultFactory.buildHttpURLConnection(urlConnection);
conn.setConnectTimeout(CAS_CONNECT_TIMEOUT_MS);
conn.setReadTimeout(CAS_READ_TIMEOUT_MS);
if (trustAllSocketFactory != null && conn instanceof HttpsURLConnection httpsConn) {
httpsConn.setSSLSocketFactory(trustAllSocketFactory);
httpsConn.setHostnameVerifier(TRUST_ALL_HOSTNAMES);
}
return conn;
});
return validator;
}
private static SSLSocketFactory createTrustAllSocketFactory() {
try {
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, new TrustManager[]{new X509TrustManager() {
@Override public void checkClientTrusted(X509Certificate[] chain, String authType) { }
@Override public void checkServerTrusted(X509Certificate[] chain, String authType) { }
@Override public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; }
}}, new SecureRandom());
return ctx.getSocketFactory();
} catch (NoSuchAlgorithmException | KeyManagementException e) {
throw new IllegalStateException("Failed to create trust-all SSLContext for CAS backchannel", e);
}
}
/**
* Builds the provider for the CAS filter alone. It must not be a bean, see
* {@link SecurityConfig#passwordAuthenticationProvider}.
*/
static CasAuthenticationProvider casAuthenticationProvider(ServiceProperties serviceProperties,
TicketValidator ticketValidator) {
CasAuthenticationProvider provider = new CasAuthenticationProvider();
provider.setServiceProperties(serviceProperties);
provider.setTicketValidator(ticketValidator);
provider.setAuthenticationUserDetailsService(token ->
new org.springframework.security.core.userdetails.User(
token.getName(),
"N/A",
List.of(new SimpleGrantedAuthority("ROLE_USER"))));
// Random key per JVM — used by CasAuthenticationToken for internal hash-based integrity checks
provider.setKey(UUID.randomUUID().toString());
provider.afterPropertiesSet();
return provider;
}
@Bean
public CasLoginSuccessHandler casLoginSuccessHandler(AuthTokenCookies authTokenCookies) {
return new CasLoginSuccessHandler(userDetailsService, principalManager, errorResponseWriter, authTokenCookies,
casConfiguration);
}
}