Update HAL browser to use DSPACE-XSRF-TOKEN header and store token in custom MyHalBrowserCsrfToken Cookie. Minor comment fixes to TokenRepo

This commit is contained in:
Tim Donohue
2021-01-05 17:30:11 -06:00
parent acaa1dbc64
commit b35a3f71be
4 changed files with 59 additions and 28 deletions

View File

@@ -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);

View File

@@ -75,7 +75,7 @@
<tr>
<td><strong><%= HAL.truncateIfUrl(rel) %></strong></td>
<td><%= link.title || '' %></td>
<td><%= link.name ? 'name: ' + link.name : 'index: ' + i %></a></td>
<td><%= link.name ? 'name: ' + link.name : 'index: ' + i %></td>
<td>
<% if (HAL.isUrl(rel)) { %>
<a class="dox" href="<%= HAL.normalizeUrl(HAL.buildUrl(rel)) %>"><i class="icon-book"></i></a>
@@ -250,6 +250,7 @@ Content-Type: application/json
</div>
</script>
<!-- Customized (to use WebJars) for DSpace -->
<script src="webjars/jquery/dist/jquery.min.js"></script>
<script src="browser/vendor/js/underscore.js"></script>
<script src="browser/vendor/js/backbone.js"></script>
@@ -260,6 +261,7 @@ Content-Type: application/json
<script src="browser/js/hal.js"></script>
<script src="browser/js/hal/browser.js"></script>
<!-- Customized for DSpace -->
<script src="js/hal/http/client.js"></script>
<script src="browser/js/hal/resource.js"></script>

View File

@@ -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);
};

View File

@@ -71,7 +71,13 @@
<script>
$(document).ready(function() {
var successHandler = function(result, status, xhr) {
// look for Authorization header & save to a MyHalBrowserToken cookie
document.cookie = "MyHalBrowserToken=" + xhr.getResponseHeader('Authorization').split(" ")[1];
// look for DSpace-XSRF-TOKEN header & save to a MyHalBrowserCsrfToken cookie (if found)
var csrfToken = xhr.getResponseHeader('DSPACE-XSRF-TOKEN');
if (csrfToken!=null) {
document.cookie = "MyHalBrowserCsrfToken=" + csrfToken;
}
toastr.success('You are now logged in. Please wait while we redirect you...', 'Login Successful');
setTimeout(function() {
window.location.href = window.location.pathname.replace("login.html", "");
@@ -101,7 +107,9 @@
beforeSend: function (xhr, settings) {
// If CSRF token found in cookie, send it back as X-XSRF-Token header
var csrfToken = getCSRFToken();
xhr.setRequestHeader('X-XSRF-Token', csrfToken ? csrfToken : '');
if (csrfToken != null) {
xhr.setRequestHeader('X-XSRF-Token', csrfToken);
}
},
success : successHandler,
error : function(result, status, xhr) {
@@ -139,14 +147,15 @@
}
/**
* 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;
}
}
@@ -164,7 +173,9 @@
beforeSend: function (xhr, settings) {
// If CSRF token found in cookie, send it back as X-XSRF-Token header
var csrfToken = getCSRFToken();
xhr.setRequestHeader('X-XSRF-Token', csrfToken ? csrfToken : '');
if (csrfToken != null) {
xhr.setRequestHeader('X-XSRF-Token', csrfToken);
}
},
success : successHandler,
error : function() {