From 17e4c9c6a76e002ffeaa7ed38053ac727827e540 Mon Sep 17 00:00:00 2001 From: Jonas Wagner Date: Wed, 11 Dec 2024 17:31:43 +0100 Subject: [PATCH] WIP --- .github/workflows/build.yml | 1 + auth/README.md | 1 + auth/compose.yaml | 23 ++++++++++++ .../application-auth:oauth2.yaml | 31 ++++++++++++++++ util/pom.xml | 15 ++++++++ .../smith/fts/util/AgentConfiguration.java | 22 ++++++++++++ .../care/smith/fts/util/WebClientFactory.java | 8 ++++- .../smith/fts/util/auth/HttpClientAuth.java | 1 + .../fts/util/auth/HttpClientOAuth2Auth.java | 35 +++++++++++++++++++ .../fts/util/auth/HttpServerAuthMethod.java | 7 ++-- .../fts/util/auth/HttpServerOAuth2Auth.java | 33 +++++++++++++++++ .../smith/fts/util/WebClientFactoryTest.java | 10 +++--- .../util/auth/HttpServerAuthConfigTest.java | 7 ++-- 13 files changed, 185 insertions(+), 9 deletions(-) create mode 100644 auth/README.md create mode 100644 auth/compose.yaml create mode 100644 clinical-domain-agent/application-auth:oauth2.yaml create mode 100644 util/src/main/java/care/smith/fts/util/auth/HttpClientOAuth2Auth.java create mode 100644 util/src/main/java/care/smith/fts/util/auth/HttpServerOAuth2Auth.java diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f224144f..64146d7e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,6 +25,7 @@ jobs: - none - basic - cert + - oauth2 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 diff --git a/auth/README.md b/auth/README.md new file mode 100644 index 00000000..79c5ceeb --- /dev/null +++ b/auth/README.md @@ -0,0 +1 @@ +# Authn/Authz Util diff --git a/auth/compose.yaml b/auth/compose.yaml new file mode 100644 index 00000000..a2fff04b --- /dev/null +++ b/auth/compose.yaml @@ -0,0 +1,23 @@ +name: fts-auth + +services: + keycloak: + image: quay.io/keycloak/keycloak:25.0.6 + command: [ "start-dev" ] + environment: + KEYCLOAK_ADMIN: admin + KEYCLOAK_ADMIN_PASSWORD: admin + ports: + - "8080:8080" + networks: [ "clinical-domain", "research-domain", "trust-center" ] + +networks: + clinical-domain: + external: true + name: fts-test_clinical-domain + research-domain: + external: true + name: fts-test_research-domain + trust-center: + external: true + name: fts-test_trust-center diff --git a/clinical-domain-agent/application-auth:oauth2.yaml b/clinical-domain-agent/application-auth:oauth2.yaml new file mode 100644 index 00000000..87ebc33e --- /dev/null +++ b/clinical-domain-agent/application-auth:oauth2.yaml @@ -0,0 +1,31 @@ +server: + port: 9090 + +projects: + directory: "../.github/test/cd-agent/projects" + +security: + auth: + oauth2: + issuer: http://localhost:8080/realms/master + +spring: + security: + oauth2: + client: + registration: + agent: + authorizationGrantType: client_credentials + clientId: fts/cd-agent + clientSecret: eA4xj1zFxsVYZGdLah9KnkcmHYDBjojr + provider: keycloak + provider: + keycloak: + issuer-uri: http://localhost:8080/realms/master + +test: + webclient: + default: + auth: + oauth2: + registration: agent diff --git a/util/pom.xml b/util/pom.xml index 8670ed4f..6a9fc3bc 100644 --- a/util/pom.xml +++ b/util/pom.xml @@ -81,6 +81,21 @@ spring-boot-starter-security + + org.springframework.security + spring-security-oauth2-client + + + + org.springframework.security + spring-security-oauth2-resource-server + + + + org.springframework.security + spring-security-oauth2-jose + + org.mock-server mockserver-junit-jupiter diff --git a/util/src/main/java/care/smith/fts/util/AgentConfiguration.java b/util/src/main/java/care/smith/fts/util/AgentConfiguration.java index 93a48f39..61080185 100644 --- a/util/src/main/java/care/smith/fts/util/AgentConfiguration.java +++ b/util/src/main/java/care/smith/fts/util/AgentConfiguration.java @@ -7,10 +7,16 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import java.net.http.HttpClient; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Primary; +import org.springframework.security.oauth2.client.AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProviderBuilder; +import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; @Configuration @Import({ @@ -38,4 +44,20 @@ public HttpClient httpClient() { public ObjectMapper defaultObjectMapper() { return new ObjectMapper().registerModule(new JavaTimeModule()); } + + @Bean + @ConditionalOnBean(ReactiveClientRegistrationRepository.class) + public ReactiveOAuth2AuthorizedClientManager authorizedClientManager( + ReactiveClientRegistrationRepository clientRegistrationRepository, + ReactiveOAuth2AuthorizedClientService authorizedClientService) { + var authorizedClientManager = + new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientService); + + var authorizedClientProvider = + ReactiveOAuth2AuthorizedClientProviderBuilder.builder().clientCredentials().build(); + + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); + return authorizedClientManager; + } } diff --git a/util/src/main/java/care/smith/fts/util/WebClientFactory.java b/util/src/main/java/care/smith/fts/util/WebClientFactory.java index 41b03c18..72a8c765 100644 --- a/util/src/main/java/care/smith/fts/util/WebClientFactory.java +++ b/util/src/main/java/care/smith/fts/util/WebClientFactory.java @@ -6,6 +6,7 @@ import care.smith.fts.util.auth.HttpClientAuth; import care.smith.fts.util.auth.HttpClientBasicAuth; import care.smith.fts.util.auth.HttpClientCookieTokenAuth; +import care.smith.fts.util.auth.HttpClientOAuth2Auth; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientSsl; import org.springframework.context.annotation.Import; @@ -14,22 +15,25 @@ import org.springframework.web.reactive.function.client.WebClient.Builder; @Component -@Import({HttpClientBasicAuth.class, HttpClientCookieTokenAuth.class}) +@Import({HttpClientBasicAuth.class, HttpClientOAuth2Auth.class, HttpClientCookieTokenAuth.class}) public class WebClientFactory { private final Builder clientBuilder; private final WebClientSsl ssl; private final HttpClientBasicAuth basic; + private final HttpClientOAuth2Auth oauth2; private final HttpClientCookieTokenAuth token; public WebClientFactory( WebClient.Builder clientBuilder, WebClientSsl ssl, @Autowired(required = false) HttpClientBasicAuth basic, + @Autowired(required = false) HttpClientOAuth2Auth oauth2, @Autowired(required = false) HttpClientCookieTokenAuth token) { this.clientBuilder = clientBuilder; this.ssl = ssl; this.basic = basic; + this.oauth2 = oauth2; this.token = token; } @@ -55,6 +59,8 @@ private void configureAuth(Builder builder, HttpClientAuth.Config auth) { configureAuth(builder, "basic", basic, auth.basic()); } else if (auth.cookieToken() != null) { configureAuth(builder, "cookieToken", token, auth.cookieToken()); + } else if (auth.oauth2() != null) { + configureAuth(builder, "cookieToken", oauth2, auth.oauth2()); } } } diff --git a/util/src/main/java/care/smith/fts/util/auth/HttpClientAuth.java b/util/src/main/java/care/smith/fts/util/auth/HttpClientAuth.java index 5b866ebb..991eec9b 100644 --- a/util/src/main/java/care/smith/fts/util/auth/HttpClientAuth.java +++ b/util/src/main/java/care/smith/fts/util/auth/HttpClientAuth.java @@ -10,6 +10,7 @@ public interface HttpClientAuth { record Config( HttpClientBasicAuth.Config basic, + HttpClientOAuth2Auth.Config oauth2, HttpClientCookieTokenAuth.Config cookieToken, Object none) { diff --git a/util/src/main/java/care/smith/fts/util/auth/HttpClientOAuth2Auth.java b/util/src/main/java/care/smith/fts/util/auth/HttpClientOAuth2Auth.java new file mode 100644 index 00000000..ce067cdf --- /dev/null +++ b/util/src/main/java/care/smith/fts/util/auth/HttpClientOAuth2Auth.java @@ -0,0 +1,35 @@ +package care.smith.fts.util.auth; + +import care.smith.fts.util.auth.HttpClientOAuth2Auth.Config; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.web.reactive.function.client.ServerOAuth2AuthorizedClientExchangeFilterFunction; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient.Builder; + +/** OAuth2 Authentication using oauth2 client credentials flow. */ +@Slf4j +@Component +@ConditionalOnBean(ReactiveOAuth2AuthorizedClientManager.class) +public class HttpClientOAuth2Auth implements HttpClientAuth { + + private final ReactiveOAuth2AuthorizedClientManager clientManager; + + public HttpClientOAuth2Auth(ReactiveOAuth2AuthorizedClientManager clientManager) { + this.clientManager = clientManager; + } + + public record Config(String registration) {} + + @Override + public void configure(Config config, Builder builder) { + log.debug( + "Configuring oauth2 client, registration '{}', clientManager: {}", + config.registration(), + clientManager); + var filter = new ServerOAuth2AuthorizedClientExchangeFilterFunction(clientManager); + filter.setDefaultClientRegistrationId(config.registration()); + builder.filter(filter); + } +} diff --git a/util/src/main/java/care/smith/fts/util/auth/HttpServerAuthMethod.java b/util/src/main/java/care/smith/fts/util/auth/HttpServerAuthMethod.java index 46210476..74d647b7 100644 --- a/util/src/main/java/care/smith/fts/util/auth/HttpServerAuthMethod.java +++ b/util/src/main/java/care/smith/fts/util/auth/HttpServerAuthMethod.java @@ -16,7 +16,10 @@ default ServerHttpSecurity filter(Endpoint endpoint, ServerHttpSecurity http) { ReactiveUserDetailsService configureUsers(); record AuthMethod( - HttpServerClientCertAuth clientCert, HttpServerBasicAuth basic, HttpServerNoneAuth none) {} + HttpServerClientCertAuth clientCert, + HttpServerBasicAuth basic, + HttpServerOAuth2Auth oauth2, + HttpServerNoneAuth none) {} - AuthMethod NONE = new AuthMethod(null, null, HttpServerNoneAuth.NONE); + AuthMethod NONE = new AuthMethod(null, null, null, HttpServerNoneAuth.NONE); } diff --git a/util/src/main/java/care/smith/fts/util/auth/HttpServerOAuth2Auth.java b/util/src/main/java/care/smith/fts/util/auth/HttpServerOAuth2Auth.java new file mode 100644 index 00000000..e4a48e56 --- /dev/null +++ b/util/src/main/java/care/smith/fts/util/auth/HttpServerOAuth2Auth.java @@ -0,0 +1,33 @@ +package care.smith.fts.util.auth; + +import static org.springframework.security.oauth2.jwt.ReactiveJwtDecoders.fromIssuerLocation; + +import care.smith.fts.util.auth.HttpServerAuthConfig.Endpoint; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.core.userdetails.ReactiveUserDetailsService; + +public record HttpServerOAuth2Auth(String issuer) implements HttpServerAuthMethod { + + @Override + public ServerHttpSecurity configure(ServerHttpSecurity http) { + return http.oauth2ResourceServer( + oauth2 -> oauth2.jwt(jwt -> jwt.jwtDecoder(fromIssuerLocation(issuer)))); + } + + @Override + public ServerHttpSecurity filter(Endpoint endpoint, ServerHttpSecurity http) { + return http.authorizeExchange( + exchange -> exchange.pathMatchers(endpoint.path()).hasAuthority(endpoint.role())); + } + + @Override + public ReactiveUserDetailsService configureUsers() { + // Since it's client credentials, we don't configure users explicitly here. + return null; + } + + @Override + public String toString() { + return "OAuth2"; + } +} diff --git a/util/src/test/java/care/smith/fts/util/WebClientFactoryTest.java b/util/src/test/java/care/smith/fts/util/WebClientFactoryTest.java index 92a8575b..2b93962a 100644 --- a/util/src/test/java/care/smith/fts/util/WebClientFactoryTest.java +++ b/util/src/test/java/care/smith/fts/util/WebClientFactoryTest.java @@ -10,6 +10,7 @@ import care.smith.fts.util.auth.HttpClientAuth.Config; import care.smith.fts.util.auth.HttpClientBasicAuth; import care.smith.fts.util.auth.HttpClientCookieTokenAuth; +import care.smith.fts.util.auth.HttpClientOAuth2Auth; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -28,8 +29,9 @@ class WebClientFactoryTest { void setUp( @Autowired WebClientSsl ssl, @Autowired HttpClientBasicAuth basic, + @Autowired HttpClientOAuth2Auth oauth2, @Autowired HttpClientCookieTokenAuth token) { - factory = new WebClientFactory(builder(), ssl, basic, token); + factory = new WebClientFactory(builder(), ssl, basic, oauth2, token); } @Test @@ -56,7 +58,7 @@ void createWithMultipleAuthBasicIsTaken() { var basic = Mockito.mock(HttpClientBasicAuth.class); var token = Mockito.mock(HttpClientCookieTokenAuth.class); Builder builder = builder(); - factory = new WebClientFactory(builder, null, basic, token); + factory = new WebClientFactory(builder, null, basic, null, token); var basicConf = new HttpClientBasicAuth.Config("user-1505512", "pwd-15054518"); var tokenConf = new HttpClientCookieTokenAuth.Config("token-152510"); @@ -70,7 +72,7 @@ void createWithMultipleAuthBasicIsTaken() { @Test void createWithBasicAuthMissingImplementation( @Autowired WebClientSsl ssl, @Autowired HttpClientCookieTokenAuth token) { - factory = new WebClientFactory(builder(), ssl, null, token); + factory = new WebClientFactory(builder(), ssl, null, null, token); var auth = new HttpClientBasicAuth.Config("user-144512", "pwd-144538"); var config = new HttpClientConfig("http://localhost", new Config(auth, null)); @@ -88,7 +90,7 @@ void createWithTokenAuth() { @Test void createWithTokenAuthMissingImplementation( @Autowired WebClientSsl ssl, @Autowired HttpClientBasicAuth basic) { - factory = new WebClientFactory(builder(), ssl, basic, null); + factory = new WebClientFactory(builder(), ssl, basic, null, null); var auth = new HttpClientCookieTokenAuth.Config("token-146520"); var config = new HttpClientConfig("http://localhost", new Config(null, auth)); diff --git a/util/src/test/java/care/smith/fts/util/auth/HttpServerAuthConfigTest.java b/util/src/test/java/care/smith/fts/util/auth/HttpServerAuthConfigTest.java index 58abecf8..af4151be 100644 --- a/util/src/test/java/care/smith/fts/util/auth/HttpServerAuthConfigTest.java +++ b/util/src/test/java/care/smith/fts/util/auth/HttpServerAuthConfigTest.java @@ -23,7 +23,10 @@ void multipleAuthMethodsThrow() { var config = new HttpServerAuthConfig(); config.setAuth( new AuthMethod( - null, new HttpServerBasicAuth(List.of()), new HttpServerNoneAuth())); + null, + new HttpServerBasicAuth(List.of()), + new HttpServerOAuth2Auth(""), + new HttpServerNoneAuth())); assertThatExceptionOfType(IllegalArgumentException.class) .isThrownBy(config::userDetailsService) @@ -33,7 +36,7 @@ void multipleAuthMethodsThrow() { @Test void noAuthMethodDefaultsToNone() { var config = new HttpServerAuthConfig(); - config.setAuth(new AuthMethod(null, null, null)); + config.setAuth(new AuthMethod(null, null, null, null)); var reactiveUserDetailsService = config.userDetailsService();