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

Add support for multiple authentication challenges in WWW-Authenticate header #9242

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/api-guide/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**.

Expand Down
7 changes: 7 additions & 0 deletions docs/api-guide/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions rest_framework/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this line necessary? In my local build I was able to trigger the new error without it; I merely copied the pattern from the line above in my PR.

21 changes: 20 additions & 1 deletion rest_framework/checks.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from django.core.checks import Tags, Warning, register
from django.core.checks import Error, Tags, Warning, register


@register(Tags.compatibility)
Expand All @@ -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
1 change: 1 addition & 0 deletions rest_framework/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
8 changes: 7 additions & 1 deletion rest_framework/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))
waxlamp marked this conversation as resolved.
Show resolved Hide resolved

def get_parser_context(self, http_request):
"""
Expand Down
Loading