Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
115 changes: 96 additions & 19 deletions api/src/main/java/io/kafbat/ui/config/auth/OAuthSecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,25 @@
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.context.annotation.Primary;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.security.config.Customizer;
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 +40,15 @@
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.jwt.ReactiveJwtDecoder;
import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector;
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,33 +61,44 @@ 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
) {
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) {
OAuth2ResourceServerProperties.Opaquetoken opaquetoken = resourceServer.getOpaquetoken();
builder.oauth2ResourceServer(
(c) -> c.opaqueToken(
(o) -> o.introspectionUri(opaquetoken.getIntrospectionUri())
.introspectionClientCredentials(opaquetoken.getClientId(), opaquetoken.getClientSecret())
)
);
}
if (getJwkSetUri() != null) {
builder.oauth2ResourceServer(c -> c.jwt(Customizer.withDefaults()));
} else if (getOpaqueTokenConfig() != null) {
builder.oauth2ResourceServer(c -> c.opaqueToken(Customizer.withDefaults()));
}

builder.addFilterAt(new StaticFileWebFilter(), SecurityWebFiltersOrder.LOGIN_PAGE_GENERATING);
Expand All @@ -84,8 +107,47 @@ 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
@Primary
public ReactiveJwtDecoder jwtDecoder(@Qualifier("oauthWebClient") WebClient webClient) {
String jwkSetUri = getJwkSetUri();
if (jwkSetUri == null) {
return token -> Mono.error(new IllegalStateException("JWT decoder not configured"));
}
log.info("Configuring JWT decoder with JWKS URI: {}", jwkSetUri);
return NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri).webClient(webClient).build();
}

@Bean
@Primary
public ReactiveOpaqueTokenIntrospector opaqueTokenIntrospector(@Qualifier("oauthWebClient") WebClient webClient) {
var config = getOpaqueTokenConfig();
if (config == null) {
return token -> Mono.error(new IllegalStateException("Opaque token introspector not configured"));
}
log.info("Configuring opaque token introspector with URI: {}", config.getIntrospectionUri());
return new SpringReactiveOpaqueTokenIntrospector(
config.getIntrospectionUri(),
webClient.mutate()
.defaultHeaders(h -> h.setBasicAuth(config.getClientId(), config.getClientSecret()))
.build());
}

@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 +162,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 +196,19 @@ public ServerLogoutSuccessHandler defaultOidcLogoutHandler(final ReactiveClientR
return new OidcClientInitiatedServerLogoutSuccessHandler(repository);
}

@Nullable
private String getJwkSetUri() {
var rs = properties.getResourceServer();
return rs != null && rs.getJwt() != null ? rs.getJwt().getJwkSetUri() : null;
}

private OAuth2ResourceServerProperties.Opaquetoken getOpaqueTokenConfig() {
var rs = properties.getResourceServer();
if (rs == null || rs.getOpaquetoken() == null) {
return null;
}
return rs.getOpaquetoken().getIntrospectionUri() != null ? rs.getOpaquetoken() : null;
}

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