committed by
GitHub
9 changed files with 317 additions and 0 deletions
@ -0,0 +1,81 @@ |
|||
# 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`. |
|||
|
|||
{* ../../docs_src/security/tutorial008.py hl=[5,7,14,23] *} |
|||
|
|||
This API now requires authentication to hit any endpoint: |
|||
|
|||
|
|||
<img src="/img/tutorial/security/image13.png"> |
|||
|
|||
!!! 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. |
|||
|
|||
{* ../../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". |
|||
|
|||
## 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. |
|||
|
|||
<!-- Note: tutorial009.py is 100 %CLONED from tests/test_security_api_key_cookie.py --> |
|||
|
|||
{* ../../docs_src/security/tutorial009.py hl=[2,7,14] *} |
|||
|
|||
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. |
|||
|
|||
<!-- Note: tutorial010.py is 100 %CLONED from tests/test_security_api_key_query.py --> |
|||
{* ../../docs_src/security/tutorial010.py hl=[2,7,14] *} |
|||
|
|||
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. |
After Width: | Height: | Size: 44 KiB |
@ -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"} |
@ -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 |
@ -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 |
@ -0,0 +1,61 @@ |
|||
from fastapi.testclient import TestClient |
|||
|
|||
from docs_src.security.tutorial008 import app |
|||
|
|||
client = TestClient(app) |
|||
|
|||
openapi_schema = { |
|||
"openapi": "3.1.0", |
|||
"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"} |
@ -0,0 +1,53 @@ |
|||
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.1.0", |
|||
"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(): |
|||
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"} |
@ -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.1.0", |
|||
"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"} |
Loading…
Reference in new issue