26

Today I upgraded from Spring Security 3.1.4 with the separate java config dependency, to the new 3.2.0 release which includes java config. CSRF is on by default and I know I can disable it in my overridden configure method with "http.csrf().disable()". But suppose I don't want to disable it, but I need the CSRF token on my login page where no JSP tag libs or Spring tag libs are being used.

My login page is purely HTML that I use in a Backbone app that I've generated using Yeoman. How would I go about including the CSRF token that's contained in the HttpSession in either the form or as a header so that I don't get the "Expected CSRF token not found. Has your session expired?" exception?

Patrick Grimard
  • 7,033
  • 8
  • 51
  • 68
  • 1
    Would it make sense to provide the CSRF token in a response header on a login page perhaps? So when the login page is loaded, the response contains the value needed for the token in a header. I could then use that header in my form login submission. Is that a secure approach? – Patrick Grimard Dec 31 '13 at 21:54

4 Answers4

28

You can obtain the CSRF using the request attribute named _csrf as outlined in the reference. To add the CSRF to an HTML page, you will need to use JavaScript to obtain the token that needs to be included in the requests.

It is safer to return the token as a header than in the body as JSON since JSON in the body could be obtained by external domains. For example your JavaScript could request a URL processed by the following:

CsrfToken token = (CsrfToken) request.getAttribute("_csrf");
// Spring Security will allow the Token to be included in this header name
response.setHeader("X-CSRF-HEADER", token.getHeaderName());
// Spring Security will allow the token to be included in this parameter name
response.setHeader("X-CSRF-PARAM", token.getParameterName());
// this is the value of the token to be included as either a header or an HTTP parameter
response.setHeader("X-CSRF-TOKEN", token.getToken());

Your JavaScript would then obtain the header name or the parameter name and the token from the response header and add it to the login request.

Rob Winch
  • 21,440
  • 2
  • 59
  • 76
  • The blog seems like quite a bit more code than my answer. Any reason you prefer your solution? – Rob Winch Jan 08 '14 at 02:42
  • Also I added a JIRA for improved documentation around this area. See https://jira.springsource.org/browse/SEC-2460 – Rob Winch Jan 08 '14 at 13:58
  • I am using AngularJS with SpringSecurity3.2 and used the meta tags in my index.html as you pointed out. I stll get "Expected CSRF token not found. Has your session expired?", is there a way I can verify in ChromeTools/Firebug if Spring is really generating a CSRF token? – Himalay Majumdar Jun 03 '14 at 21:00
  • Ensure your markup includes the token in the meta tags. You also should verify that your HttpSession doesn't change between the creation of the token and the request that uses it. – Rob Winch Jun 03 '14 at 21:30
  • I am using `` and `` in the head but I dont see the placeholders being replaced by csrf token. Also, how do I make sure that the http session doesnt change before and after login. To me, it seems http session is created/changing after login. – Himalay Majumdar Jun 05 '14 at 18:12
  • The session will change after login to protect against session fixation attacks. You will need to be sure to update the CSRF token that is used based upon the responses. – Rob Winch Jun 09 '14 at 15:04
  • Well when I send the "X-CSRF-HEADER" header, i see the header in the wieshark but i also see the csrf value in the body. How do i make sure that the CSRF token is only in the header i.e. for the reason Rob mentioned already i.e. "JSON in the body could be obtained by external domains" Does this mean that i need to disable the csrf globally and then include it manually to every page that has a form? – Tito Nov 03 '14 at 13:20
  • This approach does not seem to work with Spring OAuth2. Do you have any suggestions? Here is the link: http://stackoverflow.com/questions/37061697/invalid-xsrf-token-at-oauth-token – CodeMed May 06 '16 at 22:16
  • The link posteed by @PatrickGrimard seems broken. Here what I think the new link is : https://patrickgrimard.io/2014/01/03/spring-security-csrf-protection-in-a-backbone-single-page-app/ – nanospeck Jul 14 '16 at 02:49
4

Although @rob-winch is right I would suggest to take token from session. If Spring-Security generates new token in SessionManagementFilter using CsrfAuthenticationStrategy it will set it to Session but not on Request. So it is possible you will end up with wrong csrf token.

public static final String DEFAULT_CSRF_TOKEN_ATTR_NAME = HttpSessionCsrfTokenRepository.class.getName().concat(".CSRF_TOKEN");
CsrfToken sessionToken = (CsrfToken) request.getSession().getAttribute(DEFAULT_CSRF_TOKEN_ATTR_NAME);
bsmk
  • 1,307
  • 1
  • 14
  • 26
3

Note: I'm using CORS and AngularJS.

Note²: I found Stateless Spring Security Part 1: Stateless CSRF protection which would be interesting to keep the AngularJS' way to handle CSRF.

Instead of using Spring Security CSRF Filter which is based on answers (especially @Rob Winch's one), I used the method described in The Login Page: Angular JS and Spring Security Part II.

In addition to this, I had to add Access-Control-Allow-Headers: ..., X-CSRF-TOKEN (due to CORS).

Actually, I find this method cleaner than adding headers to the response.

Here is the code :

HttpHeaderFilter.java

@Component("httpHeaderFilter")
public class HttpHeaderFilter extends OncePerRequestFilter {
    @Autowired
    private List<HttpHeaderProvider> providerList;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        providerList.forEach(e -> e.filter(request, response));

        if (HttpMethod.OPTIONS.toString().equals(request.getMethod())) {
            response.setStatus(HttpStatus.OK.value());
        }
        else {
            filterChain.doFilter(request, response);
        }
    }
}

HttpHeaderProvider.java

public interface HttpHeaderProvider {
    void filter(HttpServletRequest request, HttpServletResponse response);
}

CsrfHttpHeaderProvider.java

@Component
public class CsrfHttpHeaderProvider implements HttpHeaderProvider {
    @Override
    public void filter(HttpServletRequest request, HttpServletResponse response) {
        response.addHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, "X-CSRF-TOKEN");
    }
}

CsrfTokenFilter.java

@Component("csrfTokenFilter")
public class CsrfTokenFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        CsrfToken csrf = (CsrfToken)request.getAttribute(CsrfToken.class.getName());

        if (csrf != null) {
            Cookie cookie = WebUtils.getCookie(request, "XSRF-TOKEN");

            String token = csrf.getToken();

            if (cookie == null || token != null && !token.equals(cookie.getValue())) {
                cookie = new Cookie("XSRF-TOKEN", token);
                cookie.setPath("/");

                response.addCookie(cookie);
            }
        }

        filterChain.doFilter(request, response);
    }
}

web.xml

...
<filter>
    <filter-name>httpHeaderFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    <async-supported>true</async-supported>
</filter>
<filter-mapping>
    <filter-name>httpHeaderFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
...

security-context.xml

...
<custom-filter ref="csrfTokenFilter" after="CSRF_FILTER"/>
...

app.js

...
.run(['$http', '$cookies', function ($http, $cookies) {
    $http.defaults.transformResponse.unshift(function (data, headers) {
        var csrfToken = $cookies['XSRF-TOKEN'];

        if (!!csrfToken) {
            $http.defaults.headers.common['X-CSRF-TOKEN'] = csrfToken;
        }

        return data;
    });
}]);
Ludovic Guillaume
  • 3,237
  • 1
  • 24
  • 41
  • 3
    You shouldn't be using cookies to store the token. It's can be hacked. – Alessandro Giannone Aug 25 '19 at 20:02
  • As said before by Alessandro, do not send the token via a cookie. The whole point in CSRF is that an attacker makes the victim send a request, which automatically will use the victims cookies and session id. The token is a secret that should not be sent automatically but instead as hidden form field or header. Putting it in a cookie completely defies the purpose. – das Keks Oct 26 '20 at 17:39
-1

I use thymeleaf with Spring boot. I had the same problem. I diagnosed problem viewing source of returned html via browser. It should be look like this:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example </title>
</head>
<body>
<form method="post" action="/login">
<div><label> User Name : <input type="text" name="username" /> </label></div>
<div><label> Password: <input type="password" name="password" /> </label></div>
<input type="hidden" name="_csrf" value=<!--"aaef0ba0-1c75-4434-b6cf-62c975dcc8ba"--> />
<div><input type="submit" value="Sign In" /></div>
</form>
</body>
</html>

If you can't see this html code. You may be forgot to put th: tag before name and value. <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>

login.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example </title>
</head>
<body>
<div th:if="${param.error}"> Invalid username and password. </div>
<div th:if="${param.logout}"> You have been logged out. </div>
<form th:action="@{/login}" method="post">
<div><label> User Name : <input type="text" name="username"/> </label></div>
<div><label> Password: <input type="password" name="password"/> </label></div>
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<div><input type="submit" value="Sign In"/></div>
</form>
</body>
</html>
Iren Saltalı
  • 516
  • 7
  • 16