]> git.ipfire.org Git - thirdparty/fastapi/fastapi.git/commitdiff
🐛 Use `401` status code in security classes when credentials are missing (#13786)
authorMotov Yurii <109919500+YuriiMotov@users.noreply.github.com>
Mon, 24 Nov 2025 19:03:06 +0000 (20:03 +0100)
committerGitHub <noreply@github.com>
Mon, 24 Nov 2025 19:03:06 +0000 (20:03 +0100)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
36 files changed:
docs/en/docs/how-to/authentication-error-status-code.md [new file with mode: 0644]
docs/en/mkdocs.yml
docs_src/authentication_error_status_code/tutorial001_an.py [new file with mode: 0644]
docs_src/authentication_error_status_code/tutorial001_an_py39.py [new file with mode: 0644]
docs_src/security/tutorial003.py
docs_src/security/tutorial003_an.py
docs_src/security/tutorial003_an_py310.py
docs_src/security/tutorial003_an_py39.py
docs_src/security/tutorial003_py310.py
fastapi/security/api_key.py
fastapi/security/http.py
fastapi/security/oauth2.py
fastapi/security/open_id_connect_url.py
tests/test_security_api_key_cookie.py
tests/test_security_api_key_cookie_description.py
tests/test_security_api_key_header.py
tests/test_security_api_key_header_description.py
tests/test_security_api_key_query.py
tests/test_security_api_key_query_description.py
tests/test_security_http_base.py
tests/test_security_http_base_description.py
tests/test_security_http_basic_optional.py
tests/test_security_http_basic_realm.py
tests/test_security_http_basic_realm_description.py
tests/test_security_http_bearer.py
tests/test_security_http_bearer_description.py
tests/test_security_http_digest.py
tests/test_security_http_digest_description.py
tests/test_security_oauth2.py
tests/test_security_openid_connect.py
tests/test_security_openid_connect_description.py
tests/test_top_level_security_scheme_in_openapi.py
tests/test_tutorial/test_authentication_error_status_code/__init__.py [new file with mode: 0644]
tests/test_tutorial/test_authentication_error_status_code/test_tutorial001.py [new file with mode: 0644]
tests/test_tutorial/test_security/test_tutorial003.py
tests/test_tutorial/test_security/test_tutorial006.py

diff --git a/docs/en/docs/how-to/authentication-error-status-code.md b/docs/en/docs/how-to/authentication-error-status-code.md
new file mode 100644 (file)
index 0000000..f9433e5
--- /dev/null
@@ -0,0 +1,17 @@
+# Use Old 403 Authentication Error Status Codes { #use-old-403-authentication-error-status-codes }
+
+Before FastAPI version `0.122.0`, when the integrated security utilities returned an error to the client after a failed authentication, they used the HTTP status code `403 Forbidden`.
+
+Starting with FastAPI version `0.122.0`, they use the more appropriate HTTP status code `401 Unauthorized`, and return a sensible `WWW-Authenticate` header in the response, following the HTTP specifications, <a href="https://datatracker.ietf.org/doc/html/rfc7235#section-3.1" class="external-link" target="_blank">RFC 7235</a>, <a href="https://datatracker.ietf.org/doc/html/rfc9110#name-401-unauthorized" class="external-link" target="_blank">RFC 9110</a>.
+
+But if for some reason your clients depend on the old behavior, you can revert to it by overriding the method `make_not_authenticated_error` in your security classes.
+
+For example, you can create a subclass of `HTTPBearer` that returns a `403 Forbidden` error instead of the default `401 Unauthorized` error:
+
+{* ../../docs_src/authentication_error_status_code/tutorial001_an_py39.py hl[9:13] *}
+
+/// tip
+
+Notice that the function returns the exception instance, it doesn't raise it. The raising is done in the rest of the internal code.
+
+///
index 8be832f117bea20667b2c9601cec662271d6faf5..fd346a3d38dfb163ada71d89e0776308e94fa025 100644 (file)
@@ -215,6 +215,7 @@ nav:
     - how-to/custom-docs-ui-assets.md
     - how-to/configure-swagger-ui.md
     - how-to/testing-database.md
+    - how-to/authentication-error-status-code.md
 - Reference (Code API):
   - reference/index.md
   - reference/fastapi.md
diff --git a/docs_src/authentication_error_status_code/tutorial001_an.py b/docs_src/authentication_error_status_code/tutorial001_an.py
new file mode 100644 (file)
index 0000000..40678e8
--- /dev/null
@@ -0,0 +1,20 @@
+from fastapi import Depends, FastAPI, HTTPException, status
+from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
+from typing_extensions import Annotated
+
+app = FastAPI()
+
+
+class HTTPBearer403(HTTPBearer):
+    def make_not_authenticated_error(self) -> HTTPException:
+        return HTTPException(
+            status_code=status.HTTP_403_FORBIDDEN, detail="Not authenticated"
+        )
+
+
+CredentialsDep = Annotated[HTTPAuthorizationCredentials, Depends(HTTPBearer403())]
+
+
+@app.get("/me")
+def read_me(credentials: CredentialsDep):
+    return {"message": "You are authenticated", "token": credentials.credentials}
diff --git a/docs_src/authentication_error_status_code/tutorial001_an_py39.py b/docs_src/authentication_error_status_code/tutorial001_an_py39.py
new file mode 100644 (file)
index 0000000..7bbc2f7
--- /dev/null
@@ -0,0 +1,21 @@
+from typing import Annotated
+
+from fastapi import Depends, FastAPI, HTTPException, status
+from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
+
+app = FastAPI()
+
+
+class HTTPBearer403(HTTPBearer):
+    def make_not_authenticated_error(self) -> HTTPException:
+        return HTTPException(
+            status_code=status.HTTP_403_FORBIDDEN, detail="Not authenticated"
+        )
+
+
+CredentialsDep = Annotated[HTTPAuthorizationCredentials, Depends(HTTPBearer403())]
+
+
+@app.get("/me")
+def read_me(credentials: CredentialsDep):
+    return {"message": "You are authenticated", "token": credentials.credentials}
index 4b324866f6850d76b2862b05e5bf2e7e44b662c1..ce7a71b68ba2d3fcfb922eac4ba55e266ef2be07 100644 (file)
@@ -60,7 +60,7 @@ async def get_current_user(token: str = Depends(oauth2_scheme)):
     if not user:
         raise HTTPException(
             status_code=status.HTTP_401_UNAUTHORIZED,
-            detail="Invalid authentication credentials",
+            detail="Not authenticated",
             headers={"WWW-Authenticate": "Bearer"},
         )
     return user
index 8fb40dd4aa8d005f2787b196ca98b49615c2679c..1b7056a20956f91c57c1e4ae25136fcfb82efc44 100644 (file)
@@ -61,7 +61,7 @@ async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
     if not user:
         raise HTTPException(
             status_code=status.HTTP_401_UNAUTHORIZED,
-            detail="Invalid authentication credentials",
+            detail="Not authenticated",
             headers={"WWW-Authenticate": "Bearer"},
         )
     return user
index ced4a2fbc18b7783820900dc3f795cc78e175a83..4a2743f6f8ed42b36b344c7122ecfcf824870f5b 100644 (file)
@@ -60,7 +60,7 @@ async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
     if not user:
         raise HTTPException(
             status_code=status.HTTP_401_UNAUTHORIZED,
-            detail="Invalid authentication credentials",
+            detail="Not authenticated",
             headers={"WWW-Authenticate": "Bearer"},
         )
     return user
index 068a3933e1a053a213b2fb8cc5bf4b9d1564525f..b396210c827ccefebf201a871ea5fc9b51146119 100644 (file)
@@ -60,7 +60,7 @@ async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
     if not user:
         raise HTTPException(
             status_code=status.HTTP_401_UNAUTHORIZED,
-            detail="Invalid authentication credentials",
+            detail="Not authenticated",
             headers={"WWW-Authenticate": "Bearer"},
         )
     return user
index af935e997dfd8e8f126dc8e860bd9a054f72d036..081259b31703aa93ac75ccc306721ef2b56e5309 100644 (file)
@@ -58,7 +58,7 @@ async def get_current_user(token: str = Depends(oauth2_scheme)):
     if not user:
         raise HTTPException(
             status_code=status.HTTP_401_UNAUTHORIZED,
-            detail="Invalid authentication credentials",
+            detail="Not authenticated",
             headers={"WWW-Authenticate": "Bearer"},
         )
     return user
index 496c815a77a295a2615bbd9ccc345f372cd85305..81c7be10d6bf4b9326804e14768e0bac4291b254 100644 (file)
@@ -1,22 +1,52 @@
-from typing import Optional
+from typing import Optional, Union
 
 from annotated_doc import Doc
 from fastapi.openapi.models import APIKey, APIKeyIn
 from fastapi.security.base import SecurityBase
 from starlette.exceptions import HTTPException
 from starlette.requests import Request
-from starlette.status import HTTP_403_FORBIDDEN
+from starlette.status import HTTP_401_UNAUTHORIZED
 from typing_extensions import Annotated
 
 
 class APIKeyBase(SecurityBase):
-    @staticmethod
-    def check_api_key(api_key: Optional[str], auto_error: bool) -> Optional[str]:
+    def __init__(
+        self,
+        location: APIKeyIn,
+        name: str,
+        description: Union[str, None],
+        scheme_name: Union[str, None],
+        auto_error: bool,
+    ):
+        self.auto_error = auto_error
+
+        self.model: APIKey = APIKey(
+            **{"in": location},
+            name=name,
+            description=description,
+        )
+        self.scheme_name = scheme_name or self.__class__.__name__
+
+    def make_not_authenticated_error(self) -> HTTPException:
+        """
+        The WWW-Authenticate header is not standardized for API Key authentication but
+        the HTTP specification requires that an error of 401 "Unauthorized" must
+        include a WWW-Authenticate header.
+
+        Ref: https://datatracker.ietf.org/doc/html/rfc9110#name-401-unauthorized
+
+        For this, this method sends a custom challenge `APIKey`.
+        """
+        return HTTPException(
+            status_code=HTTP_401_UNAUTHORIZED,
+            detail="Not authenticated",
+            headers={"WWW-Authenticate": "APIKey"},
+        )
+
+    def check_api_key(self, api_key: Optional[str]) -> Optional[str]:
         if not api_key:
-            if auto_error:
-                raise HTTPException(
-                    status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
-                )
+            if self.auto_error:
+                raise self.make_not_authenticated_error()
             return None
         return api_key
 
@@ -100,17 +130,17 @@ class APIKeyQuery(APIKeyBase):
             ),
         ] = True,
     ):
-        self.model: APIKey = APIKey(
-            **{"in": APIKeyIn.query},
+        super().__init__(
+            location=APIKeyIn.query,
             name=name,
+            scheme_name=scheme_name,
             description=description,
+            auto_error=auto_error,
         )
-        self.scheme_name = scheme_name or self.__class__.__name__
-        self.auto_error = auto_error
 
     async def __call__(self, request: Request) -> Optional[str]:
         api_key = request.query_params.get(self.model.name)
-        return self.check_api_key(api_key, self.auto_error)
+        return self.check_api_key(api_key)
 
 
 class APIKeyHeader(APIKeyBase):
@@ -188,17 +218,17 @@ class APIKeyHeader(APIKeyBase):
             ),
         ] = True,
     ):
-        self.model: APIKey = APIKey(
-            **{"in": APIKeyIn.header},
+        super().__init__(
+            location=APIKeyIn.header,
             name=name,
+            scheme_name=scheme_name,
             description=description,
+            auto_error=auto_error,
         )
-        self.scheme_name = scheme_name or self.__class__.__name__
-        self.auto_error = auto_error
 
     async def __call__(self, request: Request) -> Optional[str]:
         api_key = request.headers.get(self.model.name)
-        return self.check_api_key(api_key, self.auto_error)
+        return self.check_api_key(api_key)
 
 
 class APIKeyCookie(APIKeyBase):
@@ -276,14 +306,14 @@ class APIKeyCookie(APIKeyBase):
             ),
         ] = True,
     ):
-        self.model: APIKey = APIKey(
-            **{"in": APIKeyIn.cookie},
+        super().__init__(
+            location=APIKeyIn.cookie,
             name=name,
+            scheme_name=scheme_name,
             description=description,
+            auto_error=auto_error,
         )
-        self.scheme_name = scheme_name or self.__class__.__name__
-        self.auto_error = auto_error
 
     async def __call__(self, request: Request) -> Optional[str]:
         api_key = request.cookies.get(self.model.name)
-        return self.check_api_key(api_key, self.auto_error)
+        return self.check_api_key(api_key)
index 3a5985650a068476eaa3d0e1579b144be7fa1708..0d1bbba3a079d651bde40016df0fac896422d4ea 100644 (file)
@@ -1,6 +1,6 @@
 import binascii
 from base64 import b64decode
-from typing import Optional
+from typing import Dict, Optional
 
 from annotated_doc import Doc
 from fastapi.exceptions import HTTPException
@@ -10,7 +10,7 @@ from fastapi.security.base import SecurityBase
 from fastapi.security.utils import get_authorization_scheme_param
 from pydantic import BaseModel
 from starlette.requests import Request
-from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN
+from starlette.status import HTTP_401_UNAUTHORIZED
 from typing_extensions import Annotated
 
 
@@ -76,10 +76,22 @@ class HTTPBase(SecurityBase):
         description: Optional[str] = None,
         auto_error: bool = True,
     ):
-        self.model = HTTPBaseModel(scheme=scheme, description=description)
+        self.model: HTTPBaseModel = HTTPBaseModel(
+            scheme=scheme, description=description
+        )
         self.scheme_name = scheme_name or self.__class__.__name__
         self.auto_error = auto_error
 
+    def make_authenticate_headers(self) -> Dict[str, str]:
+        return {"WWW-Authenticate": f"{self.model.scheme.title()}"}
+
+    def make_not_authenticated_error(self) -> HTTPException:
+        return HTTPException(
+            status_code=HTTP_401_UNAUTHORIZED,
+            detail="Not authenticated",
+            headers=self.make_authenticate_headers(),
+        )
+
     async def __call__(
         self, request: Request
     ) -> Optional[HTTPAuthorizationCredentials]:
@@ -87,9 +99,7 @@ class HTTPBase(SecurityBase):
         scheme, credentials = get_authorization_scheme_param(authorization)
         if not (authorization and scheme and credentials):
             if self.auto_error:
-                raise HTTPException(
-                    status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
-                )
+                raise self.make_not_authenticated_error()
             else:
                 return None
         return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
@@ -99,6 +109,8 @@ class HTTPBasic(HTTPBase):
     """
     HTTP Basic authentication.
 
+    Ref: https://datatracker.ietf.org/doc/html/rfc7617
+
     ## Usage
 
     Create an instance object and use that object as the dependency in `Depends()`.
@@ -185,36 +197,28 @@ class HTTPBasic(HTTPBase):
         self.realm = realm
         self.auto_error = auto_error
 
+    def make_authenticate_headers(self) -> Dict[str, str]:
+        if self.realm:
+            return {"WWW-Authenticate": f'Basic realm="{self.realm}"'}
+        return {"WWW-Authenticate": "Basic"}
+
     async def __call__(  # type: ignore
         self, request: Request
     ) -> Optional[HTTPBasicCredentials]:
         authorization = request.headers.get("Authorization")
         scheme, param = get_authorization_scheme_param(authorization)
-        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(
-                    status_code=HTTP_401_UNAUTHORIZED,
-                    detail="Not authenticated",
-                    headers=unauthorized_headers,
-                )
+                raise self.make_not_authenticated_error()
             else:
                 return None
-        invalid_user_credentials_exc = HTTPException(
-            status_code=HTTP_401_UNAUTHORIZED,
-            detail="Invalid authentication credentials",
-            headers=unauthorized_headers,
-        )
         try:
             data = b64decode(param).decode("ascii")
-        except (ValueError, UnicodeDecodeError, binascii.Error):
-            raise invalid_user_credentials_exc  # noqa: B904
+        except (ValueError, UnicodeDecodeError, binascii.Error) as e:
+            raise self.make_not_authenticated_error() from e
         username, separator, password = data.partition(":")
         if not separator:
-            raise invalid_user_credentials_exc
+            raise self.make_not_authenticated_error()
         return HTTPBasicCredentials(username=username, password=password)
 
 
@@ -306,17 +310,12 @@ class HTTPBearer(HTTPBase):
         scheme, credentials = get_authorization_scheme_param(authorization)
         if not (authorization and scheme and credentials):
             if self.auto_error:
-                raise HTTPException(
-                    status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
-                )
+                raise self.make_not_authenticated_error()
             else:
                 return None
         if scheme.lower() != "bearer":
             if self.auto_error:
-                raise HTTPException(
-                    status_code=HTTP_403_FORBIDDEN,
-                    detail="Invalid authentication credentials",
-                )
+                raise self.make_not_authenticated_error()
             else:
                 return None
         return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
@@ -326,6 +325,12 @@ class HTTPDigest(HTTPBase):
     """
     HTTP Digest authentication.
 
+    **Warning**: this is only a stub to connect the components with OpenAPI in FastAPI,
+    but it doesn't implement the full Digest scheme, you would need to to subclass it
+    and implement it in your code.
+
+    Ref: https://datatracker.ietf.org/doc/html/rfc7616
+
     ## Usage
 
     Create an instance object and use that object as the dependency in `Depends()`.
@@ -408,17 +413,12 @@ class HTTPDigest(HTTPBase):
         scheme, credentials = get_authorization_scheme_param(authorization)
         if not (authorization and scheme and credentials):
             if self.auto_error:
-                raise HTTPException(
-                    status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
-                )
+                raise self.make_not_authenticated_error()
             else:
                 return None
         if scheme.lower() != "digest":
             if self.auto_error:
-                raise HTTPException(
-                    status_code=HTTP_403_FORBIDDEN,
-                    detail="Invalid authentication credentials",
-                )
+                raise self.make_not_authenticated_error()
             else:
                 return None
         return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
index f8d97d76206f6033b77c014fec0b55f36ad1bae4..b41b0f8778d8fcd35790675396aed252c6cc549a 100644 (file)
@@ -8,7 +8,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
@@ -377,13 +377,33 @@ class OAuth2(SecurityBase):
         self.scheme_name = scheme_name or self.__class__.__name__
         self.auto_error = auto_error
 
+    def make_not_authenticated_error(self) -> HTTPException:
+        """
+        The OAuth 2 specification doesn't define the challenge that should be used,
+        because a `Bearer` token is not really the only option to authenticate.
+
+        But declaring any other authentication challenge would be application-specific
+        as it's not defined in the specification.
+
+        For practical reasons, this method uses the `Bearer` challenge by default, as
+        it's probably the most common one.
+
+        If you are implementing an OAuth2 authentication scheme other than the provided
+        ones in FastAPI (based on bearer tokens), you might want to override this.
+
+        Ref: https://datatracker.ietf.org/doc/html/rfc6749
+        """
+        return HTTPException(
+            status_code=HTTP_401_UNAUTHORIZED,
+            detail="Not authenticated",
+            headers={"WWW-Authenticate": "Bearer"},
+        )
+
     async def __call__(self, request: Request) -> Optional[str]:
         authorization = request.headers.get("Authorization")
         if not authorization:
             if self.auto_error:
-                raise HTTPException(
-                    status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
-                )
+                raise self.make_not_authenticated_error()
             else:
                 return None
         return authorization
@@ -491,11 +511,7 @@ class OAuth2PasswordBearer(OAuth2):
         scheme, param = get_authorization_scheme_param(authorization)
         if not authorization or scheme.lower() != "bearer":
             if self.auto_error:
-                raise HTTPException(
-                    status_code=HTTP_401_UNAUTHORIZED,
-                    detail="Not authenticated",
-                    headers={"WWW-Authenticate": "Bearer"},
-                )
+                raise self.make_not_authenticated_error()
             else:
                 return None
         return param
@@ -601,11 +617,7 @@ class OAuth2AuthorizationCodeBearer(OAuth2):
         scheme, param = get_authorization_scheme_param(authorization)
         if not authorization or scheme.lower() != "bearer":
             if self.auto_error:
-                raise HTTPException(
-                    status_code=HTTP_401_UNAUTHORIZED,
-                    detail="Not authenticated",
-                    headers={"WWW-Authenticate": "Bearer"},
-                )
+                raise self.make_not_authenticated_error()
             else:
                 return None  # pragma: nocover
         return param
index 5e99798e6329a2c073cdcf2efe068d3779d61e1e..e574a56a82ca09cf2bdff8410f7bb5ccffb0afd7 100644 (file)
@@ -5,7 +5,7 @@ from fastapi.openapi.models import OpenIdConnect as OpenIdConnectModel
 from fastapi.security.base import SecurityBase
 from starlette.exceptions import HTTPException
 from starlette.requests import Request
-from starlette.status import HTTP_403_FORBIDDEN
+from starlette.status import HTTP_401_UNAUTHORIZED
 from typing_extensions import Annotated
 
 
@@ -13,6 +13,11 @@ class OpenIdConnect(SecurityBase):
     """
     OpenID Connect authentication class. An instance of it would be used as a
     dependency.
+
+    **Warning**: this is only a stub to connect the components with OpenAPI in FastAPI,
+    but it doesn't implement the full OpenIdConnect scheme, for example, it doesn't use
+    the OpenIDConnect URL. You would need to to subclass it and implement it in your
+    code.
     """
 
     def __init__(
@@ -73,13 +78,18 @@ class OpenIdConnect(SecurityBase):
         self.scheme_name = scheme_name or self.__class__.__name__
         self.auto_error = auto_error
 
+    def make_not_authenticated_error(self) -> HTTPException:
+        return HTTPException(
+            status_code=HTTP_401_UNAUTHORIZED,
+            detail="Not authenticated",
+            headers={"WWW-Authenticate": "Bearer"},
+        )
+
     async def __call__(self, request: Request) -> Optional[str]:
         authorization = request.headers.get("Authorization")
         if not authorization:
             if self.auto_error:
-                raise HTTPException(
-                    status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
-                )
+                raise self.make_not_authenticated_error()
             else:
                 return None
         return authorization
index 4ddb8e2eeb11bab335f0b9cb2e386d504f9fab77..9bacfc56ecd8e9d68a5d9ee08d1e9aa48a48955e 100644 (file)
@@ -32,8 +32,9 @@ def test_security_api_key():
 def test_security_api_key_no_key():
     client = TestClient(app)
     response = client.get("/users/me")
-    assert response.status_code == 403, response.text
+    assert response.status_code == 401, response.text
     assert response.json() == {"detail": "Not authenticated"}
+    assert response.headers["WWW-Authenticate"] == "APIKey"
 
 
 def test_openapi_schema():
index d99d616e0679656aa3dfc0ce4cb22b86d2a3e9ee..d0cab324eb7b6dd78051634953406dfe427f5187 100644 (file)
@@ -32,8 +32,9 @@ def test_security_api_key():
 def test_security_api_key_no_key():
     client = TestClient(app)
     response = client.get("/users/me")
-    assert response.status_code == 403, response.text
+    assert response.status_code == 401, response.text
     assert response.json() == {"detail": "Not authenticated"}
+    assert response.headers["WWW-Authenticate"] == "APIKey"
 
 
 def test_openapi_schema():
index 1ff883703d59d1f75f70ad4f1e0b915f9e29e1fd..3e761b150bfb6574f7a979e204cb99db9144904a 100644 (file)
@@ -33,8 +33,9 @@ def test_security_api_key():
 
 def test_security_api_key_no_key():
     response = client.get("/users/me")
-    assert response.status_code == 403, response.text
+    assert response.status_code == 401, response.text
     assert response.json() == {"detail": "Not authenticated"}
+    assert response.headers["WWW-Authenticate"] == "APIKey"
 
 
 def test_openapi_schema():
index 27f9d0f29e6d6d21c42623b0e330453e96181fee..38a1a88814e7d8bc042e1eb3a2657204e18ed196 100644 (file)
@@ -33,8 +33,9 @@ def test_security_api_key():
 
 def test_security_api_key_no_key():
     response = client.get("/users/me")
-    assert response.status_code == 403, response.text
+    assert response.status_code == 401, response.text
     assert response.json() == {"detail": "Not authenticated"}
+    assert response.headers["WWW-Authenticate"] == "APIKey"
 
 
 def test_openapi_schema():
index dc7a0a621aabe2217fcb870d0474b660861696b9..11ed194689eb37544f6b09f019931f6197f94bc6 100644 (file)
@@ -33,8 +33,9 @@ def test_security_api_key():
 
 def test_security_api_key_no_key():
     response = client.get("/users/me")
-    assert response.status_code == 403, response.text
+    assert response.status_code == 401, response.text
     assert response.json() == {"detail": "Not authenticated"}
+    assert response.headers["WWW-Authenticate"] == "APIKey"
 
 
 def test_openapi_schema():
index 35dc7743a23eaee9edc66ee007c4e552cb580b58..658798326109b82915b9e86b9684c0cd6ee7b694 100644 (file)
@@ -33,8 +33,9 @@ def test_security_api_key():
 
 def test_security_api_key_no_key():
     response = client.get("/users/me")
-    assert response.status_code == 403, response.text
+    assert response.status_code == 401, response.text
     assert response.json() == {"detail": "Not authenticated"}
+    assert response.headers["WWW-Authenticate"] == "APIKey"
 
 
 def test_openapi_schema():
index 51928bafd5988bbb69ece958a6d99250b7d5b12f..8cf259a750ffbf8129f474184af55ff911495950 100644 (file)
@@ -23,8 +23,9 @@ def test_security_http_base():
 
 def test_security_http_base_no_credentials():
     response = client.get("/users/me")
-    assert response.status_code == 403, response.text
+    assert response.status_code == 401, response.text
     assert response.json() == {"detail": "Not authenticated"}
+    assert response.headers["WWW-Authenticate"] == "Other"
 
 
 def test_openapi_schema():
index bc79f32424b6a9081eb5eb6d8d1b4b0f567a0e0e..791ea59f4db9d456f160bae6925d131202e72e10 100644 (file)
@@ -23,8 +23,9 @@ def test_security_http_base():
 
 def test_security_http_base_no_credentials():
     response = client.get("/users/me")
-    assert response.status_code == 403, response.text
+    assert response.status_code == 401, response.text
     assert response.json() == {"detail": "Not authenticated"}
+    assert response.headers["WWW-Authenticate"] == "Other"
 
 
 def test_openapi_schema():
index 9b6cb6c4554365a40cd78cadf05ac963fa0d3ccf..7071f381a1333cc21f4d8c248f9e4c9c64951cfe 100644 (file)
@@ -38,7 +38,7 @@ def test_security_http_basic_invalid_credentials():
     )
     assert response.status_code == 401, response.text
     assert response.headers["WWW-Authenticate"] == "Basic"
-    assert response.json() == {"detail": "Invalid authentication credentials"}
+    assert response.json() == {"detail": "Not authenticated"}
 
 
 def test_security_http_basic_non_basic_credentials():
@@ -47,7 +47,7 @@ def test_security_http_basic_non_basic_credentials():
     response = client.get("/users/me", headers={"Authorization": auth_header})
     assert response.status_code == 401, response.text
     assert response.headers["WWW-Authenticate"] == "Basic"
-    assert response.json() == {"detail": "Invalid authentication credentials"}
+    assert response.json() == {"detail": "Not authenticated"}
 
 
 def test_openapi_schema():
index 9fc33971aec6c4ce4dcc656f61648fb98fd3614c..ec7371f90fec2e91d86a98e3b6b72e68a6868187 100644 (file)
@@ -36,7 +36,7 @@ def test_security_http_basic_invalid_credentials():
     )
     assert response.status_code == 401, response.text
     assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"'
-    assert response.json() == {"detail": "Invalid authentication credentials"}
+    assert response.json() == {"detail": "Not authenticated"}
 
 
 def test_security_http_basic_non_basic_credentials():
@@ -45,7 +45,7 @@ def test_security_http_basic_non_basic_credentials():
     response = client.get("/users/me", headers={"Authorization": auth_header})
     assert response.status_code == 401, response.text
     assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"'
-    assert response.json() == {"detail": "Invalid authentication credentials"}
+    assert response.json() == {"detail": "Not authenticated"}
 
 
 def test_openapi_schema():
index 02122442eb113940b1297368eec7eb8c17669e72..a93d5fc86bdefda3f2e45212102c075cc73aa40d 100644 (file)
@@ -36,7 +36,7 @@ def test_security_http_basic_invalid_credentials():
     )
     assert response.status_code == 401, response.text
     assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"'
-    assert response.json() == {"detail": "Invalid authentication credentials"}
+    assert response.json() == {"detail": "Not authenticated"}
 
 
 def test_security_http_basic_non_basic_credentials():
@@ -45,7 +45,7 @@ def test_security_http_basic_non_basic_credentials():
     response = client.get("/users/me", headers={"Authorization": auth_header})
     assert response.status_code == 401, response.text
     assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"'
-    assert response.json() == {"detail": "Invalid authentication credentials"}
+    assert response.json() == {"detail": "Not authenticated"}
 
 
 def test_openapi_schema():
index 5b9e2d6919b0989b44eb0d4a9fb3443985bb7917..961b42f4db35a343b253b009c6c152c456824cda 100644 (file)
@@ -23,14 +23,16 @@ def test_security_http_bearer():
 
 def test_security_http_bearer_no_credentials():
     response = client.get("/users/me")
-    assert response.status_code == 403, response.text
+    assert response.status_code == 401, response.text
     assert response.json() == {"detail": "Not authenticated"}
+    assert response.headers["WWW-Authenticate"] == "Bearer"
 
 
 def test_security_http_bearer_incorrect_scheme_credentials():
     response = client.get("/users/me", headers={"Authorization": "Basic notreally"})
-    assert response.status_code == 403, response.text
-    assert response.json() == {"detail": "Invalid authentication credentials"}
+    assert response.status_code == 401, response.text
+    assert response.json() == {"detail": "Not authenticated"}
+    assert response.headers["WWW-Authenticate"] == "Bearer"
 
 
 def test_openapi_schema():
index 2f11c3a148c0dd2f5d845b88e6cc016d23f660c6..e16994abce5f0b9e190b2d1bc614fa58772622f1 100644 (file)
@@ -23,14 +23,16 @@ def test_security_http_bearer():
 
 def test_security_http_bearer_no_credentials():
     response = client.get("/users/me")
-    assert response.status_code == 403, response.text
+    assert response.status_code == 401, response.text
     assert response.json() == {"detail": "Not authenticated"}
+    assert response.headers["WWW-Authenticate"] == "Bearer"
 
 
 def test_security_http_bearer_incorrect_scheme_credentials():
     response = client.get("/users/me", headers={"Authorization": "Basic notreally"})
-    assert response.status_code == 403, response.text
-    assert response.json() == {"detail": "Invalid authentication credentials"}
+    assert response.status_code == 401, response.text
+    assert response.json() == {"detail": "Not authenticated"}
+    assert response.headers["WWW-Authenticate"] == "Bearer"
 
 
 def test_openapi_schema():
index 133d35763c3edcbbec224f2b26fb8d10fc512415..3fad4c7a564ba702559a53355323acd5ee9553a2 100644 (file)
@@ -23,16 +23,18 @@ def test_security_http_digest():
 
 def test_security_http_digest_no_credentials():
     response = client.get("/users/me")
-    assert response.status_code == 403, response.text
+    assert response.status_code == 401, response.text
     assert response.json() == {"detail": "Not authenticated"}
+    assert response.headers["WWW-Authenticate"] == "Digest"
 
 
 def test_security_http_digest_incorrect_scheme_credentials():
     response = client.get(
         "/users/me", headers={"Authorization": "Other invalidauthorization"}
     )
-    assert response.status_code == 403, response.text
-    assert response.json() == {"detail": "Invalid authentication credentials"}
+    assert response.status_code == 401, response.text
+    assert response.json() == {"detail": "Not authenticated"}
+    assert response.headers["WWW-Authenticate"] == "Digest"
 
 
 def test_openapi_schema():
index 4e31a0c00848cbed7627f31dded232d26217cc1d..319416a0718c6658a5661c95e518539dd7f76f03 100644 (file)
@@ -23,16 +23,18 @@ def test_security_http_digest():
 
 def test_security_http_digest_no_credentials():
     response = client.get("/users/me")
-    assert response.status_code == 403, response.text
+    assert response.status_code == 401, response.text
     assert response.json() == {"detail": "Not authenticated"}
+    assert response.headers["WWW-Authenticate"] == "Digest"
 
 
 def test_security_http_digest_incorrect_scheme_credentials():
     response = client.get(
         "/users/me", headers={"Authorization": "Other invalidauthorization"}
     )
-    assert response.status_code == 403, response.text
-    assert response.json() == {"detail": "Invalid authentication credentials"}
+    assert response.status_code == 401, response.text
+    assert response.json() == {"detail": "Not authenticated"}
+    assert response.headers["WWW-Authenticate"] == "Digest"
 
 
 def test_openapi_schema():
index 2b7e3457a9ded124b0add42573a5fa9e3c1270b7..804e4152dbd2e73ea092b09c621da62c790ec84d 100644 (file)
@@ -56,8 +56,9 @@ def test_security_oauth2_password_other_header():
 
 def test_security_oauth2_password_bearer_no_header():
     response = client.get("/users/me")
-    assert response.status_code == 403, response.text
+    assert response.status_code == 401, response.text
     assert response.json() == {"detail": "Not authenticated"}
+    assert response.headers["WWW-Authenticate"] == "Bearer"
 
 
 def test_strict_login_no_data():
index 1e322e640ed3685f8d7934f84e8e0bdcbc900e3a..c9a0a8db7619abfbcb1b507b485ac08deb0f530c 100644 (file)
@@ -39,8 +39,9 @@ def test_security_oauth2_password_other_header():
 
 def test_security_oauth2_password_bearer_no_header():
     response = client.get("/users/me")
-    assert response.status_code == 403, response.text
+    assert response.status_code == 401, response.text
     assert response.json() == {"detail": "Not authenticated"}
+    assert response.headers["WWW-Authenticate"] == "Bearer"
 
 
 def test_openapi_schema():
index 44cf57f86281ca6f706f0f886555d84d1e7791fb..d008cbc630356c0cde0f0f86528994ec84b5db9f 100644 (file)
@@ -41,8 +41,9 @@ def test_security_oauth2_password_other_header():
 
 def test_security_oauth2_password_bearer_no_header():
     response = client.get("/users/me")
-    assert response.status_code == 403, response.text
+    assert response.status_code == 401, response.text
     assert response.json() == {"detail": "Not authenticated"}
+    assert response.headers["WWW-Authenticate"] == "Bearer"
 
 
 def test_openapi_schema():
index e2de31af53f728818bfdb032a7a683a859ccabcd..a36c66d1ac25045767621154a5f5c7957c6b6a57 100644 (file)
@@ -27,7 +27,7 @@ def test_get_root():
 
 def test_get_root_no_token():
     response = client.get("/")
-    assert response.status_code == 403, response.text
+    assert response.status_code == 401, response.text
     assert response.json() == {"detail": "Not authenticated"}
 
 
diff --git a/tests/test_tutorial/test_authentication_error_status_code/__init__.py b/tests/test_tutorial/test_authentication_error_status_code/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tests/test_tutorial/test_authentication_error_status_code/test_tutorial001.py b/tests/test_tutorial/test_authentication_error_status_code/test_tutorial001.py
new file mode 100644 (file)
index 0000000..bbd7bff
--- /dev/null
@@ -0,0 +1,69 @@
+import importlib
+
+import pytest
+from fastapi.testclient import TestClient
+from inline_snapshot import snapshot
+
+from ...utils import needs_py39
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        "tutorial001_an",
+        pytest.param("tutorial001_an_py39", marks=needs_py39),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(
+        f"docs_src.authentication_error_status_code.{request.param}"
+    )
+
+    client = TestClient(mod.app)
+    return client
+
+
+def test_get_me(client: TestClient):
+    response = client.get("/me", headers={"Authorization": "Bearer secrettoken"})
+    assert response.status_code == 200
+    assert response.json() == {
+        "message": "You are authenticated",
+        "token": "secrettoken",
+    }
+
+
+def test_get_me_no_credentials(client: TestClient):
+    response = client.get("/me")
+    assert response.status_code == 403
+    assert response.json() == {"detail": "Not authenticated"}
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == snapshot(
+        {
+            "openapi": "3.1.0",
+            "info": {"title": "FastAPI", "version": "0.1.0"},
+            "paths": {
+                "/me": {
+                    "get": {
+                        "summary": "Read Me",
+                        "operationId": "read_me_me_get",
+                        "responses": {
+                            "200": {
+                                "description": "Successful Response",
+                                "content": {"application/json": {"schema": {}}},
+                            }
+                        },
+                        "security": [{"HTTPBearer403": []}],
+                    }
+                }
+            },
+            "components": {
+                "securitySchemes": {
+                    "HTTPBearer403": {"type": "http", "scheme": "bearer"}
+                }
+            },
+        }
+    )
index 2bbb2e8510da9b26b80a93fbdd51a4fc32c24ba7..6b873511396fc9659c471eb08ccd51725e7f45ad 100644 (file)
@@ -66,7 +66,7 @@ def test_token(client: TestClient):
 def test_incorrect_token(client: TestClient):
     response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"})
     assert response.status_code == 401, response.text
-    assert response.json() == {"detail": "Invalid authentication credentials"}
+    assert response.json() == {"detail": "Not authenticated"}
     assert response.headers["WWW-Authenticate"] == "Bearer"
 
 
index 40b413806201b5b43ef74314e3cfae25476e4f7e..9587159dc276f659fef73cd3d7b4477ad5f939f8 100644 (file)
@@ -41,7 +41,7 @@ def test_security_http_basic_invalid_credentials(client: TestClient):
     )
     assert response.status_code == 401, response.text
     assert response.headers["WWW-Authenticate"] == "Basic"
-    assert response.json() == {"detail": "Invalid authentication credentials"}
+    assert response.json() == {"detail": "Not authenticated"}
 
 
 def test_security_http_basic_non_basic_credentials(client: TestClient):
@@ -50,7 +50,7 @@ def test_security_http_basic_non_basic_credentials(client: TestClient):
     response = client.get("/users/me", headers={"Authorization": auth_header})
     assert response.status_code == 401, response.text
     assert response.headers["WWW-Authenticate"] == "Basic"
-    assert response.json() == {"detail": "Invalid authentication credentials"}
+    assert response.json() == {"detail": "Not authenticated"}
 
 
 def test_openapi_schema(client: TestClient):