Support configurable JwtDecoder for IdToken verification
Fixes gh-5717
This commit is contained in:
@@ -19,11 +19,12 @@ import org.apache.http.HttpHeaders;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.beans.PropertyAccessorFactory;
|
||||
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.web.MockFilterChain;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
@@ -50,7 +51,6 @@ import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
|
||||
import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository;
|
||||
import org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizationRequestRepository;
|
||||
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver;
|
||||
import org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||
@@ -66,6 +66,7 @@ import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoderFactory;
|
||||
import org.springframework.security.web.FilterChainProxy;
|
||||
import org.springframework.security.web.context.HttpRequestResponseHolder;
|
||||
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
|
||||
@@ -81,6 +82,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -369,8 +371,7 @@ public class OAuth2LoginConfigurerTests {
|
||||
@Test
|
||||
public void oidcLogin() throws Exception {
|
||||
// setup application context
|
||||
loadConfig(OAuth2LoginConfig.class);
|
||||
registerJwtDecoder();
|
||||
loadConfig(OAuth2LoginConfig.class, JwtDecoderFactoryConfig.class);
|
||||
|
||||
// setup authorization request
|
||||
OAuth2AuthorizationRequest authorizationRequest = createOAuth2AuthorizationRequest("openid");
|
||||
@@ -396,8 +397,7 @@ public class OAuth2LoginConfigurerTests {
|
||||
@Test
|
||||
public void oidcLoginCustomWithConfigurer() throws Exception {
|
||||
// setup application context
|
||||
loadConfig(OAuth2LoginConfigCustomWithConfigurer.class);
|
||||
registerJwtDecoder();
|
||||
loadConfig(OAuth2LoginConfigCustomWithConfigurer.class, JwtDecoderFactoryConfig.class);
|
||||
|
||||
// setup authorization request
|
||||
OAuth2AuthorizationRequest authorizationRequest = createOAuth2AuthorizationRequest("openid");
|
||||
@@ -423,8 +423,7 @@ public class OAuth2LoginConfigurerTests {
|
||||
@Test
|
||||
public void oidcLoginCustomWithBeanRegistration() throws Exception {
|
||||
// setup application context
|
||||
loadConfig(OAuth2LoginConfigCustomWithBeanRegistration.class);
|
||||
registerJwtDecoder();
|
||||
loadConfig(OAuth2LoginConfigCustomWithBeanRegistration.class, JwtDecoderFactoryConfig.class);
|
||||
|
||||
// setup authorization request
|
||||
OAuth2AuthorizationRequest authorizationRequest = createOAuth2AuthorizationRequest("openid");
|
||||
@@ -447,6 +446,15 @@ public class OAuth2LoginConfigurerTests {
|
||||
assertThat(authentication.getAuthorities()).last().hasToString("ROLE_OIDC_USER");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void oidcLoginCustomWithNoUniqueJwtDecoderFactory() {
|
||||
assertThatThrownBy(() -> loadConfig(OAuth2LoginConfig.class, NoUniqueJwtDecoderFactoryConfig.class))
|
||||
.hasRootCauseInstanceOf(NoUniqueBeanDefinitionException.class)
|
||||
.hasMessageContaining("No qualifying bean of type " +
|
||||
"'org.springframework.security.oauth2.jwt.JwtDecoderFactory<org.springframework.security.oauth2.client.registration.ClientRegistration>' " +
|
||||
"available: expected single matching bean but found 2: jwtDecoderFactory1,jwtDecoderFactory2");
|
||||
}
|
||||
|
||||
private void loadConfig(Class<?>... configs) {
|
||||
AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
|
||||
applicationContext.register(configs);
|
||||
@@ -455,25 +463,6 @@ public class OAuth2LoginConfigurerTests {
|
||||
this.context = applicationContext;
|
||||
}
|
||||
|
||||
private void registerJwtDecoder() {
|
||||
JwtDecoder decoder = token -> {
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
claims.put(IdTokenClaimNames.SUB, "sub123");
|
||||
claims.put(IdTokenClaimNames.ISS, "http://localhost/iss");
|
||||
claims.put(IdTokenClaimNames.AUD, Arrays.asList("clientId", "a", "u", "d"));
|
||||
claims.put(IdTokenClaimNames.AZP, "clientId");
|
||||
return new Jwt("token123", Instant.now(), Instant.now().plusSeconds(3600),
|
||||
Collections.singletonMap("header1", "value1"), claims);
|
||||
};
|
||||
this.springSecurityFilterChain.getFilters("/login/oauth2/code/google").stream()
|
||||
.filter(OAuth2LoginAuthenticationFilter.class::isInstance)
|
||||
.findFirst()
|
||||
.ifPresent(filter -> PropertyAccessorFactory.forDirectFieldAccess(filter)
|
||||
.setPropertyValue(
|
||||
"authenticationManager.providers[2].jwtDecoders['google']",
|
||||
decoder));
|
||||
}
|
||||
|
||||
private OAuth2AuthorizationRequest createOAuth2AuthorizationRequest(String... scopes) {
|
||||
return this.createOAuth2AuthorizationRequest(GOOGLE_CLIENT_REGISTRATION, scopes);
|
||||
}
|
||||
@@ -632,6 +621,42 @@ public class OAuth2LoginConfigurerTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class JwtDecoderFactoryConfig {
|
||||
|
||||
@Bean
|
||||
JwtDecoderFactory<ClientRegistration> jwtDecoderFactory() {
|
||||
return clientRegistration -> getJwtDecoder();
|
||||
}
|
||||
|
||||
private static JwtDecoder getJwtDecoder() {
|
||||
return token -> {
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
claims.put(IdTokenClaimNames.SUB, "sub123");
|
||||
claims.put(IdTokenClaimNames.ISS, "http://localhost/iss");
|
||||
claims.put(IdTokenClaimNames.AUD, Arrays.asList("clientId", "a", "u", "d"));
|
||||
claims.put(IdTokenClaimNames.AZP, "clientId");
|
||||
return new Jwt("token123", Instant.now(), Instant.now().plusSeconds(3600),
|
||||
Collections.singletonMap("header1", "value1"), claims);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class NoUniqueJwtDecoderFactoryConfig {
|
||||
|
||||
@Bean
|
||||
JwtDecoderFactory<ClientRegistration> jwtDecoderFactory1() {
|
||||
return clientRegistration -> JwtDecoderFactoryConfig.getJwtDecoder();
|
||||
}
|
||||
|
||||
@Bean
|
||||
JwtDecoderFactory<ClientRegistration> jwtDecoderFactory2() {
|
||||
return clientRegistration -> JwtDecoderFactoryConfig.getJwtDecoder();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> createOauth2AccessTokenResponseClient() {
|
||||
return request -> {
|
||||
Map<String, Object> additionalParameters = new HashMap<>();
|
||||
|
||||
@@ -16,12 +16,6 @@
|
||||
|
||||
package org.springframework.security.config.web.server;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
@@ -34,15 +28,29 @@ import org.springframework.security.config.annotation.web.reactive.EnableWebFlux
|
||||
import org.springframework.security.config.oauth2.client.CommonOAuth2Provider;
|
||||
import org.springframework.security.config.test.SpringTestRule;
|
||||
import org.springframework.security.htmlunit.server.WebTestClientHtmlUnitDriverBuilder;
|
||||
import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationToken;
|
||||
import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient;
|
||||
import org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeReactiveAuthenticationManager;
|
||||
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.TestOAuth2AccessTokens;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
|
||||
import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationExchanges;
|
||||
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
|
||||
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
|
||||
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
||||
import org.springframework.security.oauth2.core.oidc.user.TestOidcUsers;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.security.oauth2.core.user.TestOAuth2Users;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoderFactory;
|
||||
import org.springframework.security.test.web.reactive.server.WebTestClientBuilder;
|
||||
import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||
import org.springframework.security.web.server.WebFilterChainProxy;
|
||||
@@ -51,9 +59,17 @@ import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebFilter;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* @author Rob Winch
|
||||
* @since 5.1
|
||||
@@ -72,6 +88,12 @@ public class OAuth2LoginTests {
|
||||
.clientSecret("secret")
|
||||
.build();
|
||||
|
||||
private static ClientRegistration google = CommonOAuth2Provider.GOOGLE
|
||||
.getBuilder("google")
|
||||
.clientId("client")
|
||||
.clientSecret("secret")
|
||||
.build();
|
||||
|
||||
@Test
|
||||
public void defaultLoginPageWithMultipleClientRegistrationsThenLinks() {
|
||||
this.spring.register(OAuth2LoginWithMulitpleClientRegistrations.class).autowire();
|
||||
@@ -97,11 +119,6 @@ public class OAuth2LoginTests {
|
||||
static class OAuth2LoginWithMulitpleClientRegistrations {
|
||||
@Bean
|
||||
InMemoryReactiveClientRegistrationRepository clientRegistrationRepository() {
|
||||
ClientRegistration google = CommonOAuth2Provider.GOOGLE
|
||||
.getBuilder("google")
|
||||
.clientId("client")
|
||||
.clientSecret("secret")
|
||||
.build();
|
||||
return new InMemoryReactiveClientRegistrationRepository(github, google);
|
||||
}
|
||||
}
|
||||
@@ -182,6 +199,107 @@ public class OAuth2LoginTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void oauth2LoginWhenCustomJwtDecoderFactoryThenUsed() {
|
||||
this.spring.register(OAuth2LoginWithMulitpleClientRegistrations.class,
|
||||
OAuth2LoginWithJwtDecoderFactoryBeanConfig.class).autowire();
|
||||
|
||||
WebTestClient webTestClient = WebTestClientBuilder
|
||||
.bindToWebFilters(this.springSecurity)
|
||||
.build();
|
||||
|
||||
OAuth2LoginWithJwtDecoderFactoryBeanConfig config = this.spring.getContext()
|
||||
.getBean(OAuth2LoginWithJwtDecoderFactoryBeanConfig.class);
|
||||
|
||||
OAuth2AuthorizationExchange exchange = TestOAuth2AuthorizationExchanges.success();
|
||||
OAuth2AccessToken accessToken = TestOAuth2AccessTokens.scopes("openid");
|
||||
OAuth2AuthorizationCodeAuthenticationToken token = new OAuth2AuthorizationCodeAuthenticationToken(google, exchange, accessToken);
|
||||
|
||||
ServerAuthenticationConverter converter = config.authenticationConverter;
|
||||
when(converter.convert(any())).thenReturn(Mono.just(token));
|
||||
|
||||
Map<String, Object> additionalParameters = new HashMap<>();
|
||||
additionalParameters.put(OidcParameterNames.ID_TOKEN, "id-token");
|
||||
OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse.withToken(accessToken.getTokenValue())
|
||||
.tokenType(accessToken.getTokenType())
|
||||
.scopes(accessToken.getScopes())
|
||||
.additionalParameters(additionalParameters)
|
||||
.build();
|
||||
ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> tokenResponseClient = config.tokenResponseClient;
|
||||
when(tokenResponseClient.getTokenResponse(any())).thenReturn(Mono.just(accessTokenResponse));
|
||||
|
||||
OidcUser user = TestOidcUsers.create();
|
||||
ReactiveOAuth2UserService<OidcUserRequest, OidcUser> userService = config.userService;
|
||||
when(userService.loadUser(any())).thenReturn(Mono.just(user));
|
||||
|
||||
webTestClient.get()
|
||||
.uri("/login/oauth2/code/google")
|
||||
.exchange()
|
||||
.expectStatus().is3xxRedirection();
|
||||
|
||||
verify(config.jwtDecoderFactory).createDecoder(any());
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class OAuth2LoginWithJwtDecoderFactoryBeanConfig {
|
||||
|
||||
ServerAuthenticationConverter authenticationConverter = mock(ServerAuthenticationConverter.class);
|
||||
|
||||
ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> tokenResponseClient =
|
||||
mock(ReactiveOAuth2AccessTokenResponseClient.class);
|
||||
|
||||
ReactiveOAuth2UserService<OidcUserRequest, OidcUser> userService = mock(ReactiveOAuth2UserService.class);
|
||||
|
||||
ReactiveJwtDecoderFactory<ClientRegistration> jwtDecoderFactory = spy(new JwtDecoderFactory());
|
||||
|
||||
@Bean
|
||||
public SecurityWebFilterChain springSecurityFilter(ServerHttpSecurity http) {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeExchange()
|
||||
.anyExchange().authenticated()
|
||||
.and()
|
||||
.oauth2Login()
|
||||
.authenticationConverter(authenticationConverter)
|
||||
.authenticationManager(authenticationManager());
|
||||
return http.build();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
private ReactiveAuthenticationManager authenticationManager() {
|
||||
OidcAuthorizationCodeReactiveAuthenticationManager oidc =
|
||||
new OidcAuthorizationCodeReactiveAuthenticationManager(tokenResponseClient, userService);
|
||||
oidc.setJwtDecoderFactory(jwtDecoderFactory());
|
||||
return oidc;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ReactiveJwtDecoderFactory<ClientRegistration> jwtDecoderFactory() {
|
||||
return jwtDecoderFactory;
|
||||
}
|
||||
|
||||
private static class JwtDecoderFactory implements ReactiveJwtDecoderFactory<ClientRegistration> {
|
||||
|
||||
@Override
|
||||
public ReactiveJwtDecoder createDecoder(ClientRegistration clientRegistration) {
|
||||
return getJwtDecoder();
|
||||
}
|
||||
|
||||
private ReactiveJwtDecoder getJwtDecoder() {
|
||||
return token -> {
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
claims.put(IdTokenClaimNames.SUB, "subject");
|
||||
claims.put(IdTokenClaimNames.ISS, "http://localhost/issuer");
|
||||
claims.put(IdTokenClaimNames.AUD, Collections.singletonList("client"));
|
||||
claims.put(IdTokenClaimNames.AZP, "client");
|
||||
Jwt jwt = new Jwt("id-token", Instant.now(), Instant.now().plusSeconds(3600),
|
||||
Collections.singletonMap("header1", "value1"), claims);
|
||||
return Mono.just(jwt);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class GitHubWebFilter implements WebFilter {
|
||||
|
||||
@Override
|
||||
|
||||
Reference in New Issue
Block a user