[Django]-Django Rest Framework custom authentication

47๐Ÿ‘

How to implement a custom authentication scheme in DRF?

To implement a custom authentication scheme, we need to subclass the DRFโ€™s BaseAuthentication class and override the .authenticate(self, request) method.

The method should return a two-tuple of (user, auth) if authentication succeeds, or None otherwise. In some circumstances, we may raise an AuthenticationFailed exception from the .authenticate() method.

Example (from DRF docs):

Lets say we want to authenticate any incoming request as the user given by the username in a custom request header named 'X_USERNAME'.

Step-1: Create the Custom authentication class

To do that, we will create an authentication.py file in my_app.

# my_app/authentication.py
from django.contrib.auth.models import User
from rest_framework import authentication
from rest_framework import exceptions

class ExampleAuthentication(authentication.BaseAuthentication):
    def authenticate(self, request):
        username = request.META.get('X_USERNAME') # get the username request header
        if not username: # no username passed in request headers
            return None # authentication did not succeed

        try:
            user = User.objects.get(username=username) # get the user
        except User.DoesNotExist:
            raise exceptions.AuthenticationFailed('No such user') # raise exception if user does not exist 

        return (user, None) # authentication successful

Step-2: Specify the custom authentication class

After creating the custom authentication class, we need to define this authentication class in our DRF settings. Doing this, all the requests will be authenticated based on this authentication scheme.

'DEFAULT_AUTHENTICATION_CLASSES': (       
    'my_app.authentication.ExampleAuthentication', # custom authentication class
    ...
),

Note: If you want to use this custom authentication class on per-view basis or per-viewset basis and not on global level, you can define this authentication class explicitly in your views.

class MyView(APIView):

    authentication_classes = [ExampleAuthentication,] # specify this authentication class in your view

    ...
๐Ÿ‘คRahul Gupta

16๐Ÿ‘

Below is a simple example that can be used to achieve a custom authentication. To access the endpoint you have to pass the username&password on the POST data.

urls.py

urlpatterns = [
    url(r'^stuff/', views.MyView.as_view()),
    ...
]

views.py

from rest_framework.response import Response
from rest_framework.views import APIView    
from rest_framework.permissions import IsAuthenticated
from rest_framework import exceptions
from rest_framework import authentication
from django.contrib.auth import authenticate, get_user_model
from rest_framework.authentication import SessionAuthentication


class ExampleAuthentication(authentication.BaseAuthentication):
    def authenticate(self, request):
        # Get the username and password
        username = request.data.get('username', None)
        password = request.data.get('password', None)

        if not username or not password:
            raise exceptions.AuthenticationFailed(_('No credentials provided.'))

        credentials = {
            get_user_model().USERNAME_FIELD: username,
            'password': password
        }

        user = authenticate(**credentials)

        if user is None:
            raise exceptions.AuthenticationFailed(_('Invalid username/password.'))

        if not user.is_active:
            raise exceptions.AuthenticationFailed(_('User inactive or deleted.'))

    return (user, None)  # authentication successful


class MyView(APIView):
    authentication_classes = (SessionAuthentication, ExampleAuthentication,)
    permission_classes = (IsAuthenticated,)

    def post(self, request, format=None):    
        content = {
            'user': unicode(request.user),
            'auth': unicode(request.auth),  # None
        }
        return Response(content)

Curl

curl -v -X POST http://localhost:8000/stuff/ -d 'username=my_username&password=my_password'
๐Ÿ‘คSlipstream

2๐Ÿ‘

I used the following way

from rest_framework_jwt.settings import api_settings
from rest_framework import status, generics

 class UserLogin(generics.CreateAPIView):

    def post(self, request, *args, **kwargs):
        email = request.data['email']
        if email is None:
            return Response({'error': 'Email not informed'}, status=status.HTTP_403_FORBIDDEN)
        try:
            user = User.objects.get(email=email)
            if not user.check_password(request.data['password']):
                return Response({'error': 'Email ou senha incorreto'}, status=status.HTTP_400_BAD_REQUEST)
            jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
            jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
            payload = jwt_payload_handler(user)
            token = jwt_encode_handler(payload)
            return Response({"token": token, "user":UserSessionSerializerAuth(user,
                        context={'request': request}).data}, status=status.HTTP_200_OK)
        except User.DoesNotExist:
            return Response({'error': 'User not found'}, status=status.HTTP_403_FORBIDDEN)
๐Ÿ‘คAntonio Junior

1๐Ÿ‘

I faced similar situation where I had to implement a custom authentication class. Request coming to the rest endpoint were using basic auth but the username werenโ€™t a django user. Requests were authnticated against username and password configured in settings.py. Hereโ€™s my implementation:

โ€“ Created custom authentication class

# myapp/api/auth.py

"""Custom authentication module"""

import base64
import binascii

from django.conf import settings
from django.utils.six import text_type
from django.utils.translation import ugettext_lazy as _

from rest_framework.authentication import BaseAuthentication
from rest_framework import HTTP_HEADER_ENCODING, exceptions


class CustomAuthentication(BaseAuthentication):
    """
    Custom authentication class.
    It will authenticate any incoming request
    as the user given by the username in a
    custom request header.
    """

    def authenticate(self, request):
        """
        Returns a `User` if a correct username and password have been supplied
        using HTTP Basic authentication.  Otherwise returns `None`.
        """

        # Gets authorization from request header
        # and checks different possibility of
        # invalid header.
        # ======================================

        auth = self.get_authorization_header(request).split()

        if not auth or auth[0].lower() != b"basic":
            raise exceptions.AuthenticationFailed(_("Invalid header!"))

        if len(auth) == 1:
            msg = _("Invalid basic header. No credentials provided.")
            raise exceptions.AuthenticationFailed(msg)
        elif len(auth) > 2:
            msg = _(
                "Invalid basic header. Credentials string should not contain spaces."
            )
            raise exceptions.AuthenticationFailed(msg)

        try:
            auth_parts = (
                base64.b64decode(auth[1]).decode(HTTP_HEADER_ENCODING).partition(":")
            )
        except (TypeError, UnicodeDecodeError, binascii.Error):
            msg = _("Invalid basic header. Credentials not correctly base64 encoded.")
            raise exceptions.AuthenticationFailed(msg)

        # parses username and password.
        userid, password = auth_parts[0], auth_parts[2]

        if userid != settings.FERRATUM_CALLBACK_USERNAME:
            msg = _("Invalid basic header. Username is incorrect!")
            raise exceptions.AuthenticationFailed(msg)

        if password != settings.FERRATUM_CALLBACK_PASSWORD:
            msg = _("Invalid basic header. Password is incorrect!")
            raise exceptions.AuthenticationFailed(msg)

        # An user object is expected to be returned
        # in case of successful authentication. Therefore
        # a user object is returned with the give username
        # in the header. This user doesn't exists in the
        # django user User model.
        # ===============================================

        user = {
            "username": userid,
            "password": "",
            "email": "",
            "first_name": "",
            "last_name": "",
            "company": "",
        }

        return (user, None)

    @staticmethod
    def get_authorization_header(request):
        """
        Return request's 'Authorization:' header, as a bytestring.

        Hide some test client ickyness where the header can be unicode.
        """

        auth = request.META.get("HTTP_AUTHORIZATION", b"")
        if isinstance(auth, text_type):
            # Work around django test client oddness
            auth = auth.encode(HTTP_HEADER_ENCODING)
        return auth

โ€“ Added it in DEFAULT_AUTHENTICATION_CLASS

# myapp/settings.py

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": (
        "rest_framework.authentication.SessionAuthentication",
        "mozilla_django_oidc.contrib.drf.OIDCAuthentication",
        "rest_framework.authentication.BasicAuthentication",
        "users.auth.SaveriumTokenAuthentication",
        "api.auth.CustomAuthentication"
    ),
    'DEFAULT_RENDERER_CLASSES': DEFAULT_RENDERER_CLASSES
}

โ€“ Used the custom authentication in view

# myapp/api/view/api_view.py

from api.auth import CustomAuthentication


class UpdateStatus(APIView):
    """
    It updates application status
    """
    
    # Custom authentication scheme.
    authentication_classes = [CustomAuthentication]

    def post(self, *args, **kwargs):
        """
        Callback comes as a POST request
        with data in JSON format.
        """

        data = self.request.data
        ...
๐Ÿ‘คAbu Shumon

0๐Ÿ‘

In the folder that holds your api files, create another file to hold your custom authentication class, such as authentication.py. Then in your settings, under DEFAULT_AUTHENTICATION_CLASSES, point to your custom authentication class.

๐Ÿ‘คmcastle

Leave a comment