add CAS authentication #138

Merged
hsh-michaelhoennig merged 24 commits from feature/add-cas-authentication into master 2024-12-23 12:49:46 +01:00
7 changed files with 133 additions and 3 deletions
Showing only changes of commit 2ec44f1a43 - Show all commits

View File

@ -0,0 +1,35 @@
package net.hostsharing.hsadminng.config;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class CasAuthenticationFilter implements Filter {
@Autowired
private CasServiceTicketValidator ticketValidator;
@Override
@SneakyThrows
public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) {
final var httpRequest = (HttpServletRequest) request;
final var httpResponse = (HttpServletResponse) response;
final var ticket = httpRequest.getHeader("Authorization");
if (ticket == null || !ticketValidator.validateTicket(ticket)) {
httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
chain.doFilter(request, response);
}
}

View File

@ -0,0 +1,40 @@
package net.hostsharing.hsadminng.config;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import javax.xml.parsers.DocumentBuilderFactory;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
@Service
public class CasServiceTicketValidator {
@Value("${hsadminng.cas.server-url}")
private String casServerUrl;
@Value("${hsadminng.cas.service-url}")
private String serviceUrl;
private final RestTemplate restTemplate = new RestTemplate();
@SneakyThrows
public boolean validateTicket(final String ticket) {
if (casServerUrl.equals("fake") && ticket.equals("test")) {
return true;
}
final var url = casServerUrl + "/p3/serviceValidate" +
"?service=" + URLEncoder.encode(serviceUrl, StandardCharsets.UTF_8) +
"&ticket=" + URLEncoder.encode(ticket, StandardCharsets.UTF_8);
final var response = restTemplate.getForObject(url, String.class);
final var doc = DocumentBuilderFactory.newInstance().newDocumentBuilder()
.parse(new java.io.ByteArrayInputStream(response.getBytes()));
return doc.getElementsByTagName("cas:authenticationSuccess").getLength() > 0;
}
}

View File

@ -36,6 +36,9 @@ liquibase:
hsadminng:
postgres:
leakproof:
cas:
server-url: https://cas.example.com/cas
service-url: http://localhost:8080/api # TODO.conf: deployment target
metrics:
distribution:

View File

@ -0,0 +1,31 @@
package net.hostsharing.hsadminng.config;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.test.context.TestPropertySource;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestPropertySource(properties = {"server.port=0"})
// IMPORTANT: To test prod config, do not use test profile!
class CasAuthenticationFilterIntegrationTest {
@Value("${local.server.port}")
private int serverPort;
@Autowired
private TestRestTemplate restTemplate;
@Test
public void shouldRejectRequest() {
final var result = this.restTemplate.getForEntity(
"http://localhost:" + this.serverPort + "/api/ping", String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
}
}

View File

@ -8,13 +8,16 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.test.context.TestPropertySource;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestPropertySource(properties = {"management.port=0", "server.port=0"})
@TestPropertySource(properties = {"management.port=0", "server.port=0", "hsadminng.cas.server-url=fake"})
// IMPORTANT: To test prod config, do not use test profile!
class WebSecurityConfigIntegrationTest {
@ -29,8 +32,18 @@ class WebSecurityConfigIntegrationTest {
@Test
public void shouldSupportPingEndpoint() {
final var result = this.restTemplate.getForEntity(
"http://localhost:" + this.serverPort + "/api/ping", String.class);
// fake Authorization header
final var headers = new HttpHeaders();
headers.set("Authorization", "test");
// http request
final var result = restTemplate.exchange(
"http://localhost:" + this.serverPort + "/api/ping",
HttpMethod.GET,
new HttpEntity<Object>(null, headers),
String.class
);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(result.getBody()).startsWith("pong");
}

View File

@ -160,6 +160,7 @@ public abstract class UseCase<T extends UseCase<?>> {
.GET()
.uri(new URI("http://localhost:" + testSuite.port + uriPath))
.header("current-subject", ScenarioTest.RUN_AS_USER)
.header("Authorization", "test")
.timeout(seconds(10))
.build();
final var response = client.send(request, BodyHandlers.ofString());
@ -175,6 +176,7 @@ public abstract class UseCase<T extends UseCase<?>> {
.uri(new URI("http://localhost:" + testSuite.port + uriPath))
.header("Content-Type", "application/json")
.header("current-subject", ScenarioTest.RUN_AS_USER)
.header("Authorization", "test")
.timeout(seconds(10))
.build();
final var response = client.send(request, BodyHandlers.ofString());
@ -190,6 +192,7 @@ public abstract class UseCase<T extends UseCase<?>> {
.uri(new URI("http://localhost:" + testSuite.port + uriPath))
.header("Content-Type", "application/json")
.header("current-subject", ScenarioTest.RUN_AS_USER)
.header("Authorization", "test")
.timeout(seconds(10))
.build();
final var response = client.send(request, BodyHandlers.ofString());
@ -204,6 +207,7 @@ public abstract class UseCase<T extends UseCase<?>> {
.uri(new URI("http://localhost:" + testSuite.port + uriPath))
.header("Content-Type", "application/json")
.header("current-subject", ScenarioTest.RUN_AS_USER)
.header("Authorization", "test")
.timeout(seconds(10))
.build();
final var response = client.send(request, BodyHandlers.ofString());

View File

@ -51,3 +51,7 @@ testcontainers:
network:
mode: host
hsadminng:
cas:
server-url: fake
service-url: http://localhost:8080/api # not really used in test config