DEV Community

rhymes
rhymes

Posted on • Updated on

Why is Django REST Framework lying to me?

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/')
  },
}
Enter fullscreen mode Exit fullscreen mode

/auth/ on the server is mapped to django rest auth like this:

path('auth/', include('rest_auth.urls'))
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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'),
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
kieronjmckenna profile image
kieronjmckenna

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?

Collapse
 
rhymes profile image
rhymes

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

Collapse
 
makiten profile image
Donald

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.

Collapse
 
rhymes profile image
rhymes

Thanks, I'll try to research a bit how to do it with REST.

Collapse
 
rhymes profile image
rhymes

I've updated the post with my temporary solution.

Collapse
 
kenclary profile image
kenclary • Edited

(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.

Collapse
 
ryselis profile image
Karolis Ryselis

It seems to me you could just use Django's login view

Collapse
 
mtbsickrider profile image
Enrique Jose Padilla
Collapse
 
rhymes profile image
rhymes

I've updated the post with my temporary solution.

Collapse
 
rhymes profile image
rhymes

But how? :-)

Collapse
 
eisenheimjelid profile image
Jelid Leon

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...

Collapse
 
rhymes profile image
rhymes • Edited

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)

Collapse
 
wilkmoura profile image
Wilkinson Tavares

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.

Collapse
 
rhymes profile image
rhymes

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.

Collapse
 
imcatta profile image
imcatta

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

Collapse
 
rhymes profile image
rhymes

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.

Collapse
 
iamidan profile image
IamIdan

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...