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

Specify authentication with OpenApiAuthenticationExtension #264

Closed
vdoma opened this issue Jan 20, 2021 · 15 comments
Closed

Specify authentication with OpenApiAuthenticationExtension #264

vdoma opened this issue Jan 20, 2021 · 15 comments

Comments

@vdoma
Copy link

vdoma commented Jan 20, 2021

I'm using TokenAuthentication provided by django-rest-knox and am trying to get it to work with drf-spectacular. I created an OpenApiAuthenticationExtension subclass using your documentation, but am unsure how to register it. Where would I register it so that drf-spectacular finds it? Thanks.

Error:

Warning #0: could not resolve authenticator <class 'knox.auth.TokenAuthentication'>. There was no OpenApiAuthenticationExtension registered for that class. Try creating one by subclassing it. Ignoring for now.

Code:

    # in local file: scheme.py 
    class KnoxTokenScheme(OpenApiAuthenticationExtension):
    target_class = 'knox.auth.TokenAuthentication'
    name = 'knoxTokenAuth'
    match_subclasses = True
    priority = 1

    def get_security_definition(self, auto_schema):
        if self.target.keyword == 'Token':
            return {
                'type': 'http',
                'scheme': 'bearer',
            }
        else:
            return {
                'type': 'apiKey',
                'in': 'header',
                'name': 'Authorization',
                'description': _(
                    'Token-based authentication with required prefix "%s"'
                ) % self.target.keyword
            }
@tfranzel
Copy link
Owner

tfranzel commented Jan 20, 2021

https://drf-spectacular.readthedocs.io/en/latest/blueprints.html

the info box describes this. maybe i should also put that info box on the customization page.

point is that the interpreter needs to load it. import that file (import xxx.scheme.py) in settings, views or serializers. it does not matter where exactly as long as the interpreter sees/parses it at least once.

@tfranzel
Copy link
Owner

and by the way, if you are happy with your extensions i would appreciate a PR for that lib as it has been on the short list for a while.

@vdoma
Copy link
Author

vdoma commented Jan 22, 2021

Thanks - that helped. Let me do a bit more analysis, and present my findings and we can decide if a PR is required. I will reply back soon.

@revmischa
Copy link

I got an error pasting the example OpenApiAuthenticationExtension into my settings file, it complained about circular imports.

  File "/Users/mish/xxxxx/permissions/__init__.py", line 1, in <module>
    from drf_spectacular.openapi import OpenApiAuthenticationExtension
  File "/Users/mish/Library/Caches/pypoetry/virtualenvs/pds-Vl2mZPzh-py3.9/lib/python3.9/site-packages/drf_spectacular/openapi.py", line 12, in <module>
    from rest_framework.generics import GenericAPIView
  File "/Users/mish/Library/Caches/pypoetry/virtualenvs/pds-Vl2mZPzh-py3.9/lib/python3.9/site-packages/rest_framework/generics.py", line 24, in <module>
    class GenericAPIView(views.APIView):
AttributeError: partially initialized module 'rest_framework.views' has no attribute 'APIView' (most likely due to a circular import)

@tfranzel
Copy link
Owner

@vdoma any updates here or should i close the issue?

@vdoma
Copy link
Author

vdoma commented Mar 20, 2021

Sorry about the delayed response.
Here's what worked for me. I created a file scheme.py (below) in my authentication app and then as suggested, imported it in my views.py. Let me know if you have any questions.

# views.py
from authentication.scheme import KnoxTokenScheme
# scheme.py
from django.utils.translation import gettext_lazy as _
from drf_spectacular.extensions import OpenApiAuthenticationExtension

class KnoxTokenScheme(OpenApiAuthenticationExtension):
    target_class = 'knox.auth.TokenAuthentication'
    name = 'knoxTokenAuth'

    def get_security_definition(self, auto_schema):        
        return {
            'type': 'apiKey',
            'in': 'header',
            'name': 'Authorization',
            'description': _(
                'Token-based authentication with required prefix "%s"'
            ) % "Token"
        }

@arielkaluzhny
Copy link

Hello! I am trying to do something very similar and have basically copied this code but am getting the error:

__init__() missing 1 required positional argument: 'target'

Anyone have any thoughts on this? Thank you in advance!

@tfranzel
Copy link
Owner

not sure how that is even possible with above KnoxTokenScheme. Something must be going on. Are you overriding __init__?

def __init__(self, target):

@arielkaluzhny
Copy link

arielkaluzhny commented Nov 16, 2022

not sure how that is even possible with above KnoxTokenScheme. Something must be going on. Are you overriding __init__?

@tfranzel - Nope. Here's the code:

# scheme.py
from drf_spectacular.extensions import OpenApiAuthenticationExtension

class KnoxTokenScheme(OpenApiAuthenticationExtension):
    target_class = "knox.auth.TokenAuthentication"
    name = "knoxTokenAuth"

    def get_security_definition(self, auto_schema):        
        return {
            "type": "apiKey",
            "in": "header",
            "name": "Knox Authorization",
            "description": "Token-based authentication with required prefix 'Token'",
        }
# settings.py
REST_FRAMEWORK = {
    "DEFAULT_RENDERER_CLASSES": ["rest_framework.renderers.JSONRenderer"],
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "backend.scheme.KnoxTokenScheme",
        "rest_framework.authentication.SessionAuthentication",  # For the swagger API
    ],
    'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
}

Hypothetically, I would actually use my own Custom Token Authentication class which subclasses KnoxTokenScheme but figured I would start with this.

I assume it's something to do with using it in DEFAULT_AUTHENTICATION_CLASSES as opposed to using it directly in the view (I am trying to move from using drf_yasg to drf_spectacular and that is what we had previously).

@tfranzel
Copy link
Owner

@arielkaluzhny that is the issue. The extension is not a auth class! Adding it to DEFAULT_AUTHENTICATION_CLASSES will break DRF. Just add your actual TokenAuthentication class there. The extension is not an auth class, merely a description of what to do when the actual auth class (it is referring to) is encountered.

The extension itself does not need to be added anywhere. All you have to do is making sure the python interpreter loads it.

https://drf-spectacular.readthedocs.io/en/latest/customization.html#step-5-extensions

@arielkaluzhny
Copy link

Thank you @tfranzel, it appears to be working (or at least not failing & giving errors)!

I'm not convinced I am actually doing what I want here though. It appears that the KnoxTokenScheme is now overriding knox.auth.TokenAuthentication? But I do actually want to use knox.auth.TokenAuthentication as written, its just that without the scheme I had the could not resolve authenticator <class 'knox.auth.TokenAuthentication'>.... Ignoring for now. issue.

It may be that I just need to play around with this more and dig into the documentation. Maybe I need to change what get_security_definition is returning 🤔 I couldn't find documentation on what the possible values and results for that are.

@tfranzel
Copy link
Owner

could not resolve authenticator <class 'knox.auth.TokenAuthentication'>.... Ignoring for now.

means that the extension class KnoxTokenScheme was not discovered (i.e. loaded by the interpreter)

It appears that the KnoxTokenScheme is now overriding knox.auth.TokenAuthentication

no, that is not how this works. The KnoxTokenScheme class is an extension class and will provide a schema template, nothing more nothing less. It has no auth capabilities. knox.auth.TokenAuthentication is your auth class. While generating a schema, your auth class is discovered and spectacular will look for an extension class that matches it. Using the classes the other way round will either throw or not work.

do this:

# settings.py
REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": ["knox.auth.TokenAuthentication"],
    'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
}

# schema.py
# put your extensions here as is. no fiddling with get_security_definition required

# YOURAPP/apps.py
# only add the ready function with the import.
class YOURAPPConfig(AppConfig):
    default_auto_field = "django.db.models.BigAutoField"
    name = "YOURAPP"

    def ready(self):
        # import above schema.py
        import YOURAPP.schema  # noqa: E402

The last part will make sure your extensions are properly loaded. This is a natural thing for Celery users, but I learned the hard way that not every Django/DRF user is familiar with this idiom. Will add this example to the doc also.

@janlis-ff
Copy link
Contributor

Not sure exactly what happened here, but in my case, using drf_spectacular:

# settings.py

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": ("knox.auth.TokenAuthentication",),
    "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
    # ...
}

SPECTACULAR_SETTINGS = {
    "COMPONENT_SPLIT_REQUEST": True,
    "SCHEMA_COERCE_PATH_PK_SUFFIX": False,
    "SCHEMA_PATH_PREFIX": r"(/api/admin/)|(/api/)",
    "SWAGGER_UI_SETTINGS": {
        "persistAuthorization": True,
    },
    # other than that - only name, description
}
# scheme.py

from drf_spectacular.extensions import OpenApiAuthenticationExtension

class KnoxTokenScheme(OpenApiAuthenticationExtension):
    target_class = "knox.auth.TokenAuthentication"
    name = "knoxTokenAuth"

    def get_security_definition(self, auto_schema):        
        return {
            "type": "apiKey",
            "in": "header",
            "name": "Knox Authorization",  #      <----------- (??)
            "description": "Token-based authentication with required prefix 'Token'",
        }

Did not work until I have changed name value from Knox Authorization to just Authorization in scheme.py. Before that swagger gave me TypeError: Failed to execute 'fetch' on 'Window': Invalid name.

@callumgare
Copy link
Contributor

and by the way, if you are happy with your extensions i would appreciate a PR for that lib as it has been on the short list for a while.

@tfranzel I've made a PR for this :) #1163

@callumgare
Copy link
Contributor

Did not work until I have changed name value from Knox Authorization to just Authorization in scheme.py. Before that swagger gave me TypeError: Failed to execute 'fetch' on 'Window': Invalid name.

@janlis-ff I believe in that context name is the name of the header rather than the name of the authorisation scheme. Hence why it has to be Authorization

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

No branches or pull requests

6 participants