Besides the session cookie (which is kind of standard), I don't want to use extra cookies.
I found a solution which works for me when building a Single Page Web Application (SPA), with many AJAX requests. Note: I am using server side Java and client side JQuery, but no magic things so I think this principle can be implemented in all popular programming languages.
My solution without extra cookies is simple:
Store the CSRF token which is returned by the server after a succesful login in a global variable (if you want to use web storage instead of a global thats fine of course). Instruct JQuery to supply a X-CSRF-TOKEN header in each AJAX call.
The main "index" page contains this JavaScript snippet:
// Intialize global variable CSRF_TOKEN to empty sting.
// This variable is set after a succesful login
window.CSRF_TOKEN = '';
// the supplied callback to .ajaxSend() is called before an Ajax request is sent
$( document ).ajaxSend( function( event, jqXHR ) {
jqXHR.setRequestHeader('X-CSRF-TOKEN', window.CSRF_TOKEN);
});
On successul login, create a random (and long enough) CSRF token, store this in the server side session and return it to the client. Filter certain (sensitive) incoming requests by comparing the X-CSRF-TOKEN header value to the value stored in the session: these should match.
Sensitive AJAX calls (POST form-data and GET JSON-data), and the server side filter catching them, are under a /dataservice/* path. Login requests must not hit the filter, so these are on another path. Requests for HTML, CSS, JS and image resources are also not on the /dataservice/* path, thus not filtered. These contain nothing secret and can do no harm, so this is fine.
@WebFilter(urlPatterns = {"/dataservice/*"})
...
String sessionCSRFToken = req.getSession().getAttribute("CSRFToken") != null ? (String) req.getSession().getAttribute("CSRFToken") : null;
if (sessionCSRFToken == null || req.getHeader("X-CSRF-TOKEN") == null || !req.getHeader("X-CSRF-TOKEN").equals(sessionCSRFToken)) {
resp.sendError(401);
} else
chain.doFilter(request, response);
}