mirror of https://github.com/tiangolo/fastapi.git
Merge branch 'master' into fix-background-tasks-overwrite
This commit is contained in:
commit
68d1745dc3
|
|
@ -17,6 +17,7 @@ lang-all:
|
||||||
- docs/*/docs/**
|
- docs/*/docs/**
|
||||||
- all-globs-to-all-files:
|
- all-globs-to-all-files:
|
||||||
- '!docs/en/docs/**'
|
- '!docs/en/docs/**'
|
||||||
|
- '!docs/*/**/_*.md'
|
||||||
- '!fastapi/**'
|
- '!fastapi/**'
|
||||||
- '!pyproject.toml'
|
- '!pyproject.toml'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,9 @@ jobs:
|
||||||
env:
|
env:
|
||||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||||
run: echo "$GITHUB_CONTEXT"
|
run: echo "$GITHUB_CONTEXT"
|
||||||
- uses: actions/checkout@v6
|
# pin to actions/checkout@v5 for compatibility with latest-changes
|
||||||
|
# Ref: https://github.com/actions/checkout/issues/2313
|
||||||
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
# To allow latest-changes to commit to the main branch
|
# To allow latest-changes to commit to the main branch
|
||||||
token: ${{ secrets.FASTAPI_LATEST_CHANGES }}
|
token: ${{ secrets.FASTAPI_LATEST_CHANGES }}
|
||||||
|
|
@ -34,7 +36,7 @@ jobs:
|
||||||
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }}
|
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }}
|
||||||
with:
|
with:
|
||||||
limit-access-to-actor: true
|
limit-access-to-actor: true
|
||||||
- uses: tiangolo/latest-changes@0.4.0
|
- uses: tiangolo/latest-changes@0.4.1
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
latest_changes_file: docs/en/docs/release-notes.md
|
latest_changes_file: docs/en/docs/release-notes.md
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
name: pre-commit
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- synchronize
|
||||||
|
|
||||||
|
env:
|
||||||
|
IS_FORK: ${{ github.event.pull_request.head.repo.full_name != github.repository }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pre-commit:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Dump GitHub context
|
||||||
|
env:
|
||||||
|
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||||
|
run: echo "$GITHUB_CONTEXT"
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
name: Checkout PR for own repo
|
||||||
|
if: env.IS_FORK == 'false'
|
||||||
|
with:
|
||||||
|
# To be able to commit it needs more than the last commit
|
||||||
|
ref: ${{ github.head_ref }}
|
||||||
|
# A token other than the default GITHUB_TOKEN is needed to be able to trigger CI
|
||||||
|
token: ${{ secrets.PRE_COMMIT }}
|
||||||
|
# pre-commit lite ci needs the default checkout configs to work
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
name: Checkout PR for fork
|
||||||
|
if: env.IS_FORK == 'true'
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: "3.14"
|
||||||
|
- name: Setup uv
|
||||||
|
uses: astral-sh/setup-uv@v7
|
||||||
|
with:
|
||||||
|
cache-dependency-glob: |
|
||||||
|
requirements**.txt
|
||||||
|
pyproject.toml
|
||||||
|
uv.lock
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: |
|
||||||
|
uv venv
|
||||||
|
uv pip install -r requirements.txt
|
||||||
|
- name: Run pre-commit
|
||||||
|
id: precommit
|
||||||
|
run: |
|
||||||
|
# Fetch the base branch for comparison
|
||||||
|
git fetch origin ${{ github.base_ref }}
|
||||||
|
uvx pre-commit run --from-ref origin/${{ github.base_ref }} --to-ref HEAD --show-diff-on-failure
|
||||||
|
continue-on-error: true
|
||||||
|
- name: Commit and push changes
|
||||||
|
if: env.IS_FORK == 'false'
|
||||||
|
run: |
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
git add -A
|
||||||
|
if git diff --staged --quiet; then
|
||||||
|
echo "No changes to commit"
|
||||||
|
else
|
||||||
|
git commit -m "🎨 Auto format"
|
||||||
|
git push
|
||||||
|
fi
|
||||||
|
- uses: pre-commit-ci/lite-action@v1.1.0
|
||||||
|
if: env.IS_FORK == 'true'
|
||||||
|
with:
|
||||||
|
msg: 🎨 Auto format
|
||||||
|
- name: Error out on pre-commit errors
|
||||||
|
if: steps.precommit.outcome == 'failure'
|
||||||
|
run: exit 1
|
||||||
|
|
||||||
|
# https://github.com/marketplace/actions/alls-green#why
|
||||||
|
pre-commit-alls-green: # This job does nothing and is only used for the branch protection
|
||||||
|
if: always()
|
||||||
|
needs:
|
||||||
|
- pre-commit
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Dump GitHub context
|
||||||
|
env:
|
||||||
|
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||||
|
run: echo "$GITHUB_CONTEXT"
|
||||||
|
- name: Decide whether the needed jobs succeeded or failed
|
||||||
|
uses: re-actors/alls-green@release/v1
|
||||||
|
with:
|
||||||
|
jobs: ${{ toJSON(needs) }}
|
||||||
|
|
@ -28,3 +28,6 @@ archive.zip
|
||||||
|
|
||||||
# macOS
|
# macOS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# Ignore while the setup still depends on requirements.txt files
|
||||||
|
uv.lock
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,29 @@
|
||||||
# See https://pre-commit.com for more information
|
# See https://pre-commit.com for more information
|
||||||
# See https://pre-commit.com/hooks.html for more hooks
|
# See https://pre-commit.com/hooks.html for more hooks
|
||||||
default_language_version:
|
|
||||||
python: python3.10
|
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v6.0.0
|
rev: v6.0.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-added-large-files
|
- id: check-added-large-files
|
||||||
- id: check-toml
|
- id: check-toml
|
||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
args:
|
args:
|
||||||
- --unsafe
|
- --unsafe
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.14.3
|
rev: v0.14.3
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
args:
|
args:
|
||||||
- --fix
|
- --fix
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
ci:
|
- repo: local
|
||||||
autofix_commit_msg: 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks
|
hooks:
|
||||||
autoupdate_commit_msg: ⬆ [pre-commit.ci] pre-commit autoupdate
|
- id: local-script
|
||||||
|
language: unsupported
|
||||||
|
name: local script
|
||||||
|
entry: uv run ./scripts/docs.py add-permalinks-pages
|
||||||
|
args:
|
||||||
|
- --update-existing
|
||||||
|
files: ^docs/en/docs/.*\.md$
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,11 @@
|
||||||
|
/* Fira Code, including characters used by Rich output, like the "heavy right-pointing angle bracket ornament", not included in Google Fonts */
|
||||||
|
@import url(https://cdn.jsdelivr.net/npm/firacode@6.2.0/distr/fira_code.css);
|
||||||
|
|
||||||
|
/* Override default code font in Material for MkDocs to Fira Code */
|
||||||
|
:root {
|
||||||
|
--md-code-font: "Fira Code", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
.termynal-comment {
|
.termynal-comment {
|
||||||
color: #4a968f;
|
color: #4a968f;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
/* font-size: 18px; */
|
/* font-size: 18px; */
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
/* font-family: 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace; */
|
/* font-family: 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace; */
|
||||||
font-family: 'Roboto Mono', 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace;
|
font-family: var(--md-code-font-family), 'Roboto Mono', 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 75px 45px 35px;
|
padding: 75px 45px 35px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
|
///
|
||||||
|
|
@ -7,8 +7,21 @@ hide:
|
||||||
|
|
||||||
## Latest Changes
|
## Latest Changes
|
||||||
|
|
||||||
|
## 0.122.0
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
* 🐛 Use `401` status code in security classes when credentials are missing. PR [#13786](https://github.com/fastapi/fastapi/pull/13786) by [@YuriiMotov](https://github.com/YuriiMotov).
|
||||||
|
* If your code depended on these classes raising the old (less correct) `403` status code, check the new docs about how to override the classes, to use the same old behavior: [Use Old 403 Authentication Error Status Codes](https://fastapi.tiangolo.com/how-to/authentication-error-status-code/).
|
||||||
|
|
||||||
### Internal
|
### Internal
|
||||||
|
|
||||||
|
* 🔧 Configure labeler to exclude files that start from underscore for `lang-all` label. PR [#14213](https://github.com/fastapi/fastapi/pull/14213) by [@YuriiMotov](https://github.com/YuriiMotov).
|
||||||
|
* 👷 Add pre-commit config with local script for permalinks. PR [#14398](https://github.com/fastapi/fastapi/pull/14398) by [@tiangolo](https://github.com/tiangolo).
|
||||||
|
* 💄 Use font Fira Code to fix display of Rich panels in docs in Windows. PR [#14387](https://github.com/fastapi/fastapi/pull/14387) by [@tiangolo](https://github.com/tiangolo).
|
||||||
|
* 👷 Add custom pre-commit CI. PR [#14397](https://github.com/fastapi/fastapi/pull/14397) by [@tiangolo](https://github.com/tiangolo).
|
||||||
|
* ⬆ Bump actions/checkout from 5 to 6. PR [#14381](https://github.com/fastapi/fastapi/pull/14381) by [@dependabot[bot]](https://github.com/apps/dependabot).
|
||||||
|
* 👷 Upgrade `latest-changes` GitHub Action and pin `actions/checkout@v5`. PR [#14403](https://github.com/fastapi/fastapi/pull/14403) by [@svlandeg](https://github.com/svlandeg).
|
||||||
* 🛠️ Add `add-permalinks` and `add-permalinks-page` to `scripts/docs.py`. PR [#14033](https://github.com/fastapi/fastapi/pull/14033) by [@YuriiMotov](https://github.com/YuriiMotov).
|
* 🛠️ Add `add-permalinks` and `add-permalinks-page` to `scripts/docs.py`. PR [#14033](https://github.com/fastapi/fastapi/pull/14033) by [@YuriiMotov](https://github.com/YuriiMotov).
|
||||||
* 🔧 Upgrade Material for MkDocs and remove insiders. PR [#14375](https://github.com/fastapi/fastapi/pull/14375) by [@tiangolo](https://github.com/tiangolo).
|
* 🔧 Upgrade Material for MkDocs and remove insiders. PR [#14375](https://github.com/fastapi/fastapi/pull/14375) by [@tiangolo](https://github.com/tiangolo).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,6 +1,6 @@
|
||||||
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
|
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
|
||||||
|
|
||||||
__version__ = "0.121.3"
|
__version__ = "0.122.0"
|
||||||
|
|
||||||
from starlette import status as status
|
from starlette import status as status
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
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 not api_key:
|
||||||
if auto_error:
|
if self.auto_error:
|
||||||
raise HTTPException(
|
raise self.make_not_authenticated_error()
|
||||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
|
||||||
)
|
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
-e .[all]
|
-e .[all]
|
||||||
-r requirements-tests.txt
|
-r requirements-tests.txt
|
||||||
-r requirements-docs.txt
|
-r requirements-docs.txt
|
||||||
pre-commit >=2.17.0,<5.0.0
|
pre-commit >=4.5.0,<5.0.0
|
||||||
# For generating screenshots
|
# For generating screenshots
|
||||||
playwright
|
playwright
|
||||||
|
|
|
||||||
|
|
@ -520,6 +520,15 @@ def add_permalinks_page(path: Path, update_existing: bool = False):
|
||||||
f.writelines(updated_lines)
|
f.writelines(updated_lines)
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def add_permalinks_pages(pages: List[Path], update_existing: bool = False) -> None:
|
||||||
|
"""
|
||||||
|
Add or update header permalinks in specific pages of En docs.
|
||||||
|
"""
|
||||||
|
for md_file in pages:
|
||||||
|
add_permalinks_page(md_file, update_existing=update_existing)
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def add_permalinks(update_existing: bool = False) -> None:
|
def add_permalinks(update_existing: bool = False) -> None:
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -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