|
16 | 16 |
|
17 | 17 | package org.springframework.security.test.web.reactive.server;
|
18 | 18 |
|
| 19 | +import java.time.Instant; |
19 | 20 | import java.util.Arrays;
|
20 | 21 | import java.util.Collection;
|
21 | 22 | import java.util.Collections;
|
|
26 | 27 | import java.util.Set;
|
27 | 28 | import java.util.function.Consumer;
|
28 | 29 | import java.util.function.Supplier;
|
| 30 | +import java.util.stream.Collectors; |
29 | 31 |
|
| 32 | +import com.nimbusds.oauth2.sdk.util.StringUtils; |
30 | 33 | import reactor.core.publisher.Mono;
|
31 | 34 |
|
32 | 35 | import org.springframework.core.convert.converter.Converter;
|
|
47 | 50 | import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
|
48 | 51 | import org.springframework.security.oauth2.client.web.server.WebSessionServerOAuth2AuthorizedClientRepository;
|
49 | 52 | import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
| 53 | +import org.springframework.security.oauth2.core.DefaultOAuth2AuthenticatedPrincipal; |
50 | 54 | import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
| 55 | +import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal; |
51 | 56 | import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
|
52 | 57 | import org.springframework.security.oauth2.core.oidc.OidcIdToken;
|
53 | 58 | import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
|
|
58 | 63 | import org.springframework.security.oauth2.core.user.OAuth2User;
|
59 | 64 | import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
|
60 | 65 | import org.springframework.security.oauth2.jwt.Jwt;
|
| 66 | +import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication; |
61 | 67 | import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
|
62 | 68 | import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
|
| 69 | +import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionClaimNames; |
63 | 70 | import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors;
|
64 | 71 | import org.springframework.security.web.server.csrf.CsrfWebFilter;
|
65 | 72 | import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
|
@@ -153,6 +160,20 @@ public static JwtMutator mockJwt() {
|
153 | 160 | return new JwtMutator();
|
154 | 161 | }
|
155 | 162 |
|
| 163 | + /** |
| 164 | + * Updates the ServerWebExchange to establish a {@link SecurityContext} that has a |
| 165 | + * {@link BearerTokenAuthentication} for the |
| 166 | + * {@link Authentication} and an {@link OAuth2AuthenticatedPrincipal} for the |
| 167 | + * {@link Authentication#getPrincipal()}. All details are |
| 168 | + * declarative and do not require the token to be valid. |
| 169 | + * |
| 170 | + * @return the {@link OpaqueTokenMutator} to further configure or use |
| 171 | + * @since 5.3 |
| 172 | + */ |
| 173 | + public static OpaqueTokenMutator mockOpaqueToken() { |
| 174 | + return new OpaqueTokenMutator(); |
| 175 | + } |
| 176 | + |
156 | 177 | /**
|
157 | 178 | * Updates the ServerWebExchange to establish a {@link SecurityContext} that has a
|
158 | 179 | * {@link OAuth2AuthenticationToken} for the
|
@@ -516,6 +537,165 @@ private <T extends WebTestClientConfigurer & MockServerConfigurer> T configurer(
|
516 | 537 | }
|
517 | 538 | }
|
518 | 539 |
|
| 540 | + /** |
| 541 | + * @author Josh Cummings |
| 542 | + * @since 5.3 |
| 543 | + */ |
| 544 | + public final static class OpaqueTokenMutator implements WebTestClientConfigurer, MockServerConfigurer { |
| 545 | + private Supplier<Map<String, Object>> attributes = this::defaultAttributes; |
| 546 | + private Supplier<Collection<GrantedAuthority>> authorities = this::defaultAuthorities; |
| 547 | + |
| 548 | + private Supplier<OAuth2AuthenticatedPrincipal> principal = this::defaultPrincipal; |
| 549 | + |
| 550 | + private OpaqueTokenMutator() { } |
| 551 | + |
| 552 | + /** |
| 553 | + * Mutate the attributes using the given {@link Consumer} |
| 554 | + * |
| 555 | + * @param attributesConsumer The {@link Consumer} for mutating the {@Map} of attributes |
| 556 | + * @return the {@link OpaqueTokenMutator} for further configuration |
| 557 | + */ |
| 558 | + public OpaqueTokenMutator attributes(Consumer<Map<String, Object>> attributesConsumer) { |
| 559 | + Assert.notNull(attributesConsumer, "attributesConsumer cannot be null"); |
| 560 | + this.attributes = () -> { |
| 561 | + Map<String, Object> attributes = defaultAttributes(); |
| 562 | + attributesConsumer.accept(attributes); |
| 563 | + return attributes; |
| 564 | + }; |
| 565 | + this.principal = this::defaultPrincipal; |
| 566 | + return this; |
| 567 | + } |
| 568 | + |
| 569 | + /** |
| 570 | + * Use the provided authorities in the resulting principal |
| 571 | + * @param authorities the authorities to use |
| 572 | + * @return the {@link OpaqueTokenMutator} for further configuration |
| 573 | + */ |
| 574 | + public OpaqueTokenMutator authorities(Collection<GrantedAuthority> authorities) { |
| 575 | + Assert.notNull(authorities, "authorities cannot be null"); |
| 576 | + this.authorities = () -> authorities; |
| 577 | + this.principal = this::defaultPrincipal; |
| 578 | + return this; |
| 579 | + } |
| 580 | + |
| 581 | + /** |
| 582 | + * Use the provided authorities in the resulting principal |
| 583 | + * @param authorities the authorities to use |
| 584 | + * @return the {@link OpaqueTokenMutator} for further configuration |
| 585 | + */ |
| 586 | + public OpaqueTokenMutator authorities(GrantedAuthority... authorities) { |
| 587 | + Assert.notNull(authorities, "authorities cannot be null"); |
| 588 | + this.authorities = () -> Arrays.asList(authorities); |
| 589 | + this.principal = this::defaultPrincipal; |
| 590 | + return this; |
| 591 | + } |
| 592 | + |
| 593 | + /** |
| 594 | + * Use the provided scopes as the authorities in the resulting principal |
| 595 | + * @param scopes the scopes to use |
| 596 | + * @return the {@link OpaqueTokenMutator} for further configuration |
| 597 | + */ |
| 598 | + public OpaqueTokenMutator scopes(String... scopes) { |
| 599 | + Assert.notNull(scopes, "scopes cannot be null"); |
| 600 | + this.authorities = () -> getAuthorities(Arrays.asList(scopes)); |
| 601 | + this.principal = this::defaultPrincipal; |
| 602 | + return this; |
| 603 | + } |
| 604 | + |
| 605 | + /** |
| 606 | + * Use the provided principal |
| 607 | + * @param principal the principal to use |
| 608 | + * @return the {@link OpaqueTokenMutator} for further configuration |
| 609 | + */ |
| 610 | + public OpaqueTokenMutator principal(OAuth2AuthenticatedPrincipal principal) { |
| 611 | + Assert.notNull(principal, "principal cannot be null"); |
| 612 | + this.principal = () -> principal; |
| 613 | + return this; |
| 614 | + } |
| 615 | + |
| 616 | + @Override |
| 617 | + public void beforeServerCreated(WebHttpHandlerBuilder builder) { |
| 618 | + configurer().beforeServerCreated(builder); |
| 619 | + } |
| 620 | + |
| 621 | + @Override |
| 622 | + public void afterConfigureAdded(WebTestClient.MockServerSpec<?> serverSpec) { |
| 623 | + configurer().afterConfigureAdded(serverSpec); |
| 624 | + } |
| 625 | + |
| 626 | + @Override |
| 627 | + public void afterConfigurerAdded( |
| 628 | + WebTestClient.Builder builder, |
| 629 | + @Nullable WebHttpHandlerBuilder httpHandlerBuilder, |
| 630 | + @Nullable ClientHttpConnector connector) { |
| 631 | + httpHandlerBuilder.filter((exchange, chain) -> { |
| 632 | + CsrfWebFilter.skipExchange(exchange); |
| 633 | + return chain.filter(exchange); |
| 634 | + }); |
| 635 | + configurer().afterConfigurerAdded(builder, httpHandlerBuilder, connector); |
| 636 | + } |
| 637 | + |
| 638 | + private <T extends WebTestClientConfigurer & MockServerConfigurer> T configurer() { |
| 639 | + OAuth2AuthenticatedPrincipal principal = this.principal.get(); |
| 640 | + OAuth2AccessToken accessToken = getOAuth2AccessToken(principal); |
| 641 | + BearerTokenAuthentication token = new BearerTokenAuthentication |
| 642 | + (principal, accessToken, principal.getAuthorities()); |
| 643 | + return mockAuthentication(token); |
| 644 | + } |
| 645 | + |
| 646 | + private Map<String, Object> defaultAttributes() { |
| 647 | + Map<String, Object> attributes = new HashMap<>(); |
| 648 | + attributes.put(OAuth2IntrospectionClaimNames.SUBJECT, "user"); |
| 649 | + attributes.put(OAuth2IntrospectionClaimNames.SCOPE, "read"); |
| 650 | + return attributes; |
| 651 | + } |
| 652 | + |
| 653 | + private Collection<GrantedAuthority> defaultAuthorities() { |
| 654 | + Map<String, Object> attributes = this.attributes.get(); |
| 655 | + Object scope = attributes.get(OAuth2IntrospectionClaimNames.SCOPE); |
| 656 | + if (scope == null) { |
| 657 | + return Collections.emptyList(); |
| 658 | + } |
| 659 | + if (scope instanceof Collection) { |
| 660 | + return getAuthorities((Collection) scope); |
| 661 | + } |
| 662 | + String scopes = scope.toString(); |
| 663 | + if (StringUtils.isBlank(scopes)) { |
| 664 | + return Collections.emptyList(); |
| 665 | + } |
| 666 | + return getAuthorities(Arrays.asList(scopes.split(" "))); |
| 667 | + } |
| 668 | + |
| 669 | + private OAuth2AuthenticatedPrincipal defaultPrincipal() { |
| 670 | + return new DefaultOAuth2AuthenticatedPrincipal |
| 671 | + (this.attributes.get(), this.authorities.get()); |
| 672 | + } |
| 673 | + |
| 674 | + private Collection<GrantedAuthority> getAuthorities(Collection<?> scopes) { |
| 675 | + return scopes.stream() |
| 676 | + .map(scope -> new SimpleGrantedAuthority("SCOPE_" + scope)) |
| 677 | + .collect(Collectors.toList()); |
| 678 | + } |
| 679 | + |
| 680 | + private OAuth2AccessToken getOAuth2AccessToken(OAuth2AuthenticatedPrincipal principal) { |
| 681 | + Instant expiresAt = getInstant(principal.getAttributes(), "exp"); |
| 682 | + Instant issuedAt = getInstant(principal.getAttributes(), "iat"); |
| 683 | + return new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, |
| 684 | + "token", issuedAt, expiresAt); |
| 685 | + } |
| 686 | + |
| 687 | + private Instant getInstant(Map<String, Object> attributes, String name) { |
| 688 | + Object value = attributes.get(name); |
| 689 | + if (value == null) { |
| 690 | + return null; |
| 691 | + } |
| 692 | + if (value instanceof Instant) { |
| 693 | + return (Instant) value; |
| 694 | + } |
| 695 | + throw new IllegalArgumentException(name + " attribute must be of type Instant"); |
| 696 | + } |
| 697 | + } |
| 698 | + |
519 | 699 | /**
|
520 | 700 | * @author Josh Cummings
|
521 | 701 | * @since 5.3
|
|
0 commit comments