fastapi icon indicating copy to clipboard operation
fastapi copied to clipboard

HTTPBearer security scheme is returning 403 instead or 401

Open aaaaahaaaaa opened this issue 3 years ago • 10 comments

HTTPBearer security scheme enabled as a dependency is returning a 403 when a request is unauthenticated because of a missing or a malformed authorization header. In those scenarios, a 401 should be returned instead.

aaaaahaaaaa avatar Sep 08 '20 13:09 aaaaahaaaaa

Could you provide an example of how you are getting that? Docs say:

If it doesn't see an Authorization header, or the value doesn't have a Bearer token, it will respond with a 401 status code error (UNAUTHORIZED) directly.

ArcLightSlavik avatar Sep 08 '20 22:09 ArcLightSlavik

For API_KEY when the authorization is missing then it's a 403 https://github.com/tiangolo/fastapi/blob/e77ea635777d2494690ba3eb62bd005b9edeefde/fastapi/security/api_key.py#L27

but for bearer it's a 401 if it's missing

https://github.com/tiangolo/fastapi/blob/e77ea635777d2494690ba3eb62bd005b9edeefde/fastapi/security/oauth2.py#L158

raphaelauv avatar Sep 09 '20 16:09 raphaelauv

@raphaelauv @ArcLightSlavik I believe the status code is coming from here: https://github.com/tiangolo/fastapi/blob/55b9faeb48f9c8676cd56adc8f8d75040e3f1010/fastapi/security/http.py#L114

My setup is the following:

...
from fastapi.security import HTTPBearer

security = HTTPBearer()

@app.get("/test")
async def test(bearer_token = Depends(security)):
    ...

Then if you don't provide a bearer token via the authorization header, this produces a 403 by default with the error message "Not authenticated". Again in that scenario, I believe it should be a 401.

aaaaahaaaaa avatar Sep 10 '20 11:09 aaaaahaaaaa

To be clear here: I only want to support Authorization header to do the JWT verification. I don't want to support the full OAuth2 flows which is why I'm not using the OAuth2PasswordBearer scheme.

aaaaahaaaaa avatar Sep 10 '20 12:09 aaaaahaaaaa

@aaaaahaaaaa You could open a PR to correct this

raphaelauv avatar Sep 14 '20 21:09 raphaelauv

For API_KEY when the authorization is missing then it's a 403

https://github.com/tiangolo/fastapi/blob/e77ea635777d2494690ba3eb62bd005b9edeefde/fastapi/security/api_key.py#L27

but for bearer it's a 401 if it's missing

https://github.com/tiangolo/fastapi/blob/e77ea635777d2494690ba3eb62bd005b9edeefde/fastapi/security/oauth2.py#L158

I'd like to know why for API_KEY, the error should be 403, but 401 for bearer, thanks

OYE93 avatar Oct 29 '20 02:10 OYE93

It makes total sense to have 401 returned, I'm sure tiangolo did not mean 403 and it was just a small mishap

dorinclisu avatar Feb 08 '21 18:02 dorinclisu

This is still not fixed

iantimmis avatar Mar 30 '22 17:03 iantimmis

@tiangolo is this something that was intentionally implemented this way?

Tests are checking for this as well, but my understanding is that a 401 is the appropriate response.

mglickVA avatar Jul 09 '22 00:07 mglickVA

There is a workaround solution:

  • pass auto_error=False when you are creating HTTPBearer object
  • In this way, created HTTPBearer object will not raise an HTTPException with status code 403. It will return None within its __call__ method, therefore you will check is returned value equal to None and raise the appropriate HTTPException:
security = HTTPBearer(auto_error=False)

@app.get("/test")
async def test(bearer_token = Depends(security)):
   if not bearer_token:
      raise HTTPException(
          status_code=status.HTTP_401_UNAUTHORIZED,
          detail="Missing bearer token",
      )

icvorovic avatar Aug 08 '22 09:08 icvorovic

I think this should be qualified as bug, not question, since, from MDN:

The HyperText Transfer Protocol (HTTP) 401 Unauthorized response status code indicates that the client request has not been completed because it lacks valid authentication credentials for the requested resource.

and

The HTTP 403 Forbidden response status code indicates that the server understands the request but refuses to authorize it. This status is similar to 401, but for the 403 Forbidden status code re-authenticating makes no difference. The access is permanently forbidden and tied to the application logic, such as insufficient rights to a resource.

In this case, could (re-)authenticating permit access to the resource ? Definitively yes (As I understand, 403 is: try as much as you want, you will never have access), I tend to think 401 is authentication, 403 is permissions.

Why it does matter ? Normally, when client receive 401, either the credentials are wrong, either the app is not logged anymore (i.e session timeout), so the normal process is to go to login page again and retry. On 403, there is no need to go there again since it is already authenticated (or, login again but with different credentials).

My solution (that I find hideous) is to do this:

from typing import Optional
from fastapi.security import APIKeyHeader as ApiKeyHeader403
from starlette.exceptions import HTTPException
from starlette.status import HTTP_401_UNAUTHORIZED
from starlette.requests import Request



class APIKeyHeader(ApiKeyHeader403):
    async def __call__(self, request: Request) -> Optional[str]:
        try:
            api_key = await super().__call__(request)
        except HTTPException as exception:
            raise HTTPException(
                    status_code=HTTP_401_UNAUTHORIZED, detail=exception.detail
                )
        return api_key

And then use the APIKeyHeader as before.

PS: This should not be necessary, or I miss the point, but since the only check done is on the header presence, this should be corrected upstream with 401.

It works because right now, the only exception on APIKeyHeader is when the header is missing, but if someday fastapi implement permissions, I'm not sure it will still be valid.

potens1 avatar Oct 16 '22 12:10 potens1