fastapi
fastapi copied to clipboard
HTTPBearer security scheme is returning 403 instead or 401
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.
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.
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 @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.
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 You could open a PR to correct this
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 missinghttps://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
It makes total sense to have 401 returned, I'm sure tiangolo did not mean 403 and it was just a small mishap
This is still not fixed
@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.
There is a workaround solution:
- pass
auto_error=False
when you are creatingHTTPBearer
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 toNone
and raise the appropriateHTTPException
:
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",
)
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.