Custom authentication backend works but doens't work with Django Rest's Token Authentication

46 Views Asked by At

My User model has an email field and a username field. `USERNAME_FIELD' is set to email. But I wrote an authentication backend to also support logging in using username:

class UsernameAuthBackend(BaseBackend):
    def authenticate(self, request, email=None, password=None):
        try:
            user = User.objects.get(username=email)
            if user.check_password(password):
                return user
            return None
        except User.DoesNotExist:
            return None

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

settings.py:

AUTHENTICATION_BACKENDS = [
    "django.contrib.auth.backends.ModelBackend",
    "accounts.authentication.UsernameAuthBackend",
]

This works fine but it I can't use usernames with obtain_auth_token to get a user's token. (It also doesn't work on Django's admin panel.)

1

There are 1 best solutions below

0
Mihail Andreev On BEST ANSWER

DRF

You can overriding the serializer can provide a cleaner way to adjust the authentication data input for obtain_auth_token. This is because the actual authentication logic is often handled within the serializer.

Here's how you can create a custom serializer to handle both email and username:

  1. Custom Authentication Serializer:
from rest_framework.authtoken.serializers import AuthTokenSerializer
from rest_framework import serializers
from django.contrib.auth import authenticate

class CustomAuthTokenSerializer(AuthTokenSerializer):
    username = serializers.CharField(label="Username or Email")

    def validate(self, attrs):
        username_or_email = attrs.get('username')
        password = attrs.get('password')

        # Try authentication with email first
        try:
            user = User.objects.get(email=username_or_email)
        except User.DoesNotExist:
            # If email doesn't exist, check with username
            try:
                user = User.objects.get(username=username_or_email)
            except User.DoesNotExist:
                msg = 'Unable to log in with provided credentials.'
                raise serializers.ValidationError(msg, code='authorization')

        if not user.check_password(password):
            msg = 'Unable to log in with provided credentials.'
            raise serializers.ValidationError(msg, code='authorization')

        attrs['user'] = user
        return attrs
  1. Update CustomObtainAuthToken View to use the Custom Serializer:
from rest_framework.authtoken.views import ObtainAuthToken
from rest_framework.authtoken.models import Token
from rest_framework.response import Response
from .serializers import CustomAuthTokenSerializer  # Adjust the import path

class CustomObtainAuthToken(ObtainAuthToken):
    serializer_class = CustomAuthTokenSerializer

    def post(self, request, *args, **kwargs):
        serializer = self.serializer_class(data=request.data, context={'request': request})
        serializer.is_valid(raise_exception=True)
        user = serializer.validated_data['user']
        token, created = Token.objects.get_or_create(user=user)
        return Response({'token': token.key})

This way, you're handling the logic of determining whether the input is a username or email in the serializer's validate method, making the view simpler and more consistent with Django REST framework's patterns.

Django Admin

You can use django-username-email package or override the default authentication form to allow for username-based authentication. Here's a basic approach:

from django import forms
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth import authenticate

class CustomAdminAuthForm(AuthenticationForm):
    def clean(self):
        username_or_email = self.cleaned_data.get('username')
        password = self.cleaned_data.get('password')

        if username_or_email and password:
            self.user_cache = authenticate(self.request, email=username_or_email, password=password)
            if self.user_cache is None:
                raise forms.ValidationError(
                    self.error_messages['invalid_login'],
                    code='invalid_login',
                    params={'username': self.username_field.verbose_name},
                )
        return self.cleaned_data

# Update your admin.py
from django.contrib import admin
from .forms import CustomAdminAuthForm

admin.site.login_form = CustomAdminAuthForm

Please note that the above code snippets are more conceptual than exact code.