With Spring Security 3.2.0.RELEASE, how can I get the CSRF token in a page that is purely HTML with no tag libs
Asked Answered
N

4

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?

Nerveless answered 31/12, 2013 at 19:21 Comment(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?Nerveless
S
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.

Spheroidal answered 6/1, 2014 at 22:17 Comment(10)
The blog seems like quite a bit more code than my answer. Any reason you prefer your solution?Spheroidal
Also I added a JIRA for improved documentation around this area. See jira.springsource.org/browse/SEC-2460Spheroidal
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?Empery
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.Spheroidal
I am using <meta name="_csrf" content="${_csrf.token}"> and <meta name="_csrf_header" content="${_csrf.headerName}"> 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.Empery
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.Spheroidal
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?Bloodletting
This approach does not seem to work with Spring OAuth2. Do you have any suggestions? Here is the link: #37062197Antonietta
The link posteed by @PatrickGrimard seems broken. Here what I think the new link is : patrickgrimard.io/2014/01/03/…Svetlanasvoboda
The link to the reference is broken.Grigsby
A
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);
Abruption answered 24/9, 2014 at 11:3 Comment(1)
might be cleaner to use HttpSessionCsrfTokenRepository#loadToken()Drawbar
B
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;
    });
}]);
Benzine answered 8/6, 2015 at 10:5 Comment(2)
You shouldn't be using cookies to store the token. It's can be hacked.Pelagi
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.Allrud
A
-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>
Atalayah answered 30/9, 2015 at 18:33 Comment(1)
again. The question asks for pure html - not JSP with th:value="${_csrf.token}"Nebulose

© 2022 - 2024 — McMap. All rights reserved.