Skip to content

Commit bd593a3

Browse files
committed
Add Opaque Token WebTestClient Support
Fixes gh-7827
1 parent 9d66f2c commit bd593a3

File tree

2 files changed

+351
-0
lines changed

2 files changed

+351
-0
lines changed

test/src/main/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurers.java

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.security.test.web.reactive.server;
1818

19+
import java.time.Instant;
1920
import java.util.Arrays;
2021
import java.util.Collection;
2122
import java.util.Collections;
@@ -26,7 +27,9 @@
2627
import java.util.Set;
2728
import java.util.function.Consumer;
2829
import java.util.function.Supplier;
30+
import java.util.stream.Collectors;
2931

32+
import com.nimbusds.oauth2.sdk.util.StringUtils;
3033
import reactor.core.publisher.Mono;
3134

3235
import org.springframework.core.convert.converter.Converter;
@@ -47,7 +50,9 @@
4750
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
4851
import org.springframework.security.oauth2.client.web.server.WebSessionServerOAuth2AuthorizedClientRepository;
4952
import org.springframework.security.oauth2.core.AuthorizationGrantType;
53+
import org.springframework.security.oauth2.core.DefaultOAuth2AuthenticatedPrincipal;
5054
import org.springframework.security.oauth2.core.OAuth2AccessToken;
55+
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
5156
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
5257
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
5358
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
@@ -58,8 +63,10 @@
5863
import org.springframework.security.oauth2.core.user.OAuth2User;
5964
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
6065
import org.springframework.security.oauth2.jwt.Jwt;
66+
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication;
6167
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
6268
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
69+
import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionClaimNames;
6370
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors;
6471
import org.springframework.security.web.server.csrf.CsrfWebFilter;
6572
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
@@ -153,6 +160,20 @@ public static JwtMutator mockJwt() {
153160
return new JwtMutator();
154161
}
155162

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+
156177
/**
157178
* Updates the ServerWebExchange to establish a {@link SecurityContext} that has a
158179
* {@link OAuth2AuthenticationToken} for the
@@ -516,6 +537,165 @@ private <T extends WebTestClientConfigurer & MockServerConfigurer> T configurer(
516537
}
517538
}
518539

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+
519699
/**
520700
* @author Josh Cummings
521701
* @since 5.3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
/*
2+
* Copyright 2002-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.security.test.web.reactive.server;
17+
18+
import java.util.List;
19+
20+
import org.junit.Test;
21+
import org.junit.runner.RunWith;
22+
import org.mockito.junit.MockitoJUnitRunner;
23+
24+
import org.springframework.core.ReactiveAdapterRegistry;
25+
import org.springframework.http.HttpHeaders;
26+
import org.springframework.http.MediaType;
27+
import org.springframework.security.core.GrantedAuthority;
28+
import org.springframework.security.core.authority.SimpleGrantedAuthority;
29+
import org.springframework.security.core.context.SecurityContext;
30+
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
31+
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication;
32+
import org.springframework.security.web.reactive.result.method.annotation.CurrentSecurityContextArgumentResolver;
33+
import org.springframework.security.web.server.context.SecurityContextServerWebExchangeWebFilter;
34+
import org.springframework.test.web.reactive.server.WebTestClient;
35+
36+
import static org.assertj.core.api.Assertions.assertThat;
37+
import static org.springframework.security.oauth2.core.TestOAuth2AuthenticatedPrincipals.active;
38+
import static org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionClaimNames.SUBJECT;
39+
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockOpaqueToken;
40+
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity;
41+
42+
/**
43+
* @author Josh Cummings
44+
* @since 5.3
45+
*/
46+
@RunWith(MockitoJUnitRunner.class)
47+
public class SecurityMockServerConfigurerOpaqueTokenTests extends AbstractMockServerConfigurersTests {
48+
private GrantedAuthority authority1 = new SimpleGrantedAuthority("one");
49+
50+
private GrantedAuthority authority2 = new SimpleGrantedAuthority("two");
51+
52+
private WebTestClient client = WebTestClient
53+
.bindToController(securityContextController)
54+
.webFilter(new SecurityContextServerWebExchangeWebFilter())
55+
.argumentResolvers(resolvers -> resolvers.addCustomResolver(
56+
new CurrentSecurityContextArgumentResolver(new ReactiveAdapterRegistry())))
57+
.apply(springSecurity())
58+
.configureClient()
59+
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
60+
.build();
61+
62+
@Test
63+
public void mockOpaqueTokenWhenUsingDefaultsThenBearerTokenAuthentication() {
64+
this.client
65+
.mutateWith(mockOpaqueToken())
66+
.get()
67+
.exchange()
68+
.expectStatus().isOk();
69+
70+
SecurityContext context = securityContextController.removeSecurityContext();
71+
assertThat(context.getAuthentication()).isInstanceOf(
72+
BearerTokenAuthentication.class);
73+
BearerTokenAuthentication token = (BearerTokenAuthentication) context.getAuthentication();
74+
assertThat(token.getAuthorities()).isNotEmpty();
75+
assertThat(token.getToken()).isNotNull();
76+
assertThat(token.getTokenAttributes().get(SUBJECT)).isEqualTo("user");
77+
}
78+
79+
@Test
80+
public void mockOpaqueTokenWhenAuthoritiesThenBearerTokenAuthentication() {
81+
this.client
82+
.mutateWith(mockOpaqueToken()
83+
.authorities(this.authority1, this.authority2))
84+
.get()
85+
.exchange()
86+
.expectStatus().isOk();
87+
88+
SecurityContext context = securityContextController.removeSecurityContext();
89+
assertThat((List<GrantedAuthority>) context.getAuthentication().getAuthorities())
90+
.containsOnly(this.authority1, this.authority2);
91+
}
92+
93+
@Test
94+
public void mockOpaqueTokenWhenScopesThenBearerTokenAuthentication() {
95+
this.client
96+
.mutateWith(mockOpaqueToken().scopes("scoped", "authorities"))
97+
.get()
98+
.exchange()
99+
.expectStatus().isOk();
100+
101+
SecurityContext context = securityContextController.removeSecurityContext();
102+
assertThat((List<GrantedAuthority>) context.getAuthentication().getAuthorities())
103+
.containsOnly(new SimpleGrantedAuthority("SCOPE_scoped"),
104+
new SimpleGrantedAuthority("SCOPE_authorities"));
105+
}
106+
107+
@Test
108+
public void mockOpaqueTokenWhenAttributesThenBearerTokenAuthentication() {
109+
String sub = new String("my-subject");
110+
this.client
111+
.mutateWith(mockOpaqueToken()
112+
.attributes(attributes -> attributes.put(SUBJECT, sub)))
113+
.get()
114+
.exchange()
115+
.expectStatus().isOk();
116+
117+
SecurityContext context = securityContextController.removeSecurityContext();
118+
assertThat(context.getAuthentication()).isInstanceOf(BearerTokenAuthentication.class);
119+
BearerTokenAuthentication token = (BearerTokenAuthentication) context.getAuthentication();
120+
assertThat(token.getTokenAttributes().get(SUBJECT)).isSameAs(sub);
121+
}
122+
123+
@Test
124+
public void mockOpaqueTokenWhenPrincipalThenBearerTokenAuthentication() {
125+
OAuth2AuthenticatedPrincipal principal = active();
126+
this.client
127+
.mutateWith(mockOpaqueToken()
128+
.principal(principal))
129+
.get()
130+
.exchange()
131+
.expectStatus().isOk();
132+
133+
SecurityContext context = securityContextController.removeSecurityContext();
134+
assertThat(context.getAuthentication()).isInstanceOf(BearerTokenAuthentication.class);
135+
BearerTokenAuthentication token = (BearerTokenAuthentication) context.getAuthentication();
136+
assertThat(token.getPrincipal()).isSameAs(principal);
137+
}
138+
139+
@Test
140+
public void mockOpaqueTokenWhenPrincipalSpecifiedThenLastCalledTakesPrecedence() {
141+
OAuth2AuthenticatedPrincipal principal = active(a -> a.put("scope", "user"));
142+
143+
this.client
144+
.mutateWith(mockOpaqueToken()
145+
.attributes(a -> a.put(SUBJECT, "foo"))
146+
.principal(principal))
147+
.get()
148+
.exchange()
149+
.expectStatus().isOk();
150+
151+
SecurityContext context = securityContextController.removeSecurityContext();
152+
assertThat(context.getAuthentication()).isInstanceOf(BearerTokenAuthentication.class);
153+
BearerTokenAuthentication token = (BearerTokenAuthentication) context.getAuthentication();
154+
assertThat((String) ((OAuth2AuthenticatedPrincipal) token.getPrincipal()).getAttribute(SUBJECT))
155+
.isEqualTo(principal.getAttribute(SUBJECT));
156+
157+
this.client
158+
.mutateWith(mockOpaqueToken()
159+
.principal(principal)
160+
.attributes(a -> a.put(SUBJECT, "bar")))
161+
.get()
162+
.exchange()
163+
.expectStatus().isOk();
164+
165+
context = securityContextController.removeSecurityContext();
166+
assertThat(context.getAuthentication()).isInstanceOf(BearerTokenAuthentication.class);
167+
token = (BearerTokenAuthentication) context.getAuthentication();
168+
assertThat((String) ((OAuth2AuthenticatedPrincipal) token.getPrincipal()).getAttribute(SUBJECT))
169+
.isEqualTo("bar");
170+
}
171+
}

0 commit comments

Comments
 (0)