Skip to content

Commit e97eae7

Browse files
feat: Extend OAuth2 proxy support to all authentication endpoints
Add proxy-aware WebClient configuration for JWT decoder, opaque token introspection, and OIDC authentication manager. All OAuth2 HTTP calls now respect JVM system proxy properties. - Add ReactiveJwtDecoder bean with proxy-aware JWKS fetching - Add ReactiveOpaqueTokenIntrospector bean with proxy support - Add ReactiveOAuth2AccessTokenResponseClient for token endpoint - Configure OidcAuthorizationCodeReactiveAuthenticationManager - Add WireMock integration tests for with/without proxy scenarios
1 parent cf5e4f4 commit e97eae7

File tree

6 files changed

+824
-24
lines changed

6 files changed

+824
-24
lines changed

api/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ dependencies {
117117

118118
testImplementation libs.okhttp3
119119
testImplementation libs.okhttp3.mockwebserver
120+
testImplementation libs.wiremock
120121
testImplementation libs.prometheus.metrics.core
121122
}
122123

api/src/main/java/io/kafbat/ui/config/auth/OAuthSecurityConfig.java

Lines changed: 78 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,24 @@
1010
import java.util.Optional;
1111
import lombok.RequiredArgsConstructor;
1212
import lombok.extern.slf4j.Slf4j;
13-
import org.jetbrains.annotations.Nullable;
1413
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
1514
import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties;
1615
import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientPropertiesMapper;
1716
import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties;
1817
import org.springframework.boot.context.properties.EnableConfigurationProperties;
1918
import org.springframework.context.annotation.Bean;
2019
import org.springframework.context.annotation.Configuration;
20+
import org.springframework.context.annotation.Primary;
2121
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
2222
import org.springframework.security.config.Customizer;
2323
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;
2424
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
2525
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
2626
import org.springframework.security.config.web.server.ServerHttpSecurity;
27+
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
28+
import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient;
29+
import org.springframework.security.oauth2.client.endpoint.WebClientReactiveAuthorizationCodeTokenResponseClient;
30+
import org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeReactiveAuthenticationManager;
2731
import org.springframework.security.oauth2.client.oidc.userinfo.OidcReactiveOAuth2UserService;
2832
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
2933
import org.springframework.security.oauth2.client.oidc.web.server.logout.OidcClientInitiatedServerLogoutSuccessHandler;
@@ -35,6 +39,10 @@
3539
import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService;
3640
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
3741
import org.springframework.security.oauth2.core.user.OAuth2User;
42+
import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder;
43+
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
44+
import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector;
45+
import org.springframework.security.oauth2.server.resource.introspection.SpringReactiveOpaqueTokenIntrospector;
3846
import org.springframework.security.web.server.SecurityWebFilterChain;
3947
import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler;
4048
import org.springframework.web.reactive.function.client.WebClient;
@@ -52,40 +60,80 @@ public class OAuthSecurityConfig extends AbstractAuthSecurityConfig {
5260

5361
private final OAuthProperties properties;
5462

63+
/**
64+
* WebClient configured to use system proxy properties (-Dhttps.proxyHost, -Dhttps.proxyPort).
65+
*/
66+
private final WebClient proxyAwareWebClient = WebClient.builder()
67+
.clientConnector(new ReactorClientHttpConnector(HttpClient.create().proxyWithSystemProperties()))
68+
.build();
69+
5570
@Bean
56-
public SecurityWebFilterChain configure(ServerHttpSecurity http, OAuthLogoutSuccessHandler logoutHandler) {
71+
public SecurityWebFilterChain configure(
72+
ServerHttpSecurity http,
73+
OAuthLogoutSuccessHandler logoutHandler,
74+
ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> tokenResponseClient,
75+
ReactiveOAuth2UserService<OidcUserRequest, OidcUser> oidcUserService
76+
) {
5777
log.info("Configuring OAUTH2 authentication.");
5878

79+
var oidcAuthManager =
80+
new OidcAuthorizationCodeReactiveAuthenticationManager(tokenResponseClient, oidcUserService);
81+
5982
var builder = http.authorizeExchange(spec -> spec
6083
.pathMatchers(AUTH_WHITELIST)
6184
.permitAll()
6285
.anyExchange()
6386
.authenticated()
6487
)
65-
.oauth2Login(Customizer.withDefaults())
88+
.oauth2Login(oauth2 -> oauth2.authenticationManager(oidcAuthManager))
6689
.logout(spec -> spec.logoutSuccessHandler(logoutHandler))
6790
.csrf(ServerHttpSecurity.CsrfSpec::disable);
6891

69-
if (properties.getResourceServer() != null) {
70-
OAuth2ResourceServerProperties resourceServer = properties.getResourceServer();
71-
if (resourceServer.getJwt() != null) {
72-
builder.oauth2ResourceServer((c) -> c.jwt((j) -> j.jwkSetUri(resourceServer.getJwt().getJwkSetUri())));
73-
} else if (resourceServer.getOpaquetoken() != null) {
74-
OAuth2ResourceServerProperties.Opaquetoken opaquetoken = resourceServer.getOpaquetoken();
75-
builder.oauth2ResourceServer(
76-
(c) -> c.opaqueToken(
77-
(o) -> o.introspectionUri(opaquetoken.getIntrospectionUri())
78-
.introspectionClientCredentials(opaquetoken.getClientId(), opaquetoken.getClientSecret())
79-
)
80-
);
81-
}
92+
if (getJwkSetUri() != null) {
93+
builder.oauth2ResourceServer(c -> c.jwt(Customizer.withDefaults()));
94+
} else if (getOpaqueTokenConfig() != null) {
95+
builder.oauth2ResourceServer(c -> c.opaqueToken(Customizer.withDefaults()));
8296
}
8397

8498
builder.addFilterAt(new StaticFileWebFilter(), SecurityWebFiltersOrder.LOGIN_PAGE_GENERATING);
8599

86100
return builder.build();
87101
}
88102

103+
@Bean
104+
public ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest>
105+
authorizationCodeTokenResponseClient() {
106+
var client = new WebClientReactiveAuthorizationCodeTokenResponseClient();
107+
client.setWebClient(proxyAwareWebClient);
108+
return client;
109+
}
110+
111+
@Bean
112+
@Primary
113+
public ReactiveJwtDecoder jwtDecoder() {
114+
String jwkSetUri = getJwkSetUri();
115+
if (jwkSetUri == null) {
116+
return token -> Mono.error(new IllegalStateException("JWT decoder not configured"));
117+
}
118+
log.info("Configuring JWT decoder with JWKS URI: {}", jwkSetUri);
119+
return NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri).webClient(proxyAwareWebClient).build();
120+
}
121+
122+
@Bean
123+
@Primary
124+
public ReactiveOpaqueTokenIntrospector opaqueTokenIntrospector() {
125+
var config = getOpaqueTokenConfig();
126+
if (config == null) {
127+
return token -> Mono.error(new IllegalStateException("Opaque token introspector not configured"));
128+
}
129+
log.info("Configuring opaque token introspector with URI: {}", config.getIntrospectionUri());
130+
return new SpringReactiveOpaqueTokenIntrospector(
131+
config.getIntrospectionUri(),
132+
proxyAwareWebClient.mutate()
133+
.defaultHeaders(h -> h.setBasicAuth(config.getClientId(), config.getClientSecret()))
134+
.build());
135+
}
136+
89137
@Bean
90138
public ReactiveOAuth2UserService<OidcUserRequest, OidcUser> customOidcUserService(
91139
AccessControlService acs,
@@ -110,13 +158,7 @@ public ReactiveOAuth2UserService<OidcUserRequest, OidcUser> customOidcUserServic
110158
@Bean
111159
public ReactiveOAuth2UserService<OAuth2UserRequest, OAuth2User> customOauth2UserService(AccessControlService acs) {
112160
final DefaultReactiveOAuth2UserService delegate = new DefaultReactiveOAuth2UserService();
113-
114-
// Configure WebClient to use system proxy properties (if set)
115-
delegate.setWebClient(
116-
WebClient.builder()
117-
.clientConnector(new ReactorClientHttpConnector(
118-
HttpClient.create().proxyWithSystemProperties()))
119-
.build());
161+
delegate.setWebClient(proxyAwareWebClient);
120162

121163
return request -> delegate.loadUser(request)
122164
.flatMap(user -> {
@@ -147,7 +189,19 @@ public ServerLogoutSuccessHandler defaultOidcLogoutHandler(final ReactiveClientR
147189
return new OidcClientInitiatedServerLogoutSuccessHandler(repository);
148190
}
149191

150-
@Nullable
192+
private String getJwkSetUri() {
193+
var rs = properties.getResourceServer();
194+
return rs != null && rs.getJwt() != null ? rs.getJwt().getJwkSetUri() : null;
195+
}
196+
197+
private OAuth2ResourceServerProperties.Opaquetoken getOpaqueTokenConfig() {
198+
var rs = properties.getResourceServer();
199+
if (rs == null || rs.getOpaquetoken() == null) {
200+
return null;
201+
}
202+
return rs.getOpaquetoken().getIntrospectionUri() != null ? rs.getOpaquetoken() : null;
203+
}
204+
151205
private ProviderAuthorityExtractor getExtractor(final OAuthProperties.OAuth2Provider provider,
152206
AccessControlService acs) {
153207
Optional<ProviderAuthorityExtractor> extractor = acs.getOauthExtractors()

0 commit comments

Comments
 (0)