mirror of
https://github.com/DSpace/DSpace.git
synced 2025-10-07 01:54:22 +00:00
Update CSRF settings to align with Spring Security 6.
This commit is contained in:
@@ -9,11 +9,17 @@ package org.dspace.app.rest.security;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
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.CsrfAuthenticationStrategy;
|
||||
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.XorCsrfTokenRequestAttributeHandler;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
@@ -24,29 +30,41 @@ import org.springframework.util.StringUtils;
|
||||
* 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
|
||||
* This is essentially a customization of Spring Security's CsrfAuthenticationStrategy:
|
||||
* https://github.com/spring-projects/spring-security/blob/6.2.x/web/src/main/java/org/springframework/security/web/csrf/CsrfAuthenticationStrategy.java
|
||||
*/
|
||||
public class DSpaceCsrfAuthenticationStrategy implements SessionAuthenticationStrategy {
|
||||
|
||||
private final CsrfTokenRepository csrfTokenRepository;
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
private final CsrfTokenRepository tokenRepository;
|
||||
|
||||
private CsrfTokenRequestHandler requestHandler = new XorCsrfTokenRequestAttributeHandler();
|
||||
|
||||
/**
|
||||
* Creates a new instance
|
||||
* @param csrfTokenRepository the {@link CsrfTokenRepository} to use
|
||||
* @param tokenRepository the {@link CsrfTokenRepository} to use
|
||||
*/
|
||||
public DSpaceCsrfAuthenticationStrategy(CsrfTokenRepository csrfTokenRepository) {
|
||||
Assert.notNull(csrfTokenRepository, "csrfTokenRepository cannot be null");
|
||||
this.csrfTokenRepository = csrfTokenRepository;
|
||||
public DSpaceCsrfAuthenticationStrategy(CsrfTokenRepository tokenRepository) {
|
||||
Assert.notNull(tokenRepository, "tokenRepository cannot be null");
|
||||
this.tokenRepository = tokenRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method is copied from {@link CsrfAuthenticationStrategy#setRequestHandler(CsrfTokenRequestHandler)}
|
||||
*/
|
||||
public void setRequestHandler(CsrfTokenRequestHandler requestHandler) {
|
||||
Assert.notNull(requestHandler, "requestHandler cannot be null");
|
||||
this.requestHandler = requestHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* '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
|
||||
* Authentication object is created -- as 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 querystring parameter (i.e. "_csrf"). If so, this means
|
||||
* the client has sent the token in a less secure manner & it must then be regenerated.
|
||||
* <P>
|
||||
@@ -57,8 +75,10 @@ public class DSpaceCsrfAuthenticationStrategy implements SessionAuthenticationSt
|
||||
HttpServletRequest request, HttpServletResponse response)
|
||||
throws SessionAuthenticationException {
|
||||
|
||||
|
||||
// Check if token returned in server-side cookie
|
||||
CsrfToken token = this.csrfTokenRepository.loadToken(request);
|
||||
CsrfToken token = this.tokenRepository.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;
|
||||
|
||||
@@ -69,18 +89,28 @@ public class DSpaceCsrfAuthenticationStrategy implements SessionAuthenticationSt
|
||||
// If token exists was sent in a parameter, then we need to reset our token
|
||||
// (as sending token in a param is insecure)
|
||||
if (containsParameter) {
|
||||
// Note: We first set the token to null & then set a new one. This results in 2 cookies sent,
|
||||
// the first being empty and the second having the new token.
|
||||
// This behavior is borrowed from Spring Security's CsrfAuthenticationStrategy, see
|
||||
// https://github.com/spring-projects/spring-security/blob/5.4.x/web/src/main/java/org/springframework/security/web/csrf/CsrfAuthenticationStrategy.java
|
||||
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);
|
||||
resetCSRFToken(request, response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom utility method to force Spring Security to reset the CSRF token. This is used by DSpace to reset
|
||||
* the token whenever the CSRF token is passed insecurely (as a request param, see onAuthentication() above)
|
||||
* or on logout (see JWTTokenRestAuthenticationServiceImpl)
|
||||
* @param request current HTTP request
|
||||
* @param response current HTTP response
|
||||
* @see org.dspace.app.rest.security.jwt.JWTTokenRestAuthenticationServiceImpl
|
||||
*/
|
||||
public void resetCSRFToken(HttpServletRequest request, HttpServletResponse response) {
|
||||
// Note: We first set the token to null & then set a new one. This results in 2 cookies sent,
|
||||
// the first being empty and the second having the new token.
|
||||
// This behavior is borrowed from Spring Security's CsrfAuthenticationStrategy, see
|
||||
// https://github.com/spring-projects/spring-security/blob/6.2.x/web/src/main/java/org/springframework/security/web/csrf/CsrfAuthenticationStrategy.java
|
||||
this.tokenRepository.saveToken(null, request, response);
|
||||
DeferredCsrfToken deferredCsrfToken = this.tokenRepository.loadDeferredToken(request, response);
|
||||
this.requestHandler.handle(request, response, deferredCsrfToken::get);
|
||||
this.logger.debug("Replaced CSRF Token");
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -8,12 +8,13 @@
|
||||
package org.dspace.app.rest.security;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.ws.rs.core.HttpHeaders;
|
||||
import org.springframework.http.ResponseCookie;
|
||||
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
|
||||
import org.springframework.security.web.csrf.CsrfToken;
|
||||
import org.springframework.security.web.csrf.CsrfTokenRepository;
|
||||
import org.springframework.security.web.csrf.DefaultCsrfToken;
|
||||
@@ -25,9 +26,10 @@ import org.springframework.web.util.WebUtils;
|
||||
* 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 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
|
||||
*
|
||||
* This is essentially a customization of Spring Security's CookieCsrfTokenRepository:
|
||||
* https://github.com/spring-projects/spring-security/blob/6.2.x/web/src/main/java/org/springframework/security/web/csrf/CookieCsrfTokenRepository.java
|
||||
* However, as that class is "final" we aannot override it directly.
|
||||
* <P>
|
||||
* How it works:
|
||||
*
|
||||
* 1. Backend generates XSRF token & stores in a *server-side* cookie named DSPACE-XSRF-COOKIE. By default, this cookie
|
||||
@@ -59,6 +61,9 @@ public class DSpaceCsrfTokenRepository implements CsrfTokenRepository {
|
||||
|
||||
static final String DEFAULT_CSRF_HEADER_NAME = "X-XSRF-TOKEN";
|
||||
|
||||
private static final String CSRF_TOKEN_REMOVED_ATTRIBUTE_NAME = CookieCsrfTokenRepository.class.getName()
|
||||
.concat(".REMOVED");
|
||||
|
||||
private String parameterName = DEFAULT_CSRF_PARAMETER_NAME;
|
||||
|
||||
private String headerName = DEFAULT_CSRF_HEADER_NAME;
|
||||
@@ -71,73 +76,93 @@ public class DSpaceCsrfTokenRepository implements CsrfTokenRepository {
|
||||
|
||||
private String cookieDomain;
|
||||
|
||||
private Boolean secure;
|
||||
|
||||
private int cookieMaxAge = -1;
|
||||
|
||||
private Consumer<ResponseCookie.ResponseCookieBuilder> cookieCustomizer = (builder) -> {
|
||||
};
|
||||
|
||||
public DSpaceCsrfTokenRepository() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public CsrfToken generateToken(HttpServletRequest request) {
|
||||
return new DefaultCsrfToken(this.headerName, this.parameterName,
|
||||
createNewToken());
|
||||
/**
|
||||
* Method is copied from {@link CookieCsrfTokenRepository#setCookieCustomizer(Consumer)}
|
||||
*/
|
||||
public void setCookieCustomizer(Consumer<ResponseCookie.ResponseCookieBuilder> cookieCustomizer) {
|
||||
Assert.notNull(cookieCustomizer, "cookieCustomizer must not be null");
|
||||
this.cookieCustomizer = cookieCustomizer;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method has been modified for DSpace.
|
||||
* Method is copied from {@link CookieCsrfTokenRepository#generateToken(HttpServletRequest)}
|
||||
*/
|
||||
@Override
|
||||
public CsrfToken generateToken(HttpServletRequest request) {
|
||||
return new DefaultCsrfToken(this.headerName, this.parameterName, createNewToken());
|
||||
}
|
||||
|
||||
/**
|
||||
* This method has been modified for DSpace. It borrows MOST of the logic from
|
||||
* {@link CookieCsrfTokenRepository#saveToken(CsrfToken, HttpServletRequest, HttpServletResponse)}
|
||||
* <P>
|
||||
* It now uses ResponseCookie to build the cookie, so that the "SameSite" attribute can be applied.
|
||||
* It applies a "SameSite" attribute to every cookie by default.
|
||||
* <P>
|
||||
* It also sends the token (if not empty) in both the cookie and the custom "DSPACE-XSRF-TOKEN" header
|
||||
* It also sends the token (if not empty) back in BOTH the cookie and the custom "DSPACE-XSRF-TOKEN" header.
|
||||
* By default, Spring Security will only send the token back in the cookie.
|
||||
* @param token current token
|
||||
* @param request current request
|
||||
* @param response current response
|
||||
*/
|
||||
@Override
|
||||
public void saveToken(CsrfToken token, HttpServletRequest request,
|
||||
HttpServletResponse response) {
|
||||
String tokenValue = token == null ? "" : token.getToken();
|
||||
Cookie cookie = new Cookie(this.cookieName, tokenValue);
|
||||
cookie.setSecure(request.isSecure());
|
||||
if (this.cookiePath != null && !this.cookiePath.isEmpty()) {
|
||||
cookie.setPath(this.cookiePath);
|
||||
} else {
|
||||
cookie.setPath(this.getRequestContext(request));
|
||||
}
|
||||
if (token == null) {
|
||||
cookie.setMaxAge(0);
|
||||
} else {
|
||||
cookie.setMaxAge(-1);
|
||||
}
|
||||
cookie.setHttpOnly(cookieHttpOnly);
|
||||
if (this.cookieDomain != null && !this.cookieDomain.isEmpty()) {
|
||||
cookie.setDomain(this.cookieDomain);
|
||||
}
|
||||
public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) {
|
||||
String tokenValue = (token != null) ? token.getToken() : "";
|
||||
|
||||
// Custom: Turn the above Cookie into a ResponseCookie so that we can set "SameSite" attribute
|
||||
// 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();
|
||||
ResponseCookie.ResponseCookieBuilder cookieBuilder =
|
||||
ResponseCookie.from(this.cookieName, tokenValue)
|
||||
.secure((this.secure != null) ? this.secure : request.isSecure())
|
||||
.path(StringUtils.hasLength(this.cookiePath) ?
|
||||
this.cookiePath : this.getRequestContext(request))
|
||||
.maxAge((token != null) ? this.cookieMaxAge : 0)
|
||||
.httpOnly(this.cookieHttpOnly)
|
||||
.domain(this.cookieDomain)
|
||||
// Custom for DSpace: 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.
|
||||
.sameSite(request.isSecure() ? "None" : "Lax");;
|
||||
|
||||
// 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());
|
||||
this.cookieCustomizer.accept(cookieBuilder);
|
||||
|
||||
// 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.
|
||||
// Custom for DSpace: also send custom header to client with token.
|
||||
// We send our token via a custom header because client may be on a different domain.
|
||||
// Cookies cannot be reliably sent cross-domain.
|
||||
if (StringUtils.hasLength(tokenValue)) {
|
||||
response.setHeader("DSPACE-XSRF-TOKEN", tokenValue);
|
||||
}
|
||||
|
||||
Cookie cookie = mapToCookie(cookieBuilder.build());
|
||||
response.addCookie(cookie);
|
||||
|
||||
// Set request attribute to signal that response has blank cookie value,
|
||||
// which allows loadToken to return null when token has been removed
|
||||
if (!StringUtils.hasLength(tokenValue)) {
|
||||
request.setAttribute(CSRF_TOKEN_REMOVED_ATTRIBUTE_NAME, Boolean.TRUE);
|
||||
} else {
|
||||
request.removeAttribute(CSRF_TOKEN_REMOVED_ATTRIBUTE_NAME);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method is copied from {@link CookieCsrfTokenRepository#loadToken(HttpServletRequest)}
|
||||
*/
|
||||
@Override
|
||||
public CsrfToken loadToken(HttpServletRequest request) {
|
||||
// Return null when token has been removed during the current request
|
||||
// which allows loadDeferredToken to re-generate the token
|
||||
if (Boolean.TRUE.equals(request.getAttribute(CSRF_TOKEN_REMOVED_ATTRIBUTE_NAME))) {
|
||||
return null;
|
||||
}
|
||||
Cookie cookie = WebUtils.getCookie(request, this.cookieName);
|
||||
if (cookie == null) {
|
||||
return null;
|
||||
@@ -146,104 +171,124 @@ public class DSpaceCsrfTokenRepository implements CsrfTokenRepository {
|
||||
if (!StringUtils.hasLength(token)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new DefaultCsrfToken(this.headerName, this.parameterName, token);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets the name of the HTTP request parameter that should be used to provide a token.
|
||||
*
|
||||
* @param parameterName the name of the HTTP request parameter that should be used to
|
||||
* provide a token
|
||||
* Method is copied from {@link CookieCsrfTokenRepository#setParameterName(String)}
|
||||
*/
|
||||
public void setParameterName(String parameterName) {
|
||||
Assert.notNull(parameterName, "parameterName is not null");
|
||||
Assert.notNull(parameterName, "parameterName cannot be null");
|
||||
this.parameterName = parameterName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the name of the HTTP header that should be used to provide the token.
|
||||
*
|
||||
* @param headerName the name of the HTTP header that should be used to provide the
|
||||
* token
|
||||
* Method is copied from {@link CookieCsrfTokenRepository#setHeaderName(String)}
|
||||
*/
|
||||
public void setHeaderName(String headerName) {
|
||||
Assert.notNull(headerName, "headerName is not null");
|
||||
Assert.notNull(headerName, "headerName cannot be null");
|
||||
this.headerName = headerName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the name of the cookie that the expected CSRF token is saved to and read from.
|
||||
*
|
||||
* @param cookieName the name of the cookie that the expected CSRF token is saved to
|
||||
* and read from
|
||||
* Method is copied from {@link CookieCsrfTokenRepository#setCookieName(String)}
|
||||
*/
|
||||
public void setCookieName(String cookieName) {
|
||||
Assert.notNull(cookieName, "cookieName is not null");
|
||||
Assert.notNull(cookieName, "cookieName cannot be null");
|
||||
this.cookieName = cookieName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the HttpOnly attribute on the cookie containing the CSRF token.
|
||||
* Defaults to <code>true</code>.
|
||||
*
|
||||
* @param cookieHttpOnly <code>true</code> sets the HttpOnly attribute, <code>false</code> does not set it
|
||||
* Method is copied from {@link CookieCsrfTokenRepository#setCookieHttpOnly(boolean)}
|
||||
* @deprecated Use {@link #setCookieCustomizer(Consumer)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public void setCookieHttpOnly(boolean cookieHttpOnly) {
|
||||
this.cookieHttpOnly = cookieHttpOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method is copied from {@link CookieCsrfTokenRepository}
|
||||
*/
|
||||
private String getRequestContext(HttpServletRequest request) {
|
||||
String contextPath = request.getContextPath();
|
||||
return contextPath.length() > 0 ? contextPath : "/";
|
||||
return (contextPath.length() > 0) ? contextPath : "/";
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to conveniently create an instance that has
|
||||
* {@link #setCookieHttpOnly(boolean)} set to false.
|
||||
*
|
||||
* @return an instance of CookieCsrfTokenRepository with
|
||||
* {@link #setCookieHttpOnly(boolean)} set to false
|
||||
* Method is copied from {@link CookieCsrfTokenRepository}
|
||||
* (and only modified to return the DSpaceCsrfTokenRepository instead)
|
||||
*/
|
||||
public static DSpaceCsrfTokenRepository withHttpOnlyFalse() {
|
||||
DSpaceCsrfTokenRepository result = new DSpaceCsrfTokenRepository();
|
||||
result.setCookieHttpOnly(false);
|
||||
result.cookieHttpOnly = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method is copied from {@link CookieCsrfTokenRepository}
|
||||
*/
|
||||
private String createNewToken() {
|
||||
return UUID.randomUUID().toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the path that the Cookie will be created with. This will override the default functionality which uses the
|
||||
* request context as the path.
|
||||
*
|
||||
* @param path the path to use
|
||||
* Method is copied from {@link CookieCsrfTokenRepository}
|
||||
*/
|
||||
private Cookie mapToCookie(ResponseCookie responseCookie) {
|
||||
Cookie cookie = new Cookie(responseCookie.getName(), responseCookie.getValue());
|
||||
cookie.setSecure(responseCookie.isSecure());
|
||||
cookie.setPath(responseCookie.getPath());
|
||||
cookie.setMaxAge((int) responseCookie.getMaxAge().getSeconds());
|
||||
cookie.setHttpOnly(responseCookie.isHttpOnly());
|
||||
if (StringUtils.hasLength(responseCookie.getDomain())) {
|
||||
cookie.setDomain(responseCookie.getDomain());
|
||||
}
|
||||
if (StringUtils.hasText(responseCookie.getSameSite())) {
|
||||
cookie.setAttribute("SameSite", responseCookie.getSameSite());
|
||||
}
|
||||
return cookie;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method is copied from {@link CookieCsrfTokenRepository#setCookiePath(String)}
|
||||
*/
|
||||
public void setCookiePath(String path) {
|
||||
this.cookiePath = path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path that the CSRF cookie will be set to.
|
||||
*
|
||||
* @return the path to be used.
|
||||
* Method is copied from {@link CookieCsrfTokenRepository#getCookiePath()}
|
||||
*/
|
||||
public String getCookiePath() {
|
||||
return this.cookiePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the domain of the cookie that the expected CSRF token is saved to and read from.
|
||||
*
|
||||
* @since 5.2
|
||||
* @param cookieDomain the domain of the cookie that the expected CSRF token is saved to
|
||||
* and read from
|
||||
* Method is copied from {@link CookieCsrfTokenRepository#setCookieDomain(String)}
|
||||
* @deprecated Use {@link #setCookieCustomizer(Consumer)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public void setCookieDomain(String cookieDomain) {
|
||||
this.cookieDomain = cookieDomain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method is copied from {@link CookieCsrfTokenRepository#setSecure(Boolean)}
|
||||
* @deprecated Use {@link #setCookieCustomizer(Consumer)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public void setSecure(Boolean secure) {
|
||||
this.secure = secure;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method is copied from {@link CookieCsrfTokenRepository#setCookieMaxAge(int)}
|
||||
* @deprecated Use {@link #setCookieCustomizer(Consumer)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public void setCookieMaxAge(int cookieMaxAge) {
|
||||
Assert.isTrue(cookieMaxAge != 0, "cookieMaxAge cannot be zero");
|
||||
this.cookieMaxAge = cookieMaxAge;
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* 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 java.util.function.Supplier;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.security.web.csrf.CsrfToken;
|
||||
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
|
||||
import org.springframework.security.web.csrf.CsrfTokenRequestHandler;
|
||||
import org.springframework.security.web.csrf.XorCsrfTokenRequestAttributeHandler;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* A custom Spring Security CsrfTokenRequestAttributeHandler which uses the Spring Security BREACH protection
|
||||
* (provided by XorCsrfTokenRequestAttributeHandler) *only* when the CSRF token is sent as a "_csrf" request parameter.
|
||||
* In all other scenarios, the CsrfTokenRequestAttributeHandler is used instead.
|
||||
* <P>
|
||||
* NOTE: The DSpace UI always sends the CSRF Token as a request header. It does NOT send it as a "_csrf" request
|
||||
* paramter. So, this BREACH protection would ONLY be triggered for custom clients (not the DSpace UI).
|
||||
* Therefore, if using this custom class becomes problematic, we could revert to using the default
|
||||
* CsrfTokenRequestAttributeHandler without any negative impact on the DSpace UI.
|
||||
* <P>
|
||||
* This code is copied from the example "SpaCsrfTokenRequestHandler" (for single page applications) from the Spring
|
||||
* Security docs at
|
||||
* https://docs.spring.io/spring-security/reference/servlet/exploits/csrf.html#csrf-integration-javascript-spa-configuration
|
||||
*/
|
||||
public final class DSpaceCsrfTokenRequestHandler extends CsrfTokenRequestAttributeHandler {
|
||||
private final CsrfTokenRequestHandler delegate = new XorCsrfTokenRequestAttributeHandler();
|
||||
|
||||
@Override
|
||||
public void handle(HttpServletRequest request, HttpServletResponse response, Supplier<CsrfToken> csrfToken) {
|
||||
/*
|
||||
* Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection of
|
||||
* the CsrfToken when it is rendered in the response body.
|
||||
* NOTE: This should never occur from the DSpace UI, so it is only applicable for custom clients.
|
||||
*/
|
||||
this.delegate.handle(request, response, csrfToken);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) {
|
||||
/*
|
||||
* If the request contains a request header, use CsrfTokenRequestAttributeHandler
|
||||
* to resolve the CsrfToken. This applies to the DSpace UI which always includes
|
||||
* the raw CsrfToken in an HTTP Header.
|
||||
*/
|
||||
if (StringUtils.hasText(request.getHeader(csrfToken.getHeaderName()))) {
|
||||
return super.resolveCsrfTokenValue(request, csrfToken);
|
||||
}
|
||||
/*
|
||||
* In all other cases (e.g. if the request contains a request parameter), use
|
||||
* XorCsrfTokenRequestAttributeHandler to resolve the CsrfToken. This applies
|
||||
* when a server-side rendered form includes the _csrf request parameter as a
|
||||
* hidden input.
|
||||
* NOTE: This should never occur from the DSpace UI, so it is only applicable for custom clients.
|
||||
*/
|
||||
return this.delegate.resolveCsrfTokenValue(request, csrfToken);
|
||||
}
|
||||
}
|
@@ -28,7 +28,6 @@ import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
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;
|
||||
|
||||
@@ -112,8 +111,8 @@ public class WebSecurityConfiguration {
|
||||
// While we primarily use JWT in headers, CSRF protection is needed because we also support JWT via Cookies
|
||||
.csrf((csrf) -> csrf
|
||||
.csrfTokenRepository(this.csrfTokenRepository())
|
||||
.sessionAuthenticationStrategy(this.sessionAuthenticationStrategy())
|
||||
)
|
||||
.sessionAuthenticationStrategy(this.dSpaceCsrfAuthenticationStrategy())
|
||||
.csrfTokenRequestHandler(new DSpaceCsrfTokenRequestHandler()))
|
||||
.exceptionHandling((exceptionHandling) -> exceptionHandling
|
||||
// Return 401 on authorization failures with a correct WWWW-Authenticate header
|
||||
.authenticationEntryPoint(new DSpace401AuthenticationEntryPoint(restAuthenticationService))
|
||||
@@ -187,8 +186,13 @@ public class WebSecurityConfiguration {
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* This is defined as a bean so that it can also be used in other code to reset CSRF Tokens, see
|
||||
* JWTTokenRestAuthenticationServiceImpl
|
||||
*/
|
||||
private SessionAuthenticationStrategy sessionAuthenticationStrategy() {
|
||||
@Lazy
|
||||
@Bean
|
||||
public DSpaceCsrfAuthenticationStrategy dSpaceCsrfAuthenticationStrategy() {
|
||||
return new DSpaceCsrfAuthenticationStrategy(csrfTokenRepository());
|
||||
}
|
||||
|
||||
|
@@ -22,6 +22,7 @@ import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.dspace.app.rest.model.wrapper.AuthenticationToken;
|
||||
import org.dspace.app.rest.security.DSpaceAuthentication;
|
||||
import org.dspace.app.rest.security.DSpaceCsrfAuthenticationStrategy;
|
||||
import org.dspace.app.rest.security.RestAuthenticationService;
|
||||
import org.dspace.app.rest.utils.ContextUtil;
|
||||
import org.dspace.authenticate.AuthenticationMethod;
|
||||
@@ -33,8 +34,6 @@ import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.http.ResponseCookie;
|
||||
import org.springframework.security.web.csrf.CsrfToken;
|
||||
import org.springframework.security.web.csrf.CsrfTokenRepository;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
@@ -67,7 +66,7 @@ public class JWTTokenRestAuthenticationServiceImpl implements RestAuthentication
|
||||
|
||||
@Lazy
|
||||
@Autowired
|
||||
private CsrfTokenRepository csrfTokenRepository;
|
||||
private DSpaceCsrfAuthenticationStrategy dspaceCsrfAuthenticationStrategy;
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
@@ -332,11 +331,8 @@ public class JWTTokenRestAuthenticationServiceImpl implements RestAuthentication
|
||||
* @param response current response
|
||||
*/
|
||||
private void resetCSRFToken(HttpServletRequest request, HttpServletResponse response) {
|
||||
// Remove current CSRF token & generate a new one
|
||||
// We do this as we want the token to change anytime you login or logout
|
||||
csrfTokenRepository.saveToken(null, request, response);
|
||||
CsrfToken newToken = csrfTokenRepository.generateToken(request);
|
||||
csrfTokenRepository.saveToken(newToken, request, response);
|
||||
// Use our custom CsrfAuthenticationStrategy class to force reset the CSRF token in Spring Security
|
||||
dspaceCsrfAuthenticationStrategy.resetCSRFToken(request, response);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -8,8 +8,10 @@
|
||||
package org.dspace.app.rest.security;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.util.List;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import jakarta.ws.rs.core.HttpHeaders;
|
||||
@@ -20,10 +22,11 @@ import org.mockito.junit.MockitoJUnitRunner;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.web.csrf.CsrfToken;
|
||||
import org.springframework.security.web.csrf.DeferredCsrfToken;
|
||||
|
||||
/**
|
||||
* This is almost an exact copy of Spring Security's CookieCsrfTokenRepositoryTests
|
||||
* https://github.com/spring-projects/spring-security/blob/5.2.x/web/src/test/java/org/springframework/security/web/csrf/CookieCsrfTokenRepositoryTests.java
|
||||
* This is almost an exact copy of Spring Security's DSpaceCsrfTokenRepositoryTests
|
||||
* https://github.com/spring-projects/spring-security/blob/6.2.x/web/src/test/java/org/springframework/security/web/csrf/CookieCsrfTokenRepositoryTests.java
|
||||
*
|
||||
* The only modifications are:
|
||||
* - Updating these tests to use our custom DSpaceCsrfTokenRepository
|
||||
@@ -46,12 +49,9 @@ public class DSpaceCsrfTokenRepositoryTest {
|
||||
@Test
|
||||
public void generateToken() {
|
||||
CsrfToken generateToken = this.repository.generateToken(this.request);
|
||||
|
||||
assertThat(generateToken).isNotNull();
|
||||
assertThat(generateToken.getHeaderName())
|
||||
.isEqualTo(DSpaceCsrfTokenRepository.DEFAULT_CSRF_HEADER_NAME);
|
||||
assertThat(generateToken.getParameterName())
|
||||
.isEqualTo(DSpaceCsrfTokenRepository.DEFAULT_CSRF_PARAMETER_NAME);
|
||||
assertThat(generateToken.getHeaderName()).isEqualTo(DSpaceCsrfTokenRepository.DEFAULT_CSRF_HEADER_NAME);
|
||||
assertThat(generateToken.getParameterName()).isEqualTo(DSpaceCsrfTokenRepository.DEFAULT_CSRF_PARAMETER_NAME);
|
||||
assertThat(generateToken.getToken()).isNotEmpty();
|
||||
}
|
||||
|
||||
@@ -61,9 +61,7 @@ public class DSpaceCsrfTokenRepositoryTest {
|
||||
String parameterName = "paramName";
|
||||
this.repository.setHeaderName(headerName);
|
||||
this.repository.setParameterName(parameterName);
|
||||
|
||||
CsrfToken generateToken = this.repository.generateToken(this.request);
|
||||
|
||||
assertThat(generateToken).isNotNull();
|
||||
assertThat(generateToken.getHeaderName()).isEqualTo(headerName);
|
||||
assertThat(generateToken.getParameterName()).isEqualTo(parameterName);
|
||||
@@ -74,17 +72,21 @@ public class DSpaceCsrfTokenRepositoryTest {
|
||||
public void saveToken() {
|
||||
CsrfToken token = this.repository.generateToken(this.request);
|
||||
this.repository.saveToken(token, this.request, this.response);
|
||||
|
||||
Cookie tokenCookie = this.response
|
||||
.getCookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
|
||||
|
||||
Cookie tokenCookie = this.response.getCookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
|
||||
assertThat(tokenCookie.getMaxAge()).isEqualTo(-1);
|
||||
assertThat(tokenCookie.getName())
|
||||
.isEqualTo(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
|
||||
assertThat(tokenCookie.getName()).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());
|
||||
assertThat(tokenCookie.isHttpOnly()).isEqualTo(true);
|
||||
assertThat(tokenCookie.isHttpOnly()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveTokenShouldUseResponseAddCookie() {
|
||||
CsrfToken token = this.repository.generateToken(this.request);
|
||||
MockHttpServletResponse spyResponse = spy(this.response);
|
||||
this.repository.saveToken(token, this.request, spyResponse);
|
||||
verify(spyResponse).addCookie(any(Cookie.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -92,30 +94,73 @@ public class DSpaceCsrfTokenRepositoryTest {
|
||||
this.request.setSecure(true);
|
||||
CsrfToken token = this.repository.generateToken(this.request);
|
||||
this.repository.saveToken(token, this.request, this.response);
|
||||
|
||||
Cookie tokenCookie = this.response
|
||||
.getCookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
|
||||
|
||||
Cookie tokenCookie = this.response.getCookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
|
||||
assertThat(tokenCookie.getSecure()).isTrue();
|
||||
// DSpace Custom assert to verify SameSite attribute is set
|
||||
// The Cookie class doesn't yet support SameSite, so we have to re-read
|
||||
// the cookie from our headers, and check it.
|
||||
List<String> headers = this.response.getHeaders(HttpHeaders.SET_COOKIE);
|
||||
assertThat(headers.size()).isEqualTo(1);
|
||||
assertThat(headers.get(0)).containsIgnoringCase("SameSite=None");
|
||||
|
||||
// DSpace Custom assert to verify SameSite attribute is "None" when cookie is secure
|
||||
assertThat(tokenCookie.getAttribute("SameSite")).containsIgnoringCase("None");
|
||||
}
|
||||
|
||||
// Custom test for DSpace to verify behavior for non-secure requests
|
||||
@Test
|
||||
public void saveTokenNotSecure() {
|
||||
this.request.setSecure(false);
|
||||
CsrfToken token = this.repository.generateToken(this.request);
|
||||
this.repository.saveToken(token, this.request, this.response);
|
||||
Cookie tokenCookie = this.response.getCookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
|
||||
assertThat(tokenCookie.getSecure()).isFalse();
|
||||
|
||||
// DSpace Custom assert to verify SameSite attribute is "Lax" when cookie is NOT secure
|
||||
assertThat(tokenCookie.getAttribute("SameSite")).containsIgnoringCase("Lax");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveTokenSecureFlagTrue() {
|
||||
this.request.setSecure(false);
|
||||
this.repository.setSecure(Boolean.TRUE);
|
||||
CsrfToken token = this.repository.generateToken(this.request);
|
||||
this.repository.saveToken(token, this.request, this.response);
|
||||
Cookie tokenCookie = this.response.getCookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
|
||||
assertThat(tokenCookie.getSecure()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveTokenSecureFlagTrueUsingCustomizer() {
|
||||
this.request.setSecure(false);
|
||||
this.repository.setCookieCustomizer((customizer) -> customizer.secure(Boolean.TRUE));
|
||||
CsrfToken token = this.repository.generateToken(this.request);
|
||||
this.repository.saveToken(token, this.request, this.response);
|
||||
Cookie tokenCookie = this.response.getCookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
|
||||
assertThat(tokenCookie.getSecure()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveTokenSecureFlagFalse() {
|
||||
this.request.setSecure(true);
|
||||
this.repository.setSecure(Boolean.FALSE);
|
||||
CsrfToken token = this.repository.generateToken(this.request);
|
||||
this.repository.saveToken(token, this.request, this.response);
|
||||
Cookie tokenCookie = this.response.getCookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
|
||||
assertThat(tokenCookie.getSecure()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveTokenSecureFlagFalseUsingCustomizer() {
|
||||
this.request.setSecure(true);
|
||||
this.repository.setCookieCustomizer((customizer) -> customizer.secure(Boolean.FALSE));
|
||||
CsrfToken token = this.repository.generateToken(this.request);
|
||||
this.repository.saveToken(token, this.request, this.response);
|
||||
Cookie tokenCookie = this.response.getCookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
|
||||
assertThat(tokenCookie.getSecure()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveTokenNull() {
|
||||
this.request.setSecure(true);
|
||||
this.repository.saveToken(null, this.request, this.response);
|
||||
|
||||
Cookie tokenCookie = this.response
|
||||
.getCookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
|
||||
|
||||
Cookie tokenCookie = this.response.getCookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
|
||||
assertThat(tokenCookie.getMaxAge()).isZero();
|
||||
assertThat(tokenCookie.getName())
|
||||
.isEqualTo(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
|
||||
assertThat(tokenCookie.getName()).isEqualTo(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
|
||||
assertThat(tokenCookie.getPath()).isEqualTo(this.request.getContextPath());
|
||||
assertThat(tokenCookie.getSecure()).isEqualTo(this.request.isSecure());
|
||||
assertThat(tokenCookie.getValue()).isEmpty();
|
||||
@@ -126,10 +171,16 @@ public class DSpaceCsrfTokenRepositoryTest {
|
||||
this.repository.setCookieHttpOnly(true);
|
||||
CsrfToken token = this.repository.generateToken(this.request);
|
||||
this.repository.saveToken(token, this.request, this.response);
|
||||
Cookie tokenCookie = this.response.getCookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
|
||||
assertThat(tokenCookie.isHttpOnly()).isTrue();
|
||||
}
|
||||
|
||||
Cookie tokenCookie = this.response
|
||||
.getCookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
|
||||
|
||||
@Test
|
||||
public void saveTokenHttpOnlyTrueUsingCustomizer() {
|
||||
this.repository.setCookieCustomizer((customizer) -> customizer.httpOnly(true));
|
||||
CsrfToken token = this.repository.generateToken(this.request);
|
||||
this.repository.saveToken(token, this.request, this.response);
|
||||
Cookie tokenCookie = this.response.getCookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
|
||||
assertThat(tokenCookie.isHttpOnly()).isTrue();
|
||||
}
|
||||
|
||||
@@ -138,10 +189,16 @@ public class DSpaceCsrfTokenRepositoryTest {
|
||||
this.repository.setCookieHttpOnly(false);
|
||||
CsrfToken token = this.repository.generateToken(this.request);
|
||||
this.repository.saveToken(token, this.request, this.response);
|
||||
Cookie tokenCookie = this.response.getCookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
|
||||
assertThat(tokenCookie.isHttpOnly()).isFalse();
|
||||
}
|
||||
|
||||
Cookie tokenCookie = this.response
|
||||
.getCookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
|
||||
|
||||
@Test
|
||||
public void saveTokenHttpOnlyFalseUsingCustomizer() {
|
||||
this.repository.setCookieCustomizer((customizer) -> customizer.httpOnly(false));
|
||||
CsrfToken token = this.repository.generateToken(this.request);
|
||||
this.repository.saveToken(token, this.request, this.response);
|
||||
Cookie tokenCookie = this.response.getCookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
|
||||
assertThat(tokenCookie.isHttpOnly()).isFalse();
|
||||
}
|
||||
|
||||
@@ -150,10 +207,7 @@ public class DSpaceCsrfTokenRepositoryTest {
|
||||
this.repository = DSpaceCsrfTokenRepository.withHttpOnlyFalse();
|
||||
CsrfToken token = this.repository.generateToken(this.request);
|
||||
this.repository.saveToken(token, this.request, this.response);
|
||||
|
||||
Cookie tokenCookie = this.response
|
||||
.getCookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
|
||||
|
||||
Cookie tokenCookie = this.response.getCookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
|
||||
assertThat(tokenCookie.isHttpOnly()).isFalse();
|
||||
}
|
||||
|
||||
@@ -163,10 +217,7 @@ public class DSpaceCsrfTokenRepositoryTest {
|
||||
this.repository.setCookiePath(customPath);
|
||||
CsrfToken token = this.repository.generateToken(this.request);
|
||||
this.repository.saveToken(token, this.request, this.response);
|
||||
|
||||
Cookie tokenCookie = this.response
|
||||
.getCookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
|
||||
|
||||
Cookie tokenCookie = this.response.getCookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
|
||||
assertThat(tokenCookie.getPath()).isEqualTo(this.repository.getCookiePath());
|
||||
}
|
||||
|
||||
@@ -176,10 +227,7 @@ public class DSpaceCsrfTokenRepositoryTest {
|
||||
this.repository.setCookiePath(customPath);
|
||||
CsrfToken token = this.repository.generateToken(this.request);
|
||||
this.repository.saveToken(token, this.request, this.response);
|
||||
|
||||
Cookie tokenCookie = this.response
|
||||
.getCookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
|
||||
|
||||
Cookie tokenCookie = this.response.getCookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
|
||||
assertThat(tokenCookie.getPath()).isEqualTo(this.request.getContextPath());
|
||||
}
|
||||
|
||||
@@ -189,10 +237,7 @@ public class DSpaceCsrfTokenRepositoryTest {
|
||||
this.repository.setCookiePath(customPath);
|
||||
CsrfToken token = this.repository.generateToken(this.request);
|
||||
this.repository.saveToken(token, this.request, this.response);
|
||||
|
||||
Cookie tokenCookie = this.response
|
||||
.getCookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
|
||||
|
||||
Cookie tokenCookie = this.response.getCookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
|
||||
assertThat(tokenCookie.getPath()).isEqualTo(this.request.getContextPath());
|
||||
}
|
||||
|
||||
@@ -200,16 +245,82 @@ public class DSpaceCsrfTokenRepositoryTest {
|
||||
public void saveTokenWithCookieDomain() {
|
||||
String domainName = "example.com";
|
||||
this.repository.setCookieDomain(domainName);
|
||||
|
||||
CsrfToken token = this.repository.generateToken(this.request);
|
||||
this.repository.saveToken(token, this.request, this.response);
|
||||
|
||||
Cookie tokenCookie = this.response
|
||||
.getCookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
|
||||
|
||||
Cookie tokenCookie = this.response.getCookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
|
||||
assertThat(tokenCookie.getDomain()).isEqualTo(domainName);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveTokenWithCookieDomainUsingCustomizer() {
|
||||
String domainName = "example.com";
|
||||
this.repository.setCookieCustomizer((customizer) -> customizer.domain(domainName));
|
||||
CsrfToken token = this.repository.generateToken(this.request);
|
||||
this.repository.saveToken(token, this.request, this.response);
|
||||
Cookie tokenCookie = this.response.getCookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
|
||||
assertThat(tokenCookie.getDomain()).isEqualTo(domainName);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveTokenWithCookieMaxAge() {
|
||||
int maxAge = 1200;
|
||||
this.repository.setCookieMaxAge(maxAge);
|
||||
CsrfToken token = this.repository.generateToken(this.request);
|
||||
this.repository.saveToken(token, this.request, this.response);
|
||||
Cookie tokenCookie = this.response.getCookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
|
||||
assertThat(tokenCookie.getMaxAge()).isEqualTo(maxAge);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveTokenWithCookieMaxAgeUsingCustomizer() {
|
||||
int maxAge = 1200;
|
||||
this.repository.setCookieCustomizer((customizer) -> customizer.maxAge(maxAge));
|
||||
CsrfToken token = this.repository.generateToken(this.request);
|
||||
this.repository.saveToken(token, this.request, this.response);
|
||||
Cookie tokenCookie = this.response.getCookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
|
||||
assertThat(tokenCookie.getMaxAge()).isEqualTo(maxAge);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveTokenWithSameSiteNull() {
|
||||
String sameSitePolicy = null;
|
||||
this.repository.setCookieCustomizer((customizer) -> customizer.sameSite(sameSitePolicy));
|
||||
CsrfToken token = this.repository.generateToken(this.request);
|
||||
this.repository.saveToken(token, this.request, this.response);
|
||||
Cookie tokenCookie = this.response.getCookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
|
||||
assertThat(tokenCookie.getAttribute("SameSite")).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveTokenWithSameSiteStrict() {
|
||||
String sameSitePolicy = "Strict";
|
||||
this.repository.setCookieCustomizer((customizer) -> customizer.sameSite(sameSitePolicy));
|
||||
CsrfToken token = this.repository.generateToken(this.request);
|
||||
this.repository.saveToken(token, this.request, this.response);
|
||||
Cookie tokenCookie = this.response.getCookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
|
||||
assertThat(tokenCookie.getAttribute("SameSite")).isEqualTo(sameSitePolicy);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveTokenWithSameSiteLax() {
|
||||
String sameSitePolicy = "Lax";
|
||||
this.repository.setCookieCustomizer((customizer) -> customizer.sameSite(sameSitePolicy));
|
||||
CsrfToken token = this.repository.generateToken(this.request);
|
||||
this.repository.saveToken(token, this.request, this.response);
|
||||
Cookie tokenCookie = this.response.getCookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
|
||||
assertThat(tokenCookie.getAttribute("SameSite")).isEqualTo(sameSitePolicy);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveTokenWithExistingSetCookieThenDoesNotOverwrite() {
|
||||
this.response.setHeader(HttpHeaders.SET_COOKIE, "MyCookie=test");
|
||||
this.repository = new DSpaceCsrfTokenRepository();
|
||||
CsrfToken token = this.repository.generateToken(this.request);
|
||||
this.repository.saveToken(token, this.request, this.response);
|
||||
assertThat(this.response.getCookie("MyCookie")).isNotNull();
|
||||
assertThat(this.response.getCookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME)).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadTokenNoCookiesNull() {
|
||||
assertThat(this.repository.loadToken(this.request)).isNull();
|
||||
@@ -218,32 +329,24 @@ public class DSpaceCsrfTokenRepositoryTest {
|
||||
@Test
|
||||
public void loadTokenCookieIncorrectNameNull() {
|
||||
this.request.setCookies(new Cookie("other", "name"));
|
||||
|
||||
assertThat(this.repository.loadToken(this.request)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadTokenCookieValueEmptyString() {
|
||||
this.request.setCookies(
|
||||
new Cookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME, ""));
|
||||
|
||||
this.request.setCookies(new Cookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME, ""));
|
||||
assertThat(this.repository.loadToken(this.request)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadToken() {
|
||||
CsrfToken generateToken = this.repository.generateToken(this.request);
|
||||
|
||||
this.request
|
||||
.setCookies(new Cookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME,
|
||||
generateToken.getToken()));
|
||||
|
||||
.setCookies(new Cookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME, generateToken.getToken()));
|
||||
CsrfToken loadToken = this.repository.loadToken(this.request);
|
||||
|
||||
assertThat(loadToken).isNotNull();
|
||||
assertThat(loadToken.getHeaderName()).isEqualTo(generateToken.getHeaderName());
|
||||
assertThat(loadToken.getParameterName())
|
||||
.isEqualTo(generateToken.getParameterName());
|
||||
assertThat(loadToken.getParameterName()).isEqualTo(generateToken.getParameterName());
|
||||
assertThat(loadToken.getToken()).isNotEmpty();
|
||||
}
|
||||
|
||||
@@ -256,32 +359,95 @@ public class DSpaceCsrfTokenRepositoryTest {
|
||||
this.repository.setHeaderName(headerName);
|
||||
this.repository.setParameterName(parameterName);
|
||||
this.repository.setCookieName(cookieName);
|
||||
|
||||
this.request.setCookies(new Cookie(cookieName, value));
|
||||
|
||||
CsrfToken loadToken = this.repository.loadToken(this.request);
|
||||
|
||||
assertThat(loadToken).isNotNull();
|
||||
assertThat(loadToken.getHeaderName()).isEqualTo(headerName);
|
||||
assertThat(loadToken.getParameterName()).isEqualTo(parameterName);
|
||||
assertThat(loadToken.getToken()).isEqualTo(value);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
@Test
|
||||
public void loadDeferredTokenWhenDoesNotExistThenGeneratedAndSaved() {
|
||||
DeferredCsrfToken deferredCsrfToken = this.repository.loadDeferredToken(this.request, this.response);
|
||||
CsrfToken csrfToken = deferredCsrfToken.get();
|
||||
assertThat(csrfToken).isNotNull();
|
||||
assertThat(deferredCsrfToken.isGenerated()).isTrue();
|
||||
Cookie tokenCookie = this.response.getCookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
|
||||
assertThat(tokenCookie).isNotNull();
|
||||
assertThat(tokenCookie.getMaxAge()).isEqualTo(-1);
|
||||
assertThat(tokenCookie.getName()).isEqualTo(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
|
||||
assertThat(tokenCookie.getPath()).isEqualTo(this.request.getContextPath());
|
||||
assertThat(tokenCookie.getSecure()).isEqualTo(this.request.isSecure());
|
||||
assertThat(tokenCookie.getValue()).isEqualTo(csrfToken.getToken());
|
||||
assertThat(tokenCookie.isHttpOnly()).isEqualTo(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadDeferredTokenWhenExistsAndNullSavedThenGeneratedAndSaved() {
|
||||
CsrfToken generatedToken = this.repository.generateToken(this.request);
|
||||
this.request
|
||||
.setCookies(new Cookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME, generatedToken.getToken()));
|
||||
this.repository.saveToken(null, this.request, this.response);
|
||||
DeferredCsrfToken deferredCsrfToken = this.repository.loadDeferredToken(this.request, this.response);
|
||||
CsrfToken csrfToken = deferredCsrfToken.get();
|
||||
assertThat(csrfToken).isNotNull();
|
||||
assertThat(generatedToken).isNotEqualTo(csrfToken);
|
||||
assertThat(deferredCsrfToken.isGenerated()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cookieCustomizer() {
|
||||
String domainName = "example.com";
|
||||
String customPath = "/custompath";
|
||||
String sameSitePolicy = "Strict";
|
||||
this.repository.setCookieCustomizer((customizer) -> {
|
||||
customizer.domain(domainName);
|
||||
customizer.secure(false);
|
||||
customizer.path(customPath);
|
||||
customizer.sameSite(sameSitePolicy);
|
||||
});
|
||||
CsrfToken token = this.repository.generateToken(this.request);
|
||||
this.repository.saveToken(token, this.request, this.response);
|
||||
Cookie tokenCookie = this.response.getCookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
|
||||
assertThat(tokenCookie).isNotNull();
|
||||
assertThat(tokenCookie.getMaxAge()).isEqualTo(-1);
|
||||
assertThat(tokenCookie.getDomain()).isEqualTo(domainName);
|
||||
assertThat(tokenCookie.getPath()).isEqualTo(customPath);
|
||||
assertThat(tokenCookie.isHttpOnly()).isEqualTo(Boolean.TRUE);
|
||||
assertThat(tokenCookie.getAttribute("SameSite")).isEqualTo(sameSitePolicy);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withHttpOnlyFalseWhenCookieCustomizerThenStillDefaultsToFalse() {
|
||||
DSpaceCsrfTokenRepository repository = DSpaceCsrfTokenRepository.withHttpOnlyFalse();
|
||||
repository.setCookieCustomizer((customizer) -> customizer.maxAge(1000));
|
||||
CsrfToken token = repository.generateToken(this.request);
|
||||
repository.saveToken(token, this.request, this.response);
|
||||
Cookie tokenCookie = this.response.getCookie(DSpaceCsrfTokenRepository.DEFAULT_CSRF_COOKIE_NAME);
|
||||
assertThat(tokenCookie).isNotNull();
|
||||
assertThat(tokenCookie.getMaxAge()).isEqualTo(1000);
|
||||
assertThat(tokenCookie.isHttpOnly()).isEqualTo(Boolean.FALSE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setCookieNameNullIllegalArgumentException() {
|
||||
this.repository.setCookieName(null);
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.repository.setCookieName(null));
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
@Test
|
||||
public void setParameterNameNullIllegalArgumentException() {
|
||||
this.repository.setParameterName(null);
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.repository.setParameterName(null));
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
@Test
|
||||
public void setHeaderNameNullIllegalArgumentException() {
|
||||
this.repository.setHeaderName(null);
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.repository.setHeaderName(null));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void setCookieMaxAgeZeroIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.repository.setCookieMaxAge(0));
|
||||
}
|
||||
|
||||
}
|
||||
|
Reference in New Issue
Block a user