encode / django-rest-framework

Web APIs for Django. 🎸
https://www.django-rest-framework.org
Other
28.37k stars 6.84k forks source link

403 response instead of 401 when IsAuthenticated permission is used. #5968

Closed v-hunt closed 6 years ago

v-hunt commented 6 years ago

Let's assume we want to have an url that is accessible by authenticated users only. Lets assume I'm not authenticated. In this case the correct response is 401 (NOT AUTHENTICATED), but the API gives us 401 (FORBIDDEN). Note that we expect that the url must be accessible by every authenticated user, not by particular group only (like accountant, manager etc).

This is a link to W3C documentation that explains the difference between 401 and 403 responses. This is a link to StackOverFlow answer

Steps to reproduce

Let's assume we have an view with permission:

class MyView(APIView):
    permission_classes = (IsAuthenticated, )

Expected behavior

The unit test:


    def test_not_auth_user_access_denied(self):
        resp = self.client.get(self.URL)

        self.assertEqual(
            resp.status_code,
            status.HTTP_401_UNAUTHORIZED,
            "Not auth user has access to MyView!"
        )

Actual behavior


    def test_not_auth_user_access_denied(self):
        resp = self.client.get(self.URL)

        self.assertEqual(
            resp.status_code,
            status.HTTP_403_FORBIDDEN,
            "Not auth user has access to MyTasksList!"
        )
rpkilby commented 6 years ago

This is most likely due to not having any authenticators configured for the view. Is your application disabling the DEFAULT_AUTHENTICATION_CLASSES?

As seen below, DRF should raise a 401 otherwise.

https://github.com/encode/django-rest-framework/blob/fd4282c7fa3e50a63ec39c4d21c9e74974d6ecc8/rest_framework/views.py#L159-L165

xordoquy commented 6 years ago

You need to realize that W3C recommendations don't apply to session authentication because it doesn't support the "WWW-Authenticate" header.

DRF explains what response to expect in the documentation.

Therefore it doesn't make sense to respond with a 401 with session authentication.

v-hunt commented 6 years ago

@rpkilby

Is your application disabling the DEFAULT_AUTHENTICATION_CLASSES?

This is a new app, so for now it uses the default settings for auth. That's maybe the reason. My apologize.

@rpkilby @xordoquy But anyway. Sometimes we abuse official specifications for convenience. For example, Gmail doesn't care about case of letters in username part of address (the part before @ sign) So, does it worth to use 401 even for SessionAuthentication? My idea is that we should expect 401 response with any type of authentication.

xordoquy commented 6 years ago

So, does it worth to use 401 even for SessionAuthentication?

I don't think it'll make its way to core with the current argument. This being said, you may alter this behavior by overriding this code

chdsbd commented 6 years ago

I just ran into this issue. I think returning 401 is useful for the client to know if they need to reauthenticate. I'll try an post a follow up if I find a nice solution.

csdenboer commented 6 years ago

I would also like to return a 401 when session authentication has failed. Overriding the code is kind of a pain in the ass and I think it is much more neat to return a 401. As mentioned earlier, we now cannot distinguish a user not having the correct rights and the user not being authentication in the client side.

csdenboer commented 6 years ago

I think this is a pretty neat workaround:

class SessionAuthentication(authentication.SessionAuthentication):
    """
    This class is needed, because REST Framework's default SessionAuthentication does never return 401's,
    because they cannot fill the WWW-Authenticate header with a valid value in the 401 response. As a
    result, we cannot distinguish calls that are not unauthorized (401 unauthorized) and calls for which
    the user does not have permission (403 forbidden). See https://github.com/encode/django-rest-framework/issues/5968

    We do set authenticate_header function in SessionAuthentication, so that a value for the WWW-Authenticate
    header can be retrieved and the response code is automatically set to 401 in case of unauthenticated requests.
    """
    def authenticate_header(self, request):
        return 'Session'
hashlash commented 5 years ago

You need to realize that W3C recommendations don't apply to session authentication because it doesn't support the "WWW-Authenticate" header.

DRF explains what response to expect in the documentation.

Therefore it doesn't make sense to respond with a 401 with session authentication.

To whom it might concern: https://github.com/encode/django-rest-framework/issues/4680#issuecomment-260464834

bryanhelmig commented 4 years ago

For folks 🤔on this one, you can pretty easily substitute the exception handler to swap those back if you don't mind 🙈 on rfc2616:

# in settings.py
REST_FRAMEWORK = {
    'EXCEPTION_HANDLER': 'path.to.custom_exception_handler'
}

# in path/to.py
from rest_framework import exceptions, status, views

def custom_exception_handler(exc, context):
    response = views.exception_handler(exc, context)

    if isinstance(exc, (exceptions.AuthenticationFailed, exceptions.NotAuthenticated)):
        response.status_code = status.HTTP_401_UNAUTHORIZED

    return response
wanliqun commented 4 years ago

How about some middleware to remove the WWW-Authenticate header for login popup when using basicAuth & sessionAuth in API?

#Remove WWW-Authenticate header, in case browser popup login window when 401
class PreventAuthenticatePromptMiddleware(object):
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        response = self.get_response(request)
        if response and response.status_code == 401:
            del response['WWW-Authenticate']
        return response

settings.py

MIDDLEWARE = [
    'api.middleware.PreventAuthenticatePromptMiddleware',
......
]
kouroshezzati commented 4 years ago

If you want to get 401 instead of 403 you need to add authentication_classses = (TokenAuthentication,) because DRF need to which mechanizem is required for handling the authentication as you didn't provide it, It simply return 403 which is resonable

mazulo commented 2 years ago

@csdenboer thank you, your workaround worked like a charm! 😄

vecter commented 1 year ago

I think this is a pretty neat workaround:

class SessionAuthentication(authentication.SessionAuthentication):
    """
    This class is needed, because REST Framework's default SessionAuthentication does never return 401's,
    because they cannot fill the WWW-Authenticate header with a valid value in the 401 response. As a
    result, we cannot distinguish calls that are not unauthorized (401 unauthorized) and calls for which
    the user does not have permission (403 forbidden). See https://github.com/encode/django-rest-framework/issues/5968

    We do set authenticate_header function in SessionAuthentication, so that a value for the WWW-Authenticate
    header can be retrieved and the response code is automatically set to 401 in case of unauthenticated requests.
    """
    def authenticate_header(self, request):
        return 'Session'

Silly question, but where do you put this code?

hashlash commented 1 year ago

Silly question, but where do you put this code?

@vecter you could put it anywhere you like (for example: my_project/authentication.py). Just make sure you use it either as the default authentication scheme or per-view basis.

For more info: https://www.django-rest-framework.org/api-guide/authentication/#setting-the-authentication-scheme

johnthagen commented 1 year ago

If you are like me and you support both SessionAuthentication and JWTAuthentication, it turns out the issue I had was that the order of the DEFAULT_AUTHENTICATION_CLASSES is significant. My JWT clients were always getting 403's instead of 401's and it turned out that was because I happened to have SessionAuthentication first in the list of DEFAULT_AUTHENTICATION_CLASSES:

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework.authentication.SessionAuthentication",
        "rest_framework.authentication.BasicAuthentication",
        "rest_framework_simplejwt.authentication.JWTAuthentication",
    ],
}

Putting SessionAuthentication last in the list allowed 401's to be returned, and my clients to be able to distinguish between a token being invalid/expired (401) and missing object level permissions (403).

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework_simplejwt.authentication.JWTAuthentication",
        "rest_framework.authentication.BasicAuthentication",
        "rest_framework.authentication.SessionAuthentication",
    ],
}

See

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.

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.

OdifYltsaeb commented 1 year ago

I think this is a pretty neat workaround:

class SessionAuthentication(authentication.SessionAuthentication):
    """
    This class is needed, because REST Framework's default SessionAuthentication does never return 401's,
    because they cannot fill the WWW-Authenticate header with a valid value in the 401 response. As a
    result, we cannot distinguish calls that are not unauthorized (401 unauthorized) and calls for which
    the user does not have permission (403 forbidden). See https://github.com/encode/django-rest-framework/issues/5968

    We do set authenticate_header function in SessionAuthentication, so that a value for the WWW-Authenticate
    header can be retrieved and the response code is automatically set to 401 in case of unauthenticated requests.
    """
    def authenticate_header(self, request):
        return 'Session'

You da MVP @csdenboer

I'm playing with code for my own pleasure and trying to make NextJs and authentication work against the DRF backend only to see that I'm getting 403s at every turn when they should be 401. Google, find this and find the solution too. I'd gladly pay for your next drink.

shimafallah commented 11 months ago

try to add this one to your settings REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework.authentication.BasicAuthentication' ),}

iranzithierry commented 5 months ago

If you are like me and you support both SessionAuthentication and JWTAuthentication, it turns out the issue I had was that the order of the DEFAULT_AUTHENTICATION_CLASSES is significant. My JWT clients were always getting 403's instead of 401's and it turned out that was because I happened to have SessionAuthentication first in the list of DEFAULT_AUTHENTICATION_CLASSES:

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework.authentication.SessionAuthentication",
        "rest_framework.authentication.BasicAuthentication",
        "rest_framework_simplejwt.authentication.JWTAuthentication",
    ],
}

Putting SessionAuthentication last in the list allowed 401's to be returned, and my clients to be able to distinguish between a token being invalid/expired (401) and missing object level permissions (403).

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework_simplejwt.authentication.JWTAuthentication",
        "rest_framework.authentication.BasicAuthentication",
        "rest_framework.authentication.SessionAuthentication",
    ],
}

See

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.

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.

thanks

GuiTeK commented 3 months ago

In my case, the issue was that I was returning (None, None) in my custom authenticator instead of None when there were no credentials. This caused the authenticator to be seen as "successful" by DRF, but since the user and token were None, I was getting a 403 Forbidden.