I'm using 5.7.3 via Spring Boot security starters and the app was build using JHipster version 7.9.3
Describe the bug
If a filter of type ServerOAuth2AuthorizedClientExchangeFilterFunction is used to make any request before any incoming web request (servlet) is received, this request is not process until a timeout stop it.
http://localhost:50090/api/v2/ts-orders
[[java.net.SocketTimeoutException](http://java.net.sockettimeoutexception/)](http://java.net.sockettimeoutexception/): Read timed out
The current filter is org.springframework.security.web.server.authentication.AuthenticationWebFilter and I think it's somehow related to the fact of the processor being unable to create the claims set.
NimbusReactiveJwtDecoder.java
Converter<JWT, Mono<JWTClaimsSet>> processor() {
JWKSecurityContextJWKSet jwkSource = new JWKSecurityContextJWKSet();
DefaultJWTProcessor<JWKSecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
JWSKeySelector<JWKSecurityContext> jwsKeySelector = jwsKeySelector(jwkSource);
jwtProcessor.setJWSKeySelector(jwsKeySelector);
jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> {
});
ReactiveRemoteJWKSource source = new ReactiveRemoteJWKSource(this.jwkSetUri);
source.setWebClient(this.webClient);
Function<JWSAlgorithm, Boolean> expectedJwsAlgorithms = getExpectedJwsAlgorithms(jwsKeySelector);
Mono<ConfigurableJWTProcessor<JWKSecurityContext>> jwtProcessorMono = this.jwtProcessorCustomizer
.apply(source, jwtProcessor)
.cache((processor) -> FOREVER, (ex) -> Duration.ZERO, () -> Duration.ZERO);
return (jwt) -> {
JWKSelector selector = createSelector(expectedJwsAlgorithms, jwt.getHeader());
return jwtProcessorMono.flatMap((processor) -> source.get(selector)
.onErrorMap((ex) -> new IllegalStateException("Could not obtain the keys", ex))
.map((jwkList) -> createClaimsSet(processor, jwt, new JWKSecurityContext(jwkList))));
};
}
The selector is created, but it never execute the source.get(selector) the log shows:
2022-11-17T13:47:18.423Z DEBUG 34432 --- [ctor-http-nio-5] .w.s.u.m.NegatedServerWebExchangeMatcher : matches = true
2022-11-17T13:47:18.430Z DEBUG 34432 --- [ctor-http-nio-5] athPatternParserServerWebExchangeMatcher : Request 'POST /api/v2/ts-orders' doesn't match 'null /oauth2/authorization/{registrationId}'
If the app is started and there no communications between the microservices, if a web request is received it is correctly processed as shown in the log
2022-11-17T13:53:49.462Z DEBUG 33800 --- [ctor-http-nio-4] athPatternParserServerWebExchangeMatcher : Request 'POST /api/v2/ts-orders' doesn't match 'null /oauth2/authorization/{registrationId}'
2022-11-17T13:53:49.474Z DEBUG 33800 --- [ctor-http-nio-4] o.s.s.w.s.u.m.OrServerWebExchangeMatcher : Trying to match using PathMatcherServerWebExchangeMatcher{pattern='/logout', method=POST}
2022-11-17T13:53:49.475Z DEBUG 33800 --- [ctor-http-nio-4] athPatternParserServerWebExchangeMatcher : Request 'POST /api/v2/ts-orders' doesn't match 'POST /logout'
2022-11-17T13:53:49.475Z DEBUG 33800 --- [ctor-http-nio-4] o.s.s.w.s.u.m.OrServerWebExchangeMatcher : No matches found
2022-11-17T13:53:49.481Z DEBUG 33800 --- [ctor-http-nio-4] o.s.s.w.s.u.m.OrServerWebExchangeMatcher : Trying to match using PathMatcherServerWebExchangeMatcher{pattern='/api/authenticate', method=null}
2022-11-17T13:53:49.481Z DEBUG 33800 --- [ctor-http-nio-4] athPatternParserServerWebExchangeMatcher : Request 'POST /api/v2/ts-orders' doesn't match 'null /api/authenticate'
2022-11-17T13:53:49.481Z DEBUG 33800 --- [ctor-http-nio-4] o.s.s.w.s.u.m.OrServerWebExchangeMatcher : No matches found
2022-11-17T13:53:49.482Z DEBUG 33800 --- [ctor-http-nio-4] o.s.s.w.s.u.m.OrServerWebExchangeMatcher : Trying to match using PathMatcherServerWebExchangeMatcher{pattern='/api/auth-info', method=null}
2022-11-17T13:53:49.482Z DEBUG 33800 --- [ctor-http-nio-4] athPatternParserServerWebExchangeMatcher : Request 'POST /api/v2/ts-orders' doesn't match 'null /api/auth-info'
2022-11-17T13:53:49.482Z DEBUG 33800 --- [ctor-http-nio-4] o.s.s.w.s.u.m.OrServerWebExchangeMatcher : No matches found
2022-11-17T13:53:49.482Z DEBUG 33800 --- [ctor-http-nio-4] o.s.s.w.s.u.m.OrServerWebExchangeMatcher : Trying to match using PathMatcherServerWebExchangeMatcher{pattern='/api/admin/**', method=null}
2022-11-17T13:53:49.482Z DEBUG 33800 --- [ctor-http-nio-4] athPatternParserServerWebExchangeMatcher : Request 'POST /api/v2/ts-orders' doesn't match 'null /api/admin/**'
2022-11-17T13:53:49.482Z DEBUG 33800 --- [ctor-http-nio-4] o.s.s.w.s.u.m.OrServerWebExchangeMatcher : No matches found
2022-11-17T13:53:49.483Z DEBUG 33800 --- [ctor-http-nio-4] o.s.s.w.s.u.m.OrServerWebExchangeMatcher : Trying to match using PathMatcherServerWebExchangeMatcher{pattern='/api/**', method=null}
2022-11-17T13:53:49.483Z DEBUG 33800 --- [ctor-http-nio-4] athPatternParserServerWebExchangeMatcher : Checking match of request : '/api/v2/ts-orders'; against '/api/**'
2022-11-17T13:53:49.484Z DEBUG 33800 --- [ctor-http-nio-4] o.s.s.w.s.u.m.OrServerWebExchangeMatcher : matched
2022-11-17T13:53:49.484Z DEBUG 33800 --- [ctor-http-nio-4] a.DelegatingReactiveAuthorizationManager : Checking authorization on '/api/v2/ts-orders' using org.springframework.security.authorization.AuthenticatedReactiveAuthorizationManager@384fec71
2022-11-17T13:53:49.485Z DEBUG 33800 --- [ctor-http-nio-4] o.s.s.w.s.a.AuthorizationWebFilter : Authorization successful
2022-11-17T13:53:49.763Z DEBUG 33800 --- [ctor-http-nio-4] r.client.log.DefaultReactiveLogger : [ConfigApi#getConfig(String)]--->GET http://panther/api/v1/configs/slug/pdm-connect-enabled HTTP/1.1
2022-11-17T13:53:49.973Z DEBUG 33800 --- [ctor-http-nio-3] r.client.log.DefaultReactiveLogger : [ConfigApi#getConfig(String)]<--- headers takes 210 milliseconds
2022-11-17T13:53:49.977Z DEBUG 33800 --- [ctor-http-nio-3] i.g.r.c.i.CircuitBreakerStateMachine : CircuitBreaker 'ConfigApi#getConfig(String)' succeeded:
2022-11-17T13:53:49.979Z DEBUG 33800 --- [ctor-http-nio-3] i.g.r.c.i.CircuitBreakerStateMachine : Event SUCCESS published: 2022-11-17T13:53:49.977616300Z[Europe/Lisbon]: CircuitBreaker 'ConfigApi#getConfig(String)' recorded a successful call. Elapsed time: 218 ms
To Reproduce This is my current configuration:
SecurityConfiguration.java
mport static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers.pathMatchers;
import com.gv.zeppelin.security.AuthoritiesConstants;
import com.gv.zeppelin.security.SecurityUtils;
import com.gv.zeppelin.security.oauth2.AudienceValidator;
import com.gv.zeppelin.security.oauth2.JwtGrantedAuthorityConverter;
import java.util.HashSet;
import java.util.Set;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AbstractAuthenticationToken;
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.core.GrantedAuthority;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcReactiveOAuth2UserService;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService;
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
import org.springframework.security.oauth2.jwt.*;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.csrf.CookieServerCsrfTokenRepository;
import org.springframework.security.web.server.header.ReferrerPolicyServerHttpHeadersWriter;
import org.springframework.security.web.server.header.XFrameOptionsServerHttpHeadersWriter.Mode;
import org.springframework.security.web.server.savedrequest.NoOpServerRequestCache;
import org.springframework.security.web.server.util.matcher.NegatedServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.OrServerWebExchangeMatcher;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.zalando.problem.spring.webflux.advice.security.SecurityProblemSupport;
import reactor.core.publisher.Mono;
import tech.jhipster.config.JHipsterProperties;
import tech.jhipster.web.filter.reactive.CookieCsrfFilter;
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
@Import(SecurityProblemSupport.class)
public class SecurityConfiguration {
private final JHipsterProperties jHipsterProperties;
@Value("${spring.security.oauth2.client.provider.oidc.issuer-uri}")
private String issuerUri;
private final SecurityProblemSupport problemSupport;
private final CorsWebFilter corsWebFilter;
public SecurityConfiguration(
JHipsterProperties jHipsterProperties,
SecurityProblemSupport problemSupport,
CorsWebFilter corsWebFilter
) {
this.jHipsterProperties = jHipsterProperties;
this.problemSupport = problemSupport;
this.corsWebFilter = corsWebFilter;
}
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
// @formatter:off
http
.securityMatcher(new NegatedServerWebExchangeMatcher(new OrServerWebExchangeMatcher(
pathMatchers("/app/**", "/_app/**", "/i18n/**", "/img/**", "/content/**", "/swagger-ui/**", "/v3/api-docs/**", "/test/**"),
pathMatchers(HttpMethod.OPTIONS, "/**")
)))
.csrf()
.disable()
.addFilterBefore(corsWebFilter, SecurityWebFiltersOrder.REACTOR_CONTEXT)
.exceptionHandling()
.accessDeniedHandler(problemSupport)
.authenticationEntryPoint(problemSupport)
.and()
.headers()
.contentSecurityPolicy(jHipsterProperties.getSecurity().getContentSecurityPolicy())
.and()
.referrerPolicy(ReferrerPolicyServerHttpHeadersWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN)
.and()
.permissionsPolicy().policy("camera=(), fullscreen=(self), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), midi=(), payment=(), sync-xhr=()")
.and()
.frameOptions().mode(Mode.DENY)
.and()
.requestCache()
.requestCache(NoOpServerRequestCache.getInstance())
.and()
.authorizeExchange()
.pathMatchers("/api/authenticate").permitAll()
.pathMatchers("/api/auth-info").permitAll()
.pathMatchers("/api/admin/**").hasAuthority(AuthoritiesConstants.ADMIN)
.pathMatchers("/api/**").authenticated()
.pathMatchers("/management/health").permitAll()
.pathMatchers("/management/health/**").permitAll()
.pathMatchers("/management/info").permitAll()
.pathMatchers("/management/prometheus").permitAll()
// SPE Begin WebSocket
.pathMatchers("/websocket.html").permitAll()
.pathMatchers("/websocket/**").permitAll()
// SPE End WebSocket
.pathMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN);
http
.oauth2ResourceServer()
.jwt()
.jwtAuthenticationConverter(jwtAuthenticationConverter());
http.oauth2Client();
// @formatter:on
return http.build();
}
Converter<Jwt, Mono<AbstractAuthenticationToken>> jwtAuthenticationConverter() {
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(new JwtGrantedAuthorityConverter());
return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter);
}
/**
* Map authorities from "groups" or "roles" claim in ID Token.
*
* @return a {@link ReactiveOAuth2UserService} that has the groups from the IdP.
*/
@Bean
public ReactiveOAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
final OidcReactiveOAuth2UserService delegate = new OidcReactiveOAuth2UserService();
return userRequest -> {
// Delegate to the default implementation for loading a user
return delegate
.loadUser(userRequest)
.map(user -> {
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
user
.getAuthorities()
.forEach(authority -> {
if (authority instanceof OidcUserAuthority) {
OidcUserAuthority oidcUserAuthority = (OidcUserAuthority) authority;
mappedAuthorities.addAll(
SecurityUtils.extractAuthorityFromClaims(oidcUserAuthority.getUserInfo().getClaims())
);
}
});
return new DefaultOidcUser(mappedAuthorities, user.getIdToken(), user.getUserInfo());
});
};
}
@Bean
ReactiveJwtDecoder jwtDecoder() {
NimbusReactiveJwtDecoder jwtDecoder = (NimbusReactiveJwtDecoder) ReactiveJwtDecoders.fromOidcIssuerLocation(issuerUri);
OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator(jHipsterProperties.getSecurity().getOauth2().getAudience());
OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri);
OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);
jwtDecoder.setJwtValidator(withAudience);
return jwtDecoder;
}
}
OAuth2ClientConfiguration.java
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.*;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.reactive.function.client.ServerOAuth2AuthorizedClientExchangeFilterFunction;
@Configuration
public class OAuth2ClientConfiguration {
private static final Logger log = LogManager.getLogger(OAuth2ClientConfiguration.class);
final ReactiveOAuth2AuthorizedClientService authorizedClientService;
public OAuth2ClientConfiguration(ReactiveOAuth2AuthorizedClientService authorizedClientService) {
this.authorizedClientService = authorizedClientService;
}
/**
* (non-servlet) it's used OAuth2AuthorizedClientManager
* When operating outside of a HttpServletRequest context, use:
* - AuthorizedClientServiceOAuth2AuthorizedClientManager (non-reactive)
* - AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager (reactive)
*
* @see <a href="https://github.com/spring-projects/spring-security/issues/8444">Docs: WebClient OAuth2 Setup for Reactive Applications might be wrong</a>
*/
@Bean
public ReactiveOAuth2AuthorizedClientManager nonServletAuthorizedClientManager(
ReactiveClientRegistrationRepository clientRegistrationRepository,
ReactiveOAuth2AuthorizedClientService authorizedClientService
) {
ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder
.builder()
.clientCredentials()
.refreshToken()
.build();
var authorizedClientManager = new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
clientRegistrationRepository,
authorizedClientService
);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
/**
* Wire
*/
@Bean
public ServerOAuth2AuthorizedClientExchangeFilterFunction nonServletFilterFunction(
ReactiveOAuth2AuthorizedClientManager authorizedClientManager
) {
var oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
oauth.setDefaultClientRegistrationId("oidc");
// oauth.setAuthorizationFailureHandler(authorizationFailureHandler());
return oauth;
}
public ReactiveOAuth2AuthorizationFailureHandler authorizationFailureHandler() {
return new RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler((clientRegistrationId, principal, attributes) -> {
log.warn("ReactiveOAuth2AuthorizationFailureHandler: {} {} {}", clientRegistrationId, principal, attributes);
return authorizedClientService.removeAuthorizedClient(clientRegistrationId, principal.getName());
});
}
}
WebClientConfiguration .java
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.web.reactive.function.client.ServerOAuth2AuthorizedClientExchangeFilterFunction;
import org.springframework.web.reactive.function.client.WebClient;
@Configuration
public class WebClientConfiguration {
@Bean
@LoadBalanced
public WebClient.Builder webClientBuilder(ServerOAuth2AuthorizedClientExchangeFilterFunction oAuth2AuthorizedClientFilter) {
return WebClient.builder().filter(oAuth2AuthorizedClientFilter);
}
}
Expected behavior I spent several days trying to understand why this is happening, but I was not able to, despite I think it has something to do with createClaimsSet or with cachedJWKSet (ReactiveRemoteJWKSource). I honestly believe that the request should have an answer, e.g. not ending with timeout.
Comment From: sjohnr
Thanks for getting in touch, but it feels like this is a question that would be better suited to Stack Overflow. We prefer to use GitHub issues only for bugs and enhancements. Feel free to update this issue with a link to the re-posted question (so that other people can find it) or add a minimal sample that reproduces this issue if you feel this is a genuine bug.
Having said that, please note that for servlet environments, the class ServletOAuth2AuthorizedClientExchangeFilterFunction should be used. See WebClient Integration for Servlet Environments for more information. If you feel I have misunderstood your question, please let me know and we can re-open if necessary.