Search code examples
djangoauthenticationdjango-rest-framework

Django using email authentication with djoser for login


So i tried using djoser recently and i want to use email instead of username to login.

Djoser : http://djoser.readthedocs.io/en/latest/index.html

Then i try to customise the token create/login to change from username to email in the serializers.py

Original

class TokenCreateSerializer(serializers.Serializer):
password = serializers.CharField(
    required=False, style={'input_type': 'password'}
    )

    default_error_messages = {
        'invalid_credentials': constants.INVALID_CREDENTIALS_ERROR,
        'inactive_account': constants.INACTIVE_ACCOUNT_ERROR,
    }

    def __init__(self, *args, **kwargs):
        super(TokenCreateSerializer, self).__init__(*args, **kwargs)
    self.user = None
    self.fields[User.USERNAME_FIELD] = serializers.CharField(
        required=False
    )

def validate(self, attrs):
    self.user = authenticate(
        username=attrs.get(User.USERNAME_FIELD),
        password=attrs.get('password')
        )

        self._validate_user_exists(self.user)
        self._validate_user_is_active(self.user)
        return attrs

    def _validate_user_exists(self, user):
        if not user:
            self.fail('invalid_credentials')

    def _validate_user_is_active(self, user):
        if not user.is_active:
            self.fail('inactive_account')

Edited

class TokenCreateSerializer(serializers.Serializer):
password = serializers.CharField(
    required=False, style={'input_type': 'password'}
    )

    default_error_messages = {
        'invalid_credentials': constants.INVALID_CREDENTIALS_ERROR,
        'inactive_account': constants.INACTIVE_ACCOUNT_ERROR,
    }

    def __init__(self, *args, **kwargs):
        super(TokenCreateSerializer, self).__init__(*args, **kwargs)
    self.user = None
    self.fields[User.EMAIL_FIELD] = serializers.EmailField(
        required=False
    )

def validate(self, attrs):
    self.user = authenticate(
        email=attrs.get(User.EMAIL_FIELD),
        password=attrs.get('password')
        )

        self._validate_user_exists(self.user)
        self._validate_user_is_active(self.user)
        return attrs

    def _validate_user_exists(self, user):
        if not user:
            self.fail('invalid_credentials')

    def _validate_user_is_active(self, user):
        if not user.is_active:
            self.fail('inactive_account')

but the result i get in the api is this

{
"non_field_errors": [
    "Unable to login with provided credentials."
]

I did try other method but all have same result. Is there a way to make it using of email to authenticate instead of username ?


Solution

  • Djoser uses authenticate method from django.contrib.auth. By default the AUTHENTICATION_BACKENDS is set to usedjango.contrib.auth.backends.ModelBackend which will try to get the user model using default manager get_by_natural_key method, which does:

    def get_by_natural_key(self, username):
        return self.get(**{self.model.USERNAME_FIELD: username})
    

    You can check the source code for this.

    My approach on this was to enhance django's UserManager, assuming your User model is subclass of AbstractUser, by trying to get the user by email or username, like this:

    from django.contrib.auth.models import UserManager
    from django.db.models import Q
    
    class CustomUserManager(UserManager):
    
        def get_by_natural_key(self, username):
            return self.get(
                Q(**{self.model.USERNAME_FIELD: username}) |
                Q(**{self.model.EMAIL_FIELD: username})
            )
    

    Use that in your User model

    from django.contrib.auth.models import AbstractUser
    
    class User(AbstractUser):
        ...
        objects = CustomUserManager()
    

    You should now be able to login using email or username