From c5bd35c728c32fc7aa28fd650e7673678dd9d3a1 Mon Sep 17 00:00:00 2001 From: Subhash Dasyam Date: Wed, 25 Mar 2026 21:03:53 +0400 Subject: [PATCH] fix: treat empty bearer token as missing credentials in OAuth2 helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Authorization: Bearer (empty token after "Bearer") was returned as "" instead of triggering the auto_error path. Both OAuth2PasswordBearer and OAuth2AuthorizationCodeBearer only checked that the scheme was "bearer" but not that the credential string was non-empty. RFC 6750 Section 2.1 defines b64token = 1*(...) — at least one character required. An empty credential is syntactically malformed and should be treated the same as missing credentials: 401 with auto_error=True, None with auto_error=False. Fix: add `not param` to the guard condition in both __call__ methods. Closes: https://github.com/fastapi/fastapi/discussions/15192 --- fastapi/security/oauth2.py | 4 ++-- tests/test_security_oauth2_authorization_code_bearer.py | 6 ++++++ tests/test_security_oauth2_password_bearer_optional.py | 6 ++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/fastapi/security/oauth2.py b/fastapi/security/oauth2.py index 42674b476c..5b5a6b03c8 100644 --- a/fastapi/security/oauth2.py +++ b/fastapi/security/oauth2.py @@ -536,7 +536,7 @@ class OAuth2PasswordBearer(OAuth2): async def __call__(self, request: Request) -> str | None: authorization = request.headers.get("Authorization") scheme, param = get_authorization_scheme_param(authorization) - if not authorization or scheme.lower() != "bearer": + if not authorization or scheme.lower() != "bearer" or not param: if self.auto_error: raise self.make_not_authenticated_error() else: @@ -642,7 +642,7 @@ class OAuth2AuthorizationCodeBearer(OAuth2): async def __call__(self, request: Request) -> str | None: authorization = request.headers.get("Authorization") scheme, param = get_authorization_scheme_param(authorization) - if not authorization or scheme.lower() != "bearer": + if not authorization or scheme.lower() != "bearer" or not param: if self.auto_error: raise self.make_not_authenticated_error() else: diff --git a/tests/test_security_oauth2_authorization_code_bearer.py b/tests/test_security_oauth2_authorization_code_bearer.py index 587486c76b..a77428253d 100644 --- a/tests/test_security_oauth2_authorization_code_bearer.py +++ b/tests/test_security_oauth2_authorization_code_bearer.py @@ -30,6 +30,12 @@ def test_incorrect_token(): assert response.json() == {"detail": "Not authenticated"} +def test_empty_bearer_token(): + response = client.get("/items", headers={"Authorization": "Bearer "}) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + + def test_token(): response = client.get("/items", headers={"Authorization": "Bearer testtoken"}) assert response.status_code == 200, response.text diff --git a/tests/test_security_oauth2_password_bearer_optional.py b/tests/test_security_oauth2_password_bearer_optional.py index 263359c950..74f11e9c65 100644 --- a/tests/test_security_oauth2_password_bearer_optional.py +++ b/tests/test_security_oauth2_password_bearer_optional.py @@ -36,6 +36,12 @@ def test_incorrect_token(): assert response.json() == {"msg": "Create an account first"} +def test_empty_bearer_token(): + response = client.get("/items", headers={"Authorization": "Bearer "}) + assert response.status_code == 200, response.text + assert response.json() == {"msg": "Create an account first"} + + def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text