Support configurable JwtDecoder for IdToken verification

Fixes gh-5717
This commit is contained in:
Joe Grandja
2018-12-12 10:28:22 -05:00
committed by Rob Winch
parent be23ab8114
commit 8f4f52edb9
12 changed files with 445 additions and 130 deletions

View File

@@ -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<>();

View File

@@ -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