mirror of https://github.com/tiangolo/fastapi.git
🐛 Use `401` status code in security classes when credentials are missing (#13786)
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>
This commit is contained in:
parent
e2354a0a06
commit
51ad909ffe
|
|
@ -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.
|
||||||
|
|
||||||
|
///
|
||||||
|
|
@ -215,6 +215,7 @@ nav:
|
||||||
- how-to/custom-docs-ui-assets.md
|
- how-to/custom-docs-ui-assets.md
|
||||||
- how-to/configure-swagger-ui.md
|
- how-to/configure-swagger-ui.md
|
||||||
- how-to/testing-database.md
|
- how-to/testing-database.md
|
||||||
|
- how-to/authentication-error-status-code.md
|
||||||
- Reference (Code API):
|
- Reference (Code API):
|
||||||
- reference/index.md
|
- reference/index.md
|
||||||
- reference/fastapi.md
|
- reference/fastapi.md
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
@ -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}
|
||||||
|
|
@ -60,7 +60,7 @@ async def get_current_user(token: str = Depends(oauth2_scheme)):
|
||||||
if not user:
|
if not user:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail="Invalid authentication credentials",
|
detail="Not authenticated",
|
||||||
headers={"WWW-Authenticate": "Bearer"},
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
)
|
)
|
||||||
return user
|
return user
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
|
||||||
if not user:
|
if not user:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail="Invalid authentication credentials",
|
detail="Not authenticated",
|
||||||
headers={"WWW-Authenticate": "Bearer"},
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
)
|
)
|
||||||
return user
|
return user
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
|
||||||
if not user:
|
if not user:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail="Invalid authentication credentials",
|
detail="Not authenticated",
|
||||||
headers={"WWW-Authenticate": "Bearer"},
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
)
|
)
|
||||||
return user
|
return user
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
|
||||||
if not user:
|
if not user:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail="Invalid authentication credentials",
|
detail="Not authenticated",
|
||||||
headers={"WWW-Authenticate": "Bearer"},
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
)
|
)
|
||||||
return user
|
return user
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ async def get_current_user(token: str = Depends(oauth2_scheme)):
|
||||||
if not user:
|
if not user:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail="Invalid authentication credentials",
|
detail="Not authenticated",
|
||||||
headers={"WWW-Authenticate": "Bearer"},
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
)
|
)
|
||||||
return user
|
return user
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,52 @@
|
||||||
from typing import Optional
|
from typing import Optional, Union
|
||||||
|
|
||||||
from annotated_doc import Doc
|
from annotated_doc import Doc
|
||||||
from fastapi.openapi.models import APIKey, APIKeyIn
|
from fastapi.openapi.models import APIKey, APIKeyIn
|
||||||
from fastapi.security.base import SecurityBase
|
from fastapi.security.base import SecurityBase
|
||||||
from starlette.exceptions import HTTPException
|
from starlette.exceptions import HTTPException
|
||||||
from starlette.requests import Request
|
from starlette.requests import Request
|
||||||
from starlette.status import HTTP_403_FORBIDDEN
|
from starlette.status import HTTP_401_UNAUTHORIZED
|
||||||
from typing_extensions import Annotated
|
from typing_extensions import Annotated
|
||||||
|
|
||||||
|
|
||||||
class APIKeyBase(SecurityBase):
|
class APIKeyBase(SecurityBase):
|
||||||
@staticmethod
|
def __init__(
|
||||||
def check_api_key(api_key: Optional[str], auto_error: bool) -> Optional[str]:
|
self,
|
||||||
if not api_key:
|
location: APIKeyIn,
|
||||||
if auto_error:
|
name: str,
|
||||||
raise HTTPException(
|
description: Union[str, None],
|
||||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
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 self.auto_error:
|
||||||
|
raise self.make_not_authenticated_error()
|
||||||
return None
|
return None
|
||||||
return api_key
|
return api_key
|
||||||
|
|
||||||
|
|
@ -100,17 +130,17 @@ class APIKeyQuery(APIKeyBase):
|
||||||
),
|
),
|
||||||
] = True,
|
] = True,
|
||||||
):
|
):
|
||||||
self.model: APIKey = APIKey(
|
super().__init__(
|
||||||
**{"in": APIKeyIn.query},
|
location=APIKeyIn.query,
|
||||||
name=name,
|
name=name,
|
||||||
|
scheme_name=scheme_name,
|
||||||
description=description,
|
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]:
|
async def __call__(self, request: Request) -> Optional[str]:
|
||||||
api_key = request.query_params.get(self.model.name)
|
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):
|
class APIKeyHeader(APIKeyBase):
|
||||||
|
|
@ -188,17 +218,17 @@ class APIKeyHeader(APIKeyBase):
|
||||||
),
|
),
|
||||||
] = True,
|
] = True,
|
||||||
):
|
):
|
||||||
self.model: APIKey = APIKey(
|
super().__init__(
|
||||||
**{"in": APIKeyIn.header},
|
location=APIKeyIn.header,
|
||||||
name=name,
|
name=name,
|
||||||
|
scheme_name=scheme_name,
|
||||||
description=description,
|
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]:
|
async def __call__(self, request: Request) -> Optional[str]:
|
||||||
api_key = request.headers.get(self.model.name)
|
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):
|
class APIKeyCookie(APIKeyBase):
|
||||||
|
|
@ -276,14 +306,14 @@ class APIKeyCookie(APIKeyBase):
|
||||||
),
|
),
|
||||||
] = True,
|
] = True,
|
||||||
):
|
):
|
||||||
self.model: APIKey = APIKey(
|
super().__init__(
|
||||||
**{"in": APIKeyIn.cookie},
|
location=APIKeyIn.cookie,
|
||||||
name=name,
|
name=name,
|
||||||
|
scheme_name=scheme_name,
|
||||||
description=description,
|
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]:
|
async def __call__(self, request: Request) -> Optional[str]:
|
||||||
api_key = request.cookies.get(self.model.name)
|
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)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import binascii
|
import binascii
|
||||||
from base64 import b64decode
|
from base64 import b64decode
|
||||||
from typing import Optional
|
from typing import Dict, Optional
|
||||||
|
|
||||||
from annotated_doc import Doc
|
from annotated_doc import Doc
|
||||||
from fastapi.exceptions import HTTPException
|
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 fastapi.security.utils import get_authorization_scheme_param
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from starlette.requests import Request
|
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
|
from typing_extensions import Annotated
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -76,10 +76,22 @@ class HTTPBase(SecurityBase):
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
auto_error: bool = True,
|
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.scheme_name = scheme_name or self.__class__.__name__
|
||||||
self.auto_error = auto_error
|
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__(
|
async def __call__(
|
||||||
self, request: Request
|
self, request: Request
|
||||||
) -> Optional[HTTPAuthorizationCredentials]:
|
) -> Optional[HTTPAuthorizationCredentials]:
|
||||||
|
|
@ -87,9 +99,7 @@ class HTTPBase(SecurityBase):
|
||||||
scheme, credentials = get_authorization_scheme_param(authorization)
|
scheme, credentials = get_authorization_scheme_param(authorization)
|
||||||
if not (authorization and scheme and credentials):
|
if not (authorization and scheme and credentials):
|
||||||
if self.auto_error:
|
if self.auto_error:
|
||||||
raise HTTPException(
|
raise self.make_not_authenticated_error()
|
||||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
|
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
|
||||||
|
|
@ -99,6 +109,8 @@ class HTTPBasic(HTTPBase):
|
||||||
"""
|
"""
|
||||||
HTTP Basic authentication.
|
HTTP Basic authentication.
|
||||||
|
|
||||||
|
Ref: https://datatracker.ietf.org/doc/html/rfc7617
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Create an instance object and use that object as the dependency in `Depends()`.
|
Create an instance object and use that object as the dependency in `Depends()`.
|
||||||
|
|
@ -185,36 +197,28 @@ class HTTPBasic(HTTPBase):
|
||||||
self.realm = realm
|
self.realm = realm
|
||||||
self.auto_error = auto_error
|
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
|
async def __call__( # type: ignore
|
||||||
self, request: Request
|
self, request: Request
|
||||||
) -> Optional[HTTPBasicCredentials]:
|
) -> Optional[HTTPBasicCredentials]:
|
||||||
authorization = request.headers.get("Authorization")
|
authorization = request.headers.get("Authorization")
|
||||||
scheme, param = get_authorization_scheme_param(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 not authorization or scheme.lower() != "basic":
|
||||||
if self.auto_error:
|
if self.auto_error:
|
||||||
raise HTTPException(
|
raise self.make_not_authenticated_error()
|
||||||
status_code=HTTP_401_UNAUTHORIZED,
|
|
||||||
detail="Not authenticated",
|
|
||||||
headers=unauthorized_headers,
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
invalid_user_credentials_exc = HTTPException(
|
|
||||||
status_code=HTTP_401_UNAUTHORIZED,
|
|
||||||
detail="Invalid authentication credentials",
|
|
||||||
headers=unauthorized_headers,
|
|
||||||
)
|
|
||||||
try:
|
try:
|
||||||
data = b64decode(param).decode("ascii")
|
data = b64decode(param).decode("ascii")
|
||||||
except (ValueError, UnicodeDecodeError, binascii.Error):
|
except (ValueError, UnicodeDecodeError, binascii.Error) as e:
|
||||||
raise invalid_user_credentials_exc # noqa: B904
|
raise self.make_not_authenticated_error() from e
|
||||||
username, separator, password = data.partition(":")
|
username, separator, password = data.partition(":")
|
||||||
if not separator:
|
if not separator:
|
||||||
raise invalid_user_credentials_exc
|
raise self.make_not_authenticated_error()
|
||||||
return HTTPBasicCredentials(username=username, password=password)
|
return HTTPBasicCredentials(username=username, password=password)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -306,17 +310,12 @@ class HTTPBearer(HTTPBase):
|
||||||
scheme, credentials = get_authorization_scheme_param(authorization)
|
scheme, credentials = get_authorization_scheme_param(authorization)
|
||||||
if not (authorization and scheme and credentials):
|
if not (authorization and scheme and credentials):
|
||||||
if self.auto_error:
|
if self.auto_error:
|
||||||
raise HTTPException(
|
raise self.make_not_authenticated_error()
|
||||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
if scheme.lower() != "bearer":
|
if scheme.lower() != "bearer":
|
||||||
if self.auto_error:
|
if self.auto_error:
|
||||||
raise HTTPException(
|
raise self.make_not_authenticated_error()
|
||||||
status_code=HTTP_403_FORBIDDEN,
|
|
||||||
detail="Invalid authentication credentials",
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
|
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
|
||||||
|
|
@ -326,6 +325,12 @@ class HTTPDigest(HTTPBase):
|
||||||
"""
|
"""
|
||||||
HTTP Digest authentication.
|
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
|
## Usage
|
||||||
|
|
||||||
Create an instance object and use that object as the dependency in `Depends()`.
|
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)
|
scheme, credentials = get_authorization_scheme_param(authorization)
|
||||||
if not (authorization and scheme and credentials):
|
if not (authorization and scheme and credentials):
|
||||||
if self.auto_error:
|
if self.auto_error:
|
||||||
raise HTTPException(
|
raise self.make_not_authenticated_error()
|
||||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
if scheme.lower() != "digest":
|
if scheme.lower() != "digest":
|
||||||
if self.auto_error:
|
if self.auto_error:
|
||||||
raise HTTPException(
|
raise self.make_not_authenticated_error()
|
||||||
status_code=HTTP_403_FORBIDDEN,
|
|
||||||
detail="Invalid authentication credentials",
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
|
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ from fastapi.param_functions import Form
|
||||||
from fastapi.security.base import SecurityBase
|
from fastapi.security.base import SecurityBase
|
||||||
from fastapi.security.utils import get_authorization_scheme_param
|
from fastapi.security.utils import get_authorization_scheme_param
|
||||||
from starlette.requests import Request
|
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
|
# TODO: import from typing when deprecating Python 3.9
|
||||||
from typing_extensions import Annotated
|
from typing_extensions import Annotated
|
||||||
|
|
@ -377,13 +377,33 @@ class OAuth2(SecurityBase):
|
||||||
self.scheme_name = scheme_name or self.__class__.__name__
|
self.scheme_name = scheme_name or self.__class__.__name__
|
||||||
self.auto_error = auto_error
|
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]:
|
async def __call__(self, request: Request) -> Optional[str]:
|
||||||
authorization = request.headers.get("Authorization")
|
authorization = request.headers.get("Authorization")
|
||||||
if not authorization:
|
if not authorization:
|
||||||
if self.auto_error:
|
if self.auto_error:
|
||||||
raise HTTPException(
|
raise self.make_not_authenticated_error()
|
||||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
return authorization
|
return authorization
|
||||||
|
|
@ -491,11 +511,7 @@ class OAuth2PasswordBearer(OAuth2):
|
||||||
scheme, param = get_authorization_scheme_param(authorization)
|
scheme, param = get_authorization_scheme_param(authorization)
|
||||||
if not authorization or scheme.lower() != "bearer":
|
if not authorization or scheme.lower() != "bearer":
|
||||||
if self.auto_error:
|
if self.auto_error:
|
||||||
raise HTTPException(
|
raise self.make_not_authenticated_error()
|
||||||
status_code=HTTP_401_UNAUTHORIZED,
|
|
||||||
detail="Not authenticated",
|
|
||||||
headers={"WWW-Authenticate": "Bearer"},
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
return param
|
return param
|
||||||
|
|
@ -601,11 +617,7 @@ class OAuth2AuthorizationCodeBearer(OAuth2):
|
||||||
scheme, param = get_authorization_scheme_param(authorization)
|
scheme, param = get_authorization_scheme_param(authorization)
|
||||||
if not authorization or scheme.lower() != "bearer":
|
if not authorization or scheme.lower() != "bearer":
|
||||||
if self.auto_error:
|
if self.auto_error:
|
||||||
raise HTTPException(
|
raise self.make_not_authenticated_error()
|
||||||
status_code=HTTP_401_UNAUTHORIZED,
|
|
||||||
detail="Not authenticated",
|
|
||||||
headers={"WWW-Authenticate": "Bearer"},
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
return None # pragma: nocover
|
return None # pragma: nocover
|
||||||
return param
|
return param
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ from fastapi.openapi.models import OpenIdConnect as OpenIdConnectModel
|
||||||
from fastapi.security.base import SecurityBase
|
from fastapi.security.base import SecurityBase
|
||||||
from starlette.exceptions import HTTPException
|
from starlette.exceptions import HTTPException
|
||||||
from starlette.requests import Request
|
from starlette.requests import Request
|
||||||
from starlette.status import HTTP_403_FORBIDDEN
|
from starlette.status import HTTP_401_UNAUTHORIZED
|
||||||
from typing_extensions import Annotated
|
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
|
OpenID Connect authentication class. An instance of it would be used as a
|
||||||
dependency.
|
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__(
|
def __init__(
|
||||||
|
|
@ -73,13 +78,18 @@ class OpenIdConnect(SecurityBase):
|
||||||
self.scheme_name = scheme_name or self.__class__.__name__
|
self.scheme_name = scheme_name or self.__class__.__name__
|
||||||
self.auto_error = auto_error
|
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]:
|
async def __call__(self, request: Request) -> Optional[str]:
|
||||||
authorization = request.headers.get("Authorization")
|
authorization = request.headers.get("Authorization")
|
||||||
if not authorization:
|
if not authorization:
|
||||||
if self.auto_error:
|
if self.auto_error:
|
||||||
raise HTTPException(
|
raise self.make_not_authenticated_error()
|
||||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
return authorization
|
return authorization
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,9 @@ def test_security_api_key():
|
||||||
def test_security_api_key_no_key():
|
def test_security_api_key_no_key():
|
||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
response = client.get("/users/me")
|
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.json() == {"detail": "Not authenticated"}
|
||||||
|
assert response.headers["WWW-Authenticate"] == "APIKey"
|
||||||
|
|
||||||
|
|
||||||
def test_openapi_schema():
|
def test_openapi_schema():
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,9 @@ def test_security_api_key():
|
||||||
def test_security_api_key_no_key():
|
def test_security_api_key_no_key():
|
||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
response = client.get("/users/me")
|
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.json() == {"detail": "Not authenticated"}
|
||||||
|
assert response.headers["WWW-Authenticate"] == "APIKey"
|
||||||
|
|
||||||
|
|
||||||
def test_openapi_schema():
|
def test_openapi_schema():
|
||||||
|
|
|
||||||
|
|
@ -33,8 +33,9 @@ def test_security_api_key():
|
||||||
|
|
||||||
def test_security_api_key_no_key():
|
def test_security_api_key_no_key():
|
||||||
response = client.get("/users/me")
|
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.json() == {"detail": "Not authenticated"}
|
||||||
|
assert response.headers["WWW-Authenticate"] == "APIKey"
|
||||||
|
|
||||||
|
|
||||||
def test_openapi_schema():
|
def test_openapi_schema():
|
||||||
|
|
|
||||||
|
|
@ -33,8 +33,9 @@ def test_security_api_key():
|
||||||
|
|
||||||
def test_security_api_key_no_key():
|
def test_security_api_key_no_key():
|
||||||
response = client.get("/users/me")
|
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.json() == {"detail": "Not authenticated"}
|
||||||
|
assert response.headers["WWW-Authenticate"] == "APIKey"
|
||||||
|
|
||||||
|
|
||||||
def test_openapi_schema():
|
def test_openapi_schema():
|
||||||
|
|
|
||||||
|
|
@ -33,8 +33,9 @@ def test_security_api_key():
|
||||||
|
|
||||||
def test_security_api_key_no_key():
|
def test_security_api_key_no_key():
|
||||||
response = client.get("/users/me")
|
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.json() == {"detail": "Not authenticated"}
|
||||||
|
assert response.headers["WWW-Authenticate"] == "APIKey"
|
||||||
|
|
||||||
|
|
||||||
def test_openapi_schema():
|
def test_openapi_schema():
|
||||||
|
|
|
||||||
|
|
@ -33,8 +33,9 @@ def test_security_api_key():
|
||||||
|
|
||||||
def test_security_api_key_no_key():
|
def test_security_api_key_no_key():
|
||||||
response = client.get("/users/me")
|
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.json() == {"detail": "Not authenticated"}
|
||||||
|
assert response.headers["WWW-Authenticate"] == "APIKey"
|
||||||
|
|
||||||
|
|
||||||
def test_openapi_schema():
|
def test_openapi_schema():
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,9 @@ def test_security_http_base():
|
||||||
|
|
||||||
def test_security_http_base_no_credentials():
|
def test_security_http_base_no_credentials():
|
||||||
response = client.get("/users/me")
|
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.json() == {"detail": "Not authenticated"}
|
||||||
|
assert response.headers["WWW-Authenticate"] == "Other"
|
||||||
|
|
||||||
|
|
||||||
def test_openapi_schema():
|
def test_openapi_schema():
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,9 @@ def test_security_http_base():
|
||||||
|
|
||||||
def test_security_http_base_no_credentials():
|
def test_security_http_base_no_credentials():
|
||||||
response = client.get("/users/me")
|
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.json() == {"detail": "Not authenticated"}
|
||||||
|
assert response.headers["WWW-Authenticate"] == "Other"
|
||||||
|
|
||||||
|
|
||||||
def test_openapi_schema():
|
def test_openapi_schema():
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ def test_security_http_basic_invalid_credentials():
|
||||||
)
|
)
|
||||||
assert response.status_code == 401, response.text
|
assert response.status_code == 401, response.text
|
||||||
assert response.headers["WWW-Authenticate"] == "Basic"
|
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():
|
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})
|
response = client.get("/users/me", headers={"Authorization": auth_header})
|
||||||
assert response.status_code == 401, response.text
|
assert response.status_code == 401, response.text
|
||||||
assert response.headers["WWW-Authenticate"] == "Basic"
|
assert response.headers["WWW-Authenticate"] == "Basic"
|
||||||
assert response.json() == {"detail": "Invalid authentication credentials"}
|
assert response.json() == {"detail": "Not authenticated"}
|
||||||
|
|
||||||
|
|
||||||
def test_openapi_schema():
|
def test_openapi_schema():
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ def test_security_http_basic_invalid_credentials():
|
||||||
)
|
)
|
||||||
assert response.status_code == 401, response.text
|
assert response.status_code == 401, response.text
|
||||||
assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"'
|
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():
|
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})
|
response = client.get("/users/me", headers={"Authorization": auth_header})
|
||||||
assert response.status_code == 401, response.text
|
assert response.status_code == 401, response.text
|
||||||
assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"'
|
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():
|
def test_openapi_schema():
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ def test_security_http_basic_invalid_credentials():
|
||||||
)
|
)
|
||||||
assert response.status_code == 401, response.text
|
assert response.status_code == 401, response.text
|
||||||
assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"'
|
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():
|
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})
|
response = client.get("/users/me", headers={"Authorization": auth_header})
|
||||||
assert response.status_code == 401, response.text
|
assert response.status_code == 401, response.text
|
||||||
assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"'
|
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():
|
def test_openapi_schema():
|
||||||
|
|
|
||||||
|
|
@ -23,14 +23,16 @@ def test_security_http_bearer():
|
||||||
|
|
||||||
def test_security_http_bearer_no_credentials():
|
def test_security_http_bearer_no_credentials():
|
||||||
response = client.get("/users/me")
|
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.json() == {"detail": "Not authenticated"}
|
||||||
|
assert response.headers["WWW-Authenticate"] == "Bearer"
|
||||||
|
|
||||||
|
|
||||||
def test_security_http_bearer_incorrect_scheme_credentials():
|
def test_security_http_bearer_incorrect_scheme_credentials():
|
||||||
response = client.get("/users/me", headers={"Authorization": "Basic notreally"})
|
response = client.get("/users/me", headers={"Authorization": "Basic notreally"})
|
||||||
assert response.status_code == 403, response.text
|
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"
|
||||||
|
|
||||||
|
|
||||||
def test_openapi_schema():
|
def test_openapi_schema():
|
||||||
|
|
|
||||||
|
|
@ -23,14 +23,16 @@ def test_security_http_bearer():
|
||||||
|
|
||||||
def test_security_http_bearer_no_credentials():
|
def test_security_http_bearer_no_credentials():
|
||||||
response = client.get("/users/me")
|
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.json() == {"detail": "Not authenticated"}
|
||||||
|
assert response.headers["WWW-Authenticate"] == "Bearer"
|
||||||
|
|
||||||
|
|
||||||
def test_security_http_bearer_incorrect_scheme_credentials():
|
def test_security_http_bearer_incorrect_scheme_credentials():
|
||||||
response = client.get("/users/me", headers={"Authorization": "Basic notreally"})
|
response = client.get("/users/me", headers={"Authorization": "Basic notreally"})
|
||||||
assert response.status_code == 403, response.text
|
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"
|
||||||
|
|
||||||
|
|
||||||
def test_openapi_schema():
|
def test_openapi_schema():
|
||||||
|
|
|
||||||
|
|
@ -23,16 +23,18 @@ def test_security_http_digest():
|
||||||
|
|
||||||
def test_security_http_digest_no_credentials():
|
def test_security_http_digest_no_credentials():
|
||||||
response = client.get("/users/me")
|
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.json() == {"detail": "Not authenticated"}
|
||||||
|
assert response.headers["WWW-Authenticate"] == "Digest"
|
||||||
|
|
||||||
|
|
||||||
def test_security_http_digest_incorrect_scheme_credentials():
|
def test_security_http_digest_incorrect_scheme_credentials():
|
||||||
response = client.get(
|
response = client.get(
|
||||||
"/users/me", headers={"Authorization": "Other invalidauthorization"}
|
"/users/me", headers={"Authorization": "Other invalidauthorization"}
|
||||||
)
|
)
|
||||||
assert response.status_code == 403, response.text
|
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"] == "Digest"
|
||||||
|
|
||||||
|
|
||||||
def test_openapi_schema():
|
def test_openapi_schema():
|
||||||
|
|
|
||||||
|
|
@ -23,16 +23,18 @@ def test_security_http_digest():
|
||||||
|
|
||||||
def test_security_http_digest_no_credentials():
|
def test_security_http_digest_no_credentials():
|
||||||
response = client.get("/users/me")
|
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.json() == {"detail": "Not authenticated"}
|
||||||
|
assert response.headers["WWW-Authenticate"] == "Digest"
|
||||||
|
|
||||||
|
|
||||||
def test_security_http_digest_incorrect_scheme_credentials():
|
def test_security_http_digest_incorrect_scheme_credentials():
|
||||||
response = client.get(
|
response = client.get(
|
||||||
"/users/me", headers={"Authorization": "Other invalidauthorization"}
|
"/users/me", headers={"Authorization": "Other invalidauthorization"}
|
||||||
)
|
)
|
||||||
assert response.status_code == 403, response.text
|
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"] == "Digest"
|
||||||
|
|
||||||
|
|
||||||
def test_openapi_schema():
|
def test_openapi_schema():
|
||||||
|
|
|
||||||
|
|
@ -56,8 +56,9 @@ def test_security_oauth2_password_other_header():
|
||||||
|
|
||||||
def test_security_oauth2_password_bearer_no_header():
|
def test_security_oauth2_password_bearer_no_header():
|
||||||
response = client.get("/users/me")
|
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.json() == {"detail": "Not authenticated"}
|
||||||
|
assert response.headers["WWW-Authenticate"] == "Bearer"
|
||||||
|
|
||||||
|
|
||||||
def test_strict_login_no_data():
|
def test_strict_login_no_data():
|
||||||
|
|
|
||||||
|
|
@ -39,8 +39,9 @@ def test_security_oauth2_password_other_header():
|
||||||
|
|
||||||
def test_security_oauth2_password_bearer_no_header():
|
def test_security_oauth2_password_bearer_no_header():
|
||||||
response = client.get("/users/me")
|
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.json() == {"detail": "Not authenticated"}
|
||||||
|
assert response.headers["WWW-Authenticate"] == "Bearer"
|
||||||
|
|
||||||
|
|
||||||
def test_openapi_schema():
|
def test_openapi_schema():
|
||||||
|
|
|
||||||
|
|
@ -41,8 +41,9 @@ def test_security_oauth2_password_other_header():
|
||||||
|
|
||||||
def test_security_oauth2_password_bearer_no_header():
|
def test_security_oauth2_password_bearer_no_header():
|
||||||
response = client.get("/users/me")
|
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.json() == {"detail": "Not authenticated"}
|
||||||
|
assert response.headers["WWW-Authenticate"] == "Bearer"
|
||||||
|
|
||||||
|
|
||||||
def test_openapi_schema():
|
def test_openapi_schema():
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ def test_get_root():
|
||||||
|
|
||||||
def test_get_root_no_token():
|
def test_get_root_no_token():
|
||||||
response = client.get("/")
|
response = client.get("/")
|
||||||
assert response.status_code == 403, response.text
|
assert response.status_code == 401, response.text
|
||||||
assert response.json() == {"detail": "Not authenticated"}
|
assert response.json() == {"detail": "Not authenticated"}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
@ -66,7 +66,7 @@ def test_token(client: TestClient):
|
||||||
def test_incorrect_token(client: TestClient):
|
def test_incorrect_token(client: TestClient):
|
||||||
response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"})
|
response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"})
|
||||||
assert response.status_code == 401, response.text
|
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"
|
assert response.headers["WWW-Authenticate"] == "Bearer"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ def test_security_http_basic_invalid_credentials(client: TestClient):
|
||||||
)
|
)
|
||||||
assert response.status_code == 401, response.text
|
assert response.status_code == 401, response.text
|
||||||
assert response.headers["WWW-Authenticate"] == "Basic"
|
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):
|
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})
|
response = client.get("/users/me", headers={"Authorization": auth_header})
|
||||||
assert response.status_code == 401, response.text
|
assert response.status_code == 401, response.text
|
||||||
assert response.headers["WWW-Authenticate"] == "Basic"
|
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):
|
def test_openapi_schema(client: TestClient):
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue