Skip to content
1 change: 1 addition & 0 deletions api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ dependencies {

testImplementation libs.okhttp3
testImplementation libs.okhttp3.mockwebserver
testImplementation libs.wiremock
testImplementation libs.prometheus.metrics.core
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,23 @@
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.Nullable;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties;
import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientPropertiesMapper;
import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.WebClientReactiveAuthorizationCodeTokenResponseClient;
import org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeReactiveAuthenticationManager;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcReactiveOAuth2UserService;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.oidc.web.server.logout.OidcClientInitiatedServerLogoutSuccessHandler;
Expand All @@ -34,9 +38,13 @@
import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder;
import org.springframework.security.oauth2.server.resource.introspection.SpringReactiveOpaqueTokenIntrospector;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import reactor.netty.http.client.HttpClient;

@Configuration
@ConditionalOnProperty(value = "auth.type", havingValue = "OAUTH2")
Expand All @@ -49,32 +57,57 @@ public class OAuthSecurityConfig extends AbstractAuthSecurityConfig {

private final OAuthProperties properties;

/**
* WebClient configured to use system proxy properties (http.proxyHost/https.proxyHost,
* http.proxyPort/https.proxyPort, http.nonProxyHosts/https.nonProxyHosts).
* Created as a bean to ensure system properties are read after context initialization.
*/
@Bean(name = "oauthWebClient")
public WebClient oauthWebClient() {
return WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(HttpClient.create().proxyWithSystemProperties()))
.build();
}

@Bean
public SecurityWebFilterChain configure(ServerHttpSecurity http, OAuthLogoutSuccessHandler logoutHandler) {
public SecurityWebFilterChain configure(
ServerHttpSecurity http,
OAuthLogoutSuccessHandler logoutHandler,
ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> tokenResponseClient,
ReactiveOAuth2UserService<OidcUserRequest, OidcUser> oidcUserService,
@Qualifier("oauthWebClient") WebClient webClient
) {
log.info("Configuring OAUTH2 authentication.");

var oidcAuthManager =
new OidcAuthorizationCodeReactiveAuthenticationManager(tokenResponseClient, oidcUserService);

var builder = http.authorizeExchange(spec -> spec
.pathMatchers(AUTH_WHITELIST)
.permitAll()
.anyExchange()
.authenticated()
)
.oauth2Login(Customizer.withDefaults())
.oauth2Login(oauth2 -> oauth2.authenticationManager(oidcAuthManager))
.logout(spec -> spec.logoutSuccessHandler(logoutHandler))
.csrf(ServerHttpSecurity.CsrfSpec::disable);

if (properties.getResourceServer() != null) {
OAuth2ResourceServerProperties resourceServer = properties.getResourceServer();
if (resourceServer.getJwt() != null) {
builder.oauth2ResourceServer((c) -> c.jwt((j) -> j.jwkSetUri(resourceServer.getJwt().getJwkSetUri())));
} else if (resourceServer.getOpaquetoken() != null) {
if (resourceServer.getJwt() != null && resourceServer.getJwt().getJwkSetUri() != null) {
builder.oauth2ResourceServer(c -> c.jwt(j ->
j.jwtDecoder(NimbusReactiveJwtDecoder.withJwkSetUri(resourceServer.getJwt().getJwkSetUri())
.webClient(webClient)
.build())));
} else if (resourceServer.getOpaquetoken() != null
&& resourceServer.getOpaquetoken().getIntrospectionUri() != null) {
OAuth2ResourceServerProperties.Opaquetoken opaquetoken = resourceServer.getOpaquetoken();
builder.oauth2ResourceServer(
(c) -> c.opaqueToken(
(o) -> o.introspectionUri(opaquetoken.getIntrospectionUri())
.introspectionClientCredentials(opaquetoken.getClientId(), opaquetoken.getClientSecret())
)
);
builder.oauth2ResourceServer(c -> c.opaqueToken(o ->
o.introspector(new SpringReactiveOpaqueTokenIntrospector(
opaquetoken.getIntrospectionUri(),
webClient.mutate()
.defaultHeaders(h -> h.setBasicAuth(opaquetoken.getClientId(), opaquetoken.getClientSecret()))
.build()))));
}
}

Expand All @@ -84,8 +117,21 @@ public SecurityWebFilterChain configure(ServerHttpSecurity http, OAuthLogoutSucc
}

@Bean
public ReactiveOAuth2UserService<OidcUserRequest, OidcUser> customOidcUserService(AccessControlService acs) {
public ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest>
authorizationCodeTokenResponseClient(@Qualifier("oauthWebClient") WebClient webClient) {
var client = new WebClientReactiveAuthorizationCodeTokenResponseClient();
client.setWebClient(webClient);
return client;
}

@Bean
public ReactiveOAuth2UserService<OidcUserRequest, OidcUser> customOidcUserService(
AccessControlService acs,
ReactiveOAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService) {
final OidcReactiveOAuth2UserService delegate = new OidcReactiveOAuth2UserService();

delegate.setOauth2UserService(oauth2UserService);

return request -> delegate.loadUser(request)
.flatMap(user -> {
var provider = getProviderByProviderId(request.getClientRegistration().getRegistrationId());
Expand All @@ -100,8 +146,11 @@ public ReactiveOAuth2UserService<OidcUserRequest, OidcUser> customOidcUserServic
}

@Bean
public ReactiveOAuth2UserService<OAuth2UserRequest, OAuth2User> customOauth2UserService(AccessControlService acs) {
public ReactiveOAuth2UserService<OAuth2UserRequest, OAuth2User> customOauth2UserService(
AccessControlService acs, @Qualifier("oauthWebClient") WebClient webClient) {
final DefaultReactiveOAuth2UserService delegate = new DefaultReactiveOAuth2UserService();
delegate.setWebClient(webClient);

return request -> delegate.loadUser(request)
.flatMap(user -> {
var provider = getProviderByProviderId(request.getClientRegistration().getRegistrationId());
Expand Down Expand Up @@ -131,7 +180,6 @@ public ServerLogoutSuccessHandler defaultOidcLogoutHandler(final ReactiveClientR
return new OidcClientInitiatedServerLogoutSuccessHandler(repository);
}

@Nullable
private ProviderAuthorityExtractor getExtractor(final OAuthProperties.OAuth2Provider provider,
AccessControlService acs) {
Optional<ProviderAuthorityExtractor> extractor = acs.getOauthExtractors()
Expand Down
Loading
Loading