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 24a0617fb3..83da28b766 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 @@ -30,9 +30,9 @@ import org.springframework.web.util.WebUtils; * * How it works: * - * 1. Backend generates XSRF token & stores in a *server-side* cookie named DSPACE-XSRF-COOKIE. This cookie is - * only readable to clients on the same domain. But, it is returned (by user's browser) on every subsequent request - * to backend. See "saveToken()" method below. + * 1. Backend generates XSRF token & stores in a *server-side* cookie named DSPACE-XSRF-COOKIE. By default, this cookie + * is not readable to JS clients (HttpOnly=true). But, it is returned (by user's browser) on every subsequent + * request to backend. See "saveToken()" method below. * 2. At the same time, backend also sends the generated XSRF token in a header named DSPACE-XSRF-TOKEN to client. * See "saveToken()" method below. * 3. Client MUST look for DSPACE-XSRF-TOKEN header in a response from backend. If found, the client MUST store/save @@ -148,14 +148,6 @@ public class DSpaceCsrfTokenRepository implements CsrfTokenRepository { return null; } - // Second, verify either the header or param has been sent. This is a customization for DSpace. - // Because the server-side cookie is ALWAYS sent back, we need to verify the client has also sent the token in - // some other way. This ensures that we only *change* the Token when it has been used or attempted to be used. - //if (!StringUtils.hasLength(request.getHeader(this.headerName)) && - // !StringUtils.hasLength(request.getParameter(this.parameterName))) { - // return null; - // } - // If we got here, we know a token exists in the cookie and *either* the header or the parameter. // So, this just sends the token info back so that it can be validated by Spring Security. return new DefaultCsrfToken(this.headerName, this.parameterName, token); diff --git a/dspace-server-webapp/src/main/webapp/index.html b/dspace-server-webapp/src/main/webapp/index.html index c80a49e3f5..829c614393 100644 --- a/dspace-server-webapp/src/main/webapp/index.html +++ b/dspace-server-webapp/src/main/webapp/index.html @@ -75,7 +75,7 @@ <%= HAL.truncateIfUrl(rel) %> <%= link.title || '' %> - <%= link.name ? 'name: ' + link.name : 'index: ' + i %> + <%= link.name ? 'name: ' + link.name : 'index: ' + i %> <% if (HAL.isUrl(rel)) { %> @@ -250,6 +250,7 @@ Content-Type: application/json + @@ -260,6 +261,7 @@ Content-Type: application/json + diff --git a/dspace-server-webapp/src/main/webapp/js/hal/http/client.js b/dspace-server-webapp/src/main/webapp/js/hal/http/client.js index 2c8ac00029..eab1fe5e97 100644 --- a/dspace-server-webapp/src/main/webapp/js/hal/http/client.js +++ b/dspace-server-webapp/src/main/webapp/js/hal/http/client.js @@ -12,23 +12,33 @@ HAL.Http.Client = function(opts) { this.defaultHeaders = {'Accept': 'application/hal+json, application/json, */*; q=0.01'}; var authorizationHeader = getAuthorizationHeader(); authorizationHeader ? this.defaultHeaders.Authorization = authorizationHeader : ''; - // If we find a CSRF header (in a cookie), send it back in X-XSRF-Token header - var csrfToken = getCSRFToken(); - csrfToken ? this.defaultHeaders['X-XSRF-Token'] = csrfToken : ''; // Write all headers to console (for easy debugging) - console.log(this.defaultHeaders); + //console.log(this.defaultHeaders); this.headers = this.defaultHeaders; }; /** - * Get CSRF Token by parsing it out of the DSPACE-XSRF-COOKIE (server-side) cookie set by our DSpace server webapp + * Get CSRF Token by parsing it out of the "MyHalBrowserCsrfToken" cookie. + * This cookie is set in login.html after a successful login occurs. **/ function getCSRFToken() { - var cookie = document.cookie.match('(^|;)\\s*' + 'DSPACE-XSRF-COOKIE' + '\\s*=\\s*([^;]+)'); - if(cookie != undefined) { + var cookie = document.cookie.match('(^|;)\\s*' + 'MyHalBrowserCsrfToken' + '\\s*=\\s*([^;]+)'); + if (cookie != null) { return cookie.pop(); } else { - return undefined; + return null; + } +} + +/** + * Check current response headers to see if the CSRF Token has changed. If a new value is found in headers, + * save the new value into our "MyHalBrowserCsrfToken" cookie. + **/ +function checkForUpdatedCSRFTokenInResponse(jqxhr) { + // look for DSpace-XSRF-TOKEN header & save to our MyHalBrowserCsrfToken cookie (if found) + var updatedCsrfToken = jqxhr.getResponseHeader('DSPACE-XSRF-TOKEN'); + if (updatedCsrfToken != null) { + document.cookie = "MyHalBrowserCsrfToken=" + updatedCsrfToken; } } @@ -38,12 +48,13 @@ function getCSRFToken() { **/ function getAuthorizationHeader() { var cookie = document.cookie.match('(^|;)\\s*' + 'MyHalBrowserToken' + '\\s*=\\s*([^;]+)'); - if(cookie != undefined) { + if (cookie != null) { return 'Bearer ' + cookie.pop(); } else { - return undefined; + return null; } } + function downloadFile(url) { var request = new XMLHttpRequest(); request.open('GET', url, true); @@ -89,6 +100,9 @@ HAL.Http.Client.prototype.get = function(url) { }, headers: this.headers, success: function(resource, textStatus, jqXHR) { + // NOTE: A GET never requires sending an CSRF Token, but the response may send an updated token back. + // So, we need to check if a token came back in this GET response. + checkForUpdatedCSRFTokenInResponse(jqXHR); self.vent.trigger('response', { resource: resource, jqxhr: jqXHR, @@ -111,6 +125,18 @@ HAL.Http.Client.prototype.request = function(opts) { opts.dataType = 'json'; opts.xhrFields = opts.xhrFields || {}; opts.xhrFields.withCredentials = opts.xhrFields.withCredentials || true; + opts.headers = opts.headers || {}; + // If CSRFToken exists, append as a new X-XSRF-Token header + var csrfToken = getCSRFToken(); + if (csrfToken != null) { + opts.headers['X-XSRF-Token'] = csrfToken; + } + + // Also check response to see if CSRF Token has been updated + opts.success = function(resource, textStatus, jqXHR) { + checkForUpdatedCSRFTokenInResponse(jqXHR); + }; + self.vent.trigger('location-change', { url: opts.url }); return jqxhr = $.ajax(opts); }; diff --git a/dspace-server-webapp/src/main/webapp/login.html b/dspace-server-webapp/src/main/webapp/login.html index 98134e274d..8f6e33ce0f 100644 --- a/dspace-server-webapp/src/main/webapp/login.html +++ b/dspace-server-webapp/src/main/webapp/login.html @@ -71,7 +71,13 @@