From 4fc1e6336918ecbd70461f13b507e09c8987145b Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Mon, 16 Jul 2018 17:42:29 -0600 Subject: [PATCH] User-Specified JwtDecoder This exposes JwtConfigurer#decoder as well as makes the configurer look in the application context for a bean of type JwtDecoder. Fixes: gh-5519 --- .../OAuth2ResourceServerConfigurer.java | 66 ++++-- .../OAuth2ResourceServerConfigurerTests.java | 194 +++++++++++++++++- 2 files changed, 238 insertions(+), 22 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java index de610cfa59..ae3db1588b 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java @@ -18,8 +18,8 @@ package org.springframework.security.config.annotation.web.configurers.oauth2.se import javax.servlet.http.HttpServletRequest; +import org.springframework.context.ApplicationContext; import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer; @@ -51,7 +51,19 @@ import org.springframework.util.Assert; * * *

- * When using {@link #jwt()}, a Jwk Set Uri must be supplied via {@link JwtConfigurer#jwkSetUri} + * When using {@link #jwt()}, either + * + *

* *

Security Filters

* @@ -77,10 +89,6 @@ import org.springframework.util.Assert; *
  • {@link AuthenticationManager}
  • * * - * If {@link #jwt()} isn't supplied, then the {@link BearerTokenAuthenticationFilter} is still added, but without - * any OAuth 2.0 {@link AuthenticationProvider}s. This is useful if needing to switch out Spring Security's Jwt support - * for a custom one. - * * @author Josh Cummings * @since 5.1 * @see BearerTokenAuthenticationFilter @@ -100,9 +108,14 @@ public final class OAuth2ResourceServerConfigurer decoder(JwtDecoder decoder) { + this.decoder = decoder; + return OAuth2ResourceServerConfigurer.this; + } public OAuth2ResourceServerConfigurer jwkSetUri(String uri) { this.decoder = new NimbusJwtDecoderJwkSupport(uri); return OAuth2ResourceServerConfigurer.this; } - private JwtDecoder getJwtDecoder() { + JwtDecoder getJwtDecoder() { + if ( this.decoder == null ) { + return this.context.getBean(JwtDecoder.class); + } + return this.decoder; } } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java index 5d3291b048..6b89689ad9 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java @@ -20,6 +20,9 @@ import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; import java.lang.reflect.Field; +import java.time.Instant; +import java.util.Collections; +import java.util.Map; import java.util.stream.Collectors; import javax.annotation.PreDestroy; @@ -34,9 +37,11 @@ import org.junit.Test; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.NoUniqueBeanDefinitionException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; @@ -55,6 +60,11 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.oauth2.jose.jws.JwsAlgorithms; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtClaimNames; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoderJwkSupport; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.test.web.servlet.MockMvc; @@ -68,9 +78,13 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.support.GenericWebApplicationContext; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -86,8 +100,14 @@ import static org.springframework.web.bind.annotation.RequestMethod.POST; * @author Josh Cummings */ public class OAuth2ResourceServerConfigurerTests { + private static final String JWT_TOKEN = "token"; + private static final String JWT_SUBJECT = "mock-test-subject"; + private static final Map JWT_HEADERS = Collections.singletonMap("alg", JwsAlgorithms.RS256); + private static final Map JWT_CLAIMS = Collections.singletonMap(JwtClaimNames.SUB, JWT_SUBJECT); + private static final Jwt JWT = new Jwt(JWT_TOKEN, Instant.MIN, Instant.MAX, JWT_HEADERS, JWT_CLAIMS); + private static final String JWK_SET_URI = "https://mock.org"; - @Autowired + @Autowired(required = false) MockMvc mvc; @Autowired(required = false) @@ -506,6 +526,130 @@ public class OAuth2ResourceServerConfigurerTests { assertThat(result.getRequest().getSession(false)).isNotNull(); } + // -- custom jwt decoder + + @Test + public void requestWhenCustomJwtDecoderWiredOnDslThenUsed() + throws Exception { + + this.spring.register(CustomJwtDecoderOnDsl.class, BasicController.class).autowire(); + + CustomJwtDecoderOnDsl config = this.spring.getContext().getBean(CustomJwtDecoderOnDsl.class); + JwtDecoder decoder = config.decoder(); + + when(decoder.decode(anyString())).thenReturn(JWT); + + this.mvc.perform(get("/authenticated") + .with(bearerToken(JWT_TOKEN))) + .andExpect(status().isOk()) + .andExpect(content().string(JWT_SUBJECT)); + } + + @Test + public void requestWhenCustomJwtDecoderExposedAsBeanThenUsed() + throws Exception { + + this.spring.register(CustomJwtDecoderAsBean.class, BasicController.class).autowire(); + + JwtDecoder decoder = this.spring.getContext().getBean(JwtDecoder.class); + + when(decoder.decode(anyString())).thenReturn(JWT); + + this.mvc.perform(get("/authenticated") + .with(bearerToken(JWT_TOKEN))) + .andExpect(status().isOk()) + .andExpect(content().string(JWT_SUBJECT)); + } + + @Test + public void getJwtDecoderWhenConfiguredWithDecoderAndJwkSetUriThenLastOneWins() { + OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer = + new OAuth2ResourceServerConfigurer().new JwtConfigurer(null); + + JwtDecoder decoder = mock(JwtDecoder.class); + + jwtConfigurer.jwkSetUri(JWK_SET_URI); + jwtConfigurer.decoder(decoder); + + assertThat(jwtConfigurer.getJwtDecoder()).isEqualTo(decoder); + + jwtConfigurer = + new OAuth2ResourceServerConfigurer().new JwtConfigurer(null); + + jwtConfigurer.decoder(decoder); + jwtConfigurer.jwkSetUri(JWK_SET_URI); + + assertThat(jwtConfigurer.getJwtDecoder()).isInstanceOf(NimbusJwtDecoderJwkSupport.class); + + } + + @Test + public void getJwtDecoderWhenConflictingJwtDecodersThenTheDslWiredOneTakesPrecedence() { + + JwtDecoder decoderBean = mock(JwtDecoder.class); + JwtDecoder decoder = mock(JwtDecoder.class); + + ApplicationContext context = mock(ApplicationContext.class); + when(context.getBean(JwtDecoder.class)).thenReturn(decoderBean); + + OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer = + new OAuth2ResourceServerConfigurer().new JwtConfigurer(context); + jwtConfigurer.decoder(decoder); + + assertThat(jwtConfigurer.getJwtDecoder()).isEqualTo(decoder); + } + + @Test + public void getJwtDecoderWhenContextHasBeanAndUserConfiguresJwkSetUriThenJwkSetUriTakesPrecedence() { + + JwtDecoder decoder = mock(JwtDecoder.class); + ApplicationContext context = mock(ApplicationContext.class); + when(context.getBean(JwtDecoder.class)).thenReturn(decoder); + + OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer = + new OAuth2ResourceServerConfigurer().new JwtConfigurer(context); + + jwtConfigurer.jwkSetUri(JWK_SET_URI); + + assertThat(jwtConfigurer.getJwtDecoder()).isNotEqualTo(decoder); + assertThat(jwtConfigurer.getJwtDecoder()).isInstanceOf(NimbusJwtDecoderJwkSupport.class); + } + + @Test + public void getJwtDecoderWhenTwoJwtDecoderBeansAndAnotherWiredOnDslThenDslWiredOneTakesPrecedence() { + + JwtDecoder decoderBean = mock(JwtDecoder.class); + JwtDecoder decoder = mock(JwtDecoder.class); + + GenericWebApplicationContext context = new GenericWebApplicationContext(); + context.registerBean("decoderOne", JwtDecoder.class, () -> decoderBean); + context.registerBean("decoderTwo", JwtDecoder.class, () -> decoderBean); + this.spring.context(context).autowire(); + + OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer = + new OAuth2ResourceServerConfigurer().new JwtConfigurer(context); + jwtConfigurer.decoder(decoder); + + assertThat(jwtConfigurer.getJwtDecoder()).isEqualTo(decoder); + } + + @Test + public void getJwtDecoderWhenTwoJwtDecoderBeansThenThrowsException() { + + JwtDecoder decoder = mock(JwtDecoder.class); + GenericWebApplicationContext context = new GenericWebApplicationContext(); + context.registerBean("decoderOne", JwtDecoder.class, () -> decoder); + context.registerBean("decoderTwo", JwtDecoder.class, () -> decoder); + + this.spring.context(context).autowire(); + + OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer = + new OAuth2ResourceServerConfigurer().new JwtConfigurer(context); + + assertThatCode(() -> jwtConfigurer.getJwtDecoder()) + .isInstanceOf(NoUniqueBeanDefinitionException.class); + } + // -- In combination with other authentication providers @Test @@ -534,7 +678,7 @@ public class OAuth2ResourceServerConfigurerTests { assertThatCode(() -> this.spring.register(JwtlessConfig.class).autowire()) .isInstanceOf(BeanCreationException.class) - .hasMessageContaining("no instance of JwtDecoder"); + .hasMessageContaining("no Jwt configuration was found"); } @Test @@ -542,7 +686,7 @@ public class OAuth2ResourceServerConfigurerTests { assertThatCode(() -> this.spring.register(JwtHalfConfiguredConfig.class).autowire()) .isInstanceOf(BeanCreationException.class) - .hasMessageContaining("no instance of JwtDecoder"); + .hasMessageContaining("No qualifying bean of type"); } // -- support @@ -689,6 +833,50 @@ public class OAuth2ResourceServerConfigurerTests { } } + @EnableWebSecurity + static class CustomJwtDecoderOnDsl extends WebSecurityConfigurerAdapter { + JwtDecoder decoder = mock(JwtDecoder.class); + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .oauth2() + .resourceServer() + .jwt() + .decoder(decoder()); + // @formatter:on + } + + JwtDecoder decoder() { + return this.decoder; + } + } + + @EnableWebSecurity + static class CustomJwtDecoderAsBean extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .oauth2() + .resourceServer() + .jwt(); + // @formatter:on + } + + @Bean + public JwtDecoder decoder() { + return mock(JwtDecoder.class); + } + } + @RestController static class BasicController { @GetMapping("/")