One thing about frameworks is that they work well when you follow the tutorials and the conventions, they usually don't when you try to step a little bit out the tracks.
I've spent too much time trying to login from a VueJS SPA to a Django app, with the following requirements/constraints:
- no jwt or tokens
- the login should just send the credentials and the csrf cookie/header and get back the
HttpOnly
session cookie which is going to be automatically sent in the following requests - that's all
How hard should this be?
I've installed django-rest-framework
and django-rest-auth
but they seem to work only with token based authentication
or other stuff, not with a plain old session based authentication. After a while I started placing breakpoints into django-rest-framework
and django-rest-auth
code bases to figure out what was going on.
This is what's happening now:
The frontend sends the credentials and the CSRF Token set by Django on the first request:
const CSRF_COOKIE_NAME = 'csrftoken'
const CSRF_HEADER_NAME = 'X-CSRFToken'
const client = axios.create({
xsrfCookieName: CSRF_COOKIE_NAME,
xsrfHeaderName: CSRF_HEADER_NAME,
})
export default {
login(username, password) {
return client.post('/auth/login/', { username, password })
},
logout() {
return client.post('/auth/logout/')
},
}
/auth/
on the server is mapped to django rest auth like this:
path('auth/', include('rest_auth.urls'))
The server (django-rest-auth) sees the login attempt and invokes the authenticator (in django-rest-framework) for SessionAuthentication:
def authenticate(self, request):
"""
Returns a `User` if the request session currently has a logged in user.
Otherwise returns `None`.
"""
# Get the session-based user from the underlying HttpRequest object
user = getattr(request._request, 'user', None)
# Unauthenticated, CSRF validation not required
if not user or not user.is_active:
return None
self.enforce_csrf(request)
# CSRF passed with authenticated user
return (user, None)
This is where it gets hairy and the documentation is not exactly clearing this up.
The only thing that this method does is checking if there's already an authenticated user on the server side (not 100% sure about this), which by logic, means that to pass you already need to be authenticated (!?!?) to use this via AJAX.
So, instead of authenticating you, it checks if the authentication you already have is correct and valid.
What I need is to authenticate the credentials, which this thing doesn't really do.
Why is it called SessionAuthentication
if it doesn't actually authenticate you?
It's not like it's useless, it's just useful for server side rendered pages with traditional logins and an AJAX calls, not a SPA.
So much time wasted because of naming :D They should have called it CheckAlreadyExistingSessionAuthentication
.
Anybody has any suggestions?
I'm thinking of implementing it myself (maybe as a custom class to django rest framework?). It seems like with all these fancy authentication mechanisms (tokens, jwt, oauth and so on) they forgot to cover the basics...
UPDATE June 25th
For now I solved (I think) by bending the frameworks a bit. It's ugly and as soon as I have a little more time I'll figure out how to do it properly, it's a pity that all of these framework neglect session based authentication.
This is what I did. On the server side I had to create a few custom things.
First I had to create a custom authentication class for Django REST Framework that simply get the credentials and uses Django's own auth layer to authenticate the user:
# adapted from https://bit.ly/2K80boT and https://bit.ly/2JV1iMK
from django.contrib import auth
from django.contrib.auth.models import User
from django.middleware.csrf import CsrfViewMiddleware
from django.utils.translation import ugettext_lazy as _
from rest_framework import authentication, exceptions
class CSRFCheck(CsrfViewMiddleware):
def _reject(self, request, reason):
# Return the failure reason instead of an HttpResponse
return reason
class DRFSessionAuthentication(authentication.BaseAuthentication):
'Session authentication against username/password for DRF'
def authenticate(self, request):
'''
Returns a User if a correct username and password have been supplied
using Django Session Authentication. Otherwise returns None.
'''
username = request.data.get('username')
password = request.data.get('password')
return self.authenticate_credentials(username, password, request)
def authenticate_credentials(self, userid, password, request=None):
'''
Authenticate the userid and password against username and password
with optional request for context.
'''
credentials = {
auth.get_user_model().USERNAME_FIELD: userid,
'password': password
}
user = auth.authenticate(request=request, **credentials)
if user is None:
raise exceptions.AuthenticationFailed(
_('Invalid username/password.')
)
if not user.is_active:
raise exceptions.AuthenticationFailed(
_('User inactive or deleted.')
)
self.enforce_csrf(request)
return (user, None)
def authenticate_header(self, request):
return 'Session'
def enforce_csrf(self, request):
'Enforce CSRF validation for session based authentication.'
reason = CSRFCheck().process_view(request, None, (), {})
if reason:
# CSRF failed, bail with explicit error message
raise exceptions.PermissionDenied('CSRF Failed: %s' % reason)
Django automatically creates the session cookies, sets it to http only and (if you tell it so) sets it secure on production.
Once you have the http only session cookie in your hand the user is actually authenticated, at each HTTP call the browser will send the session token to the server.
(fyi: a seession cookie is just a token in a HTTP header, basically the same thing as those fancy auth tokens)
The next step is to have the client check if the user is authenticated, which it can just do by calling the server which responds with something like this:
def user(request):
'Returns a user object if authenticated, 401 otherwise'
if request.user.is_authenticated:
user = request.user
return JsonResponse({
'id': user.id,
'username': user.username,
'email': user.email,
'first_name': user.first_name,
'last_name': user.last_name
})
else:
return JsonResponse({}, status=status.HTTP_401_UNAUTHORIZED)
To make it all work I had to wire up the routes (again, leaving django-rest-auth
aside a bit) like this:
from django.contrib.auth import views as auth_views
from app import views
path('auth/login/', views.SessionLoginView.as_view(), name='login'),
path('auth/logout/', auth_views.logout, {'next_page': '/'}, name='logout'),
path('auth/user/', views.user, name='user'),
SessionLoginView
is a custom class that I had to write to bypass all the token authentication machinery django-rest-auth puts in:
class SessionLoginView(LoginView):
def login(self):
self.user = self.serializer.validated_data['user']
self.process_login()
def get_response(self):
return Response('', status=status.HTTP_204_NO_CONTENT)
All of these is just ugly and untested and I wasted some time to solve something that should be super simple to do...
Top comments (17)
Great Article, the amount of stack overflow articles I've read linking the rest framework section on session authentication (which has no mention of implementation), as well as answers saying put my JWT in local storage and forget about it, had left me feeling defeated using the rest framework and a SPA.
Suprising for a framework that is touted as having fantastic docs...
Two years on, have you found a cleaner solution?
Hi! Unfortunately I'm not actively working on that project anymore.
I'm a bit surprised two years later there's still not a solution for that :D
I did a lot of Vue + Django projects in 2017 and early 2018. I used different libraries with DRF for JWT, but if I remember correctly, I could use an
authenticate
method in a view. One project I'm pretty sure I used the login view, another I think I did something custom.Thanks, I'll try to research a bit how to do it with REST.
I've updated the post with my temporary solution.
(apologies for the thread necromancy)
I ran into this exact problem, trying to get an SPA to use DRF's session authentication. The docs for this only suggest it could work, but never really say how. I got lucky with some googling, and thought I would share.
1) I needed to write a new login view. I basically copied one from testdriven.io/courses/real-time-ap... after much searching. I also copied from there for sign up and logout views.
2) On the backend, I included
{% csrf_token %}
in the index.html template that bootstraps the SPA, so that the SPA gets the cookie when it loads.3) On the frontend, I made sure to include the CSRF cookie as a 'X-CSRFToken' header.
It seems to me you could just use Django's login view
dev.to/marcuscreo/the-4-letter-wor...
I've updated the post with my temporary solution.
But how? :-)
I used Basic Auth without CSRF, and did work for me. But you can implement JWT Auth in your Django project medium.com/netscape/full-stack-dja...
Thanks for the heads up! I wanted to avoid JWT, that's why I was trying to simply use the django session cookie as a token.
I think I've succeeded, I just need to read a bit more about DRF so I can clean up the code (and maybe remove django-rest-auth as a dependency)
I appreciate your piece on this,
I'm building a system using Django/DRF as backend and React Js as frontend.
The backend uses an already existent database, user and auth models.
Figure out auth in SPA is tricky... store tokens in localstorage or sessionstorage isn't the safest practice but is the fastest to get it done, imho this is why many devs do it this way.
I totally agree with you, implement the session cookie should be easy as it seems to me the right solution for this problem.
Yeah, probably if they lowered the bar on how to use standard sessions they would be used more. A lot of doc is also JWT first which doesn't always help.
Hi! I'm not a django rest expert, but i think i figured out what's wrong.
The SessionAuthentication class is not used to sign in the user, but to check if a request comes from an authenticated user.
To login using regular session cookie you're supposed to use the standard LoginView class
Thanks for the comment! I figured
SessionAuthentication
wasn't the correct one, after a few trials.The only issue I see using directly LoginView is that it entails a server side template. That's why I didn't use it. Check the update in the post about it.
I'm still a little surprised about how complicated is this but it could be 100% because I'm not a Django expert either.
As of today, have you found a better way to deal with it? ALtho not so bad of a solution, it is still an ugly one...