Skip to content

Commit 4fc1e63

Browse files
jzheauxrwinch
authored andcommitted
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
1 parent 3c461b7 commit 4fc1e63

File tree

2 files changed

+238
-22
lines changed

2 files changed

+238
-22
lines changed

config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java

Lines changed: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818

1919
import javax.servlet.http.HttpServletRequest;
2020

21+
import org.springframework.context.ApplicationContext;
2122
import org.springframework.security.authentication.AuthenticationManager;
22-
import org.springframework.security.authentication.AuthenticationProvider;
2323
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
2424
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
2525
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
@@ -51,7 +51,19 @@
5151
* </ul>
5252
*
5353
* <p>
54-
* When using {@link #jwt()}, a Jwk Set Uri must be supplied via {@link JwtConfigurer#jwkSetUri}
54+
* When using {@link #jwt()}, either
55+
*
56+
* <ul>
57+
* <li>
58+
* supply a Jwk Set Uri via {@link JwtConfigurer#jwkSetUri}, or
59+
* </li>
60+
* <li>
61+
* supply a {@link JwtDecoder} instance via {@link JwtConfigurer#decoder}, or
62+
* </li>
63+
* <li>
64+
* expose a {@link JwtDecoder} bean
65+
* </li>
66+
* </ul>
5567
*
5668
* <h2>Security Filters</h2>
5769
*
@@ -77,10 +89,6 @@
7789
* <li>{@link AuthenticationManager}</li>
7890
* </ul>
7991
*
80-
* If {@link #jwt()} isn't supplied, then the {@link BearerTokenAuthenticationFilter} is still added, but without
81-
* any OAuth 2.0 {@link AuthenticationProvider}s. This is useful if needing to switch out Spring Security's Jwt support
82-
* for a custom one.
83-
*
8492
* @author Josh Cummings
8593
* @since 5.1
8694
* @see BearerTokenAuthenticationFilter
@@ -100,9 +108,14 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
100108
private BearerTokenAccessDeniedHandler accessDeniedHandler
101109
= new BearerTokenAccessDeniedHandler();
102110

103-
private JwtConfigurer jwtConfigurer = new JwtConfigurer();
111+
private JwtConfigurer jwtConfigurer;
104112

105113
public JwtConfigurer jwt() {
114+
if ( this.jwtConfigurer == null ) {
115+
ApplicationContext context = this.getBuilder().getSharedObject(ApplicationContext.class);
116+
this.jwtConfigurer = new JwtConfigurer(context);
117+
}
118+
106119
return this.jwtConfigurer;
107120
}
108121

@@ -133,32 +146,47 @@ public void configure(H http) throws Exception {
133146

134147
http.addFilter(filter);
135148

149+
if ( this.jwtConfigurer == null ) {
150+
throw new IllegalStateException("Jwt is the only supported format for bearer tokens " +
151+
"in Spring Security and no Jwt configuration was found. Make sure to specify " +
152+
"a jwk set uri by doing http.oauth2().resourceServer().jwt().jwkSetUri(uri), or wire a " +
153+
"JwtDecoder instance by doing http.oauth2().resourceServer().jwt().decoder(decoder), or " +
154+
"expose a JwtDecoder instance as a bean and do http.oauth2().resourceServer().jwt().");
155+
}
156+
136157
JwtDecoder decoder = this.jwtConfigurer.getJwtDecoder();
137158

138-
if (decoder != null) {
139-
JwtAuthenticationProvider provider =
140-
new JwtAuthenticationProvider(decoder);
141-
provider = postProcess(provider);
159+
JwtAuthenticationProvider provider =
160+
new JwtAuthenticationProvider(decoder);
161+
provider = postProcess(provider);
142162

143-
http.authenticationProvider(provider);
144-
} else {
145-
throw new IllegalStateException("Jwt is the only supported format for bearer tokens " +
146-
"in Spring Security and no instance of JwtDecoder could be found. Make sure to specify " +
147-
"a jwk set uri by doing http.oauth2().resourceServer().jwt().jwkSetUri(uri)");
148-
}
163+
http.authenticationProvider(provider);
149164
}
150165

151166
public class JwtConfigurer {
167+
private final ApplicationContext context;
168+
152169
private JwtDecoder decoder;
153170

154-
private JwtConfigurer() {}
171+
JwtConfigurer(ApplicationContext context) {
172+
this.context = context;
173+
}
174+
175+
public OAuth2ResourceServerConfigurer<H> decoder(JwtDecoder decoder) {
176+
this.decoder = decoder;
177+
return OAuth2ResourceServerConfigurer.this;
178+
}
155179

156180
public OAuth2ResourceServerConfigurer<H> jwkSetUri(String uri) {
157181
this.decoder = new NimbusJwtDecoderJwkSupport(uri);
158182
return OAuth2ResourceServerConfigurer.this;
159183
}
160184

161-
private JwtDecoder getJwtDecoder() {
185+
JwtDecoder getJwtDecoder() {
186+
if ( this.decoder == null ) {
187+
return this.context.getBean(JwtDecoder.class);
188+
}
189+
162190
return this.decoder;
163191
}
164192
}

config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java

Lines changed: 191 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
import java.io.FileReader;
2121
import java.io.IOException;
2222
import java.lang.reflect.Field;
23+
import java.time.Instant;
24+
import java.util.Collections;
25+
import java.util.Map;
2326
import java.util.stream.Collectors;
2427
import javax.annotation.PreDestroy;
2528

@@ -34,9 +37,11 @@
3437

3538
import org.springframework.beans.BeansException;
3639
import org.springframework.beans.factory.BeanCreationException;
40+
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
3741
import org.springframework.beans.factory.annotation.Autowired;
3842
import org.springframework.beans.factory.annotation.Value;
3943
import org.springframework.beans.factory.config.BeanPostProcessor;
44+
import org.springframework.context.ApplicationContext;
4045
import org.springframework.context.annotation.Bean;
4146
import org.springframework.context.annotation.Configuration;
4247
import org.springframework.core.io.ClassPathResource;
@@ -55,6 +60,11 @@
5560
import org.springframework.security.core.GrantedAuthority;
5661
import org.springframework.security.core.annotation.AuthenticationPrincipal;
5762
import org.springframework.security.core.userdetails.UserDetailsService;
63+
import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
64+
import org.springframework.security.oauth2.jwt.Jwt;
65+
import org.springframework.security.oauth2.jwt.JwtClaimNames;
66+
import org.springframework.security.oauth2.jwt.JwtDecoder;
67+
import org.springframework.security.oauth2.jwt.NimbusJwtDecoderJwkSupport;
5868
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
5969
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
6070
import org.springframework.test.web.servlet.MockMvc;
@@ -68,9 +78,13 @@
6878
import org.springframework.web.bind.annotation.PostMapping;
6979
import org.springframework.web.bind.annotation.RequestMapping;
7080
import org.springframework.web.bind.annotation.RestController;
81+
import org.springframework.web.context.support.GenericWebApplicationContext;
7182

7283
import static org.assertj.core.api.Assertions.assertThat;
7384
import static org.assertj.core.api.Assertions.assertThatCode;
85+
import static org.mockito.ArgumentMatchers.anyString;
86+
import static org.mockito.Mockito.mock;
87+
import static org.mockito.Mockito.when;
7488
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
7589
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
7690
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
@@ -86,8 +100,14 @@
86100
* @author Josh Cummings
87101
*/
88102
public class OAuth2ResourceServerConfigurerTests {
103+
private static final String JWT_TOKEN = "token";
104+
private static final String JWT_SUBJECT = "mock-test-subject";
105+
private static final Map<String, Object> JWT_HEADERS = Collections.singletonMap("alg", JwsAlgorithms.RS256);
106+
private static final Map<String, Object> JWT_CLAIMS = Collections.singletonMap(JwtClaimNames.SUB, JWT_SUBJECT);
107+
private static final Jwt JWT = new Jwt(JWT_TOKEN, Instant.MIN, Instant.MAX, JWT_HEADERS, JWT_CLAIMS);
108+
private static final String JWK_SET_URI = "https://mock.org";
89109

90-
@Autowired
110+
@Autowired(required = false)
91111
MockMvc mvc;
92112

93113
@Autowired(required = false)
@@ -506,6 +526,130 @@ public void requestWhenSessionManagementConfiguredThenUserConfigurationOverrides
506526
assertThat(result.getRequest().getSession(false)).isNotNull();
507527
}
508528

529+
// -- custom jwt decoder
530+
531+
@Test
532+
public void requestWhenCustomJwtDecoderWiredOnDslThenUsed()
533+
throws Exception {
534+
535+
this.spring.register(CustomJwtDecoderOnDsl.class, BasicController.class).autowire();
536+
537+
CustomJwtDecoderOnDsl config = this.spring.getContext().getBean(CustomJwtDecoderOnDsl.class);
538+
JwtDecoder decoder = config.decoder();
539+
540+
when(decoder.decode(anyString())).thenReturn(JWT);
541+
542+
this.mvc.perform(get("/authenticated")
543+
.with(bearerToken(JWT_TOKEN)))
544+
.andExpect(status().isOk())
545+
.andExpect(content().string(JWT_SUBJECT));
546+
}
547+
548+
@Test
549+
public void requestWhenCustomJwtDecoderExposedAsBeanThenUsed()
550+
throws Exception {
551+
552+
this.spring.register(CustomJwtDecoderAsBean.class, BasicController.class).autowire();
553+
554+
JwtDecoder decoder = this.spring.getContext().getBean(JwtDecoder.class);
555+
556+
when(decoder.decode(anyString())).thenReturn(JWT);
557+
558+
this.mvc.perform(get("/authenticated")
559+
.with(bearerToken(JWT_TOKEN)))
560+
.andExpect(status().isOk())
561+
.andExpect(content().string(JWT_SUBJECT));
562+
}
563+
564+
@Test
565+
public void getJwtDecoderWhenConfiguredWithDecoderAndJwkSetUriThenLastOneWins() {
566+
OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer =
567+
new OAuth2ResourceServerConfigurer().new JwtConfigurer(null);
568+
569+
JwtDecoder decoder = mock(JwtDecoder.class);
570+
571+
jwtConfigurer.jwkSetUri(JWK_SET_URI);
572+
jwtConfigurer.decoder(decoder);
573+
574+
assertThat(jwtConfigurer.getJwtDecoder()).isEqualTo(decoder);
575+
576+
jwtConfigurer =
577+
new OAuth2ResourceServerConfigurer().new JwtConfigurer(null);
578+
579+
jwtConfigurer.decoder(decoder);
580+
jwtConfigurer.jwkSetUri(JWK_SET_URI);
581+
582+
assertThat(jwtConfigurer.getJwtDecoder()).isInstanceOf(NimbusJwtDecoderJwkSupport.class);
583+
584+
}
585+
586+
@Test
587+
public void getJwtDecoderWhenConflictingJwtDecodersThenTheDslWiredOneTakesPrecedence() {
588+
589+
JwtDecoder decoderBean = mock(JwtDecoder.class);
590+
JwtDecoder decoder = mock(JwtDecoder.class);
591+
592+
ApplicationContext context = mock(ApplicationContext.class);
593+
when(context.getBean(JwtDecoder.class)).thenReturn(decoderBean);
594+
595+
OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer =
596+
new OAuth2ResourceServerConfigurer().new JwtConfigurer(context);
597+
jwtConfigurer.decoder(decoder);
598+
599+
assertThat(jwtConfigurer.getJwtDecoder()).isEqualTo(decoder);
600+
}
601+
602+
@Test
603+
public void getJwtDecoderWhenContextHasBeanAndUserConfiguresJwkSetUriThenJwkSetUriTakesPrecedence() {
604+
605+
JwtDecoder decoder = mock(JwtDecoder.class);
606+
ApplicationContext context = mock(ApplicationContext.class);
607+
when(context.getBean(JwtDecoder.class)).thenReturn(decoder);
608+
609+
OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer =
610+
new OAuth2ResourceServerConfigurer().new JwtConfigurer(context);
611+
612+
jwtConfigurer.jwkSetUri(JWK_SET_URI);
613+
614+
assertThat(jwtConfigurer.getJwtDecoder()).isNotEqualTo(decoder);
615+
assertThat(jwtConfigurer.getJwtDecoder()).isInstanceOf(NimbusJwtDecoderJwkSupport.class);
616+
}
617+
618+
@Test
619+
public void getJwtDecoderWhenTwoJwtDecoderBeansAndAnotherWiredOnDslThenDslWiredOneTakesPrecedence() {
620+
621+
JwtDecoder decoderBean = mock(JwtDecoder.class);
622+
JwtDecoder decoder = mock(JwtDecoder.class);
623+
624+
GenericWebApplicationContext context = new GenericWebApplicationContext();
625+
context.registerBean("decoderOne", JwtDecoder.class, () -> decoderBean);
626+
context.registerBean("decoderTwo", JwtDecoder.class, () -> decoderBean);
627+
this.spring.context(context).autowire();
628+
629+
OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer =
630+
new OAuth2ResourceServerConfigurer().new JwtConfigurer(context);
631+
jwtConfigurer.decoder(decoder);
632+
633+
assertThat(jwtConfigurer.getJwtDecoder()).isEqualTo(decoder);
634+
}
635+
636+
@Test
637+
public void getJwtDecoderWhenTwoJwtDecoderBeansThenThrowsException() {
638+
639+
JwtDecoder decoder = mock(JwtDecoder.class);
640+
GenericWebApplicationContext context = new GenericWebApplicationContext();
641+
context.registerBean("decoderOne", JwtDecoder.class, () -> decoder);
642+
context.registerBean("decoderTwo", JwtDecoder.class, () -> decoder);
643+
644+
this.spring.context(context).autowire();
645+
646+
OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer =
647+
new OAuth2ResourceServerConfigurer().new JwtConfigurer(context);
648+
649+
assertThatCode(() -> jwtConfigurer.getJwtDecoder())
650+
.isInstanceOf(NoUniqueBeanDefinitionException.class);
651+
}
652+
509653
// -- In combination with other authentication providers
510654

511655
@Test
@@ -534,15 +678,15 @@ public void configuredWhenMissingJwtAuthenticationProviderThenWiringException()
534678

535679
assertThatCode(() -> this.spring.register(JwtlessConfig.class).autowire())
536680
.isInstanceOf(BeanCreationException.class)
537-
.hasMessageContaining("no instance of JwtDecoder");
681+
.hasMessageContaining("no Jwt configuration was found");
538682
}
539683

540684
@Test
541685
public void configureWhenMissingJwkSetUriThenWiringException() {
542686

543687
assertThatCode(() -> this.spring.register(JwtHalfConfiguredConfig.class).autowire())
544688
.isInstanceOf(BeanCreationException.class)
545-
.hasMessageContaining("no instance of JwtDecoder");
689+
.hasMessageContaining("No qualifying bean of type");
546690
}
547691

548692
// -- support
@@ -689,6 +833,50 @@ protected void configure(HttpSecurity http) throws Exception {
689833
}
690834
}
691835

836+
@EnableWebSecurity
837+
static class CustomJwtDecoderOnDsl extends WebSecurityConfigurerAdapter {
838+
JwtDecoder decoder = mock(JwtDecoder.class);
839+
840+
@Override
841+
protected void configure(HttpSecurity http) throws Exception {
842+
// @formatter:off
843+
http
844+
.authorizeRequests()
845+
.anyRequest().authenticated()
846+
.and()
847+
.oauth2()
848+
.resourceServer()
849+
.jwt()
850+
.decoder(decoder());
851+
// @formatter:on
852+
}
853+
854+
JwtDecoder decoder() {
855+
return this.decoder;
856+
}
857+
}
858+
859+
@EnableWebSecurity
860+
static class CustomJwtDecoderAsBean extends WebSecurityConfigurerAdapter {
861+
@Override
862+
protected void configure(HttpSecurity http) throws Exception {
863+
// @formatter:off
864+
http
865+
.authorizeRequests()
866+
.anyRequest().authenticated()
867+
.and()
868+
.oauth2()
869+
.resourceServer()
870+
.jwt();
871+
// @formatter:on
872+
}
873+
874+
@Bean
875+
public JwtDecoder decoder() {
876+
return mock(JwtDecoder.class);
877+
}
878+
}
879+
692880
@RestController
693881
static class BasicController {
694882
@GetMapping("/")

0 commit comments

Comments
 (0)