I'm really stuck. Here's what I'm trying to do.
- KEEP CSRF On. - please don't tell me to turn it off.
- I have an API app run by Django and Django Rest Framework
- I have a frontend app run by Vue
- I have installed django-cors-headers to manage CORS
Everything works great localy. As soon as I move it to production, I start getting CSRF errors. Here's how everything works.
I've seen answers all over that have said everything from turning off CSRF to allowing all for all the things. I want to do this right and not just shut things off and open everything up and end up with a security hole.
So, here's what I have.
Installed: django-cors-headers django-rest-framework drf-nested-routers ... and others
I have the api running at api.websitename.com and the Vue.js app is running at websitename.com.
GET requests work great. OPTION requests seem to work.
Any risky request does not work.
For my CORS I have 'corsheaders.middleware.CorsMiddleware',
installed before my other MIDDLEWARE
.
Then my CORS settings are:
CORS_ALLOW_CREDENTIALS = True
CORS_ORIGIN_WHITELIST = (
'*.websitename.com',
)
And my CSRF settings are:
CSRF_TRUSTED_ORIGINS = [
"api.websitename.com",
]
No matter how I play with these, I end up with a CSRF token error.
I've tried the approach of doing something like this in my Vue App.vue file:
mounted () {
this.getCSRFToken()
},
methods: {
getCSRFToken () {
return axios.get('token/').then(response => {
axios.defaults.headers.common['x-csrftoken'] = Cookies.get('csrftoken')
}).catch(error => {
return Promise.reject(error.response.data)
})
}
}
The idea being that I get a CSRF token as soon as the APP loads in the browser. But even with that, I'm getting failed CSRF token errors when the app tries to do anything except a GET or OPTION.
Here's the view that returns the token incase youre curios:
class CSRFTokenView(APIView):
permission_classes = (permissions.AllowAny,)
@method_decorator(ensure_csrf_cookie)
def get(self, request):
return HttpResponse()
I realize I might be mixing problems here, but any suggestions that could help me trouble shoot this are welcome.
By far the easiest way to resolve this is to serve everything from the same domain. You can have your CDN or proxy direct
/api
calls to one server and the rest to the frontend server. This way there is no need to worry about CORS at all.To get this working, I think you're just missing
withCredentials = true
in AXIOS configuration. Django requires the CSRF cookie to be sent and cookies are not sent over cross origin requests whenwithCredentials
is not set.Another setting that might be missing is Djano's
SESSION_COOKIE_DOMAIN
. You should set it like this:That first dot is important because it tells Django and then the web browser to use the cookie for
*.mywebsite.com
includingapi.mywebsite.com
.If it all still fails, I suggest setting a breakpoint on Django's CSRF middleware to see what's missing to make it work.
First of all you want to use SessionAuthentication:
This will enforce CSRF, except for anonymous users (more on this in a bit). For browser frontends the easiest solution is to have both the (browser) frontend and backend under the same domain - this lets you avoid CORS - as suggested by comments above. If you have other clients then just go with tokens (DRF tokens or JWT) - but these are not safe for browser usage due to the danger of XSS attacks (storing tokens in localStorage is inherently insecure).
As you are using axios, CSRF setup is dead easy:
So you should have safe sessions with CSRF enforced. Almost. To quote the linked page above:
This is icky - you either have to just use Django server-side views which makes your SPA design somewhat more complicated or recreate login and other auth views in DRF, with the caveat of using the @csrf_protect method decorator to enforce CSRF on these "anonymous" views. Obviously such views will break for token-using clients so you probably want to use different endpoints for these (maybe re-using the same base classes). So your browser login uses /auth/browser/login/ and your mobile login /auth/mobile/login/, the former wrapped using @csrf_protect.
Recreating login and other auth views from scratch should be done carefully after studying the contrib auth source code; for vanilla requirements I would recommend pre-existing solutions like django-rest-auth and django-all-auth. The django-rest-auth package however is not well designed for browser frontends and forces the usage of token generation, plus you would need to wrap the views as described above. On the other hand, all-auth provides AJAX responses for JS clients and might be a better bet.