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;
|
||||
|
||||
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 CasAuthenticator implements Authenticator {
|
||||
public interface 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 = 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;
|
||||
}
|
||||
}
|
||||
String authenticate(final HttpServletRequest httpRequest);
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
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.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.AuthenticationFilter;
|
||||
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class WebSecurityConfig {
|
||||
|
||||
public 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[] PERMITTED_PATHS = new String[] { "/swagger-ui/**", "/v3/api-docs/**", "/actuator/**" };
|
||||
private static final String[] AUTHENTICATED_PATHS = new String[] { "/api/**" };
|
||||
|
||||
@Lazy
|
||||
@Autowired
|
||||
private CasAuthenticationFilter authenticationFilter;
|
||||
|
||||
@Bean
|
||||
@Profile("!test")
|
||||
public SecurityFilterChain securityFilterChain(final HttpSecurity http) throws Exception {
|
||||
return http
|
||||
.authorizeHttpRequests(authorize -> authorize
|
||||
// TODO.impl: implement CAS authentication via Spring Security
|
||||
// .requestMatchers(PERMITTED_PATHS).permitAll()
|
||||
// .requestMatchers(AUTHENTICATED_PATHS).authenticated()
|
||||
// .anyRequest().denyAll()
|
||||
.anyRequest().permitAll()
|
||||
.requestMatchers(PERMITTED_PATHS).permitAll()
|
||||
.requestMatchers(AUTHENTICATED_PATHS).authenticated()
|
||||
.anyRequest().denyAll()
|
||||
)
|
||||
.addFilterBefore(authenticationFilter, AuthenticationFilter.class)
|
||||
.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();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Profile("!test")
|
||||
public Authenticator casServiceTicketValidator() {
|
||||
return new CasAuthenticator();
|
||||
public CasAuthenticator casServiceTicketValidator() {
|
||||
return new RealCasAuthenticator();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CasAuthenticationFilter authenticationFilter(final CasAuthenticator authenticator) {
|
||||
return new CasAuthenticationFilter(authenticator);
|
||||
}
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ class CasAuthenticationFilterIntegrationTest {
|
||||
private WireMockServer wireMockServer;
|
||||
|
||||
@Test
|
||||
public void shouldAcceptRequest() {
|
||||
public void shouldAcceptRequestWithValidCasTicket() {
|
||||
// given
|
||||
final var username = "test-user-" + randomAlphanumeric(4);
|
||||
wireMockServer.stubFor(get(urlEqualTo("/cas/p3/serviceValidate?service=" + serviceUrl + "&ticket=valid"))
|
||||
@ -66,7 +66,7 @@ class CasAuthenticationFilterIntegrationTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldRejectRequest() {
|
||||
public void shouldRejectRequestWithInvalidCasTicket() {
|
||||
// given
|
||||
wireMockServer.stubFor(get(urlEqualTo("/cas/p3/serviceValidate?service=" + serviceUrl + "&ticket=invalid"))
|
||||
.willReturn(aResponse()
|
||||
|
@ -10,7 +10,7 @@ import static org.mockito.Mockito.mock;
|
||||
|
||||
class CasAuthenticatorUnitTest {
|
||||
|
||||
final CasAuthenticator casAuthenticator = new CasAuthenticator();
|
||||
final RealCasAuthenticator casAuthenticator = new RealCasAuthenticator();
|
||||
|
||||
@Test
|
||||
void bypassesAuthenticationIfNoCasServerIsConfigured() {
|
||||
|
@ -21,7 +21,7 @@ public class DisableSecurityConfig {
|
||||
|
||||
@Bean
|
||||
@Profile("test")
|
||||
public Authenticator fakeAuthenticator() {
|
||||
return new FakeAuthenticator();
|
||||
public CasAuthenticator fakeAuthenticator() {
|
||||
return new FakeCasAuthenticator();
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import lombok.SneakyThrows;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
public class FakeAuthenticator implements Authenticator {
|
||||
public class FakeCasAuthenticator implements CasAuthenticator {
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
Loading…
x
Reference in New Issue
Block a user