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
This commit is contained in:
Josh Cummings
2018-07-16 17:42:29 -06:00
committed by Rob Winch
parent 3c461b704c
commit 4fc1e63369
2 changed files with 238 additions and 22 deletions

View File

@@ -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<String, Object> JWT_HEADERS = Collections.singletonMap("alg", JwsAlgorithms.RS256);
private static final Map<String, Object> 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("/")