drf-spectacular: [Possible Bug] Not being able to use custom DRF authentication

Describe the bug I am trying to migrate my project from drf-yasg to drf-spectacular, but I am facing an issue when trying to add the same functionality I had in the old version.

While using drf-yasg the swagger-ui page presented a Login button which served as a way to block unauthorized users to see all the routes schema: image

I want to achieve the same with drf-spectacular, if not possible to add the login button, at least make it work when I use an HTTP headers customization extension in the browser, like below: image

To Reproduce Project authentication: My project is currently working with a token based authentication, the class is defined as below:

from doctorsystem.apps.equipe.models import DSToken

class DSTokenAuthentication(authentication.TokenAuthentication):
    model = DSToken

DSToken model definition:


class DSToken(models.Model):
    """
    The default authorization token model.
    """

    key = models.CharField(max_length=40, primary_key=True)
    mac_address = models.CharField(max_length=150, blank=True, null=True)
    user = models.ForeignKey(
        Equipe, blank=True, null=True, on_delete=models.CASCADE
    )
    cliente = models.ForeignKey(
        Cliente, blank=True, null=True, on_delete=models.CASCADE
    )
    created = models.DateTimeField(auto_now_add=True)

    class Meta:
        verbose_name = "DSToken"
        verbose_name_plural = "DSTokens"

    def __str__(self):
        return self.key

    def save(self, *args, **kwargs):
        if not self.key:
            self.key = self.generate_key()
        return super(DSToken, self).save(*args, **kwargs)

    def generate_key(self):
        return binascii.hexlify(os.urandom(20)).decode()

Previous configuration (drf-yasg): settings.py

REST_FRAMEWORK = {
    "DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",),
    "DEFAULT_PAGINATION_CLASS": (
        "doctorsystem.apps.core.api.pagination.DSPagination"
    ),
    "PAGE_SIZE": 100,
    "DATE_INPUT_FORMATS": ["%d/%m/%Y", "%Y-%m-%d"],
    "DATETIME_INPUT_FORMATS": [
        "%d/%m/%Y %H:%M:%S",
        "%d/%m/%Y %H:%M",
        "%Y-%m-%d %H:%M:%S",
        "%Y-%m-%d %H:%M",
    ],
    "DATETIME_FORMAT": "%d/%m/%Y %H:%M:%S",
    "DATE_FORMAT": "%d/%m/%Y",
    "DEFAULT_FILTER_BACKENDS": (
        "django_filters.rest_framework.DjangoFilterBackend",
    ),
}

SWAGGER_SETTINGS = {
    "REFETCH_SCHEMA_WITH_AUTH": True,
    "SECURITY_DEFINITIONS": {
        "Basic": {"type": "basic"},
        "Bearer": {"type": "apiKey", "name": "Authorization", "in": "header"},
    },
    "APIS_SORTER": "alpha",
    "DOC_EXPANSION": "none",
    "SHOW_REQUEST_HEADERS": True,
    "SUPPORTED_SUBMIT_METHODS": ["get", "post", "put", "delete", "patch"],
    "OPERATIONS_SORTER": "alpha",
    "LOGIN_URL": "/login/",
    "LOGOUT_URL": "/logout/",
}

urls.py

path(
        "gds-api-docs/",
        schema_view.with_ui("swagger", cache_timeout=0),
        name="schema-swagger-ui",
    ),
    path(
        "gds-api-redocs/",
        schema_view.with_ui("redoc", cache_timeout=0),
        name="schema-redoc-ui",
    ),

New (last tried) configuration (drf-spectacular): settings.py

SPECTACULAR_SETTINGS = {
    "TITLE": "Doctorsystem API",
    "DESCRIPTION": "",
    "VERSION": "1.0.0",
    "SERVE_INCLUDE_SCHEMA": False,
    "SCHEMA_PATH_PREFIX": "/api/",
    "SERVE_PUBLIC": False,
    "SERVE_AUTHENTICATION": [
        "doctorsystem.apps.core.api.authentication.DSTokenAuthentication"
    ],
}

I also included the DEFAULT_SCHEMA_CLASS in my REST_FRAMEWORK object:

REST_FRAMEWORK = {
    "DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",),
    "DEFAULT_PAGINATION_CLASS": (
        "doctorsystem.apps.core.api.pagination.DSPagination"
    ),
    "PAGE_SIZE": 100,
    "DATE_INPUT_FORMATS": ["%d/%m/%Y", "%Y-%m-%d"],
    "DATETIME_INPUT_FORMATS": [
        "%d/%m/%Y %H:%M:%S",
        "%d/%m/%Y %H:%M",
        "%Y-%m-%d %H:%M:%S",
        "%Y-%m-%d %H:%M",
    ],
    "DATETIME_FORMAT": "%d/%m/%Y %H:%M:%S",
    "DATE_FORMAT": "%d/%m/%Y",
    "DEFAULT_FILTER_BACKENDS": (
        "django_filters.rest_framework.DjangoFilterBackend",
    ),
    "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
}

urls.py:

path("schema/", SpectacularAPIView.as_view(), name="schema"),
    path(
        "spetacular-docs/",
        SpectacularSwaggerView.as_view(url_name="schema"),
        name="swagger-ui",
    ),
    path(
        "spetacular-redocs/",
        SpectacularRedocView.as_view(url_name="schema"),
        name="redocs-ui",
    ),

The error received with this configuration was:

app-api-django-1  |   File "/usr/local/lib/python3.10/site-packages/rest_framework/views.py", line 332, in check_permissions
app-api-django-1  |     if not permission.has_permission(request, self):
app-api-django-1  |   File "/app/doctorsystem/apps/core/api/modules_permissions.py", line 148, in has_permission
app-api-django-1  |     return request.session.get("has_agenda_online", False)
app-api-django-1  |   File "/usr/local/lib/python3.10/site-packages/rest_framework/request.py", line 418, in __getattr__
app-api-django-1  |     return self.__getattribute__(attr)
app-api-django-1  | AttributeError: 'Request' object has no attribute 'session'

It is being raised in the following custom Django REST permission:

class HasAgendaOnlinePermission(permissions.BasePermission):
    def has_permission(self, request, view):
        return request.session.get("has_agenda_online", False)

Which is used in some routes, like following:

class ConfiguracaoAgendaOnlineViewSet(viewsets.ModelViewSet):
    permission_classes = (
        IsAuthenticated,
        CheckUserExpiredPermission,
        ClienteBasePermission,
        HasAgendaOnlinePermission,
    )
    authentication_classes = (DSTokenAuthentication,)

Important note: This error is raised when I add an Authorization header in my browser with the extension shown previously since the login button does not appear on the drf-spectacular swagger page.

I even tried adding an OpenApiAuthenticationExtension class in my settings.py, the error changed but I couldn’t proceed beyond that:

class MyAuthenticationScheme(OpenApiAuthenticationExtension):
    target_class = (
        "doctorsystem.apps.core.api.authentication.DSTokenAuthentication"
    )
    name = "MyAuth"

    def get_security_definition(self, auto_schema):
        return {
            "type": "apiKey",
            "in": "header",
            "name": "Authorization",
        }

Error:

app-api-django-1  |     data=generator.get_schema(request=request, public=self.serve_public),
app-api-django-1  |   File "/usr/local/lib/python3.10/site-packages/drf_spectacular/generators.py", line 268, in get_schema
app-api-django-1  |     paths=self.parse(request, public),
app-api-django-1  |   File "/usr/local/lib/python3.10/site-packages/drf_spectacular/generators.py", line 233, in parse
app-api-django-1  |     assert isinstance(view.schema, AutoSchema), (
app-api-django-1  | AssertionError: Incompatible AutoSchema used on View <class 'doctorsystem.apps.apidev.viewsets.AgendaMedicoDiaView'>. Is DRF's DEFAULT_SCHEMA_CLASS pointing to "drf_spectacular.openapi.AutoSchema" or any other drf-spectacular compatible AutoSchema?

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Comments: 20 (9 by maintainers)

Most upvoted comments

Yes, the project didn’t include the DSTokenAuthentication in the REST_FRAMEWORK config, only directly in the routes, but it was time to fix it.

The override of the build_mock_request worked fine and solved the problem, now when I enter in the docs page, initially, I see only the public routes, and then once I paste my “token {token_key}” in the authorize form, the page reloads and I see all routes.

The only change in this last iteration was in my schema.py and settings.py - SPECTACULAR_SETTINGS: doctorsystem.apps.core.schema.py

from drf_spectacular import extensions
from rest_framework.test import APIRequestFactory

from doctorsystem.apps.core.api.authentication import DSTokenAuthentication


class MyAuthenticationScheme(extensions.OpenApiAuthenticationExtension):
    target_class = DSTokenAuthentication
    match_subclasses = True
    name = "MyAuth"

    def get_security_definition(self, auto_schema):
        return {
            "type": "apiKey",
            "in": "header",
            "name": "Authorization",
        }


def build_mock_request(method, path, view, original_request, **kwargs):
    """build a mocked request and use original request as reference if available"""
    request = getattr(APIRequestFactory(), method.lower())(path=path)
    request = view.initialize_request(request)
    if original_request:
        request.user = original_request.user
        request.auth = original_request.auth
        request.session = getattr(original_request, "session", None)
        # ignore headers related to authorization as it has been handled above.
        # also ignore ACCEPT as the MIME type refers to SpectacularAPIView and the
        # version (if available) has already been processed by SpectacularAPIView.
        for name, value in original_request.META.items():
            if not name.startswith("HTTP_"):
                continue
            if name in ["HTTP_ACCEPT", "HTTP_COOKIE", "HTTP_AUTHORIZATION"]:
                continue
            request.META[name] = value
    return request

settings.py

SPECTACULAR_SETTINGS = {
    "TITLE": "Doctorsystem API",
    "DESCRIPTION": "",
    "VERSION": "1.0.0",
    "SERVE_INCLUDE_SCHEMA": False,
    "SCHEMA_PATH_PREFIX": "/api/",
    "SERVE_PUBLIC": False,
    "SERVE_AUTHENTICATION": [
        "doctorsystem.apps.core.api.authentication.DSTokenAuthentication"
    ],
    "GET_MOCK_REQUEST": "doctorsystem.apps.core.schema.build_mock_request",
}

Thank you for all the support