Search code examples
pythondjangodjango-rest-frameworkdjango-rest-framework-simplejwt

Django Rest Framework Custom JWT authentication


I created a Django app in which I want to be able to authenticate users by checking not only the username and password, but also a specific field in a related model. The custom request body I want to POST to the endpoint is:

payload = { 'username': user, 'password': password, 'app_id': uuid4}

I am using djangorestframework-simplejwt module to get the access token.

models.py

class Application(models.Model):

    app_name  = models.CharField(max_length=300)
    app_id    = models.UUIDField(default=uuid.uuid4, editable=False)

    def __str__(self):
        return self.app_name

class ProfileApp(models.Model):

    user       = models.OneToOneField(User, on_delete=models.CASCADE)
    app        = models.ForeignKey(Application, on_delete=models.CASCADE)
    expires_on = models.DateTimeField(default=datetime.now() + timedelta(days=15))

    def __str__(self):
        return self.app.app_name + " | "  + self.user.username

Is it possible to override the TokenObtainPairView from rest_framework_simplejwt to only authenticate an user if the expires_on date is still not expired? Or is there an architecture problem by doing it like this?


Solution

  • You can do this by creating a custom serializer that inherits from TokenObtainPairSerializer, and extending the validate method to check for custom field values. There is no architecture problem if you are careful to not override necessary functionality of the parent class.

    Here is an example:

    import datetime as dt
    import json
    from rest_framework import exceptions
    from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
    from rest_framework_simplejwt.views import TokenObtainPairView
    
    
    
    class MyTokenObtainPairSerializer(TokenObtainPairSerializer):
        def validate(self, attrs):
            try:
                request = self.context["request"]
            except KeyError:
                pass
            else:
                request_data = json.loads(request.body)
                username = request_data.get("username")
                app_id = request_data.get("app_id")
    
                profile_has_expired = False
                try:
                    profile = ProfileApp.objects.get(user__username=username, app__app_id=app_id)
                except ProfileApp.DoesNotExist:
                    profile_has_expired = True
                else:
                    profile_has_expired = dt.date.today() > profile.expires_on
                finally:
                    if profile_has_expired:
                        error_message = "This profile has expired"
                        error_name = "expired_profile"
                        raise exceptions.AuthenticationFailed(error_message, error_name)
            finally:
                return super().validate(attrs)
    
    
    class MyTokenObtainPairView(TokenObtainPairView):
        serializer_class = MyTokenObtainPairSerializer
    

    Then use MyTokenObtainPairView in place of TokenObtainPairView in your urls file.

    Also since User and ProfileApp share a one-to-one field, it looks like you can get away with not using the "app_id" key/field at all.

    Original source file: https://github.com/davesque/django-rest-framework-simplejwt/blob/04376b0305e8e2fda257b08e507ccf511359d04a/rest_framework_simplejwt/serializers.py