|
18 | 18 |
|
19 | 19 | import java.util.Arrays;
|
20 | 20 | import java.util.Collection;
|
| 21 | +import java.util.Collections; |
| 22 | +import java.util.LinkedHashSet; |
21 | 23 | import java.util.List;
|
| 24 | +import java.util.Set; |
22 | 25 | import java.util.function.Consumer;
|
23 | 26 | import java.util.function.Supplier;
|
24 | 27 |
|
|
30 | 33 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
31 | 34 | import org.springframework.security.core.Authentication;
|
32 | 35 | import org.springframework.security.core.GrantedAuthority;
|
| 36 | +import org.springframework.security.core.authority.SimpleGrantedAuthority; |
33 | 37 | import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
34 | 38 | import org.springframework.security.core.context.SecurityContext;
|
35 | 39 | import org.springframework.security.core.context.SecurityContextImpl;
|
36 | 40 | import org.springframework.security.core.userdetails.User;
|
37 | 41 | import org.springframework.security.core.userdetails.UserDetails;
|
| 42 | +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; |
| 43 | +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; |
| 44 | +import org.springframework.security.oauth2.client.registration.ClientRegistration; |
| 45 | +import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository; |
| 46 | +import org.springframework.security.oauth2.client.web.server.WebSessionServerOAuth2AuthorizedClientRepository; |
| 47 | +import org.springframework.security.oauth2.core.AuthorizationGrantType; |
| 48 | +import org.springframework.security.oauth2.core.OAuth2AccessToken; |
| 49 | +import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; |
| 50 | +import org.springframework.security.oauth2.core.oidc.OidcIdToken; |
| 51 | +import org.springframework.security.oauth2.core.oidc.OidcUserInfo; |
| 52 | +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; |
| 53 | +import org.springframework.security.oauth2.core.oidc.user.OidcUser; |
| 54 | +import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority; |
38 | 55 | import org.springframework.security.oauth2.jwt.Jwt;
|
39 | 56 | import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
|
40 | 57 | import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
|
@@ -130,6 +147,21 @@ public static JwtMutator mockJwt() {
|
130 | 147 | return new JwtMutator();
|
131 | 148 | }
|
132 | 149 |
|
| 150 | + /** |
| 151 | + * Updates the ServerWebExchange to establish a {@link SecurityContext} that has a |
| 152 | + * {@link OAuth2AuthenticationToken} for the |
| 153 | + * {@link Authentication}. All details are |
| 154 | + * declarative and do not require the corresponding OAuth 2.0 tokens to be valid. |
| 155 | + * |
| 156 | + * @return the {@link OidcLoginMutator} to further configure or use |
| 157 | + * @since 5.3 |
| 158 | + */ |
| 159 | + public static OidcLoginMutator mockOidcLogin() { |
| 160 | + OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "access-token", |
| 161 | + null, null, Collections.singleton("user")); |
| 162 | + return new OidcLoginMutator(accessToken); |
| 163 | + } |
| 164 | + |
133 | 165 | public static CsrfMutator csrf() {
|
134 | 166 | return new CsrfMutator();
|
135 | 167 | }
|
@@ -429,4 +461,185 @@ private <T extends WebTestClientConfigurer & MockServerConfigurer> T configurer(
|
429 | 461 | return mockAuthentication(new JwtAuthenticationToken(this.jwt, this.authoritiesConverter.convert(this.jwt)));
|
430 | 462 | }
|
431 | 463 | }
|
| 464 | + |
| 465 | + /** |
| 466 | + * @author Josh Cummings |
| 467 | + * @since 5.3 |
| 468 | + */ |
| 469 | + public final static class OidcLoginMutator implements WebTestClientConfigurer, MockServerConfigurer { |
| 470 | + private ClientRegistration clientRegistration; |
| 471 | + private OAuth2AccessToken accessToken; |
| 472 | + private OidcIdToken idToken; |
| 473 | + private OidcUserInfo userInfo; |
| 474 | + private OidcUser oidcUser; |
| 475 | + private Collection<GrantedAuthority> authorities; |
| 476 | + |
| 477 | + ServerOAuth2AuthorizedClientRepository authorizedClientRepository = |
| 478 | + new WebSessionServerOAuth2AuthorizedClientRepository(); |
| 479 | + |
| 480 | + private OidcLoginMutator(OAuth2AccessToken accessToken) { |
| 481 | + this.accessToken = accessToken; |
| 482 | + this.clientRegistration = clientRegistrationBuilder().build(); |
| 483 | + } |
| 484 | + |
| 485 | + /** |
| 486 | + * Use the provided authorities in the {@link Authentication} |
| 487 | + * |
| 488 | + * @param authorities the authorities to use |
| 489 | + * @return the {@link OidcLoginMutator} for further configuration |
| 490 | + */ |
| 491 | + public OidcLoginMutator authorities(Collection<GrantedAuthority> authorities) { |
| 492 | + Assert.notNull(authorities, "authorities cannot be null"); |
| 493 | + this.authorities = authorities; |
| 494 | + return this; |
| 495 | + } |
| 496 | + |
| 497 | + /** |
| 498 | + * Use the provided authorities in the {@link Authentication} |
| 499 | + * |
| 500 | + * @param authorities the authorities to use |
| 501 | + * @return the {@link OidcLoginMutator} for further configuration |
| 502 | + */ |
| 503 | + public OidcLoginMutator authorities(GrantedAuthority... authorities) { |
| 504 | + Assert.notNull(authorities, "authorities cannot be null"); |
| 505 | + this.authorities = Arrays.asList(authorities); |
| 506 | + return this; |
| 507 | + } |
| 508 | + |
| 509 | + /** |
| 510 | + * Use the provided {@link OidcIdToken} when constructing the authenticated user |
| 511 | + * |
| 512 | + * @param idTokenBuilderConsumer a {@link Consumer} of a {@link OidcIdToken.Builder} |
| 513 | + * @return the {@link OidcLoginMutator} for further configuration |
| 514 | + */ |
| 515 | + public OidcLoginMutator idToken(Consumer<OidcIdToken.Builder> idTokenBuilderConsumer) { |
| 516 | + OidcIdToken.Builder builder = OidcIdToken.withTokenValue("id-token"); |
| 517 | + builder.subject("test-subject"); |
| 518 | + idTokenBuilderConsumer.accept(builder); |
| 519 | + this.idToken = builder.build(); |
| 520 | + return this; |
| 521 | + } |
| 522 | + |
| 523 | + /** |
| 524 | + * Use the provided {@link OidcUserInfo} when constructing the authenticated user |
| 525 | + * |
| 526 | + * @param userInfoBuilderConsumer a {@link Consumer} of a {@link OidcUserInfo.Builder} |
| 527 | + * @return the {@link OidcLoginMutator} for further configuration |
| 528 | + */ |
| 529 | + public OidcLoginMutator userInfoToken(Consumer<OidcUserInfo.Builder> userInfoBuilderConsumer) { |
| 530 | + OidcUserInfo.Builder builder = OidcUserInfo.builder(); |
| 531 | + userInfoBuilderConsumer.accept(builder); |
| 532 | + this.userInfo = builder.build(); |
| 533 | + return this; |
| 534 | + } |
| 535 | + |
| 536 | + /** |
| 537 | + * Use the provided {@link OidcUser} as the authenticated user. |
| 538 | + * <p> |
| 539 | + * Supplying an {@link OidcUser} will take precedence over {@link #idToken}, {@link #userInfo}, |
| 540 | + * and list of {@link GrantedAuthority}s to use. |
| 541 | + * |
| 542 | + * @param oidcUser the {@link OidcUser} to use |
| 543 | + * @return the {@link OidcLoginMutator} for further configuration |
| 544 | + */ |
| 545 | + public OidcLoginMutator oidcUser(OidcUser oidcUser) { |
| 546 | + this.oidcUser = oidcUser; |
| 547 | + return this; |
| 548 | + } |
| 549 | + |
| 550 | + /** |
| 551 | + * Use the provided {@link ClientRegistration} as the client to authorize. |
| 552 | + * <p> |
| 553 | + * The supplied {@link ClientRegistration} will be registered into an |
| 554 | + * {@link WebSessionServerOAuth2AuthorizedClientRepository}. Tests relying on |
| 555 | + * {@link org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient} |
| 556 | + * annotations should register an {@link WebSessionServerOAuth2AuthorizedClientRepository} bean |
| 557 | + * to the application context. |
| 558 | + * |
| 559 | + * @param clientRegistration the {@link ClientRegistration} to use |
| 560 | + * @return the {@link OidcLoginMutator} for further configuration |
| 561 | + */ |
| 562 | + public OidcLoginMutator clientRegistration(ClientRegistration clientRegistration) { |
| 563 | + this.clientRegistration = clientRegistration; |
| 564 | + return this; |
| 565 | + } |
| 566 | + |
| 567 | + @Override |
| 568 | + public void beforeServerCreated(WebHttpHandlerBuilder builder) { |
| 569 | + OAuth2AuthenticationToken token = getToken(); |
| 570 | + builder.filters(addAuthorizedClientFilter(token)); |
| 571 | + mockAuthentication(getToken()).beforeServerCreated(builder); |
| 572 | + } |
| 573 | + |
| 574 | + @Override |
| 575 | + public void afterConfigureAdded(WebTestClient.MockServerSpec<?> serverSpec) { |
| 576 | + mockAuthentication(getToken()).afterConfigureAdded(serverSpec); |
| 577 | + } |
| 578 | + |
| 579 | + @Override |
| 580 | + public void afterConfigurerAdded( |
| 581 | + WebTestClient.Builder builder, |
| 582 | + @Nullable WebHttpHandlerBuilder httpHandlerBuilder, |
| 583 | + @Nullable ClientHttpConnector connector) { |
| 584 | + OAuth2AuthenticationToken token = getToken(); |
| 585 | + httpHandlerBuilder.filters(addAuthorizedClientFilter(token)); |
| 586 | + mockAuthentication(token).afterConfigurerAdded(builder, httpHandlerBuilder, connector); |
| 587 | + } |
| 588 | + |
| 589 | + private Consumer<List<WebFilter>> addAuthorizedClientFilter(OAuth2AuthenticationToken token) { |
| 590 | + OAuth2AuthorizedClient client = getClient(); |
| 591 | + return filters -> filters.add(0, (exchange, chain) -> |
| 592 | + authorizedClientRepository.saveAuthorizedClient(client, token, exchange) |
| 593 | + .then(chain.filter(exchange))); |
| 594 | + } |
| 595 | + |
| 596 | + private ClientRegistration.Builder clientRegistrationBuilder() { |
| 597 | + return ClientRegistration.withRegistrationId("test") |
| 598 | + .authorizationGrantType(AuthorizationGrantType.PASSWORD) |
| 599 | + .clientId("test-client") |
| 600 | + .tokenUri("https://token-uri.example.org"); |
| 601 | + } |
| 602 | + |
| 603 | + private OAuth2AuthenticationToken getToken() { |
| 604 | + OidcUser oidcUser = getOidcUser(); |
| 605 | + return new OAuth2AuthenticationToken(oidcUser, oidcUser.getAuthorities(), this.clientRegistration.getRegistrationId()); |
| 606 | + } |
| 607 | + |
| 608 | + private OAuth2AuthorizedClient getClient() { |
| 609 | + return new OAuth2AuthorizedClient(this.clientRegistration, getToken().getName(), this.accessToken); |
| 610 | + } |
| 611 | + |
| 612 | + private Collection<GrantedAuthority> getAuthorities() { |
| 613 | + if (this.authorities == null) { |
| 614 | + Set<GrantedAuthority> authorities = new LinkedHashSet<>(); |
| 615 | + authorities.add(new OidcUserAuthority(getOidcIdToken(), getOidcUserInfo())); |
| 616 | + for (String authority : this.accessToken.getScopes()) { |
| 617 | + authorities.add(new SimpleGrantedAuthority("SCOPE_" + authority)); |
| 618 | + } |
| 619 | + return authorities; |
| 620 | + } else { |
| 621 | + return this.authorities; |
| 622 | + } |
| 623 | + } |
| 624 | + |
| 625 | + private OidcIdToken getOidcIdToken() { |
| 626 | + if (this.idToken == null) { |
| 627 | + return new OidcIdToken("id-token", null, null, Collections.singletonMap(IdTokenClaimNames.SUB, "test-subject")); |
| 628 | + } else { |
| 629 | + return this.idToken; |
| 630 | + } |
| 631 | + } |
| 632 | + |
| 633 | + private OidcUserInfo getOidcUserInfo() { |
| 634 | + return this.userInfo; |
| 635 | + } |
| 636 | + |
| 637 | + private OidcUser getOidcUser() { |
| 638 | + if (this.oidcUser == null) { |
| 639 | + return new DefaultOidcUser(getAuthorities(), getOidcIdToken(), this.userInfo); |
| 640 | + } else { |
| 641 | + return this.oidcUser; |
| 642 | + } |
| 643 | + } |
| 644 | + } |
432 | 645 | }
|
0 commit comments