Mock Jwt Test Support and Jwt.Builder
Fixes: gh-6634 Fixes: gh-6851
This commit is contained in:
committed by
Josh Cummings
parent
f6998547b8
commit
e59d8a529b
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* 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 <ch4mp@c4-soft.com>
|
||||
* @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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,11 @@
|
||||
|
||||
package org.springframework.security.test.web.reactive.server;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.springframework.http.client.reactive.ClientHttpConnector;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
@@ -26,6 +31,9 @@ import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextImpl;
|
||||
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.web.server.csrf.CsrfWebFilter;
|
||||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
|
||||
import org.springframework.test.web.reactive.server.MockServerConfigurer;
|
||||
@@ -35,12 +43,8 @@ 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 java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* Test utilities for working with Spring Security and
|
||||
@@ -109,6 +113,23 @@ public class SecurityMockServerConfigurers {
|
||||
return new UserExchangeMutator(username);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @return the {@link JwtMutator} to further configure or use
|
||||
*/
|
||||
public static JwtMutator mockJwt() {
|
||||
return new JwtMutator();
|
||||
}
|
||||
|
||||
public static JwtMutator mockJwt(Consumer<Jwt.Builder<?>> jwt) {
|
||||
return new JwtMutator().token(jwt);
|
||||
}
|
||||
|
||||
public static CsrfMutator csrf() {
|
||||
return new CsrfMutator();
|
||||
}
|
||||
@@ -294,4 +315,31 @@ public class SecurityMockServerConfigurers {
|
||||
return webFilterChain.filter(exchange);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Jérôme Wacongne <ch4mp@c4-soft.com>
|
||||
* @since 5.2
|
||||
*/
|
||||
public static class JwtMutator extends JwtAuthenticationTokenTestingBuilder<JwtMutator>
|
||||
implements
|
||||
WebTestClientConfigurer, MockServerConfigurer {
|
||||
|
||||
@Override
|
||||
public void beforeServerCreated(WebHttpHandlerBuilder builder) {
|
||||
mockAuthentication(build()).beforeServerCreated(builder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterConfigureAdded(WebTestClient.MockServerSpec<?> serverSpec) {
|
||||
mockAuthentication(build()).afterConfigureAdded(serverSpec);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterConfigurerAdded(
|
||||
WebTestClient.Builder builder,
|
||||
@Nullable WebHttpHandlerBuilder httpHandlerBuilder,
|
||||
@Nullable ClientHttpConnector connector) {
|
||||
mockAuthentication(build()).afterConfigurerAdded(builder, httpHandlerBuilder, connector);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import java.util.Arrays;
|
||||
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;
|
||||
@@ -45,7 +46,10 @@ import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
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.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;
|
||||
@@ -195,6 +199,37 @@ public final class SecurityMockMvcRequestPostProcessors {
|
||||
return new UserDetailsRequestPostProcessor(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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>
|
||||
*
|
||||
* @return the {@link JwtRequestPostProcessor} for additional customization
|
||||
*/
|
||||
public static JwtRequestPostProcessor jwt() {
|
||||
return new JwtRequestPostProcessor();
|
||||
}
|
||||
|
||||
public static JwtRequestPostProcessor jwt(Consumer<Jwt.Builder<?>> jwt) {
|
||||
return jwt().token(jwt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Establish a {@link SecurityContext} that uses the specified {@link Authentication}
|
||||
* for the {@link Authentication#getPrincipal()} and a custom {@link UserDetails}. All
|
||||
@@ -555,7 +590,7 @@ public final class SecurityMockMvcRequestPostProcessors {
|
||||
* Support class for {@link RequestPostProcessor}'s that establish a Spring Security
|
||||
* context
|
||||
*/
|
||||
private static abstract class SecurityContextRequestPostProcessorSupport {
|
||||
static class SecurityContextRequestPostProcessorSupport {
|
||||
|
||||
/**
|
||||
* Saves the specified {@link Authentication} into an empty
|
||||
@@ -564,7 +599,7 @@ public final class SecurityMockMvcRequestPostProcessors {
|
||||
* @param authentication the {@link Authentication} to save
|
||||
* @param request the {@link HttpServletRequest} to use
|
||||
*/
|
||||
final void save(Authentication authentication, HttpServletRequest request) {
|
||||
static final void save(Authentication authentication, HttpServletRequest request) {
|
||||
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
|
||||
securityContext.setAuthentication(authentication);
|
||||
save(securityContext, request);
|
||||
@@ -576,7 +611,7 @@ public final class SecurityMockMvcRequestPostProcessors {
|
||||
* @param securityContext the {@link SecurityContext} to save
|
||||
* @param request the {@link HttpServletRequest} to use
|
||||
*/
|
||||
final void save(SecurityContext securityContext, HttpServletRequest request) {
|
||||
static final void save(SecurityContext securityContext, HttpServletRequest request) {
|
||||
SecurityContextRepository securityContextRepository = WebTestUtils
|
||||
.getSecurityContextRepository(request);
|
||||
boolean isTestRepository = securityContextRepository instanceof TestSecurityContextRepository;
|
||||
@@ -604,7 +639,7 @@ public final class SecurityMockMvcRequestPostProcessors {
|
||||
* stateless mode
|
||||
*/
|
||||
static class TestSecurityContextRepository implements SecurityContextRepository {
|
||||
private final static String ATTR_NAME = TestSecurityContextRepository.class
|
||||
final static String ATTR_NAME = TestSecurityContextRepository.class
|
||||
.getName().concat(".REPO");
|
||||
|
||||
private final SecurityContextRepository delegate;
|
||||
@@ -716,8 +751,6 @@ public final class SecurityMockMvcRequestPostProcessors {
|
||||
|
||||
@Override
|
||||
public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
|
||||
SecurityContext context = SecurityContextHolder.createEmptyContext();
|
||||
context.setAuthentication(this.authentication);
|
||||
save(this.authentication, request);
|
||||
return request;
|
||||
}
|
||||
@@ -907,4 +940,20 @@ public final class SecurityMockMvcRequestPostProcessors {
|
||||
|
||||
private SecurityMockMvcRequestPostProcessors() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Jérôme Wacongne <ch4mp@c4-soft.com>
|
||||
* @since 5.2
|
||||
*/
|
||||
public static class JwtRequestPostProcessor extends JwtAuthenticationTokenTestingBuilder<JwtRequestPostProcessor>
|
||||
implements
|
||||
RequestPostProcessor {
|
||||
|
||||
@Override
|
||||
public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
|
||||
SecurityContextRequestPostProcessorSupport.save(build(), request);
|
||||
return request;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user