Since Django 1.5.5, CSRF tokens are rotated on login. That makes it trivial to trigger a CSRF error in the following way:
- Open your app's login page in two different browser tabs.
- Log in using tab 1
- Log in using tab 2
The CSRF token sent along with the second login attempt (in a cookie) won't match the token that was embedded in the form, and so a CSRF error will be displayed.
It could be argued that the above is an odd/contrived thing to do, and so displaying a CSRF error here isn't too bad. Fine. But there's another, more subtle, way to trigger the same thing:
- Open your app's login page
- Click the login button twice, fairly fast
This problem is most obvious when the view that is displayed after login is quite slow to load. What happens is as follows:
POST /login/
logs the user in, and performs the CSRF token rotation.- The response is a 302, including a
set-cookie
header containing a new CSRF token, as well as alocation
header containing (say)/dashboard/
. - The browser sends a
GET
request to/dashboard/
, which may take some time to load. While this page is loading, the login form is still displayed to the user. - The user may get impatient (or just confused) and assume that they didn't click the login button correctly, so they click it again. This cancels the pending
GET
request to/dashboard/
and makes anotherPOST
to/login/
. But now, the CSRF token in the user's cookie doesn't match the one in the form. So they see the CSRF error page.
This is actually quite a common thing - lots of users (especially non-technical people) double-click everything, because they don't know any different.
The obvious solution is to disable the login button with JavaScript after the first form submission, but this feels like a bit of a hack.
A possible solution was suggested by @tomchristie. Django could only rotate the token in
django.contrib.auth.login
if the user isn't already logged in.