Coding Guy
Coding Guy

Reputation: 11

request.user.is_authenticated is returning False even though `X-Session-Token` header is sent in django-allauth (headless) {app}

I'm using django-allauth (headless, {app}) as my authentication backend along with django-ninja. I've set-up django-allauth configurations properly. If I login using POST http://127.0.0.1:8000/_allauth/app/v1/auth/login with a valid body (username & pass), it successfully responds with a session token. As instructed in the docs, when I set X-Session-Token header with this token value & send a request to http://localhost:8000/_allauth/app/v1/auth/session (Get authentication status), it also responds successfully with json body containing "meta": {"is_authenticated": true}, meaning the user is authenticated.

So far, everything is fine & the authentication is working perfectly. But, when I hit another api url (for example: GET http://localhost:8000/api/website/blog_category with header X-Session-Token: <TheToken>), then I find from that function, request.user is always AnonymousUser. My understanding was, since django-allauth has its own middleware, it should have populated the request.user correctly since a valid X-Session-Token is correctly sent as a header.

Hers's my urls.py:

urlpatterns = [
    path('admin/', admin.site.urls),
    path("accounts/", include("allauth.urls")),
    path("_allauth/", include("allauth.headless.urls")),
    path("api/", api.urls),
] 

api.py:

from ninja import NinjaAPI

api = NinjaAPI()
api.add_router("website/", "website.api.router")

Here is my website.api.py:

from ninja import Router
from ninja.security import django_auth # setting auth=django_auth, always returns permission denied
from typing import List
from .models import blog_category
from .schema import BlogCategoryRetrieveSchema

router = Router(auth=None) # since django-auth returns permission denied, so lets run without auth to print request.user & request.META.get('HTTP_X_SESSION_TOKEN')

@router.get("/blog_category", response=List[BlogCategoryRetrieveSchema])
async def blog_category_retrieve(request):
    print(request.user)
    print(request.META.get('HTTP_X_SESSION_TOKEN'))
    # logic here
    return something

Console says:

AnonymousUser  # for print(request.user)
8jdtkzt69n9xp2xlqf9r6hc1eyw78p0k  # for print(request.META.get('HTTP_X_SESSION_TOKEN'))

Meaning, session token is properly sent but django-allauth is not authenticating the user. Shouldn't django-allauth have done it automatically from the middleware? or am I missing anything?

I was expecting that django-allauth's middleware would take care of authenticating the user and populate request.user if a valid x-session-token is sent, in every request. Though, allauth's api urls (headless, {app}) are working perfectly; when requesting to other urls, I don't see any authentication being done leaving the request.user as AnonymousUser. I tried:

from ninja.security import django_auth

router = Router(auth=django_auth)
# But setting auth=django_auth, always returns permission denied

Now, I could write a middleware or maybe a function that uses 'Get authentication status' functionality from 'django-allauth' to validate a user & use this mechanism to set 'auth' (ninja). But isn't there any generic way of doing this? Am I missing something?

Upvotes: 1

Views: 383

Answers (4)

Coding Guy
Coding Guy

Reputation: 11

My approach was (this way we can have a little more manual control):

from allauth.headless.account.views import SessionView

class get_user_from_sessionview(SessionView):
    def dispatch(self, request, *args, **kwargs):
        return request.user

then, we can easily use it like this:

get_user_from_sessionview.as_api_view(client='app')(request) # client is either 'app' or 'browser'

If I remember correctly, it returned the user object.

Upvotes: 0

Mic
Mic

Reputation: 127

As per this comment from the man himself (pennersr) I wrote a custom authentication to look up user by session and then added that authentical class to my DRF View's authentication_classes

from django.contrib.sessions.models import Session
from django.contrib.auth.models import User
from rest_framework import authentication
from rest_framework import exceptions


class SessionTokenAuthentication(authentication.BaseAuthentication):
    def authenticate(self, request):
        session_token = request.META.get('HTTP_X_SESSION_TOKEN')
        # Alternately you could get from the request.headers
        # session_token = request.headers.get('X-Session-Token')
        if not session_token:
            return None # authentication did not succeed
        try:
            s = Session.objects.get(session_key=session_token)
            decoded = s.get_decoded()
            user = User.objects.get(pk=decoded['_auth_user_id']) # get the user
        except (Session.DoesNotExist, User.DoesNotExist):
            raise exceptions.AuthenticationFailed('No such user') # raise exception if user does not exist 

        return (user, None) # authentication successful

And then you need to either add to your DEFAULT_AUTHENTICATION_CLASSES in settings.py, or include this in your view (example below)

from rest_framework import generics
from rest_framework.permissions import IsAuthenticated, AllowAny
from myapp.cusom_auth import SessionTokenAuthentication

class MyCreateAPIView(generics.CreateAPIView):
    permission_classes = (IsAuthenticated,) 
    authentication_classes = [SessionTokenAuthentication]
    ...

Upvotes: 0

zeronineseven
zeronineseven

Reputation: 169

I had a similar issue and in my case the problem was stemming from a default "django.contrib.sessions.middleware.SessionMiddleware" implementation. It seems that this vanilla implementation looks for session_key only in cookies of a request. However, when you explicitly pass allauth's "X-Session-Token" header, Django has no idea that it should look for session_key there. The solution that seems to work (with database-based sessions at least) is to inherit from default SessionMiddleware:

from django.conf import settings
from django.contrib.sessions.middleware import SessionMiddleware as _SessionMiddleware


class SessionMiddleware(_SessionMiddleware):
    def process_request(self, request):
        session_key = request.COOKIES.get(settings.SESSION_COOKIE_NAME)
        if session_key is None:
            session_key = request.headers.get("X-Session-Token")
        request.session = self.SessionStore(session_key)

and add this implementation into settings.INSTALLED_APPS in place of the original one.

Disclaimer: I'm very new to Django myself and haven't thought through all the security implications of this solution.

Upvotes: 0

Kien Phan
Kien Phan

Reputation: 1

Hi this might be an issue similar to mine. A fix is created. Here is the github issue https://github.com/pennersr/django-allauth/issues/3981.

Upvotes: 0

Related Questions