|
20 | 20 | import java.io.FileReader;
|
21 | 21 | import java.io.IOException;
|
22 | 22 | import java.lang.reflect.Field;
|
| 23 | +import java.time.Instant; |
| 24 | +import java.util.Collections; |
| 25 | +import java.util.Map; |
23 | 26 | import java.util.stream.Collectors;
|
24 | 27 | import javax.annotation.PreDestroy;
|
25 | 28 |
|
|
34 | 37 |
|
35 | 38 | import org.springframework.beans.BeansException;
|
36 | 39 | import org.springframework.beans.factory.BeanCreationException;
|
| 40 | +import org.springframework.beans.factory.NoUniqueBeanDefinitionException; |
37 | 41 | import org.springframework.beans.factory.annotation.Autowired;
|
38 | 42 | import org.springframework.beans.factory.annotation.Value;
|
39 | 43 | import org.springframework.beans.factory.config.BeanPostProcessor;
|
| 44 | +import org.springframework.context.ApplicationContext; |
40 | 45 | import org.springframework.context.annotation.Bean;
|
41 | 46 | import org.springframework.context.annotation.Configuration;
|
42 | 47 | import org.springframework.core.io.ClassPathResource;
|
|
55 | 60 | import org.springframework.security.core.GrantedAuthority;
|
56 | 61 | import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
57 | 62 | 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; |
58 | 68 | import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
|
59 | 69 | import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
60 | 70 | import org.springframework.test.web.servlet.MockMvc;
|
|
68 | 78 | import org.springframework.web.bind.annotation.PostMapping;
|
69 | 79 | import org.springframework.web.bind.annotation.RequestMapping;
|
70 | 80 | import org.springframework.web.bind.annotation.RestController;
|
| 81 | +import org.springframework.web.context.support.GenericWebApplicationContext; |
71 | 82 |
|
72 | 83 | import static org.assertj.core.api.Assertions.assertThat;
|
73 | 84 | 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; |
74 | 88 | import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
|
75 | 89 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
76 | 90 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
|
86 | 100 | * @author Josh Cummings
|
87 | 101 | */
|
88 | 102 | 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"; |
89 | 109 |
|
90 |
| - @Autowired |
| 110 | + @Autowired(required = false) |
91 | 111 | MockMvc mvc;
|
92 | 112 |
|
93 | 113 | @Autowired(required = false)
|
@@ -506,6 +526,130 @@ public void requestWhenSessionManagementConfiguredThenUserConfigurationOverrides
|
506 | 526 | assertThat(result.getRequest().getSession(false)).isNotNull();
|
507 | 527 | }
|
508 | 528 |
|
| 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 | + |
509 | 653 | // -- In combination with other authentication providers
|
510 | 654 |
|
511 | 655 | @Test
|
@@ -534,15 +678,15 @@ public void configuredWhenMissingJwtAuthenticationProviderThenWiringException()
|
534 | 678 |
|
535 | 679 | assertThatCode(() -> this.spring.register(JwtlessConfig.class).autowire())
|
536 | 680 | .isInstanceOf(BeanCreationException.class)
|
537 |
| - .hasMessageContaining("no instance of JwtDecoder"); |
| 681 | + .hasMessageContaining("no Jwt configuration was found"); |
538 | 682 | }
|
539 | 683 |
|
540 | 684 | @Test
|
541 | 685 | public void configureWhenMissingJwkSetUriThenWiringException() {
|
542 | 686 |
|
543 | 687 | assertThatCode(() -> this.spring.register(JwtHalfConfiguredConfig.class).autowire())
|
544 | 688 | .isInstanceOf(BeanCreationException.class)
|
545 |
| - .hasMessageContaining("no instance of JwtDecoder"); |
| 689 | + .hasMessageContaining("No qualifying bean of type"); |
546 | 690 | }
|
547 | 691 |
|
548 | 692 | // -- support
|
@@ -689,6 +833,50 @@ protected void configure(HttpSecurity http) throws Exception {
|
689 | 833 | }
|
690 | 834 | }
|
691 | 835 |
|
| 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 | + |
692 | 880 | @RestController
|
693 | 881 | static class BasicController {
|
694 | 882 | @GetMapping("/")
|
|
0 commit comments