Customize CsrfTokenRepository and CsrfAuthenticationStrategy to support cross domain CSRF protection.

This commit is contained in:
Tim Donohue
2020-12-22 14:12:03 -06:00
parent 30451676ab
commit d084358e70
9 changed files with 297 additions and 86 deletions

View File

@@ -152,12 +152,10 @@ public class Application extends SpringBootServletInitializer {
// for our Access-Control-Allow-Origin header
.allowCredentials(corsAllowCredentials).allowedOrigins(corsAllowedOrigins)
// Allow list of request preflight headers allowed to be sent to us from the client
.allowedHeaders("Authorization", "Content-Type", "X-Requested-With", "accept", "Origin",
"Access-Control-Request-Method", "Access-Control-Request-Headers",
"X-On-Behalf-Of", "X-XSRF-TOKEN")
// Allow list of response headers allowed to be sent by us (the server)
.exposedHeaders("Access-Control-Allow-Origin", "Access-Control-Allow-Credentials",
"Authorization");
.allowedHeaders("Accept", "Authorization", "Content-Type", "Origin", "X-On-Behalf-Of",
"X-Requested-With", "X-XSRF-TOKEN")
// Allow list of response headers allowed to be sent by us (the server) to the client
.exposedHeaders("Authorization", "DSPACE-XSRF-TOKEN", "Location", "WWW-Authenticate");
}
}

View File

@@ -0,0 +1,56 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.exception;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.csrf.InvalidCsrfTokenException;
import org.springframework.security.web.csrf.MissingCsrfTokenException;
import org.springframework.stereotype.Component;
/**
* This Handler customizes behavior of AccessDeniedException errors thrown by Spring Security/Boot
*/
@Component
public class DSpaceAccessDeniedHandler implements AccessDeniedHandler {
/**
* Override handle() to pass these exceptions over to our DSpaceApiExceptionControllerAdvice handler
* @param request request
* @param response response
* @param ex AccessDeniedException
* @throws IOException
* @throws ServletException
*/
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException ex)
throws IOException, ServletException {
// Do nothing if response is already committed
if (response.isCommitted()) {
return;
}
// Get access to our general exception handler
DSpaceApiExceptionControllerAdvice handler = new DSpaceApiExceptionControllerAdvice();
// If a CSRF Token was passed in but was invalid pass to csrfTokenException()
if (ex instanceof InvalidCsrfTokenException || ex instanceof MissingCsrfTokenException) {
handler.csrfTokenException(request, response, ex);
return;
}
// Otherwise, our handleAuthorizeException method will deal with generic AccessDeniedExceptions
handler.handleAuthorizeException(request, response, ex);
}
}

View File

@@ -26,6 +26,8 @@ import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.csrf.InvalidCsrfTokenException;
import org.springframework.security.web.csrf.MissingCsrfTokenException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
@@ -35,13 +37,15 @@ import org.springframework.web.multipart.MultipartException;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
/**
* This Controller advice will handle all exceptions thrown by the DSpace API module
* This Controller advice will handle default exceptions thrown by the DSpace REST API module.
* <P>
* Keep in mind some specialized handlers exist for specific message types, e.g. DSpaceAccessDeniedHandler
*
* @author Tom Desair (tom dot desair at atmire dot com)
* @author Frederic Van Reet (frederic dot vanreet at atmire dot com)
* @author Andrea Bollini (andrea.bollini at 4science.it)
* @author Pasquale Cavallo (pasquale.cavallo at 4science dot it)
*
* @see DSpaceAccessDeniedHandler
*/
@ControllerAdvice
public class DSpaceApiExceptionControllerAdvice extends ResponseEntityExceptionHandler {
@@ -50,6 +54,8 @@ public class DSpaceApiExceptionControllerAdvice extends ResponseEntityExceptionH
@Autowired
private RestAuthenticationService restAuthenticationService;
// NOTE: this method is also called by DSpaceAccessDeniedHandler to handle AccessDeniedExceptions thrown by
// Spring Security
@ExceptionHandler({AuthorizeException.class, RESTAuthorizationException.class, AccessDeniedException.class})
protected void handleAuthorizeException(HttpServletRequest request, HttpServletResponse response, Exception ex)
throws IOException {
@@ -60,6 +66,14 @@ public class DSpaceApiExceptionControllerAdvice extends ResponseEntityExceptionH
}
}
// NOTE: this method is also called by DSpaceAccessDeniedHandler to handle CSRF exceptions thrown by Spring Security
@ExceptionHandler({InvalidCsrfTokenException.class, MissingCsrfTokenException.class})
protected void csrfTokenException(HttpServletRequest request, HttpServletResponse response, Exception ex)
throws IOException {
sendErrorResponse(request, response, ex, "Access is denied. Invalid CSRF token.",
HttpServletResponse.SC_FORBIDDEN);
}
@ExceptionHandler({IllegalArgumentException.class, MultipartException.class})
protected void handleWrongRequestException(HttpServletRequest request, HttpServletResponse response,
Exception ex) throws IOException {
@@ -146,7 +160,15 @@ public class DSpaceApiExceptionControllerAdvice extends ResponseEntityExceptionH
}
/**
* Send the error to the response. Some errors may also be logged.
* @param request current request
* @param response current response
* @param ex Exception thrown
* @param message message to log or send in response
* @param statusCode status code to send in response
* @throws IOException
*/
private void sendErrorResponse(final HttpServletRequest request, final HttpServletResponse response,
final Exception ex, final String message, final int statusCode) throws IOException {
//Make sure Spring picks up this exception

View File

@@ -0,0 +1,83 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.security;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.session.SessionAuthenticationException;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.CsrfTokenRepository;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Custom SessionAuthenticationStrategy to be used alongside DSpaceCsrfTokenRepository.
* <P>
* Because DSpace is Stateless, this class only resets the CSRF Token if the client has attempted to use it (either
* successfully or unsuccessfully). This ensures that the Token is not changed on every request (since we are stateless
* every request creates a new Authentication object).
* <P>
* Based on Spring Security's CsrfAuthenticationStrategy:
* https://github.com/spring-projects/spring-security/blob/5.2.x/web/src/main/java/org/springframework/security/web/csrf/CsrfAuthenticationStrategy.java
*/
public class DSpaceCsrfAuthenticationStrategy implements SessionAuthenticationStrategy {
private final CsrfTokenRepository csrfTokenRepository;
/**
* Creates a new instance
* @param csrfTokenRepository the {@link CsrfTokenRepository} to use
*/
public DSpaceCsrfAuthenticationStrategy(CsrfTokenRepository csrfTokenRepository) {
Assert.notNull(csrfTokenRepository, "csrfTokenRepository cannot be null");
this.csrfTokenRepository = csrfTokenRepository;
}
/**
* This method is triggered anytime a new Authentication occurs. As DSpace uses Stateless authentication,
* this method is triggered on _every request_ after an initial login occurs. This is because the Spring Security
* Authentication object is recreated on every request.
* <P>
* Therefore, for DSpace, we've customized this method to ensure a new CSRF Token is NOT generated each time a new
* Authentication object is created -- doing so causes the CSRF Token to change with every request. Instead, we
* check to see if the client also passed a CSRF token via a header or parameter. If so, this means the client
* has used (or attempted to use) the token & it must then be regenerated.
*/
@Override
public void onAuthentication(Authentication authentication,
HttpServletRequest request, HttpServletResponse response)
throws SessionAuthenticationException {
// Check if token returned in server-side cookie
CsrfToken token = this.csrfTokenRepository.loadToken(request);
// For DSpace, this will only be null if we are forcing CSRF token regeneration (e.g. on initial login)
boolean containsToken = token != null;
if (containsToken) {
// Check for header or parameter in request
boolean containsHeader = StringUtils.hasLength(request.getHeader(token.getHeaderName()));
boolean containsParameter = StringUtils.hasLength(request.getParameter(token.getParameterName()));
// If token exists & we've also been sent either the header or parameter
// then we need to reset our token (as it's been used)
if (containsHeader || containsParameter) {
this.csrfTokenRepository.saveToken(null, request, response);
CsrfToken newToken = this.csrfTokenRepository.generateToken(request);
this.csrfTokenRepository.saveToken(newToken, request, response);
request.setAttribute(CsrfToken.class.getName(), newToken);
request.setAttribute(newToken.getParameterName(), newToken);
}
}
}
}

View File

@@ -22,24 +22,38 @@ import org.springframework.util.StringUtils;
import org.springframework.web.util.WebUtils;
/**
* This is a Spring Security CookieCsrfTokenRepository which supports cross-site cookies (i.e. SameSite=None).
* PLEASE NOTE: It will NOT support cross-domain CSRF, as Cookies cannot be sent across domains. Therefore, this
* CsrfTokenRepository is similar to Spring Security's in that it requires the REST API and UI to be on the same domain.
* This is a custom Spring Security CsrfTokenRepository which supports *cross-domain* CSRF protection (allowing the
* client and backend to be on different domains). It's inspired by https://stackoverflow.com/a/33175322
* <P>
* This code was mostly borrowed from Spring Security's CookieCsrfTokenRepository
* https://github.com/spring-projects/spring-security/blob/5.2.x/web/src/main/java/org/springframework/security/web/csrf/CookieCsrfTokenRepository.java
* <P>
* Corresponding tests were also copied to CrossSiteCookieCsrfTokenRepositoryTest.
* <P>
* The only modification were to the saveToken() method below. See that method's JavaDocs.
* <P>
* NOTE: This class is TEMPORARY and should be REMOVED as soon as the "SameSite" attribute is supported by
* Spring Security's CookieCsrfTokenRepository. As soon as the below ticket is resolved & we upgrade Spring Security,
* then this custom class can be removed:
* https://github.com/spring-projects/spring-security/issues/7537
* This also borrows heavily from Spring Security's CookieCsrfTokenRepository:
* https://github.com/spring-projects/spring-security/blob/5.2.x/web/src/main/java/org/springframework/security/web/csrf/CookieCsrfTokenRepository.java
*
* How it works:
*
* 1. Backend generates XSRF token & stores in a *server-side* cookie named DSPACE-XSRF-COOKIE. This cookie is
* only readable to clients on the same domain. But, it is returned (by user's browser) on every subsequent request
* to backend. See "saveToken()" method below.
* 2. At the same time, backend also sends the generated XSRF token in a header named DSPACE-XSRF-TOKEN to client.
* See "saveToken()" method below.
* 3. Client MUST look for DSPACE-XSRF-TOKEN header in a response from backend. If found, the client MUST store/save
* this token for later request(s). For Angular UI, this task is performed by the XsrfInterceptor.
* 4. Whenever the client is making a mutating request (e.g. POST, PUT, DELETE, etc), the XSRF token is REQUIRED to be
* sent back in the X-XSRF-TOKEN header.
* * NOTE: non-mutating requests (e.g. GET, HEAD) do not check for an XSRF token. This is default behavior in
* Spring Security
* 5. On backend, the X-XSRF-TOKEN header is received & compared to the current value of the *server-side* cookie
* named DSPACE-XSRF-COOKIE. If tokens match, the request is accepted. If tokens don't match a 403 is returned.
* This is done automatically by Spring Security.
*
* In summary, the XSRF token is ALWAYS sent to/from the client & backend via *headers*. This is what allows the client
* and backend to be on different domains. The server-side cookie named DSPACE-XSRF-COOKIE is (usually) not accessible
* to the client. It only exists to allow the server-side to remember the currently active XSRF token, so that it can
* validate the token sent (by the client) in the X-XSRF-TOKEN header.
*/
public class CrossSiteCookieCsrfTokenRepository implements CsrfTokenRepository {
static final String DEFAULT_CSRF_COOKIE_NAME = "XSRF-TOKEN";
public class DSpaceCsrfTokenRepository implements CsrfTokenRepository {
// This cookie name is changed from the default "XSRF-TOKEN" to ensure it is uniquely named and doesn't conflict
// with any other XSRF-TOKEN cookies (e.g. in Angular UI, the XSRF-TOKEN cookie is a *client-side* only cookie)
static final String DEFAULT_CSRF_COOKIE_NAME = "DSPACE-XSRF-COOKIE";
static final String DEFAULT_CSRF_PARAMETER_NAME = "_csrf";
@@ -57,7 +71,7 @@ public class CrossSiteCookieCsrfTokenRepository implements CsrfTokenRepository {
private String cookieDomain;
public CrossSiteCookieCsrfTokenRepository() {
public DSpaceCsrfTokenRepository() {
}
@Override
@@ -67,12 +81,14 @@ public class CrossSiteCookieCsrfTokenRepository implements CsrfTokenRepository {
}
/**
* This is the only method modified for DSpace. We changed this method to use ResponseCookie to build the
* cookie, so that we could hardcode the "SameSite" attribute to a value of "None". This allows for cross site
* XSRF-TOKEN cookies.
* @param token
* @param request
* @param response
* This method has been modified for DSpace.
* <P>
* It now uses ResponseCookie to build the cookie, so that the "SameSite" attribute can be applied.
* <P>
* It also sends the token (if not empty) in both the cookie and the custom "DSPACE-XSRF-TOKEN" header
* @param token current token
* @param request current request
* @param response current response
*/
@Override
public void saveToken(CsrfToken token, HttpServletRequest request,
@@ -96,22 +112,33 @@ public class CrossSiteCookieCsrfTokenRepository implements CsrfTokenRepository {
}
// Custom: Turn the above Cookie into a ResponseCookie so that we can set "SameSite" attribute
// NOTE: ONLY set "SameSite=None" if cookie is also secure. Most modern browsers will block it otherwise.
// This means that DSpace MUST USE HTTPS if the UI is on a different domain then backend.
String sameSite = "";
if (cookie.getSecure()) {
sameSite = "None";
// If client is on a different domain than the backend, then Cookie MUST use "SameSite=None" and "Secure".
// Most modern browsers will block it otherwise.
// TODO: Make SameSite configurable? "Lax" cookies are more secure, but require client & backend on same domain.
String sameSite = "None";
if (!cookie.getSecure()) {
sameSite = "Lax";
}
ResponseCookie responseCookie = ResponseCookie.from(cookie.getName(), cookie.getValue())
.path(cookie.getPath()).maxAge(cookie.getMaxAge())
.domain(cookie.getDomain()).httpOnly(cookie.isHttpOnly())
.secure(cookie.getSecure()).sameSite(sameSite).build();
// Write the ResponseCookie to the Set-Cookie header
// This cookie is only used by the backend & not needed by client
response.addHeader(HttpHeaders.SET_COOKIE, responseCookie.toString());
// Send custom header to client with token (only if token not empty)
// We send our token via a custom header because client can be on a different domain.
// Cookies cannot be reliably sent cross-domain.
if (StringUtils.hasLength(tokenValue)) {
response.setHeader("DSPACE-XSRF-TOKEN", tokenValue);
}
}
@Override
public CsrfToken loadToken(HttpServletRequest request) {
// First, verify the (server-side) cookie was sent back
Cookie cookie = WebUtils.getCookie(request, this.cookieName);
if (cookie == null) {
return null;
@@ -120,6 +147,17 @@ public class CrossSiteCookieCsrfTokenRepository implements CsrfTokenRepository {
if (!StringUtils.hasLength(token)) {
return null;
}
// Second, verify either the header or param has been sent. This is a customization for DSpace.
// Because the server-side cookie is ALWAYS sent back, we need to verify the client has also sent the token in
// some other way. This ensures that we only *change* the Token when it has been used or attempted to be used.
//if (!StringUtils.hasLength(request.getHeader(this.headerName)) &&
// !StringUtils.hasLength(request.getParameter(this.parameterName))) {
// return null;
// }
// If we got here, we know a token exists in the cookie and *either* the header or the parameter.
// So, this just sends the token info back so that it can be validated by Spring Security.
return new DefaultCsrfToken(this.headerName, this.parameterName, token);
}
@@ -179,8 +217,8 @@ public class CrossSiteCookieCsrfTokenRepository implements CsrfTokenRepository {
* @return an instance of CookieCsrfTokenRepository with
* {@link #setCookieHttpOnly(boolean)} set to false
*/
public static CrossSiteCookieCsrfTokenRepository withHttpOnlyFalse() {
CrossSiteCookieCsrfTokenRepository result = new CrossSiteCookieCsrfTokenRepository();
public static DSpaceCsrfTokenRepository withHttpOnlyFalse() {
DSpaceCsrfTokenRepository result = new DSpaceCsrfTokenRepository();
result.setCookieHttpOnly(false);
return result;
}

View File

@@ -7,6 +7,7 @@
*/
package org.dspace.app.rest.security;
import org.dspace.app.rest.exception.DSpaceAccessDeniedHandler;
import org.dspace.authenticate.service.AuthenticationService;
import org.dspace.services.RequestService;
import org.springframework.beans.factory.annotation.Autowired;
@@ -24,6 +25,7 @@ import org.springframework.security.config.annotation.web.configuration.WebSecur
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.security.web.csrf.CsrfTokenRepository;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@@ -58,6 +60,9 @@ public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private AuthenticationService authenticationService;
@Autowired
private DSpaceAccessDeniedHandler accessDeniedHandler;
@Override
public void configure(WebSecurity webSecurity) throws Exception {
// Define URL patterns which Spring Security will ignore entirely.
@@ -91,12 +96,18 @@ public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
.servletApi().and()
// Enable CORS for Spring Security (see CORS settings in Application and ApplicationConfig)
.cors().and()
// Enable CSRF protection with custom CookieCsrfTokenRepository (see below) designed for Angular apps
// Enable CSRF protection with custom csrfTokenRepository and custom sessionAuthenticationStrategy
// (both are defined below as methods).
// While we primarily use JWT in headers, CSRF protection is needed because we also support JWT via Cookies
.csrf().csrfTokenRepository(this.getCsrfTokenRepository()).and()
// Return 401 on authorization failures with a correct WWWW-Authenticate header
.exceptionHandling().authenticationEntryPoint(
new DSpace401AuthenticationEntryPoint(restAuthenticationService))
.csrf()
.csrfTokenRepository(this.getCsrfTokenRepository())
.sessionAuthenticationStrategy(this.sessionAuthenticationStrategy())
.and()
.exceptionHandling()
// Return 401 on authorization failures with a correct WWWW-Authenticate header
.authenticationEntryPoint(new DSpace401AuthenticationEntryPoint(restAuthenticationService))
// Custom handler for AccessDeniedExceptions, including CSRF exceptions
.accessDeniedHandler(accessDeniedHandler)
.and()
// Logout configuration
@@ -137,27 +148,30 @@ public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
}
/**
* Override the defaults of CookieCsrfTokenRepository to always set the Cookie Path to "/"
* Returns a custom DSpaceCsrfTokenRepository based on Spring Security's CookieCsrfTokenRepository, which is
* designed for Angular Apps.
* <P>
* We use the CookieCsrfTokenRepository designed for Angular apps
* See https://docs.spring.io/spring-security/site/docs/current/reference/html5/#servlet-csrf
* The DSpaceCsrfTokenRepository stores the token in server-side cookie (for later verification), but sends it to
* the client as a DSPACE-XSRF-TOKEN header. The client is expected to return the token in either a header named
* X-XSRF-TOKEN *or* a URL parameter named "_csrf", at which point it is validated against the server-side cookie.
* <P>
* This CookieCsrfTokenRepository will write a cookie named XSRF-TOKEN and read it from
* a header named X-XSRF-TOKEN *or* a URL parameter named "_csrf". Angular apps will respond to
* XSRF-TOKEN automatically, see: https://angular.io/guide/http#security-xsrf-protection
* <P>
* However, currently Angular *requires* the CSR cookie path to always be "/" or it will ignore it.
* See: https://stackoverflow.com/a/50511663
* @return CookieCsrfTokenRepository with cookie path="/"
* This behavior is based on the defaults for Angular apps: https://angular.io/guide/http#security-xsrf-protection.
* However, instead of sending an XSRF-TOKEN Cookie (as is usual for Angular apps), we send the DSPACE-XSRF-TOKEN
* header...as this ensures the Angular app can receive the token even if it is on a different domain.
*
* @return CsrfTokenRepository as described above
*/
private CsrfTokenRepository getCsrfTokenRepository() {
// We are using a *custom* CrossSiteCookieCsrfTokenRepository in which sets
// "SameSite=None" to allow this XSRF-TOKEN cookie to be used in cross site requests.
// This custom class should be REMOVED when this Spring Security ticket is resolved:
// https://github.com/spring-projects/spring-security/issues/7537
CrossSiteCookieCsrfTokenRepository tokenRepository = CrossSiteCookieCsrfTokenRepository.withHttpOnlyFalse();
tokenRepository.setCookiePath("/");
return tokenRepository;
// NOTE: Created cookie is set to HttpOnly=false to allow Hal Browser (or other local JS clients) to access it.
return DSpaceCsrfTokenRepository.withHttpOnlyFalse();
}
/**
* Returns a custom DSpaceCsrfAuthenticationStrategy, which ensures that (after authenticating) the CSRF token
* is only refreshed when it is used (or attempted to be used) by the client.
*/
private SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new DSpaceCsrfAuthenticationStrategy(getCsrfTokenRepository());
}
}

View File

@@ -21,10 +21,10 @@ HAL.Http.Client = function(opts) {
};
/**
* Get CSRF Token by parsing it out of the XSRF-TOKEN cookie sent by our DSpace server webapp
* Get CSRF Token by parsing it out of the DSPACE-XSRF-COOKIE (server-side) cookie set by our DSpace server webapp
**/
function getCSRFToken() {
var cookie = document.cookie.match('(^|;)\\s*' + 'XSRF-TOKEN' + '\\s*=\\s*([^;]+)');
var cookie = document.cookie.match('(^|;)\\s*' + 'DSPACE-XSRF-COOKIE' + '\\s*=\\s*([^;]+)');
if(cookie != undefined) {
return cookie.pop();
} else {

View File

@@ -139,10 +139,10 @@
}
/**
* Get CSRF Token by parsing it out of the XSRF-TOKEN cookie sent by our DSpace server webapp
* Get CSRF Token by parsing it out of the DSPACE-XSRF-COOKIE server-side cookie set by our DSpace server webapp
**/
function getCSRFToken() {
var cookie = document.cookie.match('(^|;)\\s*' + 'XSRF-TOKEN' + '\\s*=\\s*([^;]+)');
var cookie = document.cookie.match('(^|;)\\s*' + 'DSPACE-XSRF-COOKIE' + '\\s*=\\s*([^;]+)');
if(cookie != undefined) {
return cookie.pop();
} else {

View File

@@ -26,18 +26,18 @@ import org.springframework.security.web.csrf.CsrfToken;
* https://github.com/spring-projects/spring-security/blob/5.2.x/web/src/test/java/org/springframework/security/web/csrf/CookieCsrfTokenRepositoryTests.java
*
* The only modifications are:
* - Updating these tests to use our custom CrossSiteCookieCsrfTokenRepository
* - Updating these tests to use our custom DSpaceCsrfTokenRepository
* - Updating the saveTokenSecure() test, where we check for our custom SameSite attribute.
*/
@RunWith(MockitoJUnitRunner.class)
public class CrossSiteCookieCsrfTokenRepositoryTest {
CrossSiteCookieCsrfTokenRepository repository;
public class DSpaceCsrfTokenRepositoryTest {
DSpaceCsrfTokenRepository repository;
MockHttpServletResponse response;
MockHttpServletRequest request;
@Before
public void setup() {
this.repository = new CrossSiteCookieCsrfTokenRepository();
this.repository = new DSpaceCsrfTokenRepository();
this.request = new MockHttpServletRequest();
this.response = new MockHttpServletResponse();
this.request.setContextPath("/context");
@@ -49,9 +49,9 @@ public class CrossSiteCookieCsrfTokenRepositoryTest {
assertThat(generateToken).isNotNull();
assertThat(generateToken.getHeaderName())
.isEqualTo(CrossSiteCookieCsrfTokenRepository.DEFAULT_CSRF_HEADER_NAME);
.isEqualTo(DSpaceCsrfTokenRepository.DEFAULT_CSRF_HEADER_NAME);
assertThat(generateToken.getParameterName())
.isEqualTo(CrossSiteCookieCsrfTokenRepository.DEFAULT_CSRF_PARAMETER_NAME);
.isEqualTo(DSpaceCsrfTokenRepository.DEFAULT_CSRF_PARAMETER_NAME);
assertThat(generateToken.getToken()).isNotEmpty();
}
@@ -76,11 +76,11 @@ public class CrossSiteCookieCsrfTokenRepositoryTest {
this.repository.saveToken(token, this.request, this.response);
Cookie tokenCookie = this.response
.getCookie(CrossSiteCookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
.getCookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
assertThat(tokenCookie.getMaxAge()).isEqualTo(-1);
assertThat(tokenCookie.getName())
.isEqualTo(CrossSiteCookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
.isEqualTo(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
assertThat(tokenCookie.getPath()).isEqualTo(this.request.getContextPath());
assertThat(tokenCookie.getSecure()).isEqualTo(this.request.isSecure());
assertThat(tokenCookie.getValue()).isEqualTo(token.getToken());
@@ -94,7 +94,7 @@ public class CrossSiteCookieCsrfTokenRepositoryTest {
this.repository.saveToken(token, this.request, this.response);
Cookie tokenCookie = this.response
.getCookie(CrossSiteCookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
.getCookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
assertThat(tokenCookie.getSecure()).isTrue();
// DSpace Custom assert to verify SameSite attribute is set
@@ -111,11 +111,11 @@ public class CrossSiteCookieCsrfTokenRepositoryTest {
this.repository.saveToken(null, this.request, this.response);
Cookie tokenCookie = this.response
.getCookie(CrossSiteCookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
.getCookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
assertThat(tokenCookie.getMaxAge()).isZero();
assertThat(tokenCookie.getName())
.isEqualTo(CrossSiteCookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
.isEqualTo(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
assertThat(tokenCookie.getPath()).isEqualTo(this.request.getContextPath());
assertThat(tokenCookie.getSecure()).isEqualTo(this.request.isSecure());
assertThat(tokenCookie.getValue()).isEmpty();
@@ -128,7 +128,7 @@ public class CrossSiteCookieCsrfTokenRepositoryTest {
this.repository.saveToken(token, this.request, this.response);
Cookie tokenCookie = this.response
.getCookie(CrossSiteCookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
.getCookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
assertThat(tokenCookie.isHttpOnly()).isTrue();
}
@@ -140,19 +140,19 @@ public class CrossSiteCookieCsrfTokenRepositoryTest {
this.repository.saveToken(token, this.request, this.response);
Cookie tokenCookie = this.response
.getCookie(CrossSiteCookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
.getCookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
assertThat(tokenCookie.isHttpOnly()).isFalse();
}
@Test
public void saveTokenWithHttpOnlyFalse() {
this.repository = CrossSiteCookieCsrfTokenRepository.withHttpOnlyFalse();
this.repository = DSpaceCsrfTokenRepository.withHttpOnlyFalse();
CsrfToken token = this.repository.generateToken(this.request);
this.repository.saveToken(token, this.request, this.response);
Cookie tokenCookie = this.response
.getCookie(CrossSiteCookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
.getCookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
assertThat(tokenCookie.isHttpOnly()).isFalse();
}
@@ -165,7 +165,7 @@ public class CrossSiteCookieCsrfTokenRepositoryTest {
this.repository.saveToken(token, this.request, this.response);
Cookie tokenCookie = this.response
.getCookie(CrossSiteCookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
.getCookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
assertThat(tokenCookie.getPath()).isEqualTo(this.repository.getCookiePath());
}
@@ -178,7 +178,7 @@ public class CrossSiteCookieCsrfTokenRepositoryTest {
this.repository.saveToken(token, this.request, this.response);
Cookie tokenCookie = this.response
.getCookie(CrossSiteCookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
.getCookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
assertThat(tokenCookie.getPath()).isEqualTo(this.request.getContextPath());
}
@@ -191,7 +191,7 @@ public class CrossSiteCookieCsrfTokenRepositoryTest {
this.repository.saveToken(token, this.request, this.response);
Cookie tokenCookie = this.response
.getCookie(CrossSiteCookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
.getCookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
assertThat(tokenCookie.getPath()).isEqualTo(this.request.getContextPath());
}
@@ -205,7 +205,7 @@ public class CrossSiteCookieCsrfTokenRepositoryTest {
this.repository.saveToken(token, this.request, this.response);
Cookie tokenCookie = this.response
.getCookie(CrossSiteCookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
.getCookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
assertThat(tokenCookie.getDomain()).isEqualTo(domainName);
}
@@ -225,7 +225,7 @@ public class CrossSiteCookieCsrfTokenRepositoryTest {
@Test
public void loadTokenCookieValueEmptyString() {
this.request.setCookies(
new Cookie(CrossSiteCookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME, ""));
new Cookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME, ""));
assertThat(this.repository.loadToken(this.request)).isNull();
}
@@ -235,7 +235,7 @@ public class CrossSiteCookieCsrfTokenRepositoryTest {
CsrfToken generateToken = this.repository.generateToken(this.request);
this.request
.setCookies(new Cookie(CrossSiteCookieCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME,
.setCookies(new Cookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME,
generateToken.getToken()));
CsrfToken loadToken = this.repository.loadToken(this.request);