fix Security-Chain-Integration
This commit is contained in:
parent
b6b3c588ca
commit
158e279aeb
@ -1,52 +0,0 @@
|
|||||||
package net.hostsharing.hsadminng.config;
|
|
||||||
|
|
||||||
import jakarta.servlet.FilterChain;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
|
|
||||||
import lombok.SneakyThrows;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.security.authentication.BadCredentialsException;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.util.AntPathMatcher;
|
|
||||||
import org.springframework.web.filter.OncePerRequestFilter;
|
|
||||||
|
|
||||||
|
|
||||||
import static java.util.Arrays.stream;
|
|
||||||
import static net.hostsharing.hsadminng.config.WebSecurityConfig.AUTHENTICATED_PATHS;
|
|
||||||
import static net.hostsharing.hsadminng.config.WebSecurityConfig.PERMITTED_PATHS;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class AuthenticationFilter extends OncePerRequestFilter {
|
|
||||||
|
|
||||||
private static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private Authenticator authenticator;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@SneakyThrows
|
|
||||||
protected void doFilterInternal(
|
|
||||||
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) {
|
|
||||||
|
|
||||||
final var authenticatedRequest = new AuthenticatedHttpServletRequestWrapper(request);
|
|
||||||
|
|
||||||
// TODO.impl: Make request matchers work via Spring Security, maybe use Spring Security CAS support directly?
|
|
||||||
|
|
||||||
if (stream(PERMITTED_PATHS).anyMatch(path -> PATH_MATCHER.match(path, request.getRequestURI()))) {
|
|
||||||
authenticatedRequest.addHeader("current-subject", "nobody");
|
|
||||||
filterChain.doFilter(authenticatedRequest, response);
|
|
||||||
} else if (stream(AUTHENTICATED_PATHS).anyMatch(path -> PATH_MATCHER.match(path, request.getRequestURI()))) {
|
|
||||||
try {
|
|
||||||
final var currentSubject = authenticator.authenticate(request);
|
|
||||||
authenticatedRequest.addHeader("current-subject", currentSubject);
|
|
||||||
filterChain.doFilter(authenticatedRequest, response);
|
|
||||||
} catch (final BadCredentialsException exc) {
|
|
||||||
// TODO.impl: should not be necessary if ResponseStatusException worked - FIXME: try removing
|
|
||||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
package net.hostsharing.hsadminng.config;
|
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
|
|
||||||
public interface Authenticator {
|
|
||||||
|
|
||||||
String authenticate(final HttpServletRequest httpRequest);
|
|
||||||
}
|
|
@ -0,0 +1,34 @@
|
|||||||
|
package net.hostsharing.hsadminng.config;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.SneakyThrows;
|
||||||
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
|
import jakarta.servlet.FilterChain;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
// Do NOT use @Component (or similar) here, this would register the filter directly.
|
||||||
|
// But we need to register it in the SecurityFilterChain created by WebSecurityConfig.
|
||||||
|
// The bean gets created in net.hostsharing.hsadminng.config.WebSecurityConfig.authenticationFilter.
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class CasAuthenticationFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
|
private CasAuthenticator authenticator;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SneakyThrows
|
||||||
|
protected void doFilterInternal(
|
||||||
|
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) {
|
||||||
|
|
||||||
|
final var authenticatedRequest = new AuthenticatedHttpServletRequestWrapper(request);
|
||||||
|
|
||||||
|
if (request.getHeader("Authorization") != null) {
|
||||||
|
final var currentSubject = authenticator.authenticate(request);
|
||||||
|
authenticatedRequest.addHeader("current-subject", currentSubject);
|
||||||
|
} else {
|
||||||
|
authenticatedRequest.addHeader("current-subject", "nobody");
|
||||||
|
}
|
||||||
|
filterChain.doFilter(authenticatedRequest, response);
|
||||||
|
}
|
||||||
|
}
|
@ -1,81 +1,8 @@
|
|||||||
package net.hostsharing.hsadminng.config;
|
package net.hostsharing.hsadminng.config;
|
||||||
|
|
||||||
import io.micrometer.core.annotation.Timed;
|
|
||||||
import lombok.SneakyThrows;
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.security.authentication.BadCredentialsException;
|
|
||||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
|
||||||
import org.springframework.web.client.RestTemplate;
|
|
||||||
import org.xml.sax.SAXException;
|
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import javax.xml.parsers.DocumentBuilderFactory;
|
|
||||||
import javax.xml.parsers.ParserConfigurationException;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.function.Supplier;
|
|
||||||
|
|
||||||
public class CasAuthenticator implements Authenticator {
|
public interface CasAuthenticator {
|
||||||
|
|
||||||
@Value("${hsadminng.cas.server}")
|
String authenticate(final HttpServletRequest httpRequest);
|
||||||
private String casServerUrl;
|
|
||||||
|
|
||||||
@Value("${hsadminng.cas.service}")
|
|
||||||
private String serviceUrl;
|
|
||||||
|
|
||||||
private final RestTemplate restTemplate = new RestTemplate();
|
|
||||||
|
|
||||||
@SneakyThrows
|
|
||||||
@Timed("app.cas.authenticate")
|
|
||||||
public String authenticate(final HttpServletRequest httpRequest) {
|
|
||||||
final var userName = StringUtils.isBlank(casServerUrl)
|
|
||||||
? bypassCurrentSubject(httpRequest)
|
|
||||||
: casValidation(httpRequest);
|
|
||||||
final var authentication = new UsernamePasswordAuthenticationToken(userName, null, null);
|
|
||||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
|
||||||
return authentication.getName();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String bypassCurrentSubject(final HttpServletRequest httpRequest) {
|
|
||||||
final var userName = httpRequest.getHeader("current-subject");
|
|
||||||
System.err.println("CasAuthenticator.bypassCurrentSubject: " + userName);
|
|
||||||
return userName;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String casValidation(final HttpServletRequest httpRequest)
|
|
||||||
throws SAXException, IOException, ParserConfigurationException {
|
|
||||||
|
|
||||||
final var ticket = httpRequest.getHeader("Authorization");
|
|
||||||
final var url = casServerUrl + "/p3/serviceValidate" +
|
|
||||||
"?service=" + serviceUrl +
|
|
||||||
"&ticket=" + ticket;
|
|
||||||
|
|
||||||
System.err.println("CasAuthenticator.casValidation using URL: " + url);
|
|
||||||
|
|
||||||
final var response = tryTo( () -> restTemplate.getForObject(url, String.class));
|
|
||||||
|
|
||||||
final var doc = DocumentBuilderFactory.newInstance().newDocumentBuilder()
|
|
||||||
.parse(new java.io.ByteArrayInputStream(response.getBytes()));
|
|
||||||
if (doc.getElementsByTagName("cas:authenticationSuccess").getLength() == 0) {
|
|
||||||
// TODO.impl: for unknown reasons, this results in a 403 FORBIDDEN
|
|
||||||
// throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "CAS service ticket could not be validated");
|
|
||||||
System.err.println("CAS service ticket could not be validated");
|
|
||||||
System.err.println("CAS-validation-URL: " + url);
|
|
||||||
System.err.println(response);
|
|
||||||
throw new BadCredentialsException("CAS service ticket could not be validated");
|
|
||||||
}
|
|
||||||
final var userName = doc.getElementsByTagName("cas:user").item(0).getTextContent();
|
|
||||||
System.err.println("CAS-user: " + userName);
|
|
||||||
return userName;
|
|
||||||
}
|
|
||||||
|
|
||||||
private <T> T tryTo(final Supplier<T> code) {
|
|
||||||
try {
|
|
||||||
final T resultValue = code.get();
|
|
||||||
return resultValue;
|
|
||||||
} catch (final Exception e) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,71 @@
|
|||||||
|
package net.hostsharing.hsadminng.config;
|
||||||
|
|
||||||
|
import io.micrometer.core.annotation.Timed;
|
||||||
|
import lombok.SneakyThrows;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.security.authentication.BadCredentialsException;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
import org.xml.sax.SAXException;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import javax.xml.parsers.DocumentBuilderFactory;
|
||||||
|
import javax.xml.parsers.ParserConfigurationException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
public class RealCasAuthenticator implements CasAuthenticator {
|
||||||
|
|
||||||
|
@Value("${hsadminng.cas.server}")
|
||||||
|
private String casServerUrl;
|
||||||
|
|
||||||
|
@Value("${hsadminng.cas.service}")
|
||||||
|
private String serviceUrl;
|
||||||
|
|
||||||
|
private final RestTemplate restTemplate = new RestTemplate();
|
||||||
|
|
||||||
|
@SneakyThrows
|
||||||
|
@Timed("app.cas.authenticate")
|
||||||
|
public String authenticate(final HttpServletRequest httpRequest) {
|
||||||
|
final var userName = StringUtils.isBlank(casServerUrl)
|
||||||
|
? bypassCurrentSubject(httpRequest)
|
||||||
|
: casValidation(httpRequest);
|
||||||
|
final var authentication = new UsernamePasswordAuthenticationToken(userName, null, null);
|
||||||
|
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||||
|
return authentication.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String bypassCurrentSubject(final HttpServletRequest httpRequest) {
|
||||||
|
final var userName = httpRequest.getHeader("current-subject");
|
||||||
|
System.err.println("CasAuthenticator.bypassCurrentSubject: " + userName);
|
||||||
|
return userName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String casValidation(final HttpServletRequest httpRequest)
|
||||||
|
throws SAXException, IOException, ParserConfigurationException {
|
||||||
|
|
||||||
|
final var ticket = httpRequest.getHeader("Authorization");
|
||||||
|
final var url = casServerUrl + "/p3/serviceValidate" +
|
||||||
|
"?service=" + serviceUrl +
|
||||||
|
"&ticket=" + ticket;
|
||||||
|
|
||||||
|
System.err.println("CasAuthenticator.casValidation using URL: " + url);
|
||||||
|
|
||||||
|
final var response = ((Supplier<String>) () -> restTemplate.getForObject(url, String.class)).get();
|
||||||
|
|
||||||
|
final var doc = DocumentBuilderFactory.newInstance().newDocumentBuilder()
|
||||||
|
.parse(new java.io.ByteArrayInputStream(response.getBytes()));
|
||||||
|
if (doc.getElementsByTagName("cas:authenticationSuccess").getLength() == 0) {
|
||||||
|
System.err.println("CAS service ticket could not be validated");
|
||||||
|
System.err.println("CAS-validation-URL: " + url);
|
||||||
|
System.err.println(response);
|
||||||
|
throw new BadCredentialsException("CAS service ticket could not be validated");
|
||||||
|
}
|
||||||
|
final var userName = doc.getElementsByTagName("cas:user").item(0).getTextContent();
|
||||||
|
System.err.println("CAS-user: " + userName);
|
||||||
|
return userName;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,40 +1,58 @@
|
|||||||
package net.hostsharing.hsadminng.config;
|
package net.hostsharing.hsadminng.config;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.context.annotation.Profile;
|
import org.springframework.context.annotation.Profile;
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
import org.springframework.security.web.authentication.AuthenticationFilter;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
public class WebSecurityConfig {
|
public class WebSecurityConfig {
|
||||||
|
|
||||||
public static final String[] PERMITTED_PATHS = new String[]{"/swagger-ui/**", "/v3/api-docs/**", "/actuator/**"};
|
private static final String[] PERMITTED_PATHS = new String[] { "/swagger-ui/**", "/v3/api-docs/**", "/actuator/**" };
|
||||||
public static final String[] AUTHENTICATED_PATHS = new String[]{"/api/**"};
|
private static final String[] AUTHENTICATED_PATHS = new String[] { "/api/**" };
|
||||||
|
|
||||||
|
@Lazy
|
||||||
|
@Autowired
|
||||||
|
private CasAuthenticationFilter authenticationFilter;
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@Profile("!test")
|
@Profile("!test")
|
||||||
public SecurityFilterChain securityFilterChain(final HttpSecurity http) throws Exception {
|
public SecurityFilterChain securityFilterChain(final HttpSecurity http) throws Exception {
|
||||||
return http
|
return http
|
||||||
.authorizeHttpRequests(authorize -> authorize
|
.authorizeHttpRequests(authorize -> authorize
|
||||||
// TODO.impl: implement CAS authentication via Spring Security
|
.requestMatchers(PERMITTED_PATHS).permitAll()
|
||||||
// .requestMatchers(PERMITTED_PATHS).permitAll()
|
.requestMatchers(AUTHENTICATED_PATHS).authenticated()
|
||||||
// .requestMatchers(AUTHENTICATED_PATHS).authenticated()
|
.anyRequest().denyAll()
|
||||||
// .anyRequest().denyAll()
|
|
||||||
.anyRequest().permitAll()
|
|
||||||
)
|
)
|
||||||
|
.addFilterBefore(authenticationFilter, AuthenticationFilter.class)
|
||||||
.csrf(AbstractHttpConfigurer::disable)
|
.csrf(AbstractHttpConfigurer::disable)
|
||||||
|
.exceptionHandling(exception -> exception
|
||||||
|
.authenticationEntryPoint((request, response, authException) ->
|
||||||
|
// For unknown reasons Spring security returns 403 FORBIDDEN for a BadCredentialsException.
|
||||||
|
// But it should return 401 UNAUTHORIZED.
|
||||||
|
response.sendError(HttpServletResponse.SC_UNAUTHORIZED)
|
||||||
|
)
|
||||||
|
)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@Profile("!test")
|
@Profile("!test")
|
||||||
public Authenticator casServiceTicketValidator() {
|
public CasAuthenticator casServiceTicketValidator() {
|
||||||
return new CasAuthenticator();
|
return new RealCasAuthenticator();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public CasAuthenticationFilter authenticationFilter(final CasAuthenticator authenticator) {
|
||||||
|
return new CasAuthenticationFilter(authenticator);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,7 @@ class CasAuthenticationFilterIntegrationTest {
|
|||||||
private WireMockServer wireMockServer;
|
private WireMockServer wireMockServer;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldAcceptRequest() {
|
public void shouldAcceptRequestWithValidCasTicket() {
|
||||||
// given
|
// given
|
||||||
final var username = "test-user-" + randomAlphanumeric(4);
|
final var username = "test-user-" + randomAlphanumeric(4);
|
||||||
wireMockServer.stubFor(get(urlEqualTo("/cas/p3/serviceValidate?service=" + serviceUrl + "&ticket=valid"))
|
wireMockServer.stubFor(get(urlEqualTo("/cas/p3/serviceValidate?service=" + serviceUrl + "&ticket=valid"))
|
||||||
@ -66,7 +66,7 @@ class CasAuthenticationFilterIntegrationTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldRejectRequest() {
|
public void shouldRejectRequestWithInvalidCasTicket() {
|
||||||
// given
|
// given
|
||||||
wireMockServer.stubFor(get(urlEqualTo("/cas/p3/serviceValidate?service=" + serviceUrl + "&ticket=invalid"))
|
wireMockServer.stubFor(get(urlEqualTo("/cas/p3/serviceValidate?service=" + serviceUrl + "&ticket=invalid"))
|
||||||
.willReturn(aResponse()
|
.willReturn(aResponse()
|
||||||
|
@ -10,7 +10,7 @@ import static org.mockito.Mockito.mock;
|
|||||||
|
|
||||||
class CasAuthenticatorUnitTest {
|
class CasAuthenticatorUnitTest {
|
||||||
|
|
||||||
final CasAuthenticator casAuthenticator = new CasAuthenticator();
|
final RealCasAuthenticator casAuthenticator = new RealCasAuthenticator();
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void bypassesAuthenticationIfNoCasServerIsConfigured() {
|
void bypassesAuthenticationIfNoCasServerIsConfigured() {
|
||||||
|
@ -21,7 +21,7 @@ public class DisableSecurityConfig {
|
|||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@Profile("test")
|
@Profile("test")
|
||||||
public Authenticator fakeAuthenticator() {
|
public CasAuthenticator fakeAuthenticator() {
|
||||||
return new FakeAuthenticator();
|
return new FakeCasAuthenticator();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ import lombok.SneakyThrows;
|
|||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
public class FakeAuthenticator implements Authenticator {
|
public class FakeCasAuthenticator implements CasAuthenticator {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@SneakyThrows
|
@SneakyThrows
|
Loading…
x
Reference in New Issue
Block a user