Maxime Dupré
Maxime Dupré

Reputation: 5757

How can I authenticate a user with a query parameter on any url?

Let's say the user lands on https://example.com/any/page?token=hhdo28h3do782.

What's the recommended way to authenticate and login a user with the query string?

I was thinking about creating some sort of catch-all view (I'd also like to know how to do this :D) that calls authenticate(). Then I would have in place a custom backend that would authenticate the user.

Is this the ideal way to achieve what I want?

Cheers!

Upvotes: 2

Views: 4212

Answers (3)

damon
damon

Reputation: 15128

To do this, you need to create a custom authentication backend that validates api keys.

In this example, the request is checked for a valid token automatically. You don't need to modify and of your views at all. This is because it includes custom middleware that authenticates the user.

For brevity, I'm assuming that the valid user tokens are stored in a model that is foreign keyed to the django auth.User model.

# my_project/authentication_backends.py
from django.contrib import auth
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.models import User
from django.contrib.auth.middleware import AuthenticationMiddleware

TOKEN_QUERY_PARAM = "token"

class TokenMiddleware(AuthenticationMiddleware):
    def process_request(self, request):
        try:
            token = request.GET[TOKEN_QUERY_PARAM]
        except KeyError:
            # A token isn't included in the query params
            return

        if request.user.is_authenticated:
            # Here you can check that the authenticated user has the same `token` value
            # as the one in the request. Otherwise, logout the already authenticated
            # user.
            if request.user.token.key == token:
                return
            else:
                auth.logout(request)

        user = auth.authenticate(request, token=token)
        if user:
            # The token is valid. Save the user to the request and session.
            request.user = user
            auth.login(request, user)

class TokenBackend(ModelBackend):
    def authenticate(self, request, token=None):
        if not token:
            return None

        try:
            return User.objects.get(token__key=token)
        except User.DoesNotExist:
            # A user with that token does not exist
            return None

    def get_user(self, user_id):
        try:
            return User.objects.get(pk=user_id)
        except User.DoesNotExist:
            return None

Now, you can add the paths to AUTHENTICATION_BACKENDS and MIDDLEWARE in your settings.py in addition to any existing backends or middleware you may already have. If you're using the defaults, it would look like this:

MIDDLEWARE = [
    # ...
    "django.contrib.auth.middleware.AuthenticationMiddleware",

    # This is the dotted path to your backend class. For this example,
    # I'm pretending that the class is in the file:
    #     my_project/authentication_backends.py
    "my_project.authentication_backends.TokenMiddleware",

    # ...
]

AUTHENTICATION_BACKENDS = [
    "django.contrib.auth.backends.ModelBackend",
    "my_project.authentication_backends.TokenBackend",
]

Upvotes: 3

Andee
Andee

Reputation: 793

So, start with a way of managing your tokens. Here's a basic model:

class Token(models.Model):
    code = models.CharField(max_length=255)
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    expires = models.DateTimeField()

A custom authentication backend can be produced to check the validity of the tokens:

class TokenAuthenticationBackend(ModelBackend):
    def authenticate(self, request, token=None):
        try:
            token = Token.objects.get(code=token, expires__gte=now())
        except Token.DoesNotExist:
            return None
        else:
            return token.user

If you're using class-based views, you could write a mixin that checks for the presence of the token then does your authentication logic:

class UrlTokenAuthenticationMixin:
    def dispatch(self, request, *args, **kwargs):
        if 'token' in request.GET:
            user = authenticate(request, request.GET['token'])
            if user:
                login(request, user)
        return super(UrlTokenAuthenticationMixin, self).dispatch(request, *args, **kwargs)

To use this on a given view, just declare your views as follows:

class MyView(UrlTokenAuthenticationMixin, TemplateView):
    # view code here

For example.

An alternative way to implement this as a blanket catch-all would be to use middleware rather than a mixin:

class TokenAuthMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        if 'token' in request.GET:
            user = authenticate(request, request.GET['token'])
            if user:
                login(request, user)
        return self.get_response(request)

Upvotes: 1

JPG
JPG

Reputation: 88659

I assume you are using the Django REST Framework and also enabled the TokenAuthentication mechanism in your project. If so, go ahead with this,

from rest_framework.authentication import TokenAuthentication


class QueryParamAuthentication(TokenAuthentication):
    query_param_name = 'token'

    def authenticate(self, request):
        token = request.query_params.get(self.query_param_name)
        if token:
            return self.authenticate_credentials(token)
        return None

and then, change DRF DEFAULT_AUTHENTICATION_CLASSES as

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'dotted.path.to.QueryParamAuthentication'
    ),
    # rest of your DRF settings...

}

Update

to do this without DRF, you have to write custom model backend (which is a bit lengthy topic)

Refer: Writing an authentication backend

Upvotes: 2

Related Questions