diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/DSpaceCsrfAuthenticationStrategy.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/DSpaceCsrfAuthenticationStrategy.java
index 4949c9701a..20b0c14773 100644
--- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/DSpaceCsrfAuthenticationStrategy.java
+++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/DSpaceCsrfAuthenticationStrategy.java
@@ -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).
*
- * 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.
*
* 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.
*
@@ -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");
+ }
+
}
diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/DSpaceCsrfTokenRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/DSpaceCsrfTokenRepository.java
index 2ea3a3a0ad..e2ffbb9b81 100644
--- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/DSpaceCsrfTokenRepository.java
+++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/DSpaceCsrfTokenRepository.java
@@ -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
*
- * 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.
+ *
* 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 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 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)}
*
- * 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.
*
- * 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 true
.
- *
- * @param cookieHttpOnly true
sets the HttpOnly attribute, false
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;
+ }
}
diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/DSpaceCsrfTokenRequestHandler.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/DSpaceCsrfTokenRequestHandler.java
new file mode 100644
index 0000000000..f8af92fa58
--- /dev/null
+++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/DSpaceCsrfTokenRequestHandler.java
@@ -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.
+ *
+ * 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.
+ *
+ * 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) {
+ /*
+ * 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);
+ }
+}
diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WebSecurityConfiguration.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WebSecurityConfiguration.java
index 0f9b83a4de..6fb8f285d1 100644
--- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WebSecurityConfiguration.java
+++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WebSecurityConfiguration.java
@@ -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());
}
diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenRestAuthenticationServiceImpl.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenRestAuthenticationServiceImpl.java
index 059e9aada4..a2928fc96f 100644
--- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenRestAuthenticationServiceImpl.java
+++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenRestAuthenticationServiceImpl.java
@@ -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);
}
}
diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/security/DSpaceCsrfTokenRepositoryTest.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/security/DSpaceCsrfTokenRepositoryTest.java
index 391b60a0c1..95ce6bb37f 100644
--- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/security/DSpaceCsrfTokenRepositoryTest.java
+++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/security/DSpaceCsrfTokenRepositoryTest.java
@@ -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 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));
+ }
}