Skip to content

Commit 7140d24

Browse files
authored
Merge pull request #5 from Athroniaeth/development
- docs(scopes): add basic scopes example, fastapi scopes example, rewor… - docs(mkdocs): rework all mkdocs pages, remove bad information - build(deps): update all dependencies and format/lint code - docs(readme): add pypi and mkdocs url to readme
2 parents 733f6a6 + bbd4bd5 commit 7140d24

File tree

8 files changed

+690
-460
lines changed

8 files changed

+690
-460
lines changed

README.md

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
# Fastapi Api Key
1+
# FastAPI Api Key
22

33
![Python Version from PEP 621 TOML](https://img.shields.io/python/required-version-toml?tomlFilePath=https%3A%2F%2Fraw.githubusercontent.com%2FAthroniaeth%2Ffastapi-api-key%2Fmain%2Fpyproject.toml)
44
[![Tested with pytest](https://img.shields.io/badge/tests-pytest-informational.svg)](https://pytest.org/)
5+
[![PyPI version](https://img.shields.io/pypi/v/fastapi-api-key.svg)](https://pypi.org/project/fastapi-api-key/)
6+
[![Docs](https://img.shields.io/badge/docs-online-blue.svg)](https://athroniaeth.github.io/fastapi-api-key/)
57
[![codecov](https://codecov.io/gh/Athroniaeth/fastapi-api-key/graph/badge.svg)](https://codecov.io/gh/Athroniaeth/fastapi-api-key)
68
[![Security: bandit](https://img.shields.io/badge/security-bandit-yellow.svg)](https://bandit.readthedocs.io/)
79
[![Deps: uv](https://img.shields.io/badge/deps-managed%20with%20uv-3E4DD8.svg)](https://docs.astral.sh/uv/)
@@ -11,15 +13,25 @@
1113
ships with a domain model, hashing helpers, repository contracts, and an optional FastAPI router for CRUD management of
1214
keys.
1315

16+
## Links
17+
18+
- **Documentation:** [https://athroniaeth.github.io/fastapi-api-key/](https://athroniaeth.github.io/fastapi-api-key/)
19+
- **PyPI package:** [https://pypi.org/project/fastapi-api-key/](https://pypi.org/project/fastapi-api-key/)
20+
1421
## Features
1522

1623
- **Security-first**: secrets are hashed with a salt and a pepper, and never logged or returned after creation
17-
- **Ready-to-use**: just create your repository (storage) and use service
1824
- **Prod-ready**: services and repositories are async, and battle-tested
1925

26+
2027
- **Agnostic hasher**: you can use any async-compatible hashing strategy (default: Argon2)
2128
- **Agnostic backend**: you can use any async-compatible database (default: SQLAlchemy)
22-
- **Factory**: create a Typer, FastAPI router wired to api key systems (only SQLAlchemy for now)
29+
- **Connector**: create a Typer, FastAPI router wired to api key systems
30+
31+
32+
- **Envvar support**: easily configure peppers and other secrets via environment variables
33+
- **Extensible**: customize models, repositories, hashers, and services to fit your needs
34+
- **Scopes support**: assign scopes to API keys for fine-grained access control
2335

2436
## Standards compliance
2537

@@ -32,14 +44,15 @@ This library try to follow best practices and relevant RFCs for API key manageme
3244

3345
## Installation
3446

47+
### Basic installation
3548
This project is not published to PyPI. Use a tool like [uv](https://docs.astral.sh/uv/) to manage dependencies.
3649

3750
```bash
3851
uv add fastapi-api-key
3952
uv pip install fastapi-api-key
4053
```
4154

42-
## Development installation
55+
### Development installation
4356

4457
Clone or fork the repository and install the project with the extras that fit your stack. Examples below use `uv`:
4558

@@ -48,6 +61,8 @@ uv sync --extra all # fastapi + sqlalchemy + argon2 + bcrypt
4861
uv pip install -e ".[all]"
4962
```
5063

64+
### Optional dependencies
65+
5166
For lighter setups you can choose individual extras:
5267

5368
| Installation mode | Command | Description |
@@ -125,6 +140,12 @@ service = ApiKeyService(
125140

126141
This is a classic API key if you don't modify the service behavior:
127142

143+
**Structure:**
144+
145+
`{global_prefix}`-`{delimiter}`-`{identifier}`-`{delimiter}`-`{secret}`
146+
147+
**Example:**
148+
128149
`ak-7a74caa323a5410d-mAfP3l6yAxqFz0FV2LOhu2tPCqL66lQnj3Ubd08w9RyE4rV4skUcpiUVIfsKEbzw`
129150

130151
- "-" separators so that systems can easily split

docs/index.md

Lines changed: 55 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,81 @@
1-
# Fastapi Api Key
1+
# FastAPI Api Key
2+
3+
![Python Version from PEP 621 TOML](https://img.shields.io/python/required-version-toml?tomlFilePath=https%3A%2F%2Fraw.githubusercontent.com%2FAthroniaeth%2Ffastapi-api-key%2Fmain%2Fpyproject.toml)
4+
[![Tested with pytest](https://img.shields.io/badge/tests-pytest-informational.svg)](https://pytest.org/)
5+
[![PyPI version](https://img.shields.io/pypi/v/fastapi-api-key.svg)](https://pypi.org/project/fastapi-api-key/)
6+
[![Docs](https://img.shields.io/badge/docs-online-blue.svg)](https://athroniaeth.github.io/fastapi-api-key/)
7+
[![codecov](https://codecov.io/gh/Athroniaeth/fastapi-api-key/graph/badge.svg)](https://codecov.io/gh/Athroniaeth/fastapi-api-key)
8+
[![Security: bandit](https://img.shields.io/badge/security-bandit-yellow.svg)](https://bandit.readthedocs.io/)
9+
[![Deps: uv](https://img.shields.io/badge/deps-managed%20with%20uv-3E4DD8.svg)](https://docs.astral.sh/uv/)
10+
[![Code style: Ruff](https://img.shields.io/badge/code%20style-ruff-4B32C3.svg)](https://docs.astral.sh/ruff/)
211

312
`fastapi-api-key` provides reusable building blocks to issue, persist, and verify API keys in FastAPI applications. It
413
ships with a domain model, hashing helpers, repository contracts, and an optional FastAPI router for CRUD management of
514
keys.
615

16+
## Links
17+
18+
- **Documentation:** [https://athroniaeth.github.io/fastapi-api-key/](https://athroniaeth.github.io/fastapi-api-key/)
19+
- **PyPI package:** [https://pypi.org/project/fastapi-api-key/](https://pypi.org/project/fastapi-api-key/)
20+
721
## Features
822

923
- **Security-first**: secrets are hashed with a salt and a pepper, and never logged or returned after creation
10-
- **Ready-to-use**: just create your repository (storage) and use service
1124
- **Prod-ready**: services and repositories are async, and battle-tested
1225

1326
- **Agnostic hasher**: you can use any async-compatible hashing strategy (default: Argon2)
1427
- **Agnostic backend**: you can use any async-compatible database (default: SQLAlchemy)
15-
- **Factory**: create a Typer, FastAPI router wired to api key systems (only SQLAlchemy for now)
28+
- **Connector**: create a Typer, FastAPI router wired to api key systems
29+
30+
31+
- **Envvar support**: easily configure peppers and other secrets via environment variables
32+
- **Extensible**: customize models, repositories, hashers, and services to fit your needs
33+
- **Scopes support**: assign scopes to API keys for fine-grained access control
34+
35+
## Standards compliance
36+
37+
This library try to follow best practices and relevant RFCs for API key management and authentication:
38+
39+
- **[RFC 9110/7235](https://www.rfc-editor.org/rfc/rfc9110.html)**: Router raise 401 for missing/invalid keys, 403 for
40+
valid but inactive/expired keys
41+
- **[RFC 6750](https://datatracker.ietf.org/doc/html/rfc6750)**: Supports `Authorization: Bearer <api_key>` header for
42+
key transmission (also supports deprecated `X-API-Key` header and `api_key` query param)
43+
44+
## How API Keys Work
45+
46+
### API Key Format
47+
48+
This is a classic API key if you don't modify the service behavior:
1649

17-
## Installation
50+
**Structure:**
1851

19-
This projet does not publish to PyPI. Use a tool like [uv](https://docs.astral.sh/uv/) to manage dependencies.
52+
`{global_prefix}`-`{delimiter}`-`{identifier}`-`{delimiter}`-`{secret}`
2053

21-
```bash
22-
uv add fastapi-api-key
23-
uv pip install fastapi-api-key
24-
```
54+
**Example:**
2555

26-
## Development installation
56+
`ak-7a74caa323a5410d-mAfP3l6yAxqFz0FV2LOhu2tPCqL66lQnj3Ubd08w9RyE4rV4skUcpiUVIfsKEbzw`
2757

28-
Clone the repository and install the project with the extras that fit your stack. Examples below use `uv`:
58+
- "-" separators so that systems can easily split
59+
- Prefix `ak` (for "Api Key"), to identify the key type (useful to indicate that it is an API key).
60+
- 16 first characters are the identifier (UUIDv4 without dashes)
61+
- 48 last characters are the secret (random URL-safe base64 string)
2962

30-
```bash
31-
uv sync --extra all # fastapi + sqlalchemy + argon2 + bcrypt
32-
uv pip install -e ".[all]"
33-
```
63+
When verifying an API key, the service extracts the identifier, retrieves the corresponding record from the repository,
64+
and compares the hashed secret. If found, it hashes the provided secret (with the same salt and pepper) and compares it
65+
to the stored hash.
66+
If they match, the key is valid.
3467

35-
For lighter setups you can choose individual extras:
68+
### Schema validation
3669

37-
| Installation mode | Command | Description |
38-
|-----------------------------|-------------------------------|----------------------------------------------------------------------------------|
39-
| **Base installation** | `fastapi-api-key` | Installs the core package without any optional dependencies. |
40-
| **With bcrypt support** | `fastapi-api-key[bcrypt]` | Adds support for password hashing using **bcrypt** (`bcrypt>=5.0.0`). |
41-
| **With Argon2 support** | `fastapi-api-key[argon2]` | Adds support for password hashing using **Argon2** (`argon2-cffi>=25.1.0`). |
42-
| **With SQLAlchemy support** | `fastapi-api-key[sqlalchemy]` | Adds database integration via **SQLAlchemy** (`sqlalchemy>=2.0.43`). |
43-
| **Core setup** | `fastapi-api-key[core]` | Installs the **core dependencies** (SQLAlchemy + Argon2 + bcrypt). |
44-
| **FastAPI only** | `fastapi-api-key[fastapi]` | Installs **FastAPI** as an optional dependency (`fastapi>=0.118.0`). |
45-
| **Full installation** | `fastapi-api-key[all]` | Installs **all optional dependencies**: FastAPI, SQLAlchemy, Argon2, and bcrypt. |
70+
Here is a diagram showing what happens after you initialize your API key service with a global prefix and delimiter when you provide an API key to the `.verify_key()` method.
4671

47-
```bash
48-
uv add fastapi-api-key[sqlalchemy]
49-
uv pip install fastapi-api-key[sqlalchemy]
50-
uv sync --extra sqlalchemy
51-
uv pip install -e ".[sqlalchemy]"
52-
```
72+
<img src="./schema.svg">
5373

54-
Development dependencies (pytest, ruff, etc.) are available under the `dev` group:
74+
## Additional notes
5575

56-
```bash
57-
uv sync --extra dev
58-
uv pip install -e ".[dev]"
59-
```
76+
- Python 3.9+ is required.
77+
- The library issues warnings if you keep the default pepper; always configure a secret value outside source control.
78+
- Never log peppers or plaintext API keys, change the pepper of prod will prevent you from reading API keys
6079

6180
## What to read next
6281

docs/quickstart.md

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,50 @@ This quickstart guide helps you set up the package and create your first API key
44

55
## 1. Install dependencies
66

7-
Install the package with all optional dependencies using your preferred method. Examples below use [uv](https://docs.astral.sh/uv/):
7+
### Basic installation
8+
This project is not published to PyPI. Use a tool like [uv](https://docs.astral.sh/uv/) to manage dependencies.
89

910
```bash
10-
uv sync --extra all
11+
uv add fastapi-api-key
12+
uv pip install fastapi-api-key
13+
```
14+
15+
### Development installation
16+
17+
Clone or fork the repository and install the project with the extras that fit your stack. Examples below use `uv`:
18+
19+
```bash
20+
uv sync --extra all # fastapi + sqlalchemy + argon2 + bcrypt
21+
uv pip install -e ".[all]"
22+
```
23+
24+
### Optional dependencies
25+
26+
For lighter setups you can choose individual extras:
27+
28+
| Installation mode | Command | Description |
29+
|--------------------------------|-------------------------------|-----------------------------------------------------------------------------|
30+
| **Base installation** | `fastapi-api-key` | Installs the core package without any optional dependencies. |
31+
| **With Bcrypt support** | `fastapi-api-key[bcrypt]` | Adds support for password hashing using **bcrypt** |
32+
| **With Argon2 support** | `fastapi-api-key[argon2]` | Adds support for password hashing using **Argon2** |
33+
| **With SQLAlchemy support** | `fastapi-api-key[sqlalchemy]` | Adds database integration via **SQLAlchemy** |
34+
| **With Cache Service support** | `fastapi-api-key[aiocache]` | Adds database integration via **Aiocache** |
35+
| **Core setup** | `fastapi-api-key[core]` | Installs the **core dependencies** (SQLAlchemy + Argon2 + bcrypt + aiocache |
36+
| **FastAPI only** | `fastapi-api-key[fastapi]` | Installs **FastAPI** as an optional dependency |
37+
| **Full installation** | `fastapi-api-key[all]` | Installs **all optional dependencies** |
38+
39+
```bash
40+
uv add fastapi-api-key[sqlalchemy]
41+
uv pip install fastapi-api-key[sqlalchemy]
42+
uv sync --extra sqlalchemy
43+
uv pip install -e ".[sqlalchemy]"
44+
```
45+
46+
Development dependencies (pytest, ruff, etc.) are available under the `dev` group:
47+
48+
```bash
49+
uv sync --extra dev
50+
uv pip install -e ".[dev]"
1151
```
1252

1353
## 2. Create api key

docs/usage/scopes.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Scopes
2+
3+
This libray integrates the concept of scopes to provide fine-grained access control for API keys. Scopes are strings that define the permissions associated with an API key. When creating or updating an API key, you can specify the scopes that the key should have.
4+
If you define 2 scopes "read" and "write" to an route, an API key must have both scopes to access that route.
5+
6+
## Example
7+
8+
### Simple
9+
10+
This is the canonical example from `examples/example_scopes.py`:
11+
12+
!!! tip "Always set a pepper"
13+
The default pepper is a placeholder. Set `API_KEY_PEPPER` (or pass it explicitly to the hashers) in every environment.
14+
15+
```python
16+
--8<-- "examples/example_scopes.py"
17+
18+
```
19+
20+
### FastAPI
21+
22+
You can create security Depends with required scopes like this:
23+
24+
```python
25+
--8<-- "examples/example_fastapi_scopes.py"
26+
27+
```

examples/example_fastapi_scopes.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import os
2+
from pathlib import Path
3+
from typing import AsyncIterator
4+
5+
from fastapi import FastAPI, Depends, APIRouter
6+
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
7+
8+
from fastapi_api_key import ApiKey, ApiKeyService
9+
from fastapi_api_key.hasher.argon2 import Argon2ApiKeyHasher
10+
from fastapi_api_key.repositories.sql import SqlAlchemyApiKeyRepository
11+
from fastapi_api_key.api import create_api_keys_router, create_depends_api_key
12+
13+
# Set env var to override default pepper
14+
# Using a strong, unique pepper is crucial for security
15+
# Default pepper is insecure and should not be used in production
16+
pepper = os.getenv("API_KEY_PEPPER")
17+
hasher = Argon2ApiKeyHasher(pepper=pepper)
18+
19+
path = Path(__file__).parent / "db.sqlite3"
20+
database_url = os.environ.get("DATABASE_URL", f"sqlite+aiosqlite:///{path}")
21+
22+
async_engine = create_async_engine(database_url, future=True)
23+
async_session_maker = async_sessionmaker(
24+
async_engine,
25+
class_=AsyncSession,
26+
expire_on_commit=False,
27+
)
28+
29+
app = FastAPI(title="API with API Key Management")
30+
31+
32+
async def inject_async_session() -> AsyncIterator[AsyncSession]:
33+
"""Dependency to provide an active SQLAlchemy async session."""
34+
async with async_session_maker() as session:
35+
async with session.begin():
36+
yield session
37+
38+
39+
async def inject_svc_api_keys(async_session: AsyncSession = Depends(inject_async_session)) -> ApiKeyService:
40+
"""Dependency to inject the API key service with an active SQLAlchemy async session."""
41+
repo = SqlAlchemyApiKeyRepository(async_session)
42+
43+
# Necessary if you don't use your own DeclarativeBase
44+
await repo.ensure_table()
45+
46+
return ApiKeyService(repo=repo, hasher=hasher)
47+
48+
49+
# Create security dependency that requires "write" scope
50+
security = create_depends_api_key(inject_svc_api_keys, required_scopes=["write"])
51+
router_protected = APIRouter(prefix="/protected", tags=["Protected"])
52+
53+
router = APIRouter(prefix="/api-keys", tags=["API Keys"])
54+
router_api_keys = create_api_keys_router(
55+
inject_svc_api_keys,
56+
router=router,
57+
)
58+
59+
60+
@router_protected.get("/")
61+
async def read_protected_data(api_key: ApiKey = Depends(security)):
62+
return {
63+
"message": "This is protected data",
64+
"apiKey": {
65+
"id": api_key.id_,
66+
"name": api_key.name,
67+
"description": api_key.description,
68+
"isActive": api_key.is_active,
69+
"createdAt": api_key.created_at,
70+
"expiresAt": api_key.expires_at,
71+
"lastUsedAt": api_key.last_used_at,
72+
},
73+
}
74+
75+
76+
app.include_router(router_api_keys)
77+
app.include_router(router_protected)
78+
79+
80+
async def main():
81+
async with async_session_maker() as async_session:
82+
repo = SqlAlchemyApiKeyRepository(async_session)
83+
84+
# Necessary if you don't use your own DeclarativeBase
85+
await repo.ensure_table()
86+
87+
svc = ApiKeyService(repo=repo, hasher=hasher)
88+
89+
# Create an API key without scopes
90+
bad_entity = ApiKey(name="no-scope-key", scopes=["read"])
91+
good_entity = ApiKey(name="with-scope-key", scopes=["write"])
92+
93+
_, bad_api_key = await svc.create(bad_entity)
94+
_, good_api_key = await svc.create(good_entity)
95+
96+
print(f"Bad API Key (no required scopes): '{bad_api_key}'")
97+
print(f"Good API Key (with required scopes): '{good_api_key}'")
98+
99+
await async_session.commit()
100+
101+
102+
if __name__ == "__main__":
103+
import asyncio
104+
105+
# Create an invalid and a valid API key for testing
106+
asyncio.run(main())
107+
108+
import uvicorn
109+
110+
# Run the FastAPI app and test the 2 api keys against the protected endpoint
111+
uvicorn.run(app, host="localhost", port=8000)

0 commit comments

Comments
 (0)