Mock Jwt Test Support and Jwt.Builder Polish

Simplified the initial support to introduce fewer classes and only the
features described in the ticket.

Changed tests to align with existing patterns in the repository.

Added JavaDoc to remaining public methods introduced for this feature.

Issue: gh-6634
Issue: gh-6851
This commit is contained in:
Josh Cummings
2019-05-21 17:59:55 -06:00
parent e59d8a529b
commit d0f5b42884
16 changed files with 819 additions and 747 deletions

View File

@@ -1,140 +0,0 @@
/*
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
package org.springframework.security.test.support;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimNames;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.util.StringUtils;
/**
* @author Jérôme Wacongne <ch4mp@c4-soft.com>
* @since 5.2
*/
public class JwtAuthenticationTokenTestingBuilder<T extends JwtAuthenticationTokenTestingBuilder<T>>
extends
JwtAuthenticationToken.Builder<T> {
private static final String[] DEFAULT_SCOPES = { "USER" };
private final Set<GrantedAuthority> addedAuthorities;
public JwtAuthenticationTokenTestingBuilder(Converter<Jwt, Collection<GrantedAuthority>> authoritiesConverter) {
super(new JwtTestingBuilder(), authoritiesConverter);
this.addedAuthorities = new HashSet<>();
scopes(DEFAULT_SCOPES);
}
public JwtAuthenticationTokenTestingBuilder() {
this(new JwtGrantedAuthoritiesConverter());
}
/**
* How to extract authorities from token
* @param authoritiesConverter JWT to granted-authorities converter
* @return this builder to further configure
*/
public T authorities(Converter<Jwt, Collection<GrantedAuthority>> authoritiesConverter) {
return authoritiesConverter(authoritiesConverter);
}
/**
* Adds authorities to what is extracted from the token.<br>
* Please consider using {@link #authorities(Converter)} instead.
* @param authorities authorities to add to token ones
* @return this builder to further configure
*/
public T authorities(Stream<GrantedAuthority> authorities) {
addedAuthorities.addAll(authorities.collect(Collectors.toSet()));
return downcast();
}
/**
* Adds authorities to what is extracted from the token.<br>
* Please consider using {@link #authorities(Converter)} instead.
* @param authorities authorities to add to token ones
* @return this builder to further configure
*/
public T authorities(GrantedAuthority... authorities) {
return authorities(Stream.of(authorities));
}
/**
* Adds authorities to what is extracted from the token.<br>
* Please consider using {@link #authorities(Converter)} instead.
* @param authorities authorities to add to token ones
* @return this builder to further configure
*/
public T authorities(String... authorities) {
return authorities(Stream.of(authorities).map(SimpleGrantedAuthority::new));
}
@Override
public JwtAuthenticationToken build() {
final Jwt token = getToken();
return new JwtAuthenticationToken(token, getAuthorities(token));
}
@Override
protected Collection<GrantedAuthority> getAuthorities(Jwt token) {
final Collection<GrantedAuthority> principalAuthorities = super.getAuthorities(token);
return addedAuthorities.isEmpty() ? principalAuthorities
: Stream.concat(principalAuthorities.stream(), addedAuthorities.stream()).collect(Collectors.toSet());
}
/**
* @author Jérôme Wacongne &lt;ch4mp&#64;c4-soft.com&gt;
* @since 5.2
*/
static class JwtTestingBuilder extends Jwt.Builder<JwtTestingBuilder> {
private static final String DEFAULT_SUBJECT = "user";
private static final String DEFAULT_TOKEN_VALUE = "test.jwt.value";
private static final String DEFAULT_HEADER_NAME = "test-header";
private static final String DEFAULT_HEADER_VALUE = "test-header-value";
public JwtTestingBuilder() {
super();
}
@Override
public Jwt build() {
final Object subjectClaim = claims.get(JwtClaimNames.SUB);
if (!StringUtils.hasLength(tokenValue)) {
tokenValue(DEFAULT_TOKEN_VALUE);
}
if (!StringUtils.hasLength((String) subjectClaim)) {
claim(JwtClaimNames.SUB, DEFAULT_SUBJECT);
}
if (headers.size() == 0) {
header(DEFAULT_HEADER_NAME, DEFAULT_HEADER_VALUE);
}
return super.build();
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,11 +16,15 @@
package org.springframework.security.test.web.reactive.server;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Supplier;
import reactor.core.publisher.Mono;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.lang.Nullable;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
@@ -33,18 +37,19 @@ import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.test.support.JwtAuthenticationTokenTestingBuilder;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.web.server.csrf.CsrfWebFilter;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
import org.springframework.test.web.reactive.server.MockServerConfigurer;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.test.web.reactive.server.WebTestClientConfigurer;
import org.springframework.util.Assert;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
import reactor.core.publisher.Mono;
import static org.springframework.security.oauth2.jwt.JwtClaimNames.SUB;
/**
* Test utilities for working with Spring Security and
@@ -121,13 +126,30 @@ public class SecurityMockServerConfigurers {
* declarative and do not require the JWT to be valid.
*
* @return the {@link JwtMutator} to further configure or use
* @since 5.2
*/
public static JwtMutator mockJwt() {
return new JwtMutator();
return mockJwt(jwt -> {});
}
public static JwtMutator mockJwt(Consumer<Jwt.Builder<?>> jwt) {
return new JwtMutator().token(jwt);
/**
* Updates the ServerWebExchange to establish a {@link SecurityContext} that has a
* {@link JwtAuthenticationToken} for the
* {@link Authentication} and a {@link Jwt} for the
* {@link Authentication#getPrincipal()}. All details are
* declarative and do not require the JWT to be valid.
*
* @param jwtBuilderConsumer For configuring the underlying {@link Jwt}
* @return the {@link JwtMutator} to further configure or use
* @since 5.2
*/
public static JwtMutator mockJwt(Consumer<Jwt.Builder> jwtBuilderConsumer) {
Jwt.Builder jwtBuilder = Jwt.withTokenValue("token")
.header("alg", "none")
.claim(SUB, "user")
.claim("scope", "read");
jwtBuilderConsumer.accept(jwtBuilder);
return new JwtMutator(jwtBuilder.build());
}
public static CsrfMutator csrf() {
@@ -315,23 +337,68 @@ public class SecurityMockServerConfigurers {
return webFilterChain.filter(exchange);
}
}
/**
* Updates the WebServerExchange using
* {@code {@link SecurityMockServerConfigurers#mockAuthentication(Authentication)}}.
*
* @author Jérôme Wacongne &lt;ch4mp&#64;c4-soft.com&gt;
* @author Josh Cummings
* @since 5.2
*/
public static class JwtMutator extends JwtAuthenticationTokenTestingBuilder<JwtMutator>
implements
WebTestClientConfigurer, MockServerConfigurer {
public static class JwtMutator implements WebTestClientConfigurer, MockServerConfigurer {
private Jwt jwt;
private Collection<GrantedAuthority> authorities;
private JwtMutator(Jwt jwt) {
this.jwt = jwt;
this.authorities = new JwtGrantedAuthoritiesConverter().convert(jwt);
}
/**
* Use the provided authorities in the token
* @param authorities the authorities to use
* @return the {@link JwtMutator} for further configuration
*/
public JwtMutator authorities(Collection<GrantedAuthority> authorities) {
Assert.notNull(authorities, "authorities cannot be null");
this.authorities = authorities;
return this;
}
/**
* Use the provided authorities in the token
* @param authorities the authorities to use
* @return the {@link JwtMutator} for further configuration
*/
public JwtMutator authorities(GrantedAuthority... authorities) {
Assert.notNull(authorities, "authorities cannot be null");
this.authorities = Arrays.asList(authorities);
return this;
}
/**
* Provides the configured {@link Jwt} so that custom authorities can be derived
* from it
*
* @param authoritiesConverter the conversion strategy from {@link Jwt} to a {@link Collection}
* of {@link GrantedAuthority}s
* @return the {@link JwtMutator} for further configuration
*/
public JwtMutator authorities(Converter<Jwt, Collection<GrantedAuthority>> authoritiesConverter) {
Assert.notNull(authoritiesConverter, "authoritiesConverter cannot be null");
this.authorities = authoritiesConverter.convert(this.jwt);
return this;
}
@Override
public void beforeServerCreated(WebHttpHandlerBuilder builder) {
mockAuthentication(build()).beforeServerCreated(builder);
configurer().beforeServerCreated(builder);
}
@Override
public void afterConfigureAdded(WebTestClient.MockServerSpec<?> serverSpec) {
mockAuthentication(build()).afterConfigureAdded(serverSpec);
configurer().afterConfigureAdded(serverSpec);
}
@Override
@@ -339,7 +406,11 @@ public class SecurityMockServerConfigurers {
WebTestClient.Builder builder,
@Nullable WebHttpHandlerBuilder httpHandlerBuilder,
@Nullable ClientHttpConnector connector) {
mockAuthentication(build()).afterConfigurerAdded(builder, httpHandlerBuilder, connector);
configurer().afterConfigurerAdded(builder, httpHandlerBuilder, connector);
}
private <T extends WebTestClientConfigurer & MockServerConfigurer> T configurer() {
return mockAuthentication(new JwtAuthenticationToken(this.jwt, this.authorities));
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -27,10 +27,10 @@ import java.util.Base64;
import java.util.Collection;
import java.util.List;
import java.util.function.Consumer;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
@@ -48,8 +48,8 @@ import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.test.context.TestSecurityContextHolder;
import org.springframework.security.test.support.JwtAuthenticationTokenTestingBuilder;
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers;
import org.springframework.security.test.web.support.WebTestUtils;
import org.springframework.security.web.context.HttpRequestResponseHolder;
@@ -63,6 +63,8 @@ import org.springframework.test.web.servlet.request.RequestPostProcessor;
import org.springframework.util.Assert;
import org.springframework.util.DigestUtils;
import static org.springframework.security.oauth2.jwt.JwtClaimNames.SUB;
/**
* Contains {@link MockMvc} {@link RequestPostProcessor} implementations for Spring
* Security.
@@ -223,11 +225,41 @@ public final class SecurityMockMvcRequestPostProcessors {
* @return the {@link JwtRequestPostProcessor} for additional customization
*/
public static JwtRequestPostProcessor jwt() {
return new JwtRequestPostProcessor();
return jwt(jwt -> {});
}
public static JwtRequestPostProcessor jwt(Consumer<Jwt.Builder<?>> jwt) {
return jwt().token(jwt);
/**
* Establish a {@link SecurityContext} that has a
* {@link JwtAuthenticationToken} for the
* {@link Authentication} and a {@link Jwt} for the
* {@link Authentication#getPrincipal()}. All details are
* declarative and do not require the JWT to be valid.
*
* <p>
* The support works by associating the authentication to the HttpServletRequest. To associate
* the request to the SecurityContextHolder you need to ensure that the
* SecurityContextPersistenceFilter is associated with the MockMvc instance. A few
* ways to do this are:
* </p>
*
* <ul>
* <li>Invoking apply {@link SecurityMockMvcConfigurers#springSecurity()}</li>
* <li>Adding Spring Security's FilterChainProxy to MockMvc</li>
* <li>Manually adding {@link SecurityContextPersistenceFilter} to the MockMvc
* instance may make sense when using MockMvcBuilders standaloneSetup</li>
* </ul>
*
* @param jwtBuilderConsumer For configuring the underlying {@link Jwt}
* @return the {@link JwtRequestPostProcessor} for additional customization
* @since 5.2
*/
public static JwtRequestPostProcessor jwt(Consumer<Jwt.Builder> jwtBuilderConsumer) {
Jwt.Builder jwtBuilder = Jwt.withTokenValue("token")
.header("alg", "none")
.claim(SUB, "user")
.claim("scope", "read");
jwtBuilderConsumer.accept(jwtBuilder);
return new JwtRequestPostProcessor(jwtBuilder.build());
}
/**
@@ -590,7 +622,7 @@ public final class SecurityMockMvcRequestPostProcessors {
* Support class for {@link RequestPostProcessor}'s that establish a Spring Security
* context
*/
static class SecurityContextRequestPostProcessorSupport {
private static abstract class SecurityContextRequestPostProcessorSupport {
/**
* Saves the specified {@link Authentication} into an empty
@@ -599,7 +631,7 @@ public final class SecurityMockMvcRequestPostProcessors {
* @param authentication the {@link Authentication} to save
* @param request the {@link HttpServletRequest} to use
*/
static final void save(Authentication authentication, HttpServletRequest request) {
final void save(Authentication authentication, HttpServletRequest request) {
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
securityContext.setAuthentication(authentication);
save(securityContext, request);
@@ -611,7 +643,7 @@ public final class SecurityMockMvcRequestPostProcessors {
* @param securityContext the {@link SecurityContext} to save
* @param request the {@link HttpServletRequest} to use
*/
static final void save(SecurityContext securityContext, HttpServletRequest request) {
final void save(SecurityContext securityContext, HttpServletRequest request) {
SecurityContextRepository securityContextRepository = WebTestUtils
.getSecurityContextRepository(request);
boolean isTestRepository = securityContextRepository instanceof TestSecurityContextRepository;
@@ -639,7 +671,7 @@ public final class SecurityMockMvcRequestPostProcessors {
* stateless mode
*/
static class TestSecurityContextRepository implements SecurityContextRepository {
final static String ATTR_NAME = TestSecurityContextRepository.class
private final static String ATTR_NAME = TestSecurityContextRepository.class
.getName().concat(".REPO");
private final SecurityContextRepository delegate;
@@ -751,6 +783,8 @@ public final class SecurityMockMvcRequestPostProcessors {
@Override
public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(this.authentication);
save(this.authentication, request);
return request;
}
@@ -938,22 +972,64 @@ public final class SecurityMockMvcRequestPostProcessors {
}
}
private SecurityMockMvcRequestPostProcessors() {
}
/**
* @author Jérôme Wacongne &lt;ch4mp&#64;c4-soft.com&gt;
* @author Josh Cummings
* @since 5.2
*/
public static class JwtRequestPostProcessor extends JwtAuthenticationTokenTestingBuilder<JwtRequestPostProcessor>
implements
RequestPostProcessor {
public final static class JwtRequestPostProcessor implements RequestPostProcessor {
private Jwt jwt;
private Collection<? extends GrantedAuthority> authorities;
private JwtRequestPostProcessor(Jwt jwt) {
this.jwt = jwt;
this.authorities = new JwtGrantedAuthoritiesConverter().convert(jwt);
}
/**
* Use the provided authorities in the token
* @param authorities the authorities to use
* @return the {@link JwtRequestPostProcessor} for further configuration
*/
public JwtRequestPostProcessor authorities(Collection<GrantedAuthority> authorities) {
Assert.notNull(authorities, "authorities cannot be null");
this.authorities = authorities;
return this;
}
/**
* Use the provided authorities in the token
* @param authorities the authorities to use
* @return the {@link JwtRequestPostProcessor} for further configuration
*/
public JwtRequestPostProcessor authorities(GrantedAuthority... authorities) {
Assert.notNull(authorities, "authorities cannot be null");
this.authorities = Arrays.asList(authorities);
return this;
}
/**
* Provides the configured {@link Jwt} so that custom authorities can be derived
* from it
*
* @param authoritiesConverter the conversion strategy from {@link Jwt} to a {@link Collection}
* of {@link GrantedAuthority}s
* @return the {@link JwtRequestPostProcessor} for further configuration
*/
public JwtRequestPostProcessor authorities(Converter<Jwt, Collection<GrantedAuthority>> authoritiesConverter) {
Assert.notNull(authoritiesConverter, "authoritiesConverter cannot be null");
this.authorities = authoritiesConverter.convert(this.jwt);
return this;
}
@Override
public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
SecurityContextRequestPostProcessorSupport.save(build(), request);
return request;
JwtAuthenticationToken token = new JwtAuthenticationToken(this.jwt, this.authorities);
return new AuthenticationRequestPostProcessor(token).postProcessRequest(request);
}
}
private SecurityMockMvcRequestPostProcessors() {
}
}