From 107ac18fd7fcba0c8ce562bfadd1f42400d17454 Mon Sep 17 00:00:00 2001 From: Dale Date: Sun, 23 Feb 2025 16:15:42 +1000 Subject: [PATCH 1/5] Merge in changes from PR #4818 to baseline change --- docs/en/docs/advanced/security/api-key.md | 89 +++++++++++++++++++ docs_src/security/tutorial008.py | 28 ++++++ docs_src/security/tutorial009.py | 21 +++++ docs_src/security/tutorial010.py | 21 +++++ .../test_security/test_tutorial008.py | 61 +++++++++++++ .../test_security/test_tutorial009.py | 51 +++++++++++ .../test_security/test_tutorial010.py | 51 +++++++++++ 7 files changed, 322 insertions(+) create mode 100644 docs/en/docs/advanced/security/api-key.md create mode 100644 docs_src/security/tutorial008.py create mode 100644 docs_src/security/tutorial009.py create mode 100644 docs_src/security/tutorial010.py create mode 100644 tests/test_tutorial/test_security/test_tutorial008.py create mode 100644 tests/test_tutorial/test_security/test_tutorial009.py create mode 100644 tests/test_tutorial/test_security/test_tutorial010.py diff --git a/docs/en/docs/advanced/security/api-key.md b/docs/en/docs/advanced/security/api-key.md new file mode 100644 index 000000000..f1636f115 --- /dev/null +++ b/docs/en/docs/advanced/security/api-key.md @@ -0,0 +1,89 @@ +# API Key Auth + +A common alternative to HTTP Basic Auth is using API Keys. + +In API Key Auth, the application expects the secret key, in header, or cookie, query or parameter, depending on setup. + +If header isn't received it, FastAPI can return an HTTP 403 "Forbidden" error. + +## Simple API Key Auth using header + +We'll protect the entire API under a Key (rather than single endpoints). + +* Import `APIKeyHeader`. +* Create an `APIKeyHeader`, specifying what header to parse as API key. +* Create a `get_api_key` function to check the key +* Create a `security` from the `get_api_key` function, used as a dependency in your FastAPI `app`. + +```Python hl_lines="5 7 14 23" +{!../../../docs_src/security/tutorial008.py!} +``` + +This API now requires authentication to hit any endpoint: + + + + +!!! tip + In the simplest case of a single, static API Key secret, you likely want it to be sourced from an environment variable or config file. + + Have a look at [Pydantic settings](../../settings){.internal-link target=_blank} to do it. + +## A look at the Header + +Note how the `APIKeyHeader` describes the expected header name, and the +description ends up on the documentation for the authentication: the description +is a perfect place to link to your developer documentation's "Generate a token" +section. + +```Python hl_lines="8 9" +{!../../../docs_src/security/tutorial008.py!} +``` + +As for the `auto_error` parameter, it can be set to `True` so that missing the +header returns automatic HTTP 403 "Forbidden". + +## Protecting single endpoints + +Alternatively, the `Security` dependency can be defined at path level to protect +not the whole API, but specific, sensitive endpoints. + +```Python +@app.post("/admin/password_reset", dependencies=[Security(get_api_key)] +def password_reset(user: int, new_password: str): +``` + +## API Key in Cookies + +For convenience, API Keys can be pushed in cookies instead. + + + +```Python hl_lines="2 7 14" +{!../../../docs_src/security/tutorial009.py!} +``` + +Users can call this via: + +```Python +response = client.get("/users/me", cookies={"key": "secret"}) +``` + +## API Key in Query + +To round up the multiple ways to use API Keys, one can set the API key as query parameter. + + +```Python hl_lines="2 7 14" +{!../../../docs_src/security/tutorial010.py!} +``` + +Users can call this via: + +```Python +response = client.get("/users/me?key=secret") +``` + +Note that setting `auto_error` to `False` can useful to support multiple +methods for providing API Key, checking successively for Cookie, falling back to +header, etc. diff --git a/docs_src/security/tutorial008.py b/docs_src/security/tutorial008.py new file mode 100644 index 000000000..e976ad137 --- /dev/null +++ b/docs_src/security/tutorial008.py @@ -0,0 +1,28 @@ +import secrets + +from fastapi import FastAPI, Security, status +from fastapi.exceptions import HTTPException +from fastapi.security.api_key import APIKeyHeader + +api_key_header_auth = APIKeyHeader( + name="X-API-KEY", + description="Mandatory API Token, required for all endpoints", + auto_error=True, +) + + +async def get_api_key(api_key_header: str = Security(api_key_header_auth)): + correct_api_key = secrets.compare_digest(api_key_header, "randomized-string-1234") + if not correct_api_key: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Invalid API Key", + ) + + +app = FastAPI(dependencies=[Security(get_api_key)]) + + +@app.get("/health") +async def endpoint(): + return {"Hello": "World"} diff --git a/docs_src/security/tutorial009.py b/docs_src/security/tutorial009.py new file mode 100644 index 000000000..0912f43f4 --- /dev/null +++ b/docs_src/security/tutorial009.py @@ -0,0 +1,21 @@ +from fastapi import Depends, FastAPI, Security +from fastapi.security import APIKeyCookie +from pydantic import BaseModel + +app = FastAPI() + +api_key = APIKeyCookie(name="key") + + +class User(BaseModel): + username: str + + +def get_current_user(oauth_header: str = Security(api_key)): + user = User(username=oauth_header) + return user + + +@app.get("/users/me") +def read_current_user(current_user: User = Depends(get_current_user)): + return current_user diff --git a/docs_src/security/tutorial010.py b/docs_src/security/tutorial010.py new file mode 100644 index 000000000..86c595fac --- /dev/null +++ b/docs_src/security/tutorial010.py @@ -0,0 +1,21 @@ +from fastapi import Depends, FastAPI, Security +from fastapi.security import APIKeyQuery +from pydantic import BaseModel + +app = FastAPI() + +api_key = APIKeyQuery(name="key") + + +class User(BaseModel): + username: str + + +def get_current_user(oauth_header: str = Security(api_key)): + user = User(username=oauth_header) + return user + + +@app.get("/users/me") +def read_current_user(current_user: User = Depends(get_current_user)): + return current_user diff --git a/tests/test_tutorial/test_security/test_tutorial008.py b/tests/test_tutorial/test_security/test_tutorial008.py new file mode 100644 index 000000000..edd583f82 --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial008.py @@ -0,0 +1,61 @@ +from fastapi.testclient import TestClient + +from docs_src.security.tutorial008 import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/health": { + "get": { + "summary": "Endpoint", + "operationId": "endpoint_health_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "security": [{"APIKeyHeader": []}], + } + } + }, + "components": { + "securitySchemes": { + "APIKeyHeader": { + "type": "apiKey", + "description": "Mandatory API Token, required for all endpoints", + "in": "header", + "name": "X-API-KEY", + } + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_security_apikey_header(): + auth = {"X-API-KEY": "randomized-string-1234"} + response = client.get("/health", headers=auth) + assert response.status_code == 200, response.text + assert response.json() == {"Hello": "World"} + + +def test_security_apikey_header_no_credentials(): + response = client.get("/health", headers={}) + assert response.json() == {"detail": "Not authenticated"} + assert response.status_code == 403, response.text + + +def test_security_apikey_header_invalid_credentials(): + auth = {"X-API-KEY": "totally-wrong-api-key"} + response = client.get("/health", headers=auth) + assert response.status_code == 403, response.text + assert response.json() == {"detail": "Invalid API Key"} diff --git a/tests/test_tutorial/test_security/test_tutorial009.py b/tests/test_tutorial/test_security/test_tutorial009.py new file mode 100644 index 000000000..86a738005 --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial009.py @@ -0,0 +1,51 @@ +from fastapi.testclient import TestClient + +from docs_src.security.tutorial009 import app + +# IDENTICAL COPY of tests/test_security_api_key_cookie.py + + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"APIKeyCookie": []}], + } + } + }, + "components": { + "securitySchemes": { + "APIKeyCookie": {"type": "apiKey", "name": "key", "in": "cookie"} + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_security_api_key(): + response = client.get("/users/me", cookies={"key": "secret"}) + assert response.status_code == 200, response.text + assert response.json() == {"username": "secret"} + + +def test_security_api_key_no_key(): + response = client.get("/users/me") + assert response.status_code == 403, response.text + assert response.json() == {"detail": "Not authenticated"} diff --git a/tests/test_tutorial/test_security/test_tutorial010.py b/tests/test_tutorial/test_security/test_tutorial010.py new file mode 100644 index 000000000..7ab8317b6 --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial010.py @@ -0,0 +1,51 @@ +from fastapi.testclient import TestClient + +from docs_src.security.tutorial010 import app + +# IDENTICAL COPY of tests/test_security_api_key_query.py + + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"APIKeyQuery": []}], + } + } + }, + "components": { + "securitySchemes": { + "APIKeyQuery": {"type": "apiKey", "name": "key", "in": "query"} + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_security_api_key(): + response = client.get("/users/me?key=secret") + assert response.status_code == 200, response.text + assert response.json() == {"username": "secret"} + + +def test_security_api_key_no_key(): + response = client.get("/users/me") + assert response.status_code == 403, response.text + assert response.json() == {"detail": "Not authenticated"} From 4e10d03d23ef98f3fd6b4e631773743f9eae3c18 Mon Sep 17 00:00:00 2001 From: Dale Date: Sun, 23 Feb 2025 16:43:36 +1000 Subject: [PATCH 2/5] fix: updated the openapi version in intorduced tutorial tests --- tests/test_tutorial/test_security/test_tutorial008.py | 2 +- tests/test_tutorial/test_security/test_tutorial009.py | 2 +- tests/test_tutorial/test_security/test_tutorial010.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_tutorial/test_security/test_tutorial008.py b/tests/test_tutorial/test_security/test_tutorial008.py index edd583f82..0450375af 100644 --- a/tests/test_tutorial/test_security/test_tutorial008.py +++ b/tests/test_tutorial/test_security/test_tutorial008.py @@ -5,7 +5,7 @@ from docs_src.security.tutorial008 import app client = TestClient(app) openapi_schema = { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/health": { diff --git a/tests/test_tutorial/test_security/test_tutorial009.py b/tests/test_tutorial/test_security/test_tutorial009.py index 86a738005..09cc8e972 100644 --- a/tests/test_tutorial/test_security/test_tutorial009.py +++ b/tests/test_tutorial/test_security/test_tutorial009.py @@ -8,7 +8,7 @@ from docs_src.security.tutorial009 import app client = TestClient(app) openapi_schema = { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_tutorial/test_security/test_tutorial010.py b/tests/test_tutorial/test_security/test_tutorial010.py index 7ab8317b6..d65dd3dad 100644 --- a/tests/test_tutorial/test_security/test_tutorial010.py +++ b/tests/test_tutorial/test_security/test_tutorial010.py @@ -8,7 +8,7 @@ from docs_src.security.tutorial010 import app client = TestClient(app) openapi_schema = { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { From 43df95fd579865763aed774626fbb751dcc049c4 Mon Sep 17 00:00:00 2001 From: Dale Date: Tue, 25 Feb 2025 19:23:02 +1000 Subject: [PATCH 3/5] Update the docs to align with other formatting --- docs/en/docs/advanced/security/api-key.md | 16 ++++------------ docs/en/docs/img/tutorial/security/image13.png | Bin 0 -> 44914 bytes docs/en/mkdocs.yml | 1 + 3 files changed, 5 insertions(+), 12 deletions(-) create mode 100644 docs/en/docs/img/tutorial/security/image13.png diff --git a/docs/en/docs/advanced/security/api-key.md b/docs/en/docs/advanced/security/api-key.md index f1636f115..470c4a3d7 100644 --- a/docs/en/docs/advanced/security/api-key.md +++ b/docs/en/docs/advanced/security/api-key.md @@ -15,9 +15,7 @@ We'll protect the entire API under a Key (rather than single endpoints). * Create a `get_api_key` function to check the key * Create a `security` from the `get_api_key` function, used as a dependency in your FastAPI `app`. -```Python hl_lines="5 7 14 23" -{!../../../docs_src/security/tutorial008.py!} -``` +{* ../../docs_src/security/tutorial008.py hl=[5,7,14,23] *} This API now requires authentication to hit any endpoint: @@ -36,9 +34,7 @@ description ends up on the documentation for the authentication: the description is a perfect place to link to your developer documentation's "Generate a token" section. -```Python hl_lines="8 9" -{!../../../docs_src/security/tutorial008.py!} -``` +{* ../../docs_src/security/tutorial008.py hl=[8:9] *} As for the `auto_error` parameter, it can be set to `True` so that missing the header returns automatic HTTP 403 "Forbidden". @@ -59,9 +55,7 @@ For convenience, API Keys can be pushed in cookies instead. -```Python hl_lines="2 7 14" -{!../../../docs_src/security/tutorial009.py!} -``` +{* ../../docs_src/security/tutorial009.py hl=[2,7,14] *} Users can call this via: @@ -74,9 +68,7 @@ response = client.get("/users/me", cookies={"key": "secret"}) To round up the multiple ways to use API Keys, one can set the API key as query parameter. -```Python hl_lines="2 7 14" -{!../../../docs_src/security/tutorial010.py!} -``` +{* ../../docs_src/security/tutorial010.py hl=[2,7,14] *} Users can call this via: diff --git a/docs/en/docs/img/tutorial/security/image13.png b/docs/en/docs/img/tutorial/security/image13.png new file mode 100644 index 0000000000000000000000000000000000000000..a69a84f1f8588653c88f3d18edb5b48a56dda3bf GIT binary patch literal 44914 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 E05NjrC;$Ke literal 0 HcmV?d00001 diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index e9a639d0b..ab41ede24 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -179,6 +179,7 @@ nav: - advanced/security/index.md - advanced/security/oauth2-scopes.md - advanced/security/http-basic-auth.md + - advanced/security/api-key.md - advanced/using-request-directly.md - advanced/dataclasses.md - advanced/middleware.md From c44e0da26b490be87c9c4d16cdbf3da709bdf066 Mon Sep 17 00:00:00 2001 From: Dale Date: Wed, 19 Mar 2025 20:40:47 +1000 Subject: [PATCH 4/5] Updated tests to pass tests on scripts/test-cov-html.sh --- tests/test_tutorial/test_security/test_tutorial009.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_tutorial/test_security/test_tutorial009.py b/tests/test_tutorial/test_security/test_tutorial009.py index 09cc8e972..a2c11c82b 100644 --- a/tests/test_tutorial/test_security/test_tutorial009.py +++ b/tests/test_tutorial/test_security/test_tutorial009.py @@ -40,12 +40,14 @@ def test_openapi_schema(): def test_security_api_key(): - response = client.get("/users/me", cookies={"key": "secret"}) + client.cookies={"key": "secret"} + response = client.get("/users/me") #, cookies={"key": "secret"}) assert response.status_code == 200, response.text assert response.json() == {"username": "secret"} def test_security_api_key_no_key(): + client.cookies=None response = client.get("/users/me") assert response.status_code == 403, response.text assert response.json() == {"detail": "Not authenticated"} From 31556a1b856c4390a5d7c1ab4af2fdde2d44f2e6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 19 Mar 2025 10:46:50 +0000 Subject: [PATCH 5/5] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20for?= =?UTF-8?q?mat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_tutorial/test_security/test_tutorial009.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_tutorial/test_security/test_tutorial009.py b/tests/test_tutorial/test_security/test_tutorial009.py index a2c11c82b..d9dba2748 100644 --- a/tests/test_tutorial/test_security/test_tutorial009.py +++ b/tests/test_tutorial/test_security/test_tutorial009.py @@ -40,14 +40,14 @@ def test_openapi_schema(): def test_security_api_key(): - client.cookies={"key": "secret"} - response = client.get("/users/me") #, cookies={"key": "secret"}) + client.cookies = {"key": "secret"} + response = client.get("/users/me") # , cookies={"key": "secret"}) assert response.status_code == 200, response.text assert response.json() == {"username": "secret"} def test_security_api_key_no_key(): - client.cookies=None + client.cookies = None response = client.get("/users/me") assert response.status_code == 403, response.text assert response.json() == {"detail": "Not authenticated"}