diff --git a/drf_spectacular/authentication.py b/drf_spectacular/authentication.py index 4c2855fa..afd27252 100644 --- a/drf_spectacular/authentication.py +++ b/drf_spectacular/authentication.py @@ -1,7 +1,7 @@ from django.conf import settings -from django.utils.translation import gettext_lazy as _ from drf_spectacular.extensions import OpenApiAuthenticationExtension +from drf_spectacular.plumbing import build_bearer_security_scheme_object class SessionScheme(OpenApiAuthenticationExtension): @@ -36,17 +36,7 @@ class TokenScheme(OpenApiAuthenticationExtension): priority = -1 def get_security_definition(self, auto_schema): - if self.target.keyword == 'Bearer': - return { - 'type': 'http', - 'scheme': 'bearer', - } - else: - return { - 'type': 'apiKey', - 'in': 'header', - 'name': 'Authorization', - 'description': _( - 'Token-based authentication with required prefix "%s"' - ) % self.target.keyword - } + return build_bearer_security_scheme_object( + header_name='Authorization', + token_prefix=self.target.keyword, + ) diff --git a/drf_spectacular/contrib/rest_framework_jwt.py b/drf_spectacular/contrib/rest_framework_jwt.py index 37616adf..a3ebfc6a 100644 --- a/drf_spectacular/contrib/rest_framework_jwt.py +++ b/drf_spectacular/contrib/rest_framework_jwt.py @@ -1,4 +1,5 @@ from drf_spectacular.extensions import OpenApiAuthenticationExtension +from drf_spectacular.plumbing import build_bearer_security_scheme_object class JWTScheme(OpenApiAuthenticationExtension): @@ -8,8 +9,8 @@ class JWTScheme(OpenApiAuthenticationExtension): def get_security_definition(self, auto_schema): from rest_framework_jwt.settings import api_settings - return { - 'type': 'http', - 'scheme': 'bearer', - 'bearerFormat': api_settings.JWT_AUTH_HEADER_PREFIX, - } + return build_bearer_security_scheme_object( + header_name='AUTHORIZATION', + token_prefix=api_settings.JWT_AUTH_HEADER_PREFIX, + bearer_format='JWT' + ) diff --git a/drf_spectacular/contrib/rest_framework_simplejwt.py b/drf_spectacular/contrib/rest_framework_simplejwt.py index bebad133..3decfdec 100644 --- a/drf_spectacular/contrib/rest_framework_simplejwt.py +++ b/drf_spectacular/contrib/rest_framework_simplejwt.py @@ -1,8 +1,8 @@ -from django.utils.translation import gettext_lazy as _ from rest_framework import serializers from drf_spectacular.drainage import warn from drf_spectacular.extensions import OpenApiAuthenticationExtension, OpenApiSerializerExtension +from drf_spectacular.plumbing import build_bearer_security_scheme_object from drf_spectacular.utils import inline_serializer @@ -71,29 +71,12 @@ def get_security_definition(self, auto_schema): f'OpenAPI3 can only have one "bearerFormat". JWT Settings specify ' f'{api_settings.AUTH_HEADER_TYPES}. Using the first one.' ) - header_name = getattr(api_settings, 'AUTH_HEADER_NAME', 'HTTP_AUTHORIZATION') - - if ( - api_settings.AUTH_HEADER_TYPES[0] == 'Bearer' - and header_name == 'HTTP_AUTHORIZATION' - ): - return { - 'type': 'http', - 'scheme': 'bearer', - 'bearerFormat': "JWT", - } - else: - if header_name.startswith('HTTP_'): - header_name = header_name[5:] - header_name = header_name.replace('_', '-').capitalize() - return { - 'type': 'apiKey', - 'in': 'header', - 'name': header_name, - 'description': _( - 'Token-based authentication with required prefix "%s"' - ) % api_settings.AUTH_HEADER_TYPES[0] - } + + return build_bearer_security_scheme_object( + header_name=getattr(api_settings, 'AUTH_HEADER_NAME', 'HTTP_AUTHORIZATION'), + token_prefix=api_settings.AUTH_HEADER_TYPES[0], + bearer_format='JWT' + ) class SimpleJWTTokenUserScheme(SimpleJWTScheme): diff --git a/drf_spectacular/plumbing.py b/drf_spectacular/plumbing.py index c1776460..30e36be2 100644 --- a/drf_spectacular/plumbing.py +++ b/drf_spectacular/plumbing.py @@ -29,6 +29,7 @@ ) from django.utils.functional import Promise, cached_property from django.utils.module_loading import import_string +from django.utils.translation import gettext_lazy as _ from django.utils.version import PY38 from rest_framework import exceptions, fields, mixins, serializers, versioning from rest_framework.settings import api_settings @@ -327,6 +328,30 @@ def build_choice_field(field): return schema +def build_bearer_security_scheme_object(header_name, token_prefix, bearer_format=None): + """ Either build a bearer scheme or a fallback due to OpenAPI 3.0.3 limitations """ + # normalize Django header quirks + if header_name.startswith('HTTP_'): + header_name = header_name[5:] + header_name = header_name.replace('_', '-').capitalize() + + if token_prefix == 'Bearer' and header_name == 'Authorization': + return { + 'type': 'http', + 'scheme': 'bearer', + **({'bearerFormat': bearer_format} if bearer_format else {}), + } + else: + return { + 'type': 'apiKey', + 'in': 'header', + 'name': header_name, + 'description': _( + 'Token-based authentication with required prefix "%s"' + ) % token_prefix + } + + def build_root_object(paths, components, version): settings = spectacular_settings if settings.VERSION and version: diff --git a/tests/contrib/test_drf_jwt.py b/tests/contrib/test_drf_jwt.py index cb81cfa8..bdb68c9a 100644 --- a/tests/contrib/test_drf_jwt.py +++ b/tests/contrib/test_drf_jwt.py @@ -1,3 +1,5 @@ +from unittest import mock + import pytest from django.urls import path from rest_framework import mixins, routers, serializers, viewsets @@ -35,3 +37,17 @@ def test_drf_jwt(no_warnings): schema = generate_schema(None, patterns=urlpatterns) assert_schema(schema, 'tests/contrib/test_drf_jwt.yml') + + +@pytest.mark.contrib('rest_framework_jwt') +@mock.patch('rest_framework_jwt.settings.api_settings.JWT_AUTH_HEADER_PREFIX', 'JWT') +def test_drf_jwt_non_bearer_keyword(no_warnings): + schema = generate_schema('/x', XViewset) + assert schema['components']['securitySchemes'] == { + 'jwtAuth': { + 'type': 'apiKey', + 'in': 'header', + 'name': 'Authorization', + 'description': 'Token-based authentication with required prefix "JWT"' + }, + } diff --git a/tests/contrib/test_drf_jwt.yml b/tests/contrib/test_drf_jwt.yml index 302cf42e..f4cfb393 100644 --- a/tests/contrib/test_drf_jwt.yml +++ b/tests/contrib/test_drf_jwt.yml @@ -85,4 +85,4 @@ components: jwtAuth: type: http scheme: bearer - bearerFormat: Bearer + bearerFormat: JWT