SecurityMockMvcRequestPostProcessors.java
/*
* Copyright 2004-present 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.web.servlet.request;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import jakarta.servlet.ServletContext;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.jspecify.annotations.NullUnmarked;
import org.jspecify.annotations.Nullable;
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;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.method.annotation.OAuth2AuthorizedClientArgumentResolver;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames;
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimNames;
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionAuthenticatedPrincipal;
import org.springframework.security.test.context.TestSecurityContextHolder;
import org.springframework.security.test.context.TestSecurityContextHolderStrategyAdapter;
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers;
import org.springframework.security.test.web.support.WebTestUtils;
import org.springframework.security.web.context.HttpRequestResponseHolder;
import org.springframework.security.web.context.SecurityContextPersistenceFilter;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.security.web.csrf.CsrfFilter;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.CsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfTokenRequestHandler;
import org.springframework.security.web.csrf.DeferredCsrfToken;
import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.RequestPostProcessor;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
/**
* Contains {@link MockMvc} {@link RequestPostProcessor} implementations for Spring
* Security.
*
* @author Rob Winch
* @since 4.0
*/
public final class SecurityMockMvcRequestPostProcessors {
private static final SecurityContextHolderStrategy DEFAULT_SECURITY_CONTEXT_HOLDER_STRATEGY = new TestSecurityContextHolderStrategyAdapter();
private SecurityMockMvcRequestPostProcessors() {
}
/**
* Creates a DigestRequestPostProcessor that enables easily adding digest based
* authentication to a request.
* @return the DigestRequestPostProcessor to use
*/
public static DigestRequestPostProcessor digest() {
return new DigestRequestPostProcessor();
}
/**
* Creates a DigestRequestPostProcessor that enables easily adding digest based
* authentication to a request.
* @param username the username to use
* @return the DigestRequestPostProcessor to use
*/
public static DigestRequestPostProcessor digest(String username) {
return digest().username(username);
}
/**
* Populates the provided X509Certificate instances on the request.
* @param certificates the X509Certificate instances to pouplate
* @return the
* {@link org.springframework.test.web.servlet.request.RequestPostProcessor} to use.
*/
public static RequestPostProcessor x509(X509Certificate... certificates) {
return new X509RequestPostProcessor(certificates);
}
/**
* Finds an X509Cetificate using a resoureName and populates it on the request.
* @param resourceName the name of the X509Certificate resource
* @return the
* {@link org.springframework.test.web.servlet.request.RequestPostProcessor} to use.
* @throws IOException
* @throws CertificateException
*/
public static RequestPostProcessor x509(String resourceName) throws IOException, CertificateException {
ResourceLoader loader = new DefaultResourceLoader();
Resource resource = loader.getResource(resourceName);
InputStream inputStream = resource.getInputStream();
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
X509Certificate certificate = (X509Certificate) certFactory.generateCertificate(inputStream);
return x509(certificate);
}
/**
* Creates a {@link RequestPostProcessor} that will automatically populate a valid
* {@link CsrfToken} in the request.
* @return the {@link CsrfRequestPostProcessor} for further customizations.
*/
public static CsrfRequestPostProcessor csrf() {
return new CsrfRequestPostProcessor();
}
/**
* Creates a {@link RequestPostProcessor} that can be used to ensure that the
* resulting request is ran with the user in the {@link TestSecurityContextHolder}.
* @return the {@link RequestPostProcessor} to use
*/
public static RequestPostProcessor testSecurityContext() {
return new TestSecurityContextHolderPostProcessor();
}
/**
* Establish a {@link SecurityContext} that has a
* {@link UsernamePasswordAuthenticationToken} for the
* {@link Authentication#getPrincipal()} and a {@link User} for the
* {@link UsernamePasswordAuthenticationToken#getPrincipal()}. All details are
* declarative and do not require that the user actually exists.
*
* <p>
* The support works by associating the user 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 username the username to populate
* @return the {@link UserRequestPostProcessor} for additional customization
*/
public static UserRequestPostProcessor user(String username) {
return new UserRequestPostProcessor(username);
}
/**
* Establish a {@link SecurityContext} that has a
* {@link UsernamePasswordAuthenticationToken} for the
* {@link Authentication#getPrincipal()} and a custom {@link UserDetails} for the
* {@link UsernamePasswordAuthenticationToken#getPrincipal()}. All details are
* declarative and do not require that the user actually exists.
*
* <p>
* The support works by associating the user 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 user the UserDetails to populate
* @return the {@link RequestPostProcessor} to use
*/
public static RequestPostProcessor user(UserDetails user) {
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();
}
/**
* Establish a {@link SecurityContext} that has a {@link BearerTokenAuthentication}
* for the {@link Authentication} and a {@link OAuth2AuthenticatedPrincipal} for the
* {@link Authentication#getPrincipal()}. All details are declarative and do not
* require the token 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 OpaqueTokenRequestPostProcessor} for additional customization
* @since 5.3
*/
public static OpaqueTokenRequestPostProcessor opaqueToken() {
return new OpaqueTokenRequestPostProcessor();
}
/**
* Establish a {@link SecurityContext} that uses the specified {@link Authentication}
* for the {@link Authentication#getPrincipal()} and a custom {@link UserDetails}. All
* details are declarative and do not require that the user actually exists.
*
* <p>
* The support works by associating the user 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 authentication the Authentication to populate
* @return the {@link RequestPostProcessor} to use
*/
public static RequestPostProcessor authentication(Authentication authentication) {
return new AuthenticationRequestPostProcessor(authentication);
}
/**
* Establish a {@link SecurityContext} that uses an
* {@link AnonymousAuthenticationToken}. This is useful when a user wants to run a
* majority of tests as a specific user and wishes to override a few methods to be
* anonymous. For example:
*
* <pre>
* <code>
* public class SecurityTests {
* @Before
* public void setup() {
* mockMvc = MockMvcBuilders
* .webAppContextSetup(context)
* .defaultRequest(get("/").with(user("user")))
* .build();
* }
*
* @Test
* public void anonymous() {
* mockMvc.perform(get("anonymous").with(anonymous()));
* }
* // ... lots of tests ran with a default user ...
* }
* </code> </pre>
* @return the {@link RequestPostProcessor} to use
*/
public static RequestPostProcessor anonymous() {
return new AnonymousRequestPostProcessor();
}
/**
* Establish the specified {@link SecurityContext} to be used.
*
* <p>
* This works by associating the user to the {@link HttpServletRequest}. To associate
* the request to the {@link SecurityContextHolder} you need to ensure that the
* {@link SecurityContextPersistenceFilter} (i.e. Spring Security's FilterChainProxy
* will typically do this) is associated with the {@link MockMvc} instance.
* </p>
*/
public static RequestPostProcessor securityContext(SecurityContext securityContext) {
return new SecurityContextRequestPostProcessor(securityContext);
}
/**
* Convenience mechanism for setting the Authorization header to use HTTP Basic with
* the given username and password. This method will automatically perform the
* necessary Base64 encoding.
* @param username the username to include in the Authorization header.
* @param password the password to include in the Authorization header.
* @return the {@link RequestPostProcessor} to use
*/
public static RequestPostProcessor httpBasic(String username, String password) {
return new HttpBasicRequestPostProcessor(username, password);
}
/**
* Establish a {@link SecurityContext} that has a {@link OAuth2AuthenticationToken}
* for the {@link Authentication}, a {@link OAuth2User} as the principal, and a
* {@link OAuth2AuthorizedClient} in the session. All details are declarative and do
* not require associated tokens 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 OidcLoginRequestPostProcessor} for additional customization
* @since 5.3
*/
@NullUnmarked
public static OAuth2LoginRequestPostProcessor oauth2Login() {
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "access-token", null,
null, Collections.singleton("read"));
return new OAuth2LoginRequestPostProcessor(accessToken);
}
/**
* Establish a {@link SecurityContext} that has a {@link OAuth2AuthenticationToken}
* for the {@link Authentication}, a {@link OidcUser} as the principal, and a
* {@link OAuth2AuthorizedClient} in the session. All details are declarative and do
* not require associated tokens 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 OidcLoginRequestPostProcessor} for additional customization
* @since 5.3
*/
@NullUnmarked
public static OidcLoginRequestPostProcessor oidcLogin() {
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "access-token", null,
null, Collections.singleton("read"));
return new OidcLoginRequestPostProcessor(accessToken);
}
/**
* Establish an {@link OAuth2AuthorizedClient} in the session. All details are
* declarative and do not require associated tokens to be valid.
*
* <p>
* The support works by associating the authorized client to the HttpServletRequest
* using an {@link OAuth2AuthorizedClientRepository}
* </p>
* @return the {@link OAuth2ClientRequestPostProcessor} for additional customization
* @since 5.3
*/
public static OAuth2ClientRequestPostProcessor oauth2Client() {
return new OAuth2ClientRequestPostProcessor();
}
/**
* Establish an {@link OAuth2AuthorizedClient} in the session. All details are
* declarative and do not require associated tokens to be valid.
*
* <p>
* The support works by associating the authorized client to the HttpServletRequest
* using an {@link OAuth2AuthorizedClientRepository}
* </p>
* @param registrationId The registration id for the {@link OAuth2AuthorizedClient}
* @return the {@link OAuth2ClientRequestPostProcessor} for additional customization
* @since 5.3
*/
public static OAuth2ClientRequestPostProcessor oauth2Client(String registrationId) {
return new OAuth2ClientRequestPostProcessor(registrationId);
}
private static SecurityContextHolderStrategy getSecurityContextHolderStrategy(HttpServletRequest request) {
WebApplicationContext context = WebApplicationContextUtils
.findWebApplicationContext(request.getServletContext());
if (context == null) {
return DEFAULT_SECURITY_CONTEXT_HOLDER_STRATEGY;
}
if (context.getBeanNamesForType(SecurityContextHolderStrategy.class).length == 0) {
return DEFAULT_SECURITY_CONTEXT_HOLDER_STRATEGY;
}
return context.getBean(SecurityContextHolderStrategy.class);
}
/**
* Populates the X509Certificate instances onto the request
*/
private static final class X509RequestPostProcessor implements RequestPostProcessor {
private final X509Certificate[] certificates;
private X509RequestPostProcessor(X509Certificate... certificates) {
Assert.notNull(certificates, "X509Certificate cannot be null");
this.certificates = certificates;
}
@Override
public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
request.setAttribute("jakarta.servlet.request.X509Certificate", this.certificates);
return request;
}
}
/**
* Populates a valid {@link CsrfToken} into the request.
*
* @author Rob Winch
* @since 4.0
*/
public static final class CsrfRequestPostProcessor implements RequestPostProcessor {
private static final byte[] INVALID_TOKEN_BYTES = new byte[] { 1, 1, 1, 96, 99, 98 };
private static final String INVALID_TOKEN_VALUE = Base64.getEncoder().encodeToString(INVALID_TOKEN_BYTES);
private boolean asHeader;
private boolean useInvalidToken;
private CsrfRequestPostProcessor() {
}
@NullUnmarked
@Override
public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
CsrfTokenRepository repository = WebTestUtils.getCsrfTokenRepository(request);
CsrfTokenRequestHandler handler = WebTestUtils.getCsrfTokenRequestHandler(request);
if (!(repository instanceof TestCsrfTokenRepository)) {
repository = new TestCsrfTokenRepository(new HttpSessionCsrfTokenRepository());
WebTestUtils.setCsrfTokenRepository(request, repository);
}
TestCsrfTokenRepository.enable(request);
MockHttpServletResponse response = new MockHttpServletResponse();
DeferredCsrfToken deferredCsrfToken = repository.loadDeferredToken(request, response);
handler.handle(request, response, deferredCsrfToken);
CsrfToken token = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
String tokenValue = this.useInvalidToken ? INVALID_TOKEN_VALUE : token.getToken();
if (this.asHeader) {
request.addHeader(token.getHeaderName(), tokenValue);
}
else {
request.setParameter(token.getParameterName(), tokenValue);
}
return request;
}
/**
* Instead of using the {@link CsrfToken} as a request parameter (default) will
* populate the {@link CsrfToken} as a header.
* @return the {@link CsrfRequestPostProcessor} for additional customizations
*/
public CsrfRequestPostProcessor asHeader() {
this.asHeader = true;
return this;
}
/**
* Populates an invalid token value on the request.
* @return the {@link CsrfRequestPostProcessor} for additional customizations
*/
public CsrfRequestPostProcessor useInvalidToken() {
this.useInvalidToken = true;
return this;
}
/**
* Used to wrap the CsrfTokenRepository to provide support for testing when the
* request is wrapped (i.e. Spring Session is in use).
*/
static class TestCsrfTokenRepository implements CsrfTokenRepository {
static final String TOKEN_ATTR_NAME = TestCsrfTokenRepository.class.getName().concat(".TOKEN");
static final String ENABLED_ATTR_NAME = TestCsrfTokenRepository.class.getName().concat(".ENABLED");
private final CsrfTokenRepository delegate;
TestCsrfTokenRepository(CsrfTokenRepository delegate) {
this.delegate = delegate;
}
@Override
public CsrfToken generateToken(HttpServletRequest request) {
return this.delegate.generateToken(request);
}
@Override
public void saveToken(@Nullable CsrfToken token, HttpServletRequest request, HttpServletResponse response) {
if (isEnabled(request)) {
request.setAttribute(TOKEN_ATTR_NAME, token);
}
else {
this.delegate.saveToken(token, request, response);
}
}
@Override
public @Nullable CsrfToken loadToken(HttpServletRequest request) {
if (isEnabled(request)) {
return (CsrfToken) request.getAttribute(TOKEN_ATTR_NAME);
}
else {
return this.delegate.loadToken(request);
}
}
static void enable(HttpServletRequest request) {
request.setAttribute(ENABLED_ATTR_NAME, Boolean.TRUE);
}
boolean isEnabled(HttpServletRequest request) {
return Boolean.TRUE.equals(request.getAttribute(ENABLED_ATTR_NAME));
}
}
}
public static class DigestRequestPostProcessor implements RequestPostProcessor {
private String username = "user";
private String password = "password";
private String realm = "Spring Security";
private String nonce = generateNonce(60);
private String qop = "auth";
private String nc = "00000001";
private String cnonce = "c822c727a648aba7";
/**
* Configures the username to use
* @param username the username to use
* @return the DigestRequestPostProcessor for further customization
*/
private DigestRequestPostProcessor username(String username) {
Assert.notNull(username, "username cannot be null");
this.username = username;
return this;
}
/**
* Configures the password to use
* @param password the password to use
* @return the DigestRequestPostProcessor for further customization
*/
public DigestRequestPostProcessor password(String password) {
Assert.notNull(password, "password cannot be null");
this.password = password;
return this;
}
/**
* Configures the realm to use
* @param realm the realm to use
* @return the DigestRequestPostProcessor for further customization
*/
public DigestRequestPostProcessor realm(String realm) {
Assert.notNull(realm, "realm cannot be null");
this.realm = realm;
return this;
}
private static String generateNonce(int validitySeconds) {
long expiryTime = System.currentTimeMillis() + (validitySeconds * 1000);
String toDigest = expiryTime + ":" + "key";
String signatureValue = md5Hex(toDigest);
String nonceValue = expiryTime + ":" + signatureValue;
return new String(Base64.getEncoder().encode(nonceValue.getBytes()));
}
private String createAuthorizationHeader(MockHttpServletRequest request) {
String uri = request.getRequestURI();
String responseDigest = generateDigest(this.username, this.realm, this.password, request.getMethod(), uri,
this.qop, this.nonce, this.nc, this.cnonce);
return "Digest username=\"" + this.username + "\", realm=\"" + this.realm + "\", nonce=\"" + this.nonce
+ "\", uri=\"" + uri + "\", response=\"" + responseDigest + "\", qop=" + this.qop + ", nc="
+ this.nc + ", cnonce=\"" + this.cnonce + "\"";
}
@Override
public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
request.addHeader("Authorization", createAuthorizationHeader(request));
return request;
}
/**
* Computes the <code>response</code> portion of a Digest authentication header.
* Both the server and user agent should compute the <code>response</code>
* independently. Provided as a static method to simplify the coding of user
* agents.
* @param username the user's login name.
* @param realm the name of the realm.
* @param password the user's password in plaintext or ready-encoded.
* @param httpMethod the HTTP request method (GET, POST etc.)
* @param uri the request URI.
* @param qop the qop directive, or null if not set.
* @param nonce the nonce supplied by the server
* @param nc the "nonce-count" as defined in RFC 2617.
* @param cnonce opaque string supplied by the client when qop is set.
* @return the MD5 of the digest authentication response, encoded in hex
* @throws IllegalArgumentException if the supplied qop value is unsupported.
*/
private static String generateDigest(String username, String realm, String password,
@Nullable String httpMethod, @Nullable String uri, String qop, String nonce, String nc, String cnonce)
throws IllegalArgumentException {
String a1Md5 = encodePasswordInA1Format(username, realm, password);
String a2 = httpMethod + ":" + uri;
String a2Md5 = md5Hex(a2);
if (qop == null) {
// as per RFC 2069 compliant clients (also reaffirmed by RFC 2617)
return md5Hex(a1Md5 + ":" + nonce + ":" + a2Md5);
}
if ("auth".equals(qop)) {
// As per RFC 2617 compliant clients
return md5Hex(a1Md5 + ":" + nonce + ":" + nc + ":" + cnonce + ":" + qop + ":" + a2Md5);
}
throw new IllegalArgumentException("This method does not support a qop: '" + qop + "'");
}
static String encodePasswordInA1Format(String username, String realm, String password) {
return md5Hex(username + ":" + realm + ":" + password);
}
private static String md5Hex(String a2) {
return DigestUtils.md5DigestAsHex(a2.getBytes(StandardCharsets.UTF_8));
}
}
/**
* Support class for {@link RequestPostProcessor}'s that establish a Spring Security
* context
*/
private abstract static class SecurityContextRequestPostProcessorSupport {
/**
* Saves the specified {@link Authentication} into an empty
* {@link SecurityContext} using the {@link SecurityContextRepository}.
* @param authentication the {@link Authentication} to save
* @param request the {@link HttpServletRequest} to use
*/
final void save(Authentication authentication, HttpServletRequest request) {
SecurityContext securityContext = getSecurityContextHolderStrategy(request).createEmptyContext();
securityContext.setAuthentication(authentication);
save(securityContext, request);
}
/**
* Saves the {@link SecurityContext} using the {@link SecurityContextRepository}
* @param securityContext the {@link SecurityContext} to save
* @param request the {@link HttpServletRequest} to use
*/
final void save(SecurityContext securityContext, HttpServletRequest request) {
SecurityContextRepository securityContextRepository = WebTestUtils.getSecurityContextRepository(request);
boolean isTestRepository = securityContextRepository instanceof TestSecurityContextRepository;
if (!isTestRepository) {
securityContextRepository = new TestSecurityContextRepository(securityContextRepository);
WebTestUtils.setSecurityContextRepository(request, securityContextRepository);
}
HttpServletResponse response = new MockHttpServletResponse();
HttpRequestResponseHolder requestResponseHolder = new HttpRequestResponseHolder(request, response);
securityContextRepository.loadContext(requestResponseHolder);
request = requestResponseHolder.getRequest();
response = requestResponseHolder.getResponse();
securityContextRepository.saveContext(securityContext, request, response);
}
/**
* Used to wrap the SecurityContextRepository to provide support for testing in
* stateless mode
*/
static final class TestSecurityContextRepository implements SecurityContextRepository {
private static final String ATTR_NAME = TestSecurityContextRepository.class.getName().concat(".REPO");
private final SecurityContextRepository delegate;
private TestSecurityContextRepository(SecurityContextRepository delegate) {
this.delegate = delegate;
}
@Override
public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
SecurityContext result = getContext(requestResponseHolder.getRequest());
// always load from the delegate to ensure the request/response in the
// holder are updated
// remember the SecurityContextRepository is used in many different
// locations
SecurityContext delegateResult = this.delegate.loadContext(requestResponseHolder);
return (result != null) ? result : delegateResult;
}
@Override
public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {
request.setAttribute(ATTR_NAME, context);
this.delegate.saveContext(context, request, response);
}
@Override
public boolean containsContext(HttpServletRequest request) {
return getContext(request) != null || this.delegate.containsContext(request);
}
private static SecurityContext getContext(HttpServletRequest request) {
return (SecurityContext) request.getAttribute(ATTR_NAME);
}
}
}
/**
* Associates the {@link SecurityContext} found in
* {@link TestSecurityContextHolder#getContext()} with the
* {@link MockHttpServletRequest}.
*
* @author Rob Winch
* @since 4.0
*/
private static final class TestSecurityContextHolderPostProcessor extends SecurityContextRequestPostProcessorSupport
implements RequestPostProcessor {
@Override
public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
// TestSecurityContextHolder is only a default value
SecurityContext existingContext = TestSecurityContextRepository.getContext(request);
if (existingContext != null) {
return request;
}
SecurityContextHolderStrategy strategy = getSecurityContextHolderStrategy(request);
SecurityContext empty = strategy.createEmptyContext();
SecurityContext context = strategy.getContext();
if (!empty.equals(context)) {
save(context, request);
}
return request;
}
}
/**
* Associates the specified {@link SecurityContext} with the
* {@link MockHttpServletRequest}.
*
* @author Rob Winch
* @since 4.0
*/
private static final class SecurityContextRequestPostProcessor extends SecurityContextRequestPostProcessorSupport
implements RequestPostProcessor {
private final SecurityContext securityContext;
private SecurityContextRequestPostProcessor(SecurityContext securityContext) {
this.securityContext = securityContext;
}
@Override
public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
save(this.securityContext, request);
return request;
}
}
/**
* Sets the specified {@link Authentication} on an empty {@link SecurityContext} and
* associates it to the {@link MockHttpServletRequest}
*
* @author Rob Winch
* @since 4.0
*
*/
private static final class AuthenticationRequestPostProcessor extends SecurityContextRequestPostProcessorSupport
implements RequestPostProcessor {
private final Authentication authentication;
private AuthenticationRequestPostProcessor(Authentication authentication) {
this.authentication = authentication;
}
@Override
public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
SecurityContext context = getSecurityContextHolderStrategy(request).createEmptyContext();
context.setAuthentication(this.authentication);
save(this.authentication, request);
return request;
}
}
/**
* Creates a {@link UsernamePasswordAuthenticationToken} and sets the
* {@link UserDetails} as the principal and associates it to the
* {@link MockHttpServletRequest}.
*
* @author Rob Winch
* @since 4.0
*/
private static final class UserDetailsRequestPostProcessor implements RequestPostProcessor {
private final AuthenticationRequestPostProcessor delegate;
UserDetailsRequestPostProcessor(UserDetails user) {
Authentication token = UsernamePasswordAuthenticationToken.authenticated(user, user.getPassword(),
user.getAuthorities());
this.delegate = new AuthenticationRequestPostProcessor(token);
}
@Override
public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
return this.delegate.postProcessRequest(request);
}
}
/**
* Creates a {@link UsernamePasswordAuthenticationToken} and sets the principal to be
* a {@link User} and associates it to the {@link MockHttpServletRequest}.
*
* @author Rob Winch
* @since 4.0
*/
public static final class UserRequestPostProcessor extends SecurityContextRequestPostProcessorSupport
implements RequestPostProcessor {
private String username;
private String password = "password";
private static final String ROLE_PREFIX = "ROLE_";
private Collection<? extends GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("ROLE_USER");
private boolean enabled = true;
private boolean accountNonExpired = true;
private boolean credentialsNonExpired = true;
private boolean accountNonLocked = true;
/**
* Creates a new instance with the given username
* @param username the username to use
*/
private UserRequestPostProcessor(String username) {
Assert.notNull(username, "username cannot be null");
this.username = username;
}
/**
* Specify the roles of the user to authenticate as. This method is similar to
* {@link #authorities(GrantedAuthority...)}, but just not as flexible.
* @param roles The roles to populate. Note that if the role does not start with
* {@link #ROLE_PREFIX} it will automatically be prepended. This means by default
* {@code roles("ROLE_USER")} and {@code roles("USER")} are equivalent.
* @return the UserRequestPostProcessor for further customizations
* @see #authorities(GrantedAuthority...)
* @see #ROLE_PREFIX
*/
public UserRequestPostProcessor roles(String... roles) {
List<GrantedAuthority> authorities = new ArrayList<>(roles.length);
for (String role : roles) {
Assert.isTrue(!role.startsWith(ROLE_PREFIX), () -> "Role should not start with " + ROLE_PREFIX
+ " since this method automatically prefixes with this value. Got " + role);
authorities.add(new SimpleGrantedAuthority(ROLE_PREFIX + role));
}
this.authorities = authorities;
return this;
}
/**
* Populates the user's {@link GrantedAuthority}'s. The default is ROLE_USER.
* @param authorities
* @return the UserRequestPostProcessor for further customizations
* @see #roles(String...)
*/
public UserRequestPostProcessor authorities(GrantedAuthority... authorities) {
return authorities(Arrays.asList(authorities));
}
/**
* Populates the user's {@link GrantedAuthority}'s. The default is ROLE_USER.
* @param authorities
* @return the UserRequestPostProcessor for further customizations
* @see #roles(String...)
*/
public UserRequestPostProcessor authorities(Collection<? extends GrantedAuthority> authorities) {
this.authorities = authorities;
return this;
}
/**
* Populates the user's password. The default is "password"
* @param password the user's password
* @return the UserRequestPostProcessor for further customizations
*/
public UserRequestPostProcessor password(String password) {
this.password = password;
return this;
}
@Override
public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
UserDetailsRequestPostProcessor delegate = new UserDetailsRequestPostProcessor(createUser());
return delegate.postProcessRequest(request);
}
/**
* Creates a new {@link User}
* @return the {@link User} for the principal
*/
private User createUser() {
return new User(this.username, this.password, this.enabled, this.accountNonExpired,
this.credentialsNonExpired, this.accountNonLocked, this.authorities);
}
}
private static class AnonymousRequestPostProcessor extends SecurityContextRequestPostProcessorSupport
implements RequestPostProcessor {
private AuthenticationRequestPostProcessor delegate = new AuthenticationRequestPostProcessor(
new AnonymousAuthenticationToken("key", "anonymous",
AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")));
@Override
public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
return this.delegate.postProcessRequest(request);
}
}
private static final class HttpBasicRequestPostProcessor implements RequestPostProcessor {
private String headerValue;
private HttpBasicRequestPostProcessor(String username, String password) {
byte[] toEncode = (username + ":" + password).getBytes(StandardCharsets.UTF_8);
this.headerValue = "Basic " + new String(Base64.getEncoder().encode(toEncode));
}
@Override
public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
request.addHeader("Authorization", this.headerValue);
return request;
}
}
/**
* @author J��r��me Wacongne <ch4mp@c4-soft.com>
* @author Josh Cummings
* @since 5.2
*/
public static final class JwtRequestPostProcessor implements RequestPostProcessor {
private Jwt jwt;
private Converter<Jwt, Collection<GrantedAuthority>> authoritiesConverter = new JwtGrantedAuthoritiesConverter();
private JwtRequestPostProcessor() {
this.jwt((jwt) -> {
});
}
/**
* Use the given {@link Jwt.Builder} {@link Consumer} to configure the underlying
* {@link Jwt}
*
* This method first creates a default {@link Jwt.Builder} instance with default
* values for the {@code alg}, {@code sub}, and {@code scope} claims. The
* {@link Consumer} can then modify these or provide additional configuration.
*
* Calling {@link SecurityMockMvcRequestPostProcessors#jwt()} is the equivalent of
* calling {@code SecurityMockMvcRequestPostProcessors.jwt().jwt(() -> {})}.
* @param jwtBuilderConsumer For configuring the underlying {@link Jwt}
* @return the {@link JwtRequestPostProcessor} for additional customization
*/
public JwtRequestPostProcessor jwt(Consumer<Jwt.Builder> jwtBuilderConsumer) {
Jwt.Builder jwtBuilder = Jwt.withTokenValue("token")
.header("alg", "none")
.claim(JwtClaimNames.SUB, "user")
.claim("scope", "read");
jwtBuilderConsumer.accept(jwtBuilder);
this.jwt = jwtBuilder.build();
return this;
}
/**
* Use the given {@link Jwt}
* @param jwt The {@link Jwt} to use
* @return the {@link JwtRequestPostProcessor} for additional customization
*/
public JwtRequestPostProcessor jwt(Jwt jwt) {
this.jwt = jwt;
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(Collection<GrantedAuthority> authorities) {
Assert.notNull(authorities, "authorities cannot be null");
this.authoritiesConverter = (jwt) -> 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.authoritiesConverter = (jwt) -> 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.authoritiesConverter = authoritiesConverter;
return this;
}
@NullUnmarked
@Override
public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
CsrfFilter.skipRequest(request);
JwtAuthenticationToken token = new JwtAuthenticationToken(this.jwt,
this.authoritiesConverter.convert(this.jwt));
return new AuthenticationRequestPostProcessor(token).postProcessRequest(request);
}
}
/**
* @author Josh Cummings
* @since 5.3
*/
public static final class OpaqueTokenRequestPostProcessor implements RequestPostProcessor {
private Supplier<Map<String, Object>> attributes = this::defaultAttributes;
private Supplier<Collection<GrantedAuthority>> authorities = this::defaultAuthorities;
private Supplier<OAuth2AuthenticatedPrincipal> principal = this::defaultPrincipal;
private OpaqueTokenRequestPostProcessor() {
}
/**
* Mutate the attributes using the given {@link Consumer}
* @param attributesConsumer The {@link Consumer} for mutating the {@code Map} of
* attributes
* @return the {@link OpaqueTokenRequestPostProcessor} for further configuration
*/
public OpaqueTokenRequestPostProcessor attributes(Consumer<Map<String, Object>> attributesConsumer) {
Assert.notNull(attributesConsumer, "attributesConsumer cannot be null");
this.attributes = () -> {
Map<String, Object> attributes = defaultAttributes();
attributesConsumer.accept(attributes);
return attributes;
};
this.principal = this::defaultPrincipal;
return this;
}
/**
* Use the provided authorities in the resulting principal
* @param authorities the authorities to use
* @return the {@link OpaqueTokenRequestPostProcessor} for further configuration
*/
public OpaqueTokenRequestPostProcessor authorities(Collection<GrantedAuthority> authorities) {
Assert.notNull(authorities, "authorities cannot be null");
this.authorities = () -> authorities;
this.principal = this::defaultPrincipal;
return this;
}
/**
* Use the provided authorities in the resulting principal
* @param authorities the authorities to use
* @return the {@link OpaqueTokenRequestPostProcessor} for further configuration
*/
public OpaqueTokenRequestPostProcessor authorities(GrantedAuthority... authorities) {
Assert.notNull(authorities, "authorities cannot be null");
this.authorities = () -> Arrays.asList(authorities);
this.principal = this::defaultPrincipal;
return this;
}
/**
* Use the provided principal
* @param principal the principal to use
* @return the {@link OpaqueTokenRequestPostProcessor} for further configuration
*/
public OpaqueTokenRequestPostProcessor principal(OAuth2AuthenticatedPrincipal principal) {
Assert.notNull(principal, "principal cannot be null");
this.principal = () -> principal;
return this;
}
@Override
public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
CsrfFilter.skipRequest(request);
OAuth2AuthenticatedPrincipal principal = this.principal.get();
OAuth2AccessToken accessToken = getOAuth2AccessToken(principal);
BearerTokenAuthentication token = new BearerTokenAuthentication(principal, accessToken,
principal.getAuthorities());
return new AuthenticationRequestPostProcessor(token).postProcessRequest(request);
}
private Map<String, Object> defaultAttributes() {
Map<String, Object> attributes = new HashMap<>();
attributes.put(OAuth2TokenIntrospectionClaimNames.SUB, "user");
attributes.put(OAuth2TokenIntrospectionClaimNames.SCOPE, "read");
return attributes;
}
private Collection<GrantedAuthority> defaultAuthorities() {
Map<String, Object> attributes = this.attributes.get();
Object scope = attributes.get(OAuth2TokenIntrospectionClaimNames.SCOPE);
if (scope == null) {
return Collections.emptyList();
}
if (scope instanceof Collection) {
return getAuthorities((Collection) scope);
}
String scopes = scope.toString();
if (!StringUtils.hasText(scopes)) {
return Collections.emptyList();
}
return getAuthorities(Arrays.asList(scopes.split(" ")));
}
private OAuth2AuthenticatedPrincipal defaultPrincipal() {
return new OAuth2IntrospectionAuthenticatedPrincipal(this.attributes.get(), this.authorities.get());
}
private Collection<GrantedAuthority> getAuthorities(Collection<?> scopes) {
return scopes.stream()
.map((scope) -> new SimpleGrantedAuthority("SCOPE_" + scope))
.collect(Collectors.toList());
}
private OAuth2AccessToken getOAuth2AccessToken(OAuth2AuthenticatedPrincipal principal) {
Instant expiresAt = getInstant(principal.getAttributes(), "exp");
Instant issuedAt = getInstant(principal.getAttributes(), "iat");
return new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "token", issuedAt, expiresAt);
}
@NullUnmarked
private Instant getInstant(Map<String, Object> attributes, String name) {
Object value = attributes.get(name);
if (value == null) {
return null;
}
if (value instanceof Instant) {
return (Instant) value;
}
throw new IllegalArgumentException(name + " attribute must be of type Instant");
}
}
/**
* @author Josh Cummings
* @since 5.3
*/
public static final class OAuth2LoginRequestPostProcessor implements RequestPostProcessor {
private final String nameAttributeKey = "sub";
private ClientRegistration clientRegistration;
private OAuth2AccessToken accessToken;
private Supplier<Collection<GrantedAuthority>> authorities = this::defaultAuthorities;
private Supplier<Map<String, Object>> attributes = this::defaultAttributes;
private Supplier<OAuth2User> oauth2User = this::defaultPrincipal;
private OAuth2LoginRequestPostProcessor(OAuth2AccessToken accessToken) {
this.accessToken = accessToken;
this.clientRegistration = clientRegistrationBuilder().build();
}
/**
* Use the provided authorities in the {@link Authentication}
* @param authorities the authorities to use
* @return the {@link OAuth2LoginRequestPostProcessor} for further configuration
*/
public OAuth2LoginRequestPostProcessor authorities(Collection<GrantedAuthority> authorities) {
Assert.notNull(authorities, "authorities cannot be null");
this.authorities = () -> authorities;
this.oauth2User = this::defaultPrincipal;
return this;
}
/**
* Use the provided authorities in the {@link Authentication}
* @param authorities the authorities to use
* @return the {@link OAuth2LoginRequestPostProcessor} for further configuration
*/
public OAuth2LoginRequestPostProcessor authorities(GrantedAuthority... authorities) {
Assert.notNull(authorities, "authorities cannot be null");
this.authorities = () -> Arrays.asList(authorities);
this.oauth2User = this::defaultPrincipal;
return this;
}
/**
* Mutate the attributes using the given {@link Consumer}
* @param attributesConsumer The {@link Consumer} for mutating the {@code Map} of
* attributes
* @return the {@link OAuth2LoginRequestPostProcessor} for further configuration
*/
public OAuth2LoginRequestPostProcessor attributes(Consumer<Map<String, Object>> attributesConsumer) {
Assert.notNull(attributesConsumer, "attributesConsumer cannot be null");
this.attributes = () -> {
Map<String, Object> attributes = defaultAttributes();
attributesConsumer.accept(attributes);
return attributes;
};
this.oauth2User = this::defaultPrincipal;
return this;
}
/**
* Use the provided {@link OAuth2User} as the authenticated user.
* @param oauth2User the {@link OAuth2User} to use
* @return the {@link OAuth2LoginRequestPostProcessor} for further configuration
*/
public OAuth2LoginRequestPostProcessor oauth2User(OAuth2User oauth2User) {
this.oauth2User = () -> oauth2User;
return this;
}
/**
* Use the provided {@link ClientRegistration} as the client to authorize.
*
* The supplied {@link ClientRegistration} will be registered into an
* {@link OAuth2AuthorizedClientRepository}.
* @param clientRegistration the {@link ClientRegistration} to use
* @return the {@link OAuth2LoginRequestPostProcessor} for further configuration
*/
public OAuth2LoginRequestPostProcessor clientRegistration(ClientRegistration clientRegistration) {
this.clientRegistration = clientRegistration;
return this;
}
@Override
public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
OAuth2User oauth2User = this.oauth2User.get();
OAuth2AuthenticationToken token = new OAuth2AuthenticationToken(oauth2User, oauth2User.getAuthorities(),
this.clientRegistration.getRegistrationId());
request = new AuthenticationRequestPostProcessor(token).postProcessRequest(request);
return new OAuth2ClientRequestPostProcessor().clientRegistration(this.clientRegistration)
.principalName(oauth2User.getName())
.accessToken(this.accessToken)
.postProcessRequest(request);
}
private ClientRegistration.Builder clientRegistrationBuilder() {
return ClientRegistration.withRegistrationId("test")
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUri("https://client.example.com")
.clientId("test-client")
.authorizationUri("https://authorize-uri.example.org")
.tokenUri("https://token-uri.example.org");
}
private Collection<GrantedAuthority> defaultAuthorities() {
Set<GrantedAuthority> authorities = new LinkedHashSet<>();
authorities.add(new OAuth2UserAuthority(this.attributes.get(), this.nameAttributeKey));
for (String authority : this.accessToken.getScopes()) {
authorities.add(new SimpleGrantedAuthority("SCOPE_" + authority));
}
return authorities;
}
private Map<String, Object> defaultAttributes() {
Map<String, Object> attributes = new HashMap<>();
attributes.put(this.nameAttributeKey, "user");
return attributes;
}
private OAuth2User defaultPrincipal() {
return new DefaultOAuth2User(this.authorities.get(), this.attributes.get(), this.nameAttributeKey);
}
}
/**
* @author Josh Cummings
* @since 5.3
*/
public static final class OidcLoginRequestPostProcessor implements RequestPostProcessor {
private ClientRegistration clientRegistration;
private OAuth2AccessToken accessToken;
private @Nullable OidcIdToken idToken;
@SuppressWarnings("NullAway.Init")
private OidcUserInfo userInfo;
private Supplier<OidcUser> oidcUser = this::defaultPrincipal;
private @Nullable Collection<GrantedAuthority> authorities;
private OidcLoginRequestPostProcessor(OAuth2AccessToken accessToken) {
this.accessToken = accessToken;
this.clientRegistration = clientRegistrationBuilder().build();
}
/**
* Use the provided authorities in the {@link Authentication}
* @param authorities the authorities to use
* @return the {@link OidcLoginRequestPostProcessor} for further configuration
*/
public OidcLoginRequestPostProcessor authorities(Collection<GrantedAuthority> authorities) {
Assert.notNull(authorities, "authorities cannot be null");
this.authorities = authorities;
this.oidcUser = this::defaultPrincipal;
return this;
}
/**
* Use the provided authorities in the {@link Authentication}
* @param authorities the authorities to use
* @return the {@link OidcLoginRequestPostProcessor} for further configuration
*/
public OidcLoginRequestPostProcessor authorities(GrantedAuthority... authorities) {
Assert.notNull(authorities, "authorities cannot be null");
this.authorities = Arrays.asList(authorities);
this.oidcUser = this::defaultPrincipal;
return this;
}
/**
* Use the provided {@link OidcIdToken} when constructing the authenticated user
* @param idTokenBuilderConsumer a {@link Consumer} of a
* {@link OidcIdToken.Builder}
* @return the {@link OidcLoginRequestPostProcessor} for further configuration
*/
public OidcLoginRequestPostProcessor idToken(Consumer<OidcIdToken.Builder> idTokenBuilderConsumer) {
OidcIdToken.Builder builder = OidcIdToken.withTokenValue("id-token");
builder.subject("user");
idTokenBuilderConsumer.accept(builder);
this.idToken = builder.build();
this.oidcUser = this::defaultPrincipal;
return this;
}
/**
* Use the provided {@link OidcUserInfo} when constructing the authenticated user
* @param userInfoBuilderConsumer a {@link Consumer} of a
* {@link OidcUserInfo.Builder}
* @return the {@link OidcLoginRequestPostProcessor} for further configuration
*/
public OidcLoginRequestPostProcessor userInfoToken(Consumer<OidcUserInfo.Builder> userInfoBuilderConsumer) {
OidcUserInfo.Builder builder = OidcUserInfo.builder();
userInfoBuilderConsumer.accept(builder);
this.userInfo = builder.build();
this.oidcUser = this::defaultPrincipal;
return this;
}
/**
* Use the provided {@link OidcUser} as the authenticated user.
* @param oidcUser the {@link OidcUser} to use
* @return the {@link OidcLoginRequestPostProcessor} for further configuration
*/
public OidcLoginRequestPostProcessor oidcUser(OidcUser oidcUser) {
this.oidcUser = () -> oidcUser;
return this;
}
/**
* Use the provided {@link ClientRegistration} as the client to authorize.
*
* The supplied {@link ClientRegistration} will be registered into an
* {@link HttpSessionOAuth2AuthorizedClientRepository}.
* @param clientRegistration the {@link ClientRegistration} to use
* @return the {@link OidcLoginRequestPostProcessor} for further configuration
*/
public OidcLoginRequestPostProcessor clientRegistration(ClientRegistration clientRegistration) {
this.clientRegistration = clientRegistration;
return this;
}
@Override
public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
OidcUser oidcUser = this.oidcUser.get();
return new OAuth2LoginRequestPostProcessor(this.accessToken).oauth2User(oidcUser)
.clientRegistration(this.clientRegistration)
.postProcessRequest(request);
}
private ClientRegistration.Builder clientRegistrationBuilder() {
return ClientRegistration.withRegistrationId("test")
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUri("https://client.example.com")
.clientId("test-client")
.authorizationUri("https://authorize-uri.example.org")
.tokenUri("https://token-uri.example.org");
}
private Collection<GrantedAuthority> getAuthorities() {
if (this.authorities != null) {
return this.authorities;
}
Set<GrantedAuthority> authorities = new LinkedHashSet<>();
authorities.add(new OidcUserAuthority(getOidcIdToken(), getOidcUserInfo()));
for (String authority : this.accessToken.getScopes()) {
authorities.add(new SimpleGrantedAuthority("SCOPE_" + authority));
}
return authorities;
}
@NullUnmarked
private OidcIdToken getOidcIdToken() {
if (this.idToken != null) {
return this.idToken;
}
return new OidcIdToken("id-token", null, null, Collections.singletonMap(IdTokenClaimNames.SUB, "user"));
}
private OidcUserInfo getOidcUserInfo() {
return this.userInfo;
}
private OidcUser defaultPrincipal() {
return new DefaultOidcUser(getAuthorities(), getOidcIdToken(), this.userInfo);
}
}
/**
* @author Josh Cummings
* @since 5.3
*/
@NullUnmarked
public static final class OAuth2ClientRequestPostProcessor implements RequestPostProcessor {
private String registrationId = "test";
private @Nullable ClientRegistration clientRegistration;
private String principalName = "user";
private OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
"access-token", null, null, Collections.singleton("read"));
private OAuth2ClientRequestPostProcessor() {
}
private OAuth2ClientRequestPostProcessor(String registrationId) {
this.registrationId = registrationId;
clientRegistration((c) -> {
});
}
/**
* Use this {@link ClientRegistration}
* @param clientRegistration
* @return the {@link OAuth2ClientRequestPostProcessor} for further configuration
*/
public OAuth2ClientRequestPostProcessor clientRegistration(ClientRegistration clientRegistration) {
this.clientRegistration = clientRegistration;
return this;
}
/**
* Use this {@link Consumer} to configure a {@link ClientRegistration}
* @param clientRegistrationConfigurer the {@link ClientRegistration} configurer
* @return the {@link OAuth2ClientRequestPostProcessor} for further configuration
*/
public OAuth2ClientRequestPostProcessor clientRegistration(
Consumer<ClientRegistration.Builder> clientRegistrationConfigurer) {
ClientRegistration.Builder builder = clientRegistrationBuilder();
clientRegistrationConfigurer.accept(builder);
this.clientRegistration = builder.build();
return this;
}
/**
* Use this as the resource owner's principal name
* @param principalName the resource owner's principal name
* @return the {@link OAuth2ClientRequestPostProcessor} for further configuration
*/
public OAuth2ClientRequestPostProcessor principalName(String principalName) {
Assert.notNull(principalName, "principalName cannot be null");
this.principalName = principalName;
return this;
}
/**
* Use this {@link OAuth2AccessToken}
* @param accessToken the {@link OAuth2AccessToken} to use
* @return the {@link OAuth2ClientRequestPostProcessor} for further configuration
*/
public OAuth2ClientRequestPostProcessor accessToken(OAuth2AccessToken accessToken) {
this.accessToken = accessToken;
return this;
}
@NullUnmarked
@Override
public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
if (this.clientRegistration == null) {
throw new IllegalArgumentException(
"Please specify a ClientRegistration via one " + "of the clientRegistration methods");
}
OAuth2AuthorizedClient client = new OAuth2AuthorizedClient(this.clientRegistration, this.principalName,
this.accessToken);
OAuth2AuthorizedClientRepository authorizedClientRepository = OAuth2ClientServletTestUtils
.getAuthorizedClientRepository(request);
if (!(authorizedClientRepository instanceof TestOAuth2AuthorizedClientRepository)) {
authorizedClientRepository = new TestOAuth2AuthorizedClientRepository(authorizedClientRepository);
OAuth2ClientServletTestUtils.setAuthorizedClientRepository(request, authorizedClientRepository);
}
TestOAuth2AuthorizedClientRepository.enable(request);
authorizedClientRepository.saveAuthorizedClient(client, null, request, new MockHttpServletResponse());
return request;
}
private ClientRegistration.Builder clientRegistrationBuilder() {
return ClientRegistration.withRegistrationId(this.registrationId)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUri("https://client.example.com")
.clientId("test-client")
.clientSecret("test-secret")
.authorizationUri("https://idp.example.org/oauth/authorize")
.tokenUri("https://idp.example.org/oauth/token");
}
/**
* Used to wrap the {@link OAuth2AuthorizedClientRepository} to provide support
* for testing when the request is wrapped
*/
private static final class TestOAuth2AuthorizedClientManager implements OAuth2AuthorizedClientManager {
static final String ENABLED_ATTR_NAME = TestOAuth2AuthorizedClientManager.class.getName()
.concat(".ENABLED");
private final OAuth2AuthorizedClientManager delegate;
private @Nullable OAuth2AuthorizedClientRepository authorizedClientRepository;
TestOAuth2AuthorizedClientManager(OAuth2AuthorizedClientManager delegate) {
this.delegate = delegate;
}
@NullUnmarked
@Override
public @Nullable OAuth2AuthorizedClient authorize(OAuth2AuthorizeRequest authorizeRequest) {
HttpServletRequest request = authorizeRequest.getAttribute(HttpServletRequest.class.getName());
if (isEnabled(request)) {
return this.authorizedClientRepository.loadAuthorizedClient(
authorizeRequest.getClientRegistrationId(), authorizeRequest.getPrincipal(), request);
}
return this.delegate.authorize(authorizeRequest);
}
static void enable(HttpServletRequest request) {
request.setAttribute(ENABLED_ATTR_NAME, Boolean.TRUE);
}
@NullUnmarked
boolean isEnabled(@Nullable HttpServletRequest request) {
return Boolean.TRUE.equals(request.getAttribute(ENABLED_ATTR_NAME));
}
}
/**
* Used to wrap the {@link OAuth2AuthorizedClientRepository} to provide support
* for testing when the request is wrapped
*/
static final class TestOAuth2AuthorizedClientRepository implements OAuth2AuthorizedClientRepository {
static final String TOKEN_ATTR_NAME = TestOAuth2AuthorizedClientRepository.class.getName().concat(".TOKEN");
static final String ENABLED_ATTR_NAME = TestOAuth2AuthorizedClientRepository.class.getName()
.concat(".ENABLED");
private final OAuth2AuthorizedClientRepository delegate;
@NullUnmarked
TestOAuth2AuthorizedClientRepository(@Nullable OAuth2AuthorizedClientRepository delegate) {
this.delegate = delegate;
}
@Override
public <T extends OAuth2AuthorizedClient> T loadAuthorizedClient(String clientRegistrationId,
Authentication principal, HttpServletRequest request) {
if (isEnabled(request)) {
return (T) request.getAttribute(TOKEN_ATTR_NAME);
}
return this.delegate.loadAuthorizedClient(clientRegistrationId, principal, request);
}
@Override
public void saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal,
HttpServletRequest request, HttpServletResponse response) {
if (isEnabled(request)) {
request.setAttribute(TOKEN_ATTR_NAME, authorizedClient);
return;
}
this.delegate.saveAuthorizedClient(authorizedClient, principal, request, response);
}
@Override
public void removeAuthorizedClient(String clientRegistrationId, Authentication principal,
HttpServletRequest request, HttpServletResponse response) {
if (isEnabled(request)) {
request.removeAttribute(TOKEN_ATTR_NAME);
return;
}
this.delegate.removeAuthorizedClient(clientRegistrationId, principal, request, response);
}
static void enable(HttpServletRequest request) {
request.setAttribute(ENABLED_ATTR_NAME, Boolean.TRUE);
}
boolean isEnabled(HttpServletRequest request) {
return Boolean.TRUE.equals(request.getAttribute(ENABLED_ATTR_NAME));
}
}
private static final class OAuth2ClientServletTestUtils {
private static final OAuth2AuthorizedClientRepository DEFAULT_CLIENT_REPO = new HttpSessionOAuth2AuthorizedClientRepository();
private OAuth2ClientServletTestUtils() {
}
/**
* Gets the {@link OAuth2AuthorizedClientRepository} for the specified
* {@link HttpServletRequest}. If one is not found, one based off of
* {@link HttpSessionOAuth2AuthorizedClientRepository} is used.
* @param request the {@link HttpServletRequest} to obtain the
* {@link OAuth2AuthorizedClientManager}
* @return the {@link OAuth2AuthorizedClientManager} for the specified
* {@link HttpServletRequest}
*/
static @Nullable OAuth2AuthorizedClientRepository getAuthorizedClientRepository(
HttpServletRequest request) {
OAuth2AuthorizedClientManager manager = getOAuth2AuthorizedClientManager(request);
if (manager == null) {
return DEFAULT_CLIENT_REPO;
}
if (manager instanceof DefaultOAuth2AuthorizedClientManager) {
return (OAuth2AuthorizedClientRepository) ReflectionTestUtils.getField(manager,
"authorizedClientRepository");
}
if (manager instanceof TestOAuth2AuthorizedClientManager) {
return ((TestOAuth2AuthorizedClientManager) manager).authorizedClientRepository;
}
return DEFAULT_CLIENT_REPO;
}
static void setAuthorizedClientRepository(HttpServletRequest request,
OAuth2AuthorizedClientRepository repository) {
OAuth2AuthorizedClientManager manager = getOAuth2AuthorizedClientManager(request);
if (manager == null) {
return;
}
if (manager instanceof DefaultOAuth2AuthorizedClientManager) {
ReflectionTestUtils.setField(manager, "authorizedClientRepository", repository);
return;
}
if (!(manager instanceof TestOAuth2AuthorizedClientManager)) {
manager = new TestOAuth2AuthorizedClientManager(manager);
setOAuth2AuthorizedClientManager(request, manager);
}
TestOAuth2AuthorizedClientManager.enable(request);
((TestOAuth2AuthorizedClientManager) manager).authorizedClientRepository = repository;
}
static @Nullable OAuth2AuthorizedClientManager getOAuth2AuthorizedClientManager(
HttpServletRequest request) {
OAuth2AuthorizedClientArgumentResolver resolver = findResolver(request,
OAuth2AuthorizedClientArgumentResolver.class);
if (resolver == null) {
return null;
}
return (OAuth2AuthorizedClientManager) ReflectionTestUtils.getField(resolver,
"authorizedClientManager");
}
/**
* Sets the {@link OAuth2AuthorizedClientManager} for the specified
* {@link HttpServletRequest}.
* @param request the {@link HttpServletRequest} to obtain the
* {@link OAuth2AuthorizedClientManager}
* @param manager the {@link OAuth2AuthorizedClientManager} to set
*/
static void setOAuth2AuthorizedClientManager(HttpServletRequest request,
OAuth2AuthorizedClientManager manager) {
OAuth2AuthorizedClientArgumentResolver resolver = findResolver(request,
OAuth2AuthorizedClientArgumentResolver.class);
if (resolver == null) {
return;
}
ReflectionTestUtils.setField(resolver, "authorizedClientManager", manager);
}
@SuppressWarnings("unchecked")
static <T extends HandlerMethodArgumentResolver> @Nullable T findResolver(HttpServletRequest request,
Class<T> resolverClass) {
if (!ClassUtils.isPresent(
"org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter", null)) {
return null;
}
return WebMvcClasspathGuard.findResolver(request, resolverClass);
}
private static class WebMvcClasspathGuard {
static <T extends HandlerMethodArgumentResolver> @Nullable T findResolver(HttpServletRequest request,
Class<T> resolverClass) {
ServletContext servletContext = request.getServletContext();
RequestMappingHandlerAdapter mapping = getRequestMappingHandlerAdapter(servletContext);
if (mapping == null) {
return null;
}
List<HandlerMethodArgumentResolver> resolvers = mapping.getCustomArgumentResolvers();
if (resolvers == null) {
return null;
}
for (HandlerMethodArgumentResolver resolver : resolvers) {
if (resolverClass.isAssignableFrom(resolver.getClass())) {
return (T) resolver;
}
}
return null;
}
private static @Nullable RequestMappingHandlerAdapter getRequestMappingHandlerAdapter(
ServletContext servletContext) {
WebApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(servletContext);
if (context != null) {
String[] names = context.getBeanNamesForType(RequestMappingHandlerAdapter.class);
if (names.length > 0) {
return (RequestMappingHandlerAdapter) context.getBean(names[0]);
}
}
return null;
}
}
}
}
}