From 1cd663cd68a5580840298221c62aa8ca1a431a6b Mon Sep 17 00:00:00 2001 From: Roni Choudhury Date: Thu, 25 Jan 2024 20:05:32 -0500 Subject: [PATCH 1/5] Add a setting to control WWW-Authenticate header behavior --- rest_framework/settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rest_framework/settings.py b/rest_framework/settings.py index b0d7bacecc..2a05e68798 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -78,6 +78,7 @@ # Authentication 'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser', 'UNAUTHENTICATED_TOKEN': None, + 'WWW_AUTHENTICATE_BEHAVIOR': 'first', # View configuration 'VIEW_NAME_FUNCTION': 'rest_framework.views.get_view_name', From 0a53bb01190d7d529e182b3d842d1f443b2ddb9b Mon Sep 17 00:00:00 2001 From: Roni Choudhury Date: Thu, 25 Jan 2024 20:06:28 -0500 Subject: [PATCH 2/5] Implement alternative WWW-Authenticate generation behaviors --- rest_framework/views.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/rest_framework/views.py b/rest_framework/views.py index 4c30029fdc..830557033a 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -107,6 +107,7 @@ class APIView(View): renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES parser_classes = api_settings.DEFAULT_PARSER_CLASSES authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES + www_authenticate_behavior = api_settings.WWW_AUTHENTICATE_BEHAVIOR throttle_classes = api_settings.DEFAULT_THROTTLE_CLASSES permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS @@ -186,8 +187,13 @@ def get_authenticate_header(self, request): header to use for 401 responses, if any. """ authenticators = self.get_authenticators() + www_authenticate_behavior = self.www_authenticate_behavior if authenticators: - return authenticators[0].authenticate_header(request) + if www_authenticate_behavior == 'first': + return authenticators[0].authenticate_header(request) + elif www_authenticate_behavior == 'all': + challenges = (a.authenticate_header(request) for a in authenticators) + return ', '.join((c for c in challenges if c is not None)) def get_parser_context(self, http_request): """ From 8c23de27e848038fc9b633f3767745f66613cedd Mon Sep 17 00:00:00 2001 From: Roni Choudhury Date: Thu, 25 Jan 2024 20:07:04 -0500 Subject: [PATCH 3/5] Add documentation for new setting and behavior --- docs/api-guide/authentication.md | 2 +- docs/api-guide/settings.md | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index d6e6293fd9..dc53af7197 100644 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -84,7 +84,7 @@ When an unauthenticated request is denied permission there are two different err * [HTTP 401 Unauthorized][http401] * [HTTP 403 Permission Denied][http403] -HTTP 401 responses must always include a `WWW-Authenticate` header, that instructs the client how to authenticate. HTTP 403 responses do not include the `WWW-Authenticate` header. +HTTP 401 responses must always include a `WWW-Authenticate` header, that instructs the client how to authenticate. The `www_authenticate_behavior` setting controls how the header is generated: if set to `'first'` (the default), then only the text for the first scheme in the list will be used; if set to `'all'`, then a comma-separated list of the text for all the schemes will be used (see [MDN WWW-Authenticate](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/WWW-Authenticate) for more details). HTTP 403 responses do not include the `WWW-Authenticate` header. The kind of response that will be used depends on the authentication scheme. Although multiple authentication schemes may be in use, only one scheme may be used to determine the type of response. **The first authentication class set on the view is used when determining the type of response**. diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md index 47e2ce993f..0c166227dc 100644 --- a/docs/api-guide/settings.md +++ b/docs/api-guide/settings.md @@ -189,6 +189,13 @@ The class that should be used to initialize `request.auth` for unauthenticated r Default: `None` +#### WWW_AUTHENTICATE_BEHAVIOR + +Determines whether a single or multiple challenges are presented in the `WWW-Authenticate` header. + +This should be set to `'first'` (the default value) or `'all'`. When set to `'first'`, the `WWW-Authenticate` header will be set to an appropriate challenge for the first authentication scheme in the list. +When set to `'all'`, a comma-separated list of the challenge for all specified authentication schemes will be used instead (following the [syntax specification](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/WWW-Authenticate)). + --- ## Test settings From 525979e343b37a818bd6370998e222e694fade90 Mon Sep 17 00:00:00 2001 From: Roni Choudhury Date: Mon, 29 Jan 2024 13:21:33 -0500 Subject: [PATCH 4/5] Add a system check for WWW_AUTHENTICATE_BEHAVIOR setting --- rest_framework/apps.py | 1 + rest_framework/checks.py | 21 ++++++++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/rest_framework/apps.py b/rest_framework/apps.py index f6013eb7e0..9298c4bf56 100644 --- a/rest_framework/apps.py +++ b/rest_framework/apps.py @@ -8,3 +8,4 @@ class RestFrameworkConfig(AppConfig): def ready(self): # Add System checks from .checks import pagination_system_check # NOQA + from .checks import www_authenticate_behavior_setting_check # NOQA diff --git a/rest_framework/checks.py b/rest_framework/checks.py index d5d77bc59b..12c75db3ce 100644 --- a/rest_framework/checks.py +++ b/rest_framework/checks.py @@ -1,4 +1,4 @@ -from django.core.checks import Tags, Warning, register +from django.core.checks import Tags, Error, Warning, register @register(Tags.compatibility) @@ -19,3 +19,22 @@ def pagination_system_check(app_configs, **kwargs): ) ) return errors + + +@register(Tags.compatibility) +def www_authenticate_behavior_setting_check(app_configs, **kwargs): + errors = [] + # WWW_AUTHENTICATE_BEHAVIOR setting must be 'first' or 'all' + from rest_framework.settings import api_settings + setting = api_settings.WWW_AUTHENTICATE_BEHAVIOR + if setting not in ['first', 'all']: + errors.append( + Error( + "The rest_framework setting WWW_AUTHENTICATE_BEHAVIOR must be either " + f"'first' or 'all' (it is currently set to '{setting}').", + hint="Set WWW_AUTHENTICATE_BEHAVIOR to either 'first' or 'all', " + "or leave it unset (the default value is 'first').", + id="rest_framework.E001", + ) + ) + return errors From ea612e2d15bd1a41a416e8eb888817f1d0dc44bf Mon Sep 17 00:00:00 2001 From: Roni Choudhury Date: Mon, 29 Jan 2024 13:54:55 -0500 Subject: [PATCH 5/5] Fix isort error --- rest_framework/checks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/checks.py b/rest_framework/checks.py index 12c75db3ce..d6a6d7d90e 100644 --- a/rest_framework/checks.py +++ b/rest_framework/checks.py @@ -1,4 +1,4 @@ -from django.core.checks import Tags, Error, Warning, register +from django.core.checks import Error, Tags, Warning, register @register(Tags.compatibility)