Compare commits

..

1 Commits

Author SHA1 Message Date
Marcelo Trylesinski
df987e8d3d Use 401 instead of 403 in security classes 2025-03-20 12:19:07 +00:00
4 changed files with 28 additions and 14 deletions

View File

@@ -181,7 +181,7 @@ class HTTPBasic(HTTPBase):
):
self.model = HTTPBaseModel(scheme="basic", description=description)
self.scheme_name = scheme_name or self.__class__.__name__
self.realm = realm or ""
self.realm = realm
self.auto_error = auto_error
async def __call__( # type: ignore
@@ -189,8 +189,10 @@ class HTTPBasic(HTTPBase):
) -> Optional[HTTPBasicCredentials]:
authorization = request.headers.get("Authorization")
scheme, param = get_authorization_scheme_param(authorization)
# The "realm" is required, as per https://datatracker.ietf.org/doc/html/rfc7617#section-2.
unauthorized_headers = {"WWW-Authenticate": f'Basic realm="{self.realm}"'}
if self.realm:
unauthorized_headers = {"WWW-Authenticate": f'Basic realm="{self.realm}"'}
else:
unauthorized_headers = {"WWW-Authenticate": "Basic"}
if not authorization or scheme.lower() != "basic":
if self.auto_error:
raise HTTPException(
@@ -301,18 +303,23 @@ class HTTPBearer(HTTPBase):
) -> Optional[HTTPAuthorizationCredentials]:
authorization = request.headers.get("Authorization")
scheme, credentials = get_authorization_scheme_param(authorization)
# All fields besides the scheme are optional, as per https://www.rfc-editor.org/rfc/rfc6750.html#section-3.
unauthorized_headers = {"WWW-Authenticate": "Bearer"}
if not (authorization and scheme and credentials):
if self.auto_error:
raise HTTPException(
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
status_code=HTTP_401_UNAUTHORIZED,
detail="Not authenticated",
headers=unauthorized_headers,
)
else:
return None
if scheme.lower() != "bearer":
if self.auto_error:
raise HTTPException(
status_code=HTTP_403_FORBIDDEN,
status_code=HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
headers=unauthorized_headers,
)
else:
return None
@@ -403,18 +410,23 @@ class HTTPDigest(HTTPBase):
) -> Optional[HTTPAuthorizationCredentials]:
authorization = request.headers.get("Authorization")
scheme, credentials = get_authorization_scheme_param(authorization)
# All fields besides the scheme are optional, as per https://datatracker.ietf.org/doc/html/rfc7616#section-3.3.
unauthorized_headers = {"WWW-Authenticate": "Digest"}
if not (authorization and scheme and credentials):
if self.auto_error:
raise HTTPException(
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
status_code=HTTP_401_UNAUTHORIZED,
detail="Not authenticated",
headers=unauthorized_headers,
)
else:
return None
if scheme.lower() != "digest":
if self.auto_error:
raise HTTPException(
status_code=HTTP_403_FORBIDDEN,
status_code=HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
headers=unauthorized_headers,
)
else:
return None

View File

@@ -7,7 +7,7 @@ from fastapi.param_functions import Form
from fastapi.security.base import SecurityBase
from fastapi.security.utils import get_authorization_scheme_param
from starlette.requests import Request
from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN
from starlette.status import HTTP_401_UNAUTHORIZED
# TODO: import from typing when deprecating Python 3.9
from typing_extensions import Annotated, Doc
@@ -381,7 +381,9 @@ class OAuth2(SecurityBase):
if not authorization:
if self.auto_error:
raise HTTPException(
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
status_code=HTTP_401_UNAUTHORIZED,
detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"},
)
else:
return None

View File

@@ -37,7 +37,7 @@ def test_security_http_basic_invalid_credentials():
"/users/me", headers={"Authorization": "Basic notabase64token"}
)
assert response.status_code == 401, response.text
assert response.headers["WWW-Authenticate"] == 'Basic realm=""'
assert response.headers["WWW-Authenticate"] == "Basic"
assert response.json() == {"detail": "Invalid authentication credentials"}
@@ -46,7 +46,7 @@ def test_security_http_basic_non_basic_credentials():
auth_header = f"Basic {payload}"
response = client.get("/users/me", headers={"Authorization": auth_header})
assert response.status_code == 401, response.text
assert response.headers["WWW-Authenticate"] == 'Basic realm=""'
assert response.headers["WWW-Authenticate"] == "Basic"
assert response.json() == {"detail": "Invalid authentication credentials"}

View File

@@ -32,7 +32,7 @@ def test_security_http_basic_no_credentials(client: TestClient):
response = client.get("/users/me")
assert response.json() == {"detail": "Not authenticated"}
assert response.status_code == 401, response.text
assert response.headers["WWW-Authenticate"] == 'Basic realm=""'
assert response.headers["WWW-Authenticate"] == "Basic"
def test_security_http_basic_invalid_credentials(client: TestClient):
@@ -40,7 +40,7 @@ def test_security_http_basic_invalid_credentials(client: TestClient):
"/users/me", headers={"Authorization": "Basic notabase64token"}
)
assert response.status_code == 401, response.text
assert response.headers["WWW-Authenticate"] == 'Basic realm=""'
assert response.headers["WWW-Authenticate"] == "Basic"
assert response.json() == {"detail": "Invalid authentication credentials"}
@@ -49,7 +49,7 @@ def test_security_http_basic_non_basic_credentials(client: TestClient):
auth_header = f"Basic {payload}"
response = client.get("/users/me", headers={"Authorization": auth_header})
assert response.status_code == 401, response.text
assert response.headers["WWW-Authenticate"] == 'Basic realm=""'
assert response.headers["WWW-Authenticate"] == "Basic"
assert response.json() == {"detail": "Invalid authentication credentials"}