Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide django-rest-framework-simplejwt integration #407

Closed
johnthagen opened this issue Jul 12, 2019 · 10 comments · Fixed by jazzband/djangorestframework-simplejwt#145
Closed

Comments

@johnthagen
Copy link
Collaborator

johnthagen commented Jul 12, 2019

We are using django-rest-framework-simplejwt to support JWT tokens.

By default, drf-yasg seems to create correct POST parameters, but JWT endpoints return different values than they consume.

For example:

  • http://localhost:8000/api/token/
User sends:
{"username": "davidattenborough", "password": "boatymcboatface"}

Response: 
{
  "access":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX3BrIjoxLCJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiY29sZF9zdHVmZiI6IuKYgyIsImV4cCI6MTIzNDU2LCJqdGkiOiJmZDJmOWQ1ZTFhN2M0MmU4OTQ5MzVlMzYyYmNhOGJjYSJ9.NHlztMGER7UADHZJlxNG0WSi22a2KaYSfd1S-AuT7lU",
  "refresh":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX3BrIjoxLCJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImNvbGRfc3R1ZmYiOiLimIMiLCJleHAiOjIzNDU2NywianRpIjoiZGUxMmY0ZTY3MDY4NDI3ODg5ZjE1YWMyNzcwZGEwNTEifQ.aEoAYkSJjoWH1boshQAaTkf8G3yn0kapko6HFRt7Rh4"
}

The default generated schema from drf-yasg:

yasg-token

Could drf-yasg support this out of the box? If not, I would be happy to try to contribute some docs about how to work around this so that others could discover how to support this in the future. I'm just not sure what the best way forward is.

@johnthagen
Copy link
Collaborator Author

johnthagen commented Jul 12, 2019

Here is the Serializer that is used:

https://github.com/davesque/django-rest-framework-simplejwt/blob/6eb9f6d1d8bd933e7c35cd4521006809c62eef86/rest_framework_simplejwt/serializers.py#L65

class TokenObtainSerializer(serializers.Serializer):
    username_field = User.USERNAME_FIELD

    default_error_messages = {
        'no_active_account': _('No active account found with the given credentials')
    }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.fields[self.username_field] = serializers.CharField()
        self.fields['password'] = PasswordField()

    def validate(self, attrs):
        authenticate_kwargs = {
            self.username_field: attrs[self.username_field],
            'password': attrs['password'],
        }
        try:
            authenticate_kwargs['request'] = self.context['request']
        except KeyError:
            pass

        self.user = authenticate(**authenticate_kwargs)

        # Prior to Django 1.10, inactive users could be authenticated with the
        # default `ModelBackend`.  As of Django 1.10, the `ModelBackend`
        # prevents inactive users from authenticating.  App designers can still
        # allow inactive users to authenticate by opting for the new
        # `AllowAllUsersModelBackend`.  However, we explicitly prevent inactive
        # users from authenticating to enforce a reasonable policy and provide
        # sensible backwards compatibility with older Django versions.
        if self.user is None or not self.user.is_active:
            raise exceptions.AuthenticationFailed(
                self.error_messages['no_active_account'],
                'no_active_account',
            )

        return {}

    @classmethod
    def get_token(cls, user):
        raise NotImplementedError('Must implement `get_token` method for `TokenObtainSerializer` subclasses')


class TokenObtainPairSerializer(TokenObtainSerializer):
    @classmethod
    def get_token(cls, user):
        return RefreshToken.for_user(user)

    def validate(self, attrs):
        data = super().validate(attrs)

        refresh = self.get_token(self.user)

        data['refresh'] = str(refresh)
        data['access'] = str(refresh.access_token)

        return data

@johnthagen
Copy link
Collaborator Author

johnthagen commented Jul 12, 2019

I tried playing around with swagger_auto_schema like so:

decorated_token_obtain_pair_view = (
    swagger_auto_schema(
        method='post',
        responses={status.HTTP_200_OK: TokenObtainPairResponseSerializer}
    )(TokenObtainPairView.as_view())
)

decorated_token_refresh_view = (
    swagger_auto_schema(
        method='post',
        responses={status.HTTP_200_OK: TokenRefreshResponseSerializer}
    )(TokenRefreshView.as_view())
)

Where I created my own ...Response serializers. But I think I think I am running into the warning from the docs:

However, do note that both of the methods above can lead to unexpected (and maybe surprising) results by replacing/decorating methods on the base class itself.

All of the Token views share a common base TokenViewBase class and when I decorate multiple of them, they seem to be conflicting/overriding each other 😕

@johnthagen
Copy link
Collaborator Author

So the best solution I could come up with is:

from drf_yasg.utils import swagger_auto_schema
from rest_framework import serializers, status
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView, TokenVerifyView


class TokenObtainPairResponseSerializer(serializers.Serializer):
    access = serializers.CharField()
    refresh = serializers.CharField()

    def create(self, validated_data):
        raise NotImplementedError()

    def update(self, instance, validated_data):
        raise NotImplementedError()


class DecoratedTokenObtainPairView(TokenObtainPairView):
    @swagger_auto_schema(responses={status.HTTP_200_OK: TokenObtainPairResponseSerializer})
    def post(self, request, *args, **kwargs):
        return super().post(request, *args, **kwargs)


class TokenRefreshResponseSerializer(serializers.Serializer):
    access = serializers.CharField()

    def create(self, validated_data):
        raise NotImplementedError()

    def update(self, instance, validated_data):
        raise NotImplementedError()


class DecoratedTokenRefreshView(TokenRefreshView):
    @swagger_auto_schema(responses={status.HTTP_200_OK: TokenRefreshResponseSerializer})
    def post(self, request, *args, **kwargs):
        return super().post(request, *args, **kwargs)


class TokenVerifyResponseSerializer(serializers.Serializer):
    def create(self, validated_data):
        raise NotImplementedError()

    def update(self, instance, validated_data):
        raise NotImplementedError()


class DecoratedTokenVerifyView(TokenVerifyView):
    @swagger_auto_schema(responses={status.HTTP_200_OK: TokenVerifyResponseSerializer})
    def post(self, request, *args, **kwargs):
        return super().post(request, *args, **kwargs)

I had to subclass the Views to deal with the conflict/override base View issue alluded to in the docs.

Is this the correct way to do this? Is there a more succinct solution?

@axnsan12
Copy link
Owner

axnsan12 commented Jul 12, 2019

Yeah, this seems right. You could also probably wrap the as_view result directly in urls.py, something like path(... , swagger_auto_schema(methods=['post'], responses={your stuff})(TokenObtainPairView.as_view()))

@johnthagen
Copy link
Collaborator Author

@axnsan12 Thanks for the comment. When I tried that, it seemed like the different swagger_auto_schema's were clobbering each other, perhaps because they were all writing to the same base class? I don't know the drf-yasg internals, so I couldn't say for sure why it wasn't working.

@johnthagen
Copy link
Collaborator Author

Also, if I'm not actually using the Serializers I've defined (they are soley for drf-yasg) is there a simpler way to define the interface for drf-yasg that doesn't involve creating Serializers?

@estyxx
Copy link

estyxx commented Mar 30, 2020

Hello,
I would like to use drf-yasg with django-rest-framework-simplejwt too...
I'm trying to follow the workaround of @johnthagen but it's not working properly: when I go to the swagger page I entered the credentials and I'm receiving 200, but I was expecting to automatically go to the "original swagger page" but maybe that is not the behavior...

Try also to use DecoratedTokenRefreshView, I'm getting the access token but ones that I get I don't understand how to give to the schema view...

I added the serializers, in the urls.py I added:


decorated_token_obtain_pair_view = (
    swagger_auto_schema(
        method='post',
        responses={status.HTTP_200_OK: TokenObtainPairResponseSerializer}
    )(TokenObtainPairView.as_view())
)

decorated_token_refresh_view = (
    swagger_auto_schema(
        method='post',
        responses={status.HTTP_200_OK: TokenRefreshResponseSerializer}
    )(TokenRefreshView.as_view())
)


urlpatterns = [
    url(
        r"^swagger/$",decorated_token_obtain_pair_view,
        name="schema-swagger-ui",
    ),
    url(
        r"^swagger-refresh/$",decorated_token_refresh_view,
        name="schema-swagger-refresh-ui",
    ),
...

Did I'm missing something?

Thank you :)

@johnthagen
Copy link
Collaborator Author

@estyxx
Copy link

estyxx commented Mar 31, 2020

@johnthagen yes, I included those serializers in my code, but I'm confusing about how to use them in urls... 🤔 Because as I did, I can not access to the swagger page...
Do I have to use them in my "api/token/" url?

    path("api/token/", DecoratedTokenObtainPairView.as_view(), name="token_obtain_pair"),
    path("api/token/refresh/", DecoratedTokenRefreshView.as_view(), name="token_refresh"),

by-Exist added a commit to by-Exist/piku_backend_api that referenced this issue Feb 26, 2021
@johnthagen
Copy link
Collaborator Author

Documentation for this is now included upstream: https://django-rest-framework-simplejwt.readthedocs.io/en/latest/drf_yasg_integration.html

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants