-
-
Notifications
You must be signed in to change notification settings - Fork 7.1k
Add support for multiple authentication challenges in WWW-Authenticate header #9242
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
base: main
Are you sure you want to change the base?
Changes from all commits
1cd663c
0a53bb0
8c23de2
525979e
ea612e2
146e732
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -79,7 +79,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. | ||||||
|
||||||
| 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. | |
| 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. |
Copilot
AI
Apr 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With WWW_AUTHENTICATE_BEHAVIOR='all', the 401 vs 403 decision is no longer determined solely by the first authentication class (any authenticator that returns a challenge will keep the response as 401). Please update/qualify this sentence so it remains accurate under the new setting.
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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)). | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. RFC 9110 also warns:
Perhaps we should have a similar warning somewhere, either here or in
|
||||||
| 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)). | |
| When set to `'all'`, a comma-separated list of the challenges 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)). |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good question; have you been able to understand this further?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is following this advice from the Django docs:
When you say you were able to trigger the new error without this: did this happen on startup, or when you ran
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I understand this better now. The I tested this theory by removing both lines; in that case, my check does not run. But this means that both lines can be replaced with just I hope all this made sense 🙂. If it does, I'll plan to make that change and resolve this conversation. |
||
| 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) | ||
|
|
@@ -19,3 +19,22 @@ def pagination_system_check(app_configs, **kwargs): | |
| ) | ||
| ) | ||
| return errors | ||
|
|
||
|
|
||
| @register(Tags.compatibility) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This probably should be Those checks also use
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I went with I'm happy to go with any of these options, just let me know what you prefer.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure, I have no strong preference. |
||
| 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 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -78,6 +78,7 @@ | |
| # Authentication | ||
| 'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser', | ||
| 'UNAUTHENTICATED_TOKEN': None, | ||
| 'WWW_AUTHENTICATE_BEHAVIOR': 'first', | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why isn't this a boolean, e.g.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suppose I was leaving the door open for other modes beyond |
||
|
|
||
| # View configuration | ||
| 'VIEW_NAME_FUNCTION': 'rest_framework.views.get_view_name', | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -108,6 +108,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 | ||||||||||||||||||
|
|
@@ -192,8 +193,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 | ||||||||||||||||||
|
||||||||||||||||||
| www_authenticate_behavior = self.www_authenticate_behavior | |
| www_authenticate_behavior = self.www_authenticate_behavior | |
| # Ensure that misconfiguration of `www_authenticate_behavior` does not | |
| # silently change response semantics. Fall back to the default | |
| # behavior of using the first authenticator if an unexpected value | |
| # is provided at the view level. | |
| if www_authenticate_behavior not in ('first', 'all'): | |
| www_authenticate_behavior = 'first' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Setting name should be uppercase