fastapi-auth-manager-dep 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastapi_auth_manager_dep-0.1.0/LICENSE +20 -0
- fastapi_auth_manager_dep-0.1.0/MANIFEST.in +3 -0
- fastapi_auth_manager_dep-0.1.0/PKG-INFO +266 -0
- fastapi_auth_manager_dep-0.1.0/README.md +228 -0
- fastapi_auth_manager_dep-0.1.0/pyproject.toml +61 -0
- fastapi_auth_manager_dep-0.1.0/setup.cfg +4 -0
- fastapi_auth_manager_dep-0.1.0/src/fastapi_auth/__init__.py +11 -0
- fastapi_auth_manager_dep-0.1.0/src/fastapi_auth/dependency.py +253 -0
- fastapi_auth_manager_dep-0.1.0/src/fastapi_auth/enums.py +6 -0
- fastapi_auth_manager_dep-0.1.0/src/fastapi_auth/public.py +36 -0
- fastapi_auth_manager_dep-0.1.0/src/fastapi_auth/schemas.py +23 -0
- fastapi_auth_manager_dep-0.1.0/src/fastapi_auth/settings.py +113 -0
- fastapi_auth_manager_dep-0.1.0/src/fastapi_auth_manager_dep.egg-info/PKG-INFO +266 -0
- fastapi_auth_manager_dep-0.1.0/src/fastapi_auth_manager_dep.egg-info/SOURCES.txt +15 -0
- fastapi_auth_manager_dep-0.1.0/src/fastapi_auth_manager_dep.egg-info/dependency_links.txt +1 -0
- fastapi_auth_manager_dep-0.1.0/src/fastapi_auth_manager_dep.egg-info/requires.txt +9 -0
- fastapi_auth_manager_dep-0.1.0/src/fastapi_auth_manager_dep.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright 2026 Edmundo Andrade
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
|
6
|
+
and associated documentation files (the “Software”), to deal in the Software without
|
|
7
|
+
restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
|
8
|
+
distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
|
9
|
+
Software is furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all copies or
|
|
12
|
+
substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
17
|
+
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
18
|
+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
19
|
+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
20
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fastapi-auth-manager-dep
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Reusable authentication dependency for FastAPI with **API Key** and **JWT Bearer** support.
|
|
5
|
+
Author-email: Edmundo Andrade <edmon.af@gmail.com>
|
|
6
|
+
Project-URL: Homepage, https://github.com/edmon1024/fastapi-auth-manager-dep
|
|
7
|
+
Project-URL: Documentation, https://github.com/edmon1024/fastapi-auth-manager-dep
|
|
8
|
+
Project-URL: Repository, https://github.com/edmon1024/fastapi-auth-manager-dep
|
|
9
|
+
Project-URL: Issues, https://github.com/edmon1024/fastapi-auth-manager-dep/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/edmon1024/fastapi-auth-manager-dep/CHANGELOG.md
|
|
11
|
+
Keywords: fastapi,fastapi-auth-middleware,fastapi-jwt-auth,fastapi-jwt
|
|
12
|
+
Classifier: Framework :: FastAPI
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Topic :: Internet
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
17
|
+
Classifier: Topic :: Software Development
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
26
|
+
Requires-Python: >=3.10
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Requires-Dist: fastapi>=0.100.0
|
|
30
|
+
Requires-Dist: pydantic>=2.0.0
|
|
31
|
+
Requires-Dist: pydantic-settings>=2.0.0
|
|
32
|
+
Requires-Dist: pyjwt>=2.4.0
|
|
33
|
+
Provides-Extra: test
|
|
34
|
+
Requires-Dist: pytest>=7.0.0; extra == "test"
|
|
35
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "test"
|
|
36
|
+
Requires-Dist: httpx>=0.24.0; extra == "test"
|
|
37
|
+
Dynamic: license-file
|
|
38
|
+
|
|
39
|
+
# fastapi-auth-manager-dep
|
|
40
|
+
|
|
41
|
+
Reusable authentication dependency for FastAPI with **API Key** and **JWT Bearer** support.
|
|
42
|
+
|
|
43
|
+
- One mandatory ADMIN key via envvar (super-key, always valid)
|
|
44
|
+
- Additional api-keys with labels/roles via JSON envvar
|
|
45
|
+
- Fine-grained per-endpoint control: which roles each endpoint accepts
|
|
46
|
+
- HMAC JWT with configurable algorithms
|
|
47
|
+
- Public endpoints via explicit opt-out
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Install
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install fastapi-auth-manager-dep
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Environment variables
|
|
60
|
+
|
|
61
|
+
| Variable | Required | Description |
|
|
62
|
+
|-------------------|----------|-------------------------------------------------------------------------------|
|
|
63
|
+
| `AUTH_ADMIN_API_KEY` | Yes | Administrator api-key. Super-key: valid on every endpoint. |
|
|
64
|
+
| `AUTH_API_KEYS` | No | JSON object mapping api-keys to their labels. See format below. |
|
|
65
|
+
| `AUTH_JWT_SECRET_KEY`| No* | HMAC secret key for verifying user JWTs. Required when using `"jwt"`. |
|
|
66
|
+
| `AUTH_JWT_ALGORITHMS` | No | List of allowed algorithms (default: `["HS256", "HS384", "HS512"]`). |
|
|
67
|
+
|
|
68
|
+
### `AUTH_API_KEYS` format
|
|
69
|
+
|
|
70
|
+
A JSON object where each key is the raw api-key value and the value is its label/role:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
AUTH_API_KEYS='{"key-abc123": "reports", "key-xyz789": "billing", "key-qrs456": "billing"}'
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
- Multiple keys can share the same label (same role).
|
|
77
|
+
- Empty string (`AUTH_API_KEYS=""`) is equivalent to having no additional keys.
|
|
78
|
+
|
|
79
|
+
### `.env` example
|
|
80
|
+
|
|
81
|
+
```dotenv
|
|
82
|
+
AUTH_ADMIN_API_KEY=super-secret-admin-key
|
|
83
|
+
AUTH_API_KEYS={"key-reports-1": "reports", "key-billing-1": "billing", "key-billing-2": "billing"}
|
|
84
|
+
AUTH_JWT_SECRET_KEY=my-jwt-secret-key
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Usage
|
|
90
|
+
|
|
91
|
+
### 1. Global authentication (entire app)
|
|
92
|
+
|
|
93
|
+
All endpoints are protected with the ADMIN key by default:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from fastapi import Depends, FastAPI
|
|
97
|
+
from fastapi_auth import AuthDependency
|
|
98
|
+
|
|
99
|
+
app = FastAPI(dependencies=[Depends(AuthDependency())])
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### 2. Restrict by api-key label
|
|
103
|
+
|
|
104
|
+
Only the ADMIN key and keys labelled `"reports"` can access:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from fastapi_auth import AuthDependency
|
|
108
|
+
|
|
109
|
+
@app.get(
|
|
110
|
+
"/reports",
|
|
111
|
+
dependencies=[Depends(AuthDependency(valid_token_types={"reports"}))]
|
|
112
|
+
)
|
|
113
|
+
async def get_reports():
|
|
114
|
+
...
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### 3. Combine api-key and JWT on the same endpoint
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from fastapi_auth import AuthDependency
|
|
121
|
+
|
|
122
|
+
@app.get(
|
|
123
|
+
"/billing",
|
|
124
|
+
dependencies=[Depends(AuthDependency(valid_token_types={"billing", "jwt"}))]
|
|
125
|
+
)
|
|
126
|
+
async def get_billing():
|
|
127
|
+
...
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### 4. Allow all additional api-keys
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from fastapi_auth import AuthDependency
|
|
134
|
+
|
|
135
|
+
# Using the AuthDependency.ALL sentinel
|
|
136
|
+
@app.get(
|
|
137
|
+
"/any",
|
|
138
|
+
dependencies=[Depends(AuthDependency(valid_token_types=AuthDependency.ALL))]
|
|
139
|
+
)
|
|
140
|
+
async def any_key_endpoint():
|
|
141
|
+
...
|
|
142
|
+
|
|
143
|
+
# Equivalent using a string
|
|
144
|
+
AuthDependency(valid_token_types="*")
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### 5. Access the authenticated principal inside a handler
|
|
148
|
+
|
|
149
|
+
`AuthDependency` returns an `AuthPrincipal` with the method used, role, and JWT payload:
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
from fastapi_auth import AuthDependency, AuthPrincipal
|
|
153
|
+
|
|
154
|
+
@app.get("/me")
|
|
155
|
+
async def me(
|
|
156
|
+
principal: AuthPrincipal = Depends(
|
|
157
|
+
AuthDependency(valid_token_types={"jwt", "billing"})
|
|
158
|
+
)
|
|
159
|
+
):
|
|
160
|
+
return {
|
|
161
|
+
"method": principal.method, # "jwt" | "api_key"
|
|
162
|
+
"sub": principal.sub, # user_id (JWT) or raw key value (api-key)
|
|
163
|
+
"role": principal.role, # "admin" | "reports" | "billing" | None (JWT)
|
|
164
|
+
"payload": principal.payload, # full JWT dict | None
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### 6. Public endpoint (opt-out of global auth)
|
|
169
|
+
|
|
170
|
+
When auth is configured globally, use `PublicRoute` to exclude specific endpoints:
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
from fastapi_auth import PublicRoute
|
|
174
|
+
|
|
175
|
+
@app.get("/health", dependencies=[Depends(PublicRoute())])
|
|
176
|
+
async def health():
|
|
177
|
+
return {"status": "ok"}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## `valid_token_types` behaviour reference
|
|
183
|
+
|
|
184
|
+
| `valid_token_types` | ADMIN key | Additional keys | User JWT |
|
|
185
|
+
|------------------------------|-----------|-------------------------|----------|
|
|
186
|
+
| `None` (default) | Yes | No | No |
|
|
187
|
+
| `{"reports"}` | Yes | `reports` label only | No |
|
|
188
|
+
| `{"billing", "jwt"}` | Yes | `billing` label only | Yes |
|
|
189
|
+
| `AuthDependency.ALL` / `"*"` | Yes | All | No |
|
|
190
|
+
| `{"jwt"}` | Yes | No | Yes |
|
|
191
|
+
|
|
192
|
+
> The ADMIN key is always valid regardless of the endpoint configuration.
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## `AuthPrincipal` — return object
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
class AuthPrincipal(BaseModel):
|
|
200
|
+
method: AuthMethod # AuthMethod.JWT | AuthMethod.AUTH_ADMIN_API_KEY
|
|
201
|
+
sub: str # user_id (JWT) or raw api-key value
|
|
202
|
+
role: str | None = None # key role/label; None for JWT
|
|
203
|
+
payload: dict | None = None # full JWT payload; None for api-key
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## Error responses
|
|
209
|
+
|
|
210
|
+
All authentication errors return HTTP `401 Unauthorized` with a `detail` field:
|
|
211
|
+
|
|
212
|
+
| Situation | `detail` |
|
|
213
|
+
|--------------------------------------|-------------------------------------------|
|
|
214
|
+
| No credentials provided | `"Authentication credentials are required"` |
|
|
215
|
+
| Api-key not found or not allowed | `"Invalid authentication credentials"` |
|
|
216
|
+
| Expired JWT | `"JWT token has expired"` |
|
|
217
|
+
| Invalid JWT signature | `"Invalid JWT token: ..."` |
|
|
218
|
+
| Disallowed JWT algorithm | `"JWT algorithm not allowed: RS256"` |
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## Tests
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
pip install pytest pytest-asyncio
|
|
226
|
+
pytest tests/ -v
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Test coverage includes:
|
|
230
|
+
|
|
231
|
+
- ADMIN key as super-key across endpoints with different restrictions
|
|
232
|
+
- Label-based restriction: accepts the correct label, rejects others
|
|
233
|
+
- Multiple keys sharing the same label
|
|
234
|
+
- `ALL` sentinel and its `"*"` string equivalent
|
|
235
|
+
- Valid JWT, expired JWT, JWT ignored when `"jwt"` is not enabled
|
|
236
|
+
- No credentials
|
|
237
|
+
- `AUTH_API_KEYS` validation in settings (JSON string, dict, invalid JSON)
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## Full example
|
|
242
|
+
|
|
243
|
+
```python
|
|
244
|
+
from fastapi import Depends, FastAPI
|
|
245
|
+
from fastapi_auth import AuthDependency, AuthPrincipal, PublicRoute
|
|
246
|
+
|
|
247
|
+
app = FastAPI(dependencies=[Depends(AuthDependency())]) # global: ADMIN key only
|
|
248
|
+
|
|
249
|
+
@app.get("/health", dependencies=[Depends(PublicRoute())])
|
|
250
|
+
async def health():
|
|
251
|
+
return {"status": "ok"}
|
|
252
|
+
|
|
253
|
+
@app.get("/reports", dependencies=[Depends(AuthDependency(valid_token_types={"reports"}))])
|
|
254
|
+
async def reports():
|
|
255
|
+
return {"data": "..."}
|
|
256
|
+
|
|
257
|
+
@app.get("/me")
|
|
258
|
+
async def me(
|
|
259
|
+
principal: AuthPrincipal = Depends(AuthDependency(valid_token_types={"jwt"}))
|
|
260
|
+
):
|
|
261
|
+
return {"sub": principal.sub, "payload": principal.payload}
|
|
262
|
+
|
|
263
|
+
@app.get("/internal", dependencies=[Depends(AuthDependency(valid_token_types=AuthDependency.ALL))])
|
|
264
|
+
async def internal():
|
|
265
|
+
return {"message": "any valid api-key accepted"}
|
|
266
|
+
```
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# fastapi-auth-manager-dep
|
|
2
|
+
|
|
3
|
+
Reusable authentication dependency for FastAPI with **API Key** and **JWT Bearer** support.
|
|
4
|
+
|
|
5
|
+
- One mandatory ADMIN key via envvar (super-key, always valid)
|
|
6
|
+
- Additional api-keys with labels/roles via JSON envvar
|
|
7
|
+
- Fine-grained per-endpoint control: which roles each endpoint accepts
|
|
8
|
+
- HMAC JWT with configurable algorithms
|
|
9
|
+
- Public endpoints via explicit opt-out
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install fastapi-auth-manager-dep
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Environment variables
|
|
22
|
+
|
|
23
|
+
| Variable | Required | Description |
|
|
24
|
+
|-------------------|----------|-------------------------------------------------------------------------------|
|
|
25
|
+
| `AUTH_ADMIN_API_KEY` | Yes | Administrator api-key. Super-key: valid on every endpoint. |
|
|
26
|
+
| `AUTH_API_KEYS` | No | JSON object mapping api-keys to their labels. See format below. |
|
|
27
|
+
| `AUTH_JWT_SECRET_KEY`| No* | HMAC secret key for verifying user JWTs. Required when using `"jwt"`. |
|
|
28
|
+
| `AUTH_JWT_ALGORITHMS` | No | List of allowed algorithms (default: `["HS256", "HS384", "HS512"]`). |
|
|
29
|
+
|
|
30
|
+
### `AUTH_API_KEYS` format
|
|
31
|
+
|
|
32
|
+
A JSON object where each key is the raw api-key value and the value is its label/role:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
AUTH_API_KEYS='{"key-abc123": "reports", "key-xyz789": "billing", "key-qrs456": "billing"}'
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- Multiple keys can share the same label (same role).
|
|
39
|
+
- Empty string (`AUTH_API_KEYS=""`) is equivalent to having no additional keys.
|
|
40
|
+
|
|
41
|
+
### `.env` example
|
|
42
|
+
|
|
43
|
+
```dotenv
|
|
44
|
+
AUTH_ADMIN_API_KEY=super-secret-admin-key
|
|
45
|
+
AUTH_API_KEYS={"key-reports-1": "reports", "key-billing-1": "billing", "key-billing-2": "billing"}
|
|
46
|
+
AUTH_JWT_SECRET_KEY=my-jwt-secret-key
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
|
|
53
|
+
### 1. Global authentication (entire app)
|
|
54
|
+
|
|
55
|
+
All endpoints are protected with the ADMIN key by default:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from fastapi import Depends, FastAPI
|
|
59
|
+
from fastapi_auth import AuthDependency
|
|
60
|
+
|
|
61
|
+
app = FastAPI(dependencies=[Depends(AuthDependency())])
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 2. Restrict by api-key label
|
|
65
|
+
|
|
66
|
+
Only the ADMIN key and keys labelled `"reports"` can access:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from fastapi_auth import AuthDependency
|
|
70
|
+
|
|
71
|
+
@app.get(
|
|
72
|
+
"/reports",
|
|
73
|
+
dependencies=[Depends(AuthDependency(valid_token_types={"reports"}))]
|
|
74
|
+
)
|
|
75
|
+
async def get_reports():
|
|
76
|
+
...
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 3. Combine api-key and JWT on the same endpoint
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from fastapi_auth import AuthDependency
|
|
83
|
+
|
|
84
|
+
@app.get(
|
|
85
|
+
"/billing",
|
|
86
|
+
dependencies=[Depends(AuthDependency(valid_token_types={"billing", "jwt"}))]
|
|
87
|
+
)
|
|
88
|
+
async def get_billing():
|
|
89
|
+
...
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 4. Allow all additional api-keys
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from fastapi_auth import AuthDependency
|
|
96
|
+
|
|
97
|
+
# Using the AuthDependency.ALL sentinel
|
|
98
|
+
@app.get(
|
|
99
|
+
"/any",
|
|
100
|
+
dependencies=[Depends(AuthDependency(valid_token_types=AuthDependency.ALL))]
|
|
101
|
+
)
|
|
102
|
+
async def any_key_endpoint():
|
|
103
|
+
...
|
|
104
|
+
|
|
105
|
+
# Equivalent using a string
|
|
106
|
+
AuthDependency(valid_token_types="*")
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### 5. Access the authenticated principal inside a handler
|
|
110
|
+
|
|
111
|
+
`AuthDependency` returns an `AuthPrincipal` with the method used, role, and JWT payload:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from fastapi_auth import AuthDependency, AuthPrincipal
|
|
115
|
+
|
|
116
|
+
@app.get("/me")
|
|
117
|
+
async def me(
|
|
118
|
+
principal: AuthPrincipal = Depends(
|
|
119
|
+
AuthDependency(valid_token_types={"jwt", "billing"})
|
|
120
|
+
)
|
|
121
|
+
):
|
|
122
|
+
return {
|
|
123
|
+
"method": principal.method, # "jwt" | "api_key"
|
|
124
|
+
"sub": principal.sub, # user_id (JWT) or raw key value (api-key)
|
|
125
|
+
"role": principal.role, # "admin" | "reports" | "billing" | None (JWT)
|
|
126
|
+
"payload": principal.payload, # full JWT dict | None
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### 6. Public endpoint (opt-out of global auth)
|
|
131
|
+
|
|
132
|
+
When auth is configured globally, use `PublicRoute` to exclude specific endpoints:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from fastapi_auth import PublicRoute
|
|
136
|
+
|
|
137
|
+
@app.get("/health", dependencies=[Depends(PublicRoute())])
|
|
138
|
+
async def health():
|
|
139
|
+
return {"status": "ok"}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## `valid_token_types` behaviour reference
|
|
145
|
+
|
|
146
|
+
| `valid_token_types` | ADMIN key | Additional keys | User JWT |
|
|
147
|
+
|------------------------------|-----------|-------------------------|----------|
|
|
148
|
+
| `None` (default) | Yes | No | No |
|
|
149
|
+
| `{"reports"}` | Yes | `reports` label only | No |
|
|
150
|
+
| `{"billing", "jwt"}` | Yes | `billing` label only | Yes |
|
|
151
|
+
| `AuthDependency.ALL` / `"*"` | Yes | All | No |
|
|
152
|
+
| `{"jwt"}` | Yes | No | Yes |
|
|
153
|
+
|
|
154
|
+
> The ADMIN key is always valid regardless of the endpoint configuration.
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## `AuthPrincipal` — return object
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
class AuthPrincipal(BaseModel):
|
|
162
|
+
method: AuthMethod # AuthMethod.JWT | AuthMethod.AUTH_ADMIN_API_KEY
|
|
163
|
+
sub: str # user_id (JWT) or raw api-key value
|
|
164
|
+
role: str | None = None # key role/label; None for JWT
|
|
165
|
+
payload: dict | None = None # full JWT payload; None for api-key
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Error responses
|
|
171
|
+
|
|
172
|
+
All authentication errors return HTTP `401 Unauthorized` with a `detail` field:
|
|
173
|
+
|
|
174
|
+
| Situation | `detail` |
|
|
175
|
+
|--------------------------------------|-------------------------------------------|
|
|
176
|
+
| No credentials provided | `"Authentication credentials are required"` |
|
|
177
|
+
| Api-key not found or not allowed | `"Invalid authentication credentials"` |
|
|
178
|
+
| Expired JWT | `"JWT token has expired"` |
|
|
179
|
+
| Invalid JWT signature | `"Invalid JWT token: ..."` |
|
|
180
|
+
| Disallowed JWT algorithm | `"JWT algorithm not allowed: RS256"` |
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Tests
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
pip install pytest pytest-asyncio
|
|
188
|
+
pytest tests/ -v
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Test coverage includes:
|
|
192
|
+
|
|
193
|
+
- ADMIN key as super-key across endpoints with different restrictions
|
|
194
|
+
- Label-based restriction: accepts the correct label, rejects others
|
|
195
|
+
- Multiple keys sharing the same label
|
|
196
|
+
- `ALL` sentinel and its `"*"` string equivalent
|
|
197
|
+
- Valid JWT, expired JWT, JWT ignored when `"jwt"` is not enabled
|
|
198
|
+
- No credentials
|
|
199
|
+
- `AUTH_API_KEYS` validation in settings (JSON string, dict, invalid JSON)
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Full example
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
from fastapi import Depends, FastAPI
|
|
207
|
+
from fastapi_auth import AuthDependency, AuthPrincipal, PublicRoute
|
|
208
|
+
|
|
209
|
+
app = FastAPI(dependencies=[Depends(AuthDependency())]) # global: ADMIN key only
|
|
210
|
+
|
|
211
|
+
@app.get("/health", dependencies=[Depends(PublicRoute())])
|
|
212
|
+
async def health():
|
|
213
|
+
return {"status": "ok"}
|
|
214
|
+
|
|
215
|
+
@app.get("/reports", dependencies=[Depends(AuthDependency(valid_token_types={"reports"}))])
|
|
216
|
+
async def reports():
|
|
217
|
+
return {"data": "..."}
|
|
218
|
+
|
|
219
|
+
@app.get("/me")
|
|
220
|
+
async def me(
|
|
221
|
+
principal: AuthPrincipal = Depends(AuthDependency(valid_token_types={"jwt"}))
|
|
222
|
+
):
|
|
223
|
+
return {"sub": principal.sub, "payload": principal.payload}
|
|
224
|
+
|
|
225
|
+
@app.get("/internal", dependencies=[Depends(AuthDependency(valid_token_types=AuthDependency.ALL))])
|
|
226
|
+
async def internal():
|
|
227
|
+
return {"message": "any valid api-key accepted"}
|
|
228
|
+
```
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=82.0.1", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "fastapi-auth-manager-dep"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name="Edmundo Andrade", email="edmon.af@gmail.com" },
|
|
10
|
+
]
|
|
11
|
+
description = "Reusable authentication dependency for FastAPI with **API Key** and **JWT Bearer** support."
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.10"
|
|
14
|
+
keywords = [
|
|
15
|
+
"fastapi",
|
|
16
|
+
"fastapi-auth-middleware",
|
|
17
|
+
"fastapi-jwt-auth",
|
|
18
|
+
"fastapi-jwt",
|
|
19
|
+
]
|
|
20
|
+
classifiers = [
|
|
21
|
+
"Framework :: FastAPI",
|
|
22
|
+
"Operating System :: OS Independent",
|
|
23
|
+
"Topic :: Internet",
|
|
24
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
25
|
+
"Topic :: Software Development :: Libraries",
|
|
26
|
+
"Topic :: Software Development",
|
|
27
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
28
|
+
"Programming Language :: Python :: 3",
|
|
29
|
+
"Programming Language :: Python",
|
|
30
|
+
"Programming Language :: Python :: 3.10",
|
|
31
|
+
"Programming Language :: Python :: 3.11",
|
|
32
|
+
"Programming Language :: Python :: 3.12",
|
|
33
|
+
"Programming Language :: Python :: 3.13",
|
|
34
|
+
"Programming Language :: Python :: 3.14",
|
|
35
|
+
]
|
|
36
|
+
dependencies = [
|
|
37
|
+
"fastapi>=0.100.0",
|
|
38
|
+
"pydantic>=2.0.0",
|
|
39
|
+
"pydantic-settings>=2.0.0",
|
|
40
|
+
"pyjwt>=2.4.0",
|
|
41
|
+
]
|
|
42
|
+
[project.optional-dependencies]
|
|
43
|
+
test = [
|
|
44
|
+
"pytest>=7.0.0",
|
|
45
|
+
"pytest-asyncio>=0.21.0",
|
|
46
|
+
"httpx>=0.24.0",
|
|
47
|
+
]
|
|
48
|
+
[tool.setuptools.packages.find]
|
|
49
|
+
where = ["src"]
|
|
50
|
+
|
|
51
|
+
[tool.setuptools]
|
|
52
|
+
exclude-package-data = {"fastapi_auth" = ["tests*", "example_app.py"]}
|
|
53
|
+
|
|
54
|
+
[project.urls]
|
|
55
|
+
Homepage = "https://github.com/edmon1024/fastapi-auth-manager-dep"
|
|
56
|
+
Documentation = "https://github.com/edmon1024/fastapi-auth-manager-dep"
|
|
57
|
+
Repository = "https://github.com/edmon1024/fastapi-auth-manager-dep"
|
|
58
|
+
Issues = "https://github.com/edmon1024/fastapi-auth-manager-dep/issues"
|
|
59
|
+
Changelog = "https://github.com/edmon1024/fastapi-auth-manager-dep/CHANGELOG.md"
|
|
60
|
+
|
|
61
|
+
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import jwt as pyjwt
|
|
6
|
+
from fastapi import HTTPException, Security, status
|
|
7
|
+
from fastapi.security import APIKeyHeader, HTTPAuthorizationCredentials, HTTPBearer
|
|
8
|
+
|
|
9
|
+
from .enums import AuthMethod
|
|
10
|
+
from .schemas import AuthPrincipal
|
|
11
|
+
from .settings import AuthSettings, get_auth_settings
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
# Security schemes shared across all instances (module-level singletons)
|
|
16
|
+
_bearer_scheme = HTTPBearer(auto_error=False)
|
|
17
|
+
_api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
|
|
18
|
+
|
|
19
|
+
# Sentinel value for "allow all additional api-keys"
|
|
20
|
+
_ALL = object()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AuthDependency:
|
|
24
|
+
"""
|
|
25
|
+
Authentication dependency for FastAPI.
|
|
26
|
+
|
|
27
|
+
Supported mechanisms
|
|
28
|
+
--------------------
|
|
29
|
+
- **API Key** via ``X-API-Key`` header
|
|
30
|
+
- **JWT Bearer** via ``Authorization: Bearer <token>`` header
|
|
31
|
+
(only when ``"jwt"`` is included in ``valid_token_types``)
|
|
32
|
+
|
|
33
|
+
Api-key logic
|
|
34
|
+
-------------
|
|
35
|
+
The ADMIN key (``AUTH_ADMIN_API_KEY`` envvar) is a **super-key**: always valid on any
|
|
36
|
+
endpoint, regardless of ``valid_token_types``.
|
|
37
|
+
|
|
38
|
+
Additional keys are defined in the ``AUTH_API_KEYS`` envvar as JSON::
|
|
39
|
+
|
|
40
|
+
AUTH_API_KEYS='{"key-abc": "reports", "key-xyz": "billing"}'
|
|
41
|
+
|
|
42
|
+
Each key's label must be used for per-endpoint restrictions via ``valid_token_types``.
|
|
43
|
+
|
|
44
|
+
Controlling which keys a given instance accepts
|
|
45
|
+
-----------------------------------------------
|
|
46
|
+
``valid_token_types=None``
|
|
47
|
+
ADMIN key only (default behaviour).
|
|
48
|
+
|
|
49
|
+
``valid_token_types={"reports"}``
|
|
50
|
+
ADMIN key + all keys labelled ``"reports"``.
|
|
51
|
+
|
|
52
|
+
``valid_token_types="*"`` or ``valid_token_types=AuthDependency.ALL``
|
|
53
|
+
ADMIN key + **all** additional keys with no label filter.
|
|
54
|
+
|
|
55
|
+
Quick reference
|
|
56
|
+
---------------
|
|
57
|
+
::
|
|
58
|
+
|
|
59
|
+
# Global: ADMIN key only
|
|
60
|
+
app = FastAPI(dependencies=[Depends(AuthDependency())])
|
|
61
|
+
|
|
62
|
+
# Endpoint: accept "reports" keys and user JWTs
|
|
63
|
+
@router.get("/report", dependencies=[Depends(
|
|
64
|
+
AuthDependency(valid_token_types={"reports", "jwt"})
|
|
65
|
+
)])
|
|
66
|
+
|
|
67
|
+
# Endpoint: accept all additional keys
|
|
68
|
+
@router.get("/open", dependencies=[Depends(
|
|
69
|
+
AuthDependency(valid_token_types=AuthDependency.ALL)
|
|
70
|
+
)])
|
|
71
|
+
|
|
72
|
+
# Access the authenticated principal inside the handler
|
|
73
|
+
@router.get("/me")
|
|
74
|
+
async def me(principal: AuthPrincipal = Depends(AuthDependency(...))):
|
|
75
|
+
return principal
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
#: Sentinel to allow all additional api-keys without label filtering.
|
|
79
|
+
ALL = _ALL
|
|
80
|
+
|
|
81
|
+
def __init__(
|
|
82
|
+
self,
|
|
83
|
+
valid_token_types: "set[str | Enum] | object | None" = None,
|
|
84
|
+
settings: AuthSettings | None = None,
|
|
85
|
+
) -> None:
|
|
86
|
+
"""
|
|
87
|
+
Parameters
|
|
88
|
+
----------
|
|
89
|
+
valid_token_types:
|
|
90
|
+
- ``None`` → ADMIN key only.
|
|
91
|
+
- ``{"role-a"}`` → ADMIN key + keys labelled ``"role-a"``.
|
|
92
|
+
- ``AuthDependency.ALL`` / ``"*"`` → ADMIN key + all additional keys.
|
|
93
|
+
- Include ``"jwt"`` to enable user JWT authentication.
|
|
94
|
+
settings:
|
|
95
|
+
Configuration injection. Defaults to ``get_auth_settings()``.
|
|
96
|
+
"""
|
|
97
|
+
self._settings = settings or get_auth_settings()
|
|
98
|
+
self._allow_all_keys: bool = valid_token_types is _ALL or valid_token_types == "*"
|
|
99
|
+
|
|
100
|
+
if self._allow_all_keys:
|
|
101
|
+
self._valid_token_types: set[str] = set()
|
|
102
|
+
else:
|
|
103
|
+
self._valid_token_types = self._normalize(valid_token_types)
|
|
104
|
+
|
|
105
|
+
# ------------------------------------------------------------------
|
|
106
|
+
# Static helpers
|
|
107
|
+
# ------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
@staticmethod
|
|
110
|
+
def _normalize(raw: "set[str | Enum] | None") -> set[str]:
|
|
111
|
+
"""Converts a mixed Enum/str set to a plain set of strings."""
|
|
112
|
+
if not raw:
|
|
113
|
+
return set()
|
|
114
|
+
result: set[str] = set()
|
|
115
|
+
for item in raw:
|
|
116
|
+
result.add(item.value if isinstance(item, Enum) else str(item))
|
|
117
|
+
return result
|
|
118
|
+
|
|
119
|
+
# ------------------------------------------------------------------
|
|
120
|
+
# Allowed api-key set construction
|
|
121
|
+
# ------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
def _allowed_api_keys(self) -> set[str]:
|
|
124
|
+
"""
|
|
125
|
+
Returns the set of valid api-keys for this instance.
|
|
126
|
+
|
|
127
|
+
Rules:
|
|
128
|
+
1. ADMIN key is always included (super-key).
|
|
129
|
+
2. If ``_allow_all_keys`` → every key from ``AUTH_API_KEYS``.
|
|
130
|
+
3. Otherwise → only keys whose label is in ``_valid_token_types``.
|
|
131
|
+
"""
|
|
132
|
+
allowed: set[str] = set()
|
|
133
|
+
|
|
134
|
+
# 1. ADMIN super-key (always)
|
|
135
|
+
if self._settings.AUTH_ADMIN_API_KEY:
|
|
136
|
+
allowed.add(self._settings.AUTH_ADMIN_API_KEY)
|
|
137
|
+
|
|
138
|
+
# 2. Additional keys according to policy
|
|
139
|
+
if self._allow_all_keys:
|
|
140
|
+
allowed.update(k for k in self._settings.AUTH_API_KEYS if k)
|
|
141
|
+
else:
|
|
142
|
+
for label in self._valid_token_types:
|
|
143
|
+
if label == "jwt":
|
|
144
|
+
continue # label is "jwt", which indicates JWT auth, not an api-key
|
|
145
|
+
allowed.update(self._settings.keys_for_label(label))
|
|
146
|
+
|
|
147
|
+
return allowed
|
|
148
|
+
|
|
149
|
+
# ------------------------------------------------------------------
|
|
150
|
+
# JWT verification
|
|
151
|
+
# ------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
def _verify_jwt(self, token: str) -> dict:
|
|
154
|
+
"""
|
|
155
|
+
Decodes and validates an HMAC JWT.
|
|
156
|
+
|
|
157
|
+
Raises
|
|
158
|
+
------
|
|
159
|
+
HTTPException 401 for invalid, expired, or disallowed-algorithm tokens.
|
|
160
|
+
"""
|
|
161
|
+
try:
|
|
162
|
+
header = pyjwt.get_unverified_header(token)
|
|
163
|
+
alg = header.get("alg")
|
|
164
|
+
if alg not in self._settings.AUTH_JWT_ALGORITHMS:
|
|
165
|
+
raise HTTPException(
|
|
166
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
167
|
+
detail=f"JWT algorithm not allowed: {alg}",
|
|
168
|
+
)
|
|
169
|
+
payload = pyjwt.decode(
|
|
170
|
+
token,
|
|
171
|
+
self._settings.AUTH_JWT_SECRET_KEY,
|
|
172
|
+
algorithms=self._settings.AUTH_JWT_ALGORITHMS,
|
|
173
|
+
)
|
|
174
|
+
return payload
|
|
175
|
+
except HTTPException:
|
|
176
|
+
raise
|
|
177
|
+
except pyjwt.ExpiredSignatureError:
|
|
178
|
+
raise HTTPException(
|
|
179
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
180
|
+
detail="JWT token has expired",
|
|
181
|
+
)
|
|
182
|
+
except pyjwt.InvalidTokenError as exc:
|
|
183
|
+
raise HTTPException(
|
|
184
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
185
|
+
detail=f"Invalid JWT token: {exc}",
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# ------------------------------------------------------------------
|
|
189
|
+
# Api-key verification
|
|
190
|
+
# ------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
def _verify_api_key(self, api_key: str) -> str:
|
|
193
|
+
"""
|
|
194
|
+
Validates the api-key against the allowed set.
|
|
195
|
+
|
|
196
|
+
Returns
|
|
197
|
+
-------
|
|
198
|
+
The label/role associated with the key (``"admin"`` for the ADMIN key).
|
|
199
|
+
|
|
200
|
+
Raises
|
|
201
|
+
------
|
|
202
|
+
HTTPException 401 if the key is not in the allowed set.
|
|
203
|
+
"""
|
|
204
|
+
if api_key not in self._allowed_api_keys():
|
|
205
|
+
raise HTTPException(
|
|
206
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
207
|
+
detail="Invalid authentication credentials",
|
|
208
|
+
)
|
|
209
|
+
return self._settings.label_for_key(api_key) or "unknown"
|
|
210
|
+
|
|
211
|
+
# ------------------------------------------------------------------
|
|
212
|
+
# Main callable
|
|
213
|
+
# ------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
async def __call__(
|
|
216
|
+
self,
|
|
217
|
+
credentials: Optional[HTTPAuthorizationCredentials] = Security(_bearer_scheme),
|
|
218
|
+
api_key: Optional[str] = Security(_api_key_header),
|
|
219
|
+
) -> AuthPrincipal:
|
|
220
|
+
"""
|
|
221
|
+
Evaluation order:
|
|
222
|
+
1. JWT Bearer → if ``"jwt"`` is enabled and a Bearer token is present.
|
|
223
|
+
2. API Key → if the ``X-API-Key`` header is present.
|
|
224
|
+
3. 401 → no valid mechanism found.
|
|
225
|
+
|
|
226
|
+
Returns
|
|
227
|
+
-------
|
|
228
|
+
:class:`AuthPrincipal` with method, sub, role, and optional payload.
|
|
229
|
+
"""
|
|
230
|
+
# 1. JWT
|
|
231
|
+
jwt_enabled = "jwt" in self._valid_token_types
|
|
232
|
+
if jwt_enabled and credentials and credentials.scheme.lower() == "bearer":
|
|
233
|
+
payload = self._verify_jwt(credentials.credentials)
|
|
234
|
+
return AuthPrincipal(
|
|
235
|
+
method=AuthMethod.JWT,
|
|
236
|
+
sub=payload.get("sub", ""),
|
|
237
|
+
payload=payload,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
# 2. API Key
|
|
241
|
+
if api_key:
|
|
242
|
+
label = self._verify_api_key(api_key)
|
|
243
|
+
return AuthPrincipal(
|
|
244
|
+
method=AuthMethod.AUTH_ADMIN_API_KEY,
|
|
245
|
+
sub=api_key,
|
|
246
|
+
role=label,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
# 3. No credentials → 401
|
|
250
|
+
raise HTTPException(
|
|
251
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
252
|
+
detail="Authentication credentials are required",
|
|
253
|
+
)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from fastapi import Security
|
|
4
|
+
from fastapi.security import APIKeyHeader, HTTPAuthorizationCredentials, HTTPBearer
|
|
5
|
+
|
|
6
|
+
from .schemas import AuthPrincipal
|
|
7
|
+
|
|
8
|
+
_bearer_scheme = HTTPBearer(auto_error=False)
|
|
9
|
+
_api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PublicRoute:
|
|
13
|
+
"""
|
|
14
|
+
No-auth dependency for public endpoints.
|
|
15
|
+
|
|
16
|
+
When authentication is configured globally on the app
|
|
17
|
+
(via ``app = FastAPI(dependencies=[Depends(AuthDependency())])``),
|
|
18
|
+
use this dependency to opt specific endpoints out of authentication.
|
|
19
|
+
It accepts any request without validation.
|
|
20
|
+
|
|
21
|
+
Usage
|
|
22
|
+
-----
|
|
23
|
+
```python
|
|
24
|
+
@router.get("/health", dependencies=[Depends(PublicRoute())])
|
|
25
|
+
async def health():
|
|
26
|
+
return {"status": "ok"}
|
|
27
|
+
```
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
async def __call__(
|
|
31
|
+
self,
|
|
32
|
+
credentials: Optional[HTTPAuthorizationCredentials] = Security(_bearer_scheme),
|
|
33
|
+
api_key: Optional[str] = Security(_api_key_header),
|
|
34
|
+
) -> None:
|
|
35
|
+
"""Always returns None without validating anything."""
|
|
36
|
+
return None
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from typing import Any, Optional
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
from .enums import AuthMethod
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AuthPrincipal(BaseModel):
|
|
9
|
+
"""
|
|
10
|
+
Authentication result returned by AuthDependency.
|
|
11
|
+
|
|
12
|
+
Attributes
|
|
13
|
+
----------
|
|
14
|
+
method : Mechanism used (``"jwt"`` or ``"api_key"``).
|
|
15
|
+
sub : Subject identifier (``user_id`` for JWT, raw key value for api-key).
|
|
16
|
+
role : Api-key label/role (``"admin"``, ``"reports"``…). None for JWT.
|
|
17
|
+
payload : Full JWT payload. None for api-key.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
method: AuthMethod
|
|
21
|
+
sub: str
|
|
22
|
+
role: Optional[str] = None
|
|
23
|
+
payload: Optional[dict[str, Any]] = None
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
from functools import lru_cache
|
|
4
|
+
|
|
5
|
+
from pydantic import field_validator, model_validator
|
|
6
|
+
from pydantic_settings import BaseSettings
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AuthSettings(BaseSettings):
|
|
12
|
+
"""
|
|
13
|
+
Environment variables for the authentication module.
|
|
14
|
+
|
|
15
|
+
Required
|
|
16
|
+
--------
|
|
17
|
+
AUTH_ADMIN_API_KEY Admin api-key (super-key, always required).
|
|
18
|
+
|
|
19
|
+
Optional
|
|
20
|
+
--------
|
|
21
|
+
AUTH_API_KEYS JSON object mapping raw api-keys to their labels/roles.
|
|
22
|
+
Format: '{"key-abc": "reports", "key-xyz": "billing"}'
|
|
23
|
+
Labels are used with valid_token_types in AuthDependency.
|
|
24
|
+
|
|
25
|
+
AUTH_JWT_SECRET_KEY HMAC secret key for verifying user JWTs.
|
|
26
|
+
Required only when using "jwt" in valid_token_types.
|
|
27
|
+
|
|
28
|
+
AUTH_JWT_ALGORITHMS List of allowed HMAC algorithms (default: HS256/384/512).
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
# --- Admin api-key (required) ---
|
|
32
|
+
AUTH_ADMIN_API_KEY: str
|
|
33
|
+
|
|
34
|
+
# --- Additional api-keys: {"<raw-key>": "<label>", ...} ---
|
|
35
|
+
AUTH_API_KEYS: dict[str, str] = {}
|
|
36
|
+
|
|
37
|
+
# --- JWT ---
|
|
38
|
+
AUTH_JWT_SECRET_KEY: str = ""
|
|
39
|
+
AUTH_JWT_ALGORITHMS: list[str] = ["HS256", "HS384", "HS512"]
|
|
40
|
+
|
|
41
|
+
@field_validator("AUTH_API_KEYS", mode="before")
|
|
42
|
+
@classmethod
|
|
43
|
+
def _parse_api_keys(cls, v: object) -> dict[str, str]:
|
|
44
|
+
"""Accepts the envvar as a JSON string or as a dict (useful for test injection)."""
|
|
45
|
+
if isinstance(v, str):
|
|
46
|
+
v = v.strip()
|
|
47
|
+
if not v:
|
|
48
|
+
return {}
|
|
49
|
+
try:
|
|
50
|
+
parsed = json.loads(v)
|
|
51
|
+
except json.JSONDecodeError as exc:
|
|
52
|
+
raise ValueError(
|
|
53
|
+
f"AUTH_API_KEYS must be a valid JSON string: {exc}. "
|
|
54
|
+
'Example: \'{"key-abc": "reports", "key-xyz": "billing"}\''
|
|
55
|
+
) from exc
|
|
56
|
+
if not isinstance(parsed, dict):
|
|
57
|
+
raise ValueError("AUTH_API_KEYS must be a JSON object (dict).")
|
|
58
|
+
return parsed
|
|
59
|
+
if isinstance(v, dict):
|
|
60
|
+
return v
|
|
61
|
+
raise ValueError(f"Unexpected type for AUTH_API_KEYS: {type(v)}")
|
|
62
|
+
|
|
63
|
+
@field_validator("AUTH_API_KEYS")
|
|
64
|
+
@classmethod
|
|
65
|
+
def _validate_api_keys_structure(cls, v: dict[str, str]) -> dict[str, str]:
|
|
66
|
+
"""Ensures all keys and labels are non-empty strings."""
|
|
67
|
+
for key, label in v.items():
|
|
68
|
+
if not isinstance(key, str) or not key.strip():
|
|
69
|
+
raise ValueError(f"Api-key '{key}' cannot be an empty string.")
|
|
70
|
+
if not isinstance(label, str) or not label.strip():
|
|
71
|
+
raise ValueError(
|
|
72
|
+
f"Label for api-key '{key}' cannot be an empty string."
|
|
73
|
+
)
|
|
74
|
+
return v
|
|
75
|
+
|
|
76
|
+
@model_validator(mode="after")
|
|
77
|
+
def _warn_duplicate_keys(self) -> "AuthSettings":
|
|
78
|
+
"""Warns if any additional key shares the same value as the ADMIN key."""
|
|
79
|
+
duplicates = [k for k in self.AUTH_API_KEYS if k == self.AUTH_ADMIN_API_KEY]
|
|
80
|
+
if duplicates:
|
|
81
|
+
logger.warning(
|
|
82
|
+
"AUTH: One of the AUTH_API_KEYS has the same value as AUTH_ADMIN_API_KEY (admin). "
|
|
83
|
+
"Consider using unique keys per role."
|
|
84
|
+
)
|
|
85
|
+
return self
|
|
86
|
+
|
|
87
|
+
# ------------------------------------------------------------------
|
|
88
|
+
# Query helpers (used by AuthDependency)
|
|
89
|
+
# ------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
def label_for_key(self, api_key: str) -> str | None:
|
|
92
|
+
"""
|
|
93
|
+
Returns the label/role associated with an api-key.
|
|
94
|
+
The ADMIN key always returns "admin".
|
|
95
|
+
"""
|
|
96
|
+
if api_key == self.AUTH_ADMIN_API_KEY:
|
|
97
|
+
return "admin"
|
|
98
|
+
return self.AUTH_API_KEYS.get(api_key)
|
|
99
|
+
|
|
100
|
+
def keys_for_label(self, label: str) -> set[str]:
|
|
101
|
+
"""Returns all api-keys that share a given label."""
|
|
102
|
+
return {k for k, lbl in self.AUTH_API_KEYS.items() if lbl == label}
|
|
103
|
+
|
|
104
|
+
def all_extra_labels(self) -> set[str]:
|
|
105
|
+
"""Returns the set of unique labels defined in AUTH_API_KEYS."""
|
|
106
|
+
return set(self.AUTH_API_KEYS.values())
|
|
107
|
+
|
|
108
|
+
model_config = {"env_file": ".env", "extra": "ignore"}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@lru_cache
|
|
112
|
+
def get_auth_settings() -> AuthSettings:
|
|
113
|
+
return AuthSettings()
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fastapi-auth-manager-dep
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Reusable authentication dependency for FastAPI with **API Key** and **JWT Bearer** support.
|
|
5
|
+
Author-email: Edmundo Andrade <edmon.af@gmail.com>
|
|
6
|
+
Project-URL: Homepage, https://github.com/edmon1024/fastapi-auth-manager-dep
|
|
7
|
+
Project-URL: Documentation, https://github.com/edmon1024/fastapi-auth-manager-dep
|
|
8
|
+
Project-URL: Repository, https://github.com/edmon1024/fastapi-auth-manager-dep
|
|
9
|
+
Project-URL: Issues, https://github.com/edmon1024/fastapi-auth-manager-dep/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/edmon1024/fastapi-auth-manager-dep/CHANGELOG.md
|
|
11
|
+
Keywords: fastapi,fastapi-auth-middleware,fastapi-jwt-auth,fastapi-jwt
|
|
12
|
+
Classifier: Framework :: FastAPI
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Topic :: Internet
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
17
|
+
Classifier: Topic :: Software Development
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
26
|
+
Requires-Python: >=3.10
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Requires-Dist: fastapi>=0.100.0
|
|
30
|
+
Requires-Dist: pydantic>=2.0.0
|
|
31
|
+
Requires-Dist: pydantic-settings>=2.0.0
|
|
32
|
+
Requires-Dist: pyjwt>=2.4.0
|
|
33
|
+
Provides-Extra: test
|
|
34
|
+
Requires-Dist: pytest>=7.0.0; extra == "test"
|
|
35
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "test"
|
|
36
|
+
Requires-Dist: httpx>=0.24.0; extra == "test"
|
|
37
|
+
Dynamic: license-file
|
|
38
|
+
|
|
39
|
+
# fastapi-auth-manager-dep
|
|
40
|
+
|
|
41
|
+
Reusable authentication dependency for FastAPI with **API Key** and **JWT Bearer** support.
|
|
42
|
+
|
|
43
|
+
- One mandatory ADMIN key via envvar (super-key, always valid)
|
|
44
|
+
- Additional api-keys with labels/roles via JSON envvar
|
|
45
|
+
- Fine-grained per-endpoint control: which roles each endpoint accepts
|
|
46
|
+
- HMAC JWT with configurable algorithms
|
|
47
|
+
- Public endpoints via explicit opt-out
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Install
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install fastapi-auth-manager-dep
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Environment variables
|
|
60
|
+
|
|
61
|
+
| Variable | Required | Description |
|
|
62
|
+
|-------------------|----------|-------------------------------------------------------------------------------|
|
|
63
|
+
| `AUTH_ADMIN_API_KEY` | Yes | Administrator api-key. Super-key: valid on every endpoint. |
|
|
64
|
+
| `AUTH_API_KEYS` | No | JSON object mapping api-keys to their labels. See format below. |
|
|
65
|
+
| `AUTH_JWT_SECRET_KEY`| No* | HMAC secret key for verifying user JWTs. Required when using `"jwt"`. |
|
|
66
|
+
| `AUTH_JWT_ALGORITHMS` | No | List of allowed algorithms (default: `["HS256", "HS384", "HS512"]`). |
|
|
67
|
+
|
|
68
|
+
### `AUTH_API_KEYS` format
|
|
69
|
+
|
|
70
|
+
A JSON object where each key is the raw api-key value and the value is its label/role:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
AUTH_API_KEYS='{"key-abc123": "reports", "key-xyz789": "billing", "key-qrs456": "billing"}'
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
- Multiple keys can share the same label (same role).
|
|
77
|
+
- Empty string (`AUTH_API_KEYS=""`) is equivalent to having no additional keys.
|
|
78
|
+
|
|
79
|
+
### `.env` example
|
|
80
|
+
|
|
81
|
+
```dotenv
|
|
82
|
+
AUTH_ADMIN_API_KEY=super-secret-admin-key
|
|
83
|
+
AUTH_API_KEYS={"key-reports-1": "reports", "key-billing-1": "billing", "key-billing-2": "billing"}
|
|
84
|
+
AUTH_JWT_SECRET_KEY=my-jwt-secret-key
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Usage
|
|
90
|
+
|
|
91
|
+
### 1. Global authentication (entire app)
|
|
92
|
+
|
|
93
|
+
All endpoints are protected with the ADMIN key by default:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from fastapi import Depends, FastAPI
|
|
97
|
+
from fastapi_auth import AuthDependency
|
|
98
|
+
|
|
99
|
+
app = FastAPI(dependencies=[Depends(AuthDependency())])
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### 2. Restrict by api-key label
|
|
103
|
+
|
|
104
|
+
Only the ADMIN key and keys labelled `"reports"` can access:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from fastapi_auth import AuthDependency
|
|
108
|
+
|
|
109
|
+
@app.get(
|
|
110
|
+
"/reports",
|
|
111
|
+
dependencies=[Depends(AuthDependency(valid_token_types={"reports"}))]
|
|
112
|
+
)
|
|
113
|
+
async def get_reports():
|
|
114
|
+
...
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### 3. Combine api-key and JWT on the same endpoint
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from fastapi_auth import AuthDependency
|
|
121
|
+
|
|
122
|
+
@app.get(
|
|
123
|
+
"/billing",
|
|
124
|
+
dependencies=[Depends(AuthDependency(valid_token_types={"billing", "jwt"}))]
|
|
125
|
+
)
|
|
126
|
+
async def get_billing():
|
|
127
|
+
...
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### 4. Allow all additional api-keys
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from fastapi_auth import AuthDependency
|
|
134
|
+
|
|
135
|
+
# Using the AuthDependency.ALL sentinel
|
|
136
|
+
@app.get(
|
|
137
|
+
"/any",
|
|
138
|
+
dependencies=[Depends(AuthDependency(valid_token_types=AuthDependency.ALL))]
|
|
139
|
+
)
|
|
140
|
+
async def any_key_endpoint():
|
|
141
|
+
...
|
|
142
|
+
|
|
143
|
+
# Equivalent using a string
|
|
144
|
+
AuthDependency(valid_token_types="*")
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### 5. Access the authenticated principal inside a handler
|
|
148
|
+
|
|
149
|
+
`AuthDependency` returns an `AuthPrincipal` with the method used, role, and JWT payload:
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
from fastapi_auth import AuthDependency, AuthPrincipal
|
|
153
|
+
|
|
154
|
+
@app.get("/me")
|
|
155
|
+
async def me(
|
|
156
|
+
principal: AuthPrincipal = Depends(
|
|
157
|
+
AuthDependency(valid_token_types={"jwt", "billing"})
|
|
158
|
+
)
|
|
159
|
+
):
|
|
160
|
+
return {
|
|
161
|
+
"method": principal.method, # "jwt" | "api_key"
|
|
162
|
+
"sub": principal.sub, # user_id (JWT) or raw key value (api-key)
|
|
163
|
+
"role": principal.role, # "admin" | "reports" | "billing" | None (JWT)
|
|
164
|
+
"payload": principal.payload, # full JWT dict | None
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### 6. Public endpoint (opt-out of global auth)
|
|
169
|
+
|
|
170
|
+
When auth is configured globally, use `PublicRoute` to exclude specific endpoints:
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
from fastapi_auth import PublicRoute
|
|
174
|
+
|
|
175
|
+
@app.get("/health", dependencies=[Depends(PublicRoute())])
|
|
176
|
+
async def health():
|
|
177
|
+
return {"status": "ok"}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## `valid_token_types` behaviour reference
|
|
183
|
+
|
|
184
|
+
| `valid_token_types` | ADMIN key | Additional keys | User JWT |
|
|
185
|
+
|------------------------------|-----------|-------------------------|----------|
|
|
186
|
+
| `None` (default) | Yes | No | No |
|
|
187
|
+
| `{"reports"}` | Yes | `reports` label only | No |
|
|
188
|
+
| `{"billing", "jwt"}` | Yes | `billing` label only | Yes |
|
|
189
|
+
| `AuthDependency.ALL` / `"*"` | Yes | All | No |
|
|
190
|
+
| `{"jwt"}` | Yes | No | Yes |
|
|
191
|
+
|
|
192
|
+
> The ADMIN key is always valid regardless of the endpoint configuration.
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## `AuthPrincipal` — return object
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
class AuthPrincipal(BaseModel):
|
|
200
|
+
method: AuthMethod # AuthMethod.JWT | AuthMethod.AUTH_ADMIN_API_KEY
|
|
201
|
+
sub: str # user_id (JWT) or raw api-key value
|
|
202
|
+
role: str | None = None # key role/label; None for JWT
|
|
203
|
+
payload: dict | None = None # full JWT payload; None for api-key
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## Error responses
|
|
209
|
+
|
|
210
|
+
All authentication errors return HTTP `401 Unauthorized` with a `detail` field:
|
|
211
|
+
|
|
212
|
+
| Situation | `detail` |
|
|
213
|
+
|--------------------------------------|-------------------------------------------|
|
|
214
|
+
| No credentials provided | `"Authentication credentials are required"` |
|
|
215
|
+
| Api-key not found or not allowed | `"Invalid authentication credentials"` |
|
|
216
|
+
| Expired JWT | `"JWT token has expired"` |
|
|
217
|
+
| Invalid JWT signature | `"Invalid JWT token: ..."` |
|
|
218
|
+
| Disallowed JWT algorithm | `"JWT algorithm not allowed: RS256"` |
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## Tests
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
pip install pytest pytest-asyncio
|
|
226
|
+
pytest tests/ -v
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Test coverage includes:
|
|
230
|
+
|
|
231
|
+
- ADMIN key as super-key across endpoints with different restrictions
|
|
232
|
+
- Label-based restriction: accepts the correct label, rejects others
|
|
233
|
+
- Multiple keys sharing the same label
|
|
234
|
+
- `ALL` sentinel and its `"*"` string equivalent
|
|
235
|
+
- Valid JWT, expired JWT, JWT ignored when `"jwt"` is not enabled
|
|
236
|
+
- No credentials
|
|
237
|
+
- `AUTH_API_KEYS` validation in settings (JSON string, dict, invalid JSON)
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## Full example
|
|
242
|
+
|
|
243
|
+
```python
|
|
244
|
+
from fastapi import Depends, FastAPI
|
|
245
|
+
from fastapi_auth import AuthDependency, AuthPrincipal, PublicRoute
|
|
246
|
+
|
|
247
|
+
app = FastAPI(dependencies=[Depends(AuthDependency())]) # global: ADMIN key only
|
|
248
|
+
|
|
249
|
+
@app.get("/health", dependencies=[Depends(PublicRoute())])
|
|
250
|
+
async def health():
|
|
251
|
+
return {"status": "ok"}
|
|
252
|
+
|
|
253
|
+
@app.get("/reports", dependencies=[Depends(AuthDependency(valid_token_types={"reports"}))])
|
|
254
|
+
async def reports():
|
|
255
|
+
return {"data": "..."}
|
|
256
|
+
|
|
257
|
+
@app.get("/me")
|
|
258
|
+
async def me(
|
|
259
|
+
principal: AuthPrincipal = Depends(AuthDependency(valid_token_types={"jwt"}))
|
|
260
|
+
):
|
|
261
|
+
return {"sub": principal.sub, "payload": principal.payload}
|
|
262
|
+
|
|
263
|
+
@app.get("/internal", dependencies=[Depends(AuthDependency(valid_token_types=AuthDependency.ALL))])
|
|
264
|
+
async def internal():
|
|
265
|
+
return {"message": "any valid api-key accepted"}
|
|
266
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
README.md
|
|
4
|
+
pyproject.toml
|
|
5
|
+
src/fastapi_auth/__init__.py
|
|
6
|
+
src/fastapi_auth/dependency.py
|
|
7
|
+
src/fastapi_auth/enums.py
|
|
8
|
+
src/fastapi_auth/public.py
|
|
9
|
+
src/fastapi_auth/schemas.py
|
|
10
|
+
src/fastapi_auth/settings.py
|
|
11
|
+
src/fastapi_auth_manager_dep.egg-info/PKG-INFO
|
|
12
|
+
src/fastapi_auth_manager_dep.egg-info/SOURCES.txt
|
|
13
|
+
src/fastapi_auth_manager_dep.egg-info/dependency_links.txt
|
|
14
|
+
src/fastapi_auth_manager_dep.egg-info/requires.txt
|
|
15
|
+
src/fastapi_auth_manager_dep.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fastapi_auth
|