fastapi-startkit-auth 0.3.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_startkit_auth-0.3.0/LICENSE +21 -0
- fastapi_startkit_auth-0.3.0/PKG-INFO +243 -0
- fastapi_startkit_auth-0.3.0/pyproject.toml +107 -0
- fastapi_startkit_auth-0.3.0/readme.md +201 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/__init__.py +67 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/apitokens/__init__.py +13 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/apitokens/manager.py +122 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/apitokens/models.py +29 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/apitokens/repository.py +120 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/apitokens/sql.py +152 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/application.py +38 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/clients/__init__.py +4 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/clients/models.py +29 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/clients/repository.py +69 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/config.py +103 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/dependencies.py +93 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/exceptions.py +90 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/facade.py +107 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/grants/__init__.py +13 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/grants/authorization_code.py +83 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/grants/client_credentials.py +19 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/grants/password.py +32 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/grants/pkce.py +27 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/grants/refresh.py +13 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/guards/__init__.py +5 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/guards/guard.py +85 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/guards/session.py +81 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/guards/token.py +58 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/manager.py +320 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/middleware/__init__.py +4 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/middleware/csrf.py +160 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/middleware/session.py +117 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/passwords/__init__.py +4 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/passwords/broker.py +69 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/passwords/repository.py +44 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/provider.py +69 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/providers/__init__.py +4 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/providers/base.py +38 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/providers/memory.py +69 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/providers/model.py +70 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/publishable/cors.py +44 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/py.typed +0 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/routes.py +362 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/routes_spa.py +34 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/security/__init__.py +4 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/security/hashing.py +43 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/security/jwt.py +49 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/sessions/__init__.py +5 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/sessions/models.py +29 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/sessions/sql.py +158 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/sessions/state.py +11 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/sessions/store.py +100 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/startkit.py +38 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/tokens/__init__.py +9 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/tokens/models.py +68 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/tokens/repository.py +129 -0
- fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/tokens/service.py +189 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 FastAPI Startkit
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fastapi-startkit-auth
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Laravel-style authentication for FastAPI: OAuth2/JWT authorization server, cookie/session guard, SPA + CSRF protection, and Sanctum-style API tokens.
|
|
5
|
+
Keywords: fastapi,oauth2,jwt,auth,passport,sanctum,authentication,authorization,security,tokens,session,cookie,csrf,spa,starter-kit
|
|
6
|
+
Author: Bedram Tamang
|
|
7
|
+
Author-email: Bedram Tamang <tmgbedu@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Framework :: FastAPI
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Session
|
|
21
|
+
Classifier: Topic :: Security
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Requires-Dist: fastapi>=0.110
|
|
24
|
+
Requires-Dist: pyjwt>=2.8
|
|
25
|
+
Requires-Dist: bcrypt>=4.1
|
|
26
|
+
Requires-Dist: pydantic>=2
|
|
27
|
+
Requires-Dist: python-multipart>=0.0.9
|
|
28
|
+
Requires-Dist: masonite-orm>=2.18 ; extra == 'masoniteorm'
|
|
29
|
+
Requires-Dist: fastapi-startkit>=0.51 ; python_full_version >= '3.12' and extra == 'startkit'
|
|
30
|
+
Requires-Dist: pytest>=8 ; extra == 'test'
|
|
31
|
+
Requires-Dist: pytest-asyncio>=0.23 ; extra == 'test'
|
|
32
|
+
Requires-Dist: httpx>=0.27 ; extra == 'test'
|
|
33
|
+
Requires-Python: >=3.9
|
|
34
|
+
Project-URL: Homepage, https://github.com/fastapi-startkit/fastapi-startkit-auth
|
|
35
|
+
Project-URL: Repository, https://github.com/fastapi-startkit/fastapi-startkit-auth
|
|
36
|
+
Project-URL: Issues, https://github.com/fastapi-startkit/fastapi-startkit-auth/issues
|
|
37
|
+
Project-URL: Changelog, https://github.com/fastapi-startkit/fastapi-startkit-auth/releases
|
|
38
|
+
Provides-Extra: masoniteorm
|
|
39
|
+
Provides-Extra: startkit
|
|
40
|
+
Provides-Extra: test
|
|
41
|
+
Description-Content-Type: text/markdown
|
|
42
|
+
|
|
43
|
+
# fastapi-startkit-auth
|
|
44
|
+
|
|
45
|
+
Passport-style OAuth2 + JWT authentication for FastAPI.
|
|
46
|
+
|
|
47
|
+
`fastapi-startkit-auth` brings the ergonomics of [Laravel Passport](https://laravel.com/docs/13.x/passport)
|
|
48
|
+
to FastAPI: a config-driven guard/provider/passwords model layered on top of
|
|
49
|
+
OAuth2 grants and signed JWT access tokens (per the
|
|
50
|
+
[FastAPI security tutorial](https://fastapi.tiangolo.com/tutorial/security/oauth2-jwt/)).
|
|
51
|
+
|
|
52
|
+
## Features
|
|
53
|
+
|
|
54
|
+
| Area | What you get |
|
|
55
|
+
| --- | --- |
|
|
56
|
+
| **Config layer** | `AuthConfig` (`default` / `guards` / `providers` / `passwords`) + `AuthProvider` that registers into the app |
|
|
57
|
+
| **Password grant** | OAuth2 password grant → signed JWT access tokens with expiry (`/oauth/token`, `/token`) |
|
|
58
|
+
| **Refresh tokens** | Opaque refresh tokens with **rotation** and configurable TTL |
|
|
59
|
+
| **Personal access tokens** | Named, long-lived tokens with scopes/abilities |
|
|
60
|
+
| **Client credentials** | Machine-to-machine grant |
|
|
61
|
+
| **Authorization code + PKCE** | Browser/SPA flow with S256 & plain PKCE |
|
|
62
|
+
| **Clients** | Register / list / delete confidential & public clients |
|
|
63
|
+
| **Revocation & introspection** | RFC 7009 revoke + RFC 7662 introspect |
|
|
64
|
+
| **Password reset** | `password_reset_tokens` flow with configurable `expire` + `throttle` |
|
|
65
|
+
| **Guards & deps** | `current_user`, `optional_user`, `require_scopes` FastAPI dependencies |
|
|
66
|
+
| **Pluggable providers** | In-memory, generic ORM/masoniteorm model, or any custom `UserProvider` |
|
|
67
|
+
|
|
68
|
+
## Installation
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pip install fastapi-startkit-auth
|
|
72
|
+
# optional ORM provider driver
|
|
73
|
+
pip install "fastapi-startkit-auth[masoniteorm]"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
> Uses `bcrypt` directly (not `passlib`, which imports the `crypt` stdlib module
|
|
77
|
+
> removed in Python 3.13+), so it runs on modern Python.
|
|
78
|
+
|
|
79
|
+
## Quickstart
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from fastapi import Depends
|
|
83
|
+
from fastapi_startkit_auth import Application, AuthProvider, AuthConfig, current_user, require_scopes
|
|
84
|
+
from myapp.models import User # any active-record-style model
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class Config(AuthConfig):
|
|
88
|
+
key = "change-me-to-a-long-random-secret" # JWT signing key
|
|
89
|
+
|
|
90
|
+
default = {"guard": "api", "passwords": "users"}
|
|
91
|
+
guards = {"api": {"driver": "passport", "provider": "users"}}
|
|
92
|
+
providers = {"users": {"driver": "masoniteorm", "model": User}}
|
|
93
|
+
passwords = {
|
|
94
|
+
"users": {"provider": "users", "table": "password_reset_tokens",
|
|
95
|
+
"expire": 60, "throttle": 60},
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
app = Application([(AuthProvider, Config)])
|
|
100
|
+
api = app.api # the underlying FastAPI instance
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@api.get("/me")
|
|
104
|
+
def me(user=Depends(current_user)):
|
|
105
|
+
return user
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@api.get("/reports")
|
|
109
|
+
def reports(ctx=Depends(require_scopes("reports:read"))):
|
|
110
|
+
return {"ok": True}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Serve it:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
uvicorn myapp:app # Application is ASGI-callable
|
|
117
|
+
uvicorn myapp:app.api # or serve the FastAPI instance directly
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Configuration
|
|
121
|
+
|
|
122
|
+
`AuthConfig` mirrors Laravel's `config/auth.php` and adds JWT/token knobs:
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
class AuthConfig:
|
|
126
|
+
default = {"guard": "api", "passwords": "users"}
|
|
127
|
+
guards = {"api": {"driver": "passport", "provider": "users"}}
|
|
128
|
+
providers = {"users": {"driver": "masoniteorm", "model": User}}
|
|
129
|
+
passwords = {"users": {"provider": "users", "table": "password_reset_tokens",
|
|
130
|
+
"expire": 60, "throttle": 60}}
|
|
131
|
+
|
|
132
|
+
# token settings (all optional, sensible defaults shown)
|
|
133
|
+
key = None # JWT secret (required in production)
|
|
134
|
+
algorithm = "HS256"
|
|
135
|
+
access_token_ttl = 3600 # seconds
|
|
136
|
+
refresh_token_ttl = 60 * 60 * 24 * 14
|
|
137
|
+
personal_access_token_ttl = 60 * 60 * 24 * 365
|
|
138
|
+
authorization_code_ttl = 600
|
|
139
|
+
bcrypt_rounds = 12
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Provider drivers
|
|
143
|
+
|
|
144
|
+
| driver | meaning |
|
|
145
|
+
| --- | --- |
|
|
146
|
+
| `masoniteorm` / `orm` / `model` | wrap a model exposing `find(id)` and `where(field, value).first()` |
|
|
147
|
+
| `memory` | in-memory dict store (`users=[...]`) — great for tests/demos |
|
|
148
|
+
| `instance` | pass a ready `UserProvider` via `{"instance": ...}` |
|
|
149
|
+
| `factory` | pass a zero-arg callable returning a `UserProvider` |
|
|
150
|
+
|
|
151
|
+
Any object implementing the `UserProvider` protocol
|
|
152
|
+
(`retrieve_by_id`, `retrieve_by_credentials`, `validate_credentials`,
|
|
153
|
+
`get_identifier`, `update_password`) is a valid provider.
|
|
154
|
+
|
|
155
|
+
## HTTP endpoints
|
|
156
|
+
|
|
157
|
+
| Method & path | Purpose |
|
|
158
|
+
| --- | --- |
|
|
159
|
+
| `POST /oauth/token` | Unified token endpoint: `password`, `refresh_token`, `client_credentials`, `authorization_code` |
|
|
160
|
+
| `POST /token` | Simple password grant (FastAPI-tutorial style) |
|
|
161
|
+
| `POST /oauth/authorize` | Approve an auth-code request (requires an authenticated user) → returns `code` |
|
|
162
|
+
| `POST /oauth/introspect` | RFC 7662 token introspection (**requires client authentication**) |
|
|
163
|
+
| `POST /oauth/revoke` | RFC 7009 access/refresh token revocation (**requires client authentication**) |
|
|
164
|
+
| `POST/GET /oauth/clients`, `DELETE /oauth/clients/{id}` | Client registration & management |
|
|
165
|
+
| `POST/GET /oauth/personal-access-tokens`, `DELETE .../{jti}` | Personal access tokens |
|
|
166
|
+
| `POST /password/email` | Trigger a password-reset token (delivered out-of-band; see below) |
|
|
167
|
+
| `POST /password/reset` | Reset the password with a token |
|
|
168
|
+
|
|
169
|
+
### Password resets
|
|
170
|
+
|
|
171
|
+
`POST /password/email` always returns the same generic response whether or not
|
|
172
|
+
the account exists (no user enumeration) and **never** puts the token in the
|
|
173
|
+
response body. Configure how the token reaches the user with a notifier:
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
class AuthConfig(BaseAuthConfig):
|
|
177
|
+
password_reset_notifier = staticmethod(lambda email, token: send_email(email, token))
|
|
178
|
+
# debug_expose_reset_token = True # DEV ONLY: echo the token in the response
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
The `code_challenge` for public clients is mandatory — a public (secretless)
|
|
182
|
+
client cannot obtain an authorization code without PKCE, and codes are verified
|
|
183
|
+
against the `code_verifier` at exchange.
|
|
184
|
+
|
|
185
|
+
### Example: password grant
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
curl -X POST localhost:8000/oauth/token \
|
|
189
|
+
-d grant_type=password -d username=ada@example.com -d password=secret -d scope="read write"
|
|
190
|
+
# → { "access_token": "...", "token_type": "Bearer", "expires_in": 3600,
|
|
191
|
+
# "refresh_token": "...", "scope": "read write" }
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Example: authorization code + PKCE
|
|
195
|
+
|
|
196
|
+
1. `POST /oauth/authorize` with a bearer token and `code_challenge` → returns a single-use `code`.
|
|
197
|
+
2. `POST /oauth/token` with `grant_type=authorization_code`, the `code`, and the `code_verifier`.
|
|
198
|
+
|
|
199
|
+
## Scopes / abilities
|
|
200
|
+
|
|
201
|
+
Tokens carry scopes; enforce them with the `require_scopes` dependency:
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
require_scopes("posts:write") # must have this scope
|
|
205
|
+
require_scopes("a", "b") # must have all
|
|
206
|
+
require_scopes("a", "b", mode="any") # must have at least one
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
`*` is a wildcard scope that satisfies any check. Inside a handler you can also
|
|
210
|
+
inspect the `AuthContext` (`ctx.can(...)`, `ctx.can_any(...)`, `ctx.scopes`).
|
|
211
|
+
|
|
212
|
+
## Testing
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
pip install -e ".[test]"
|
|
216
|
+
pytest
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Or with [uv](https://docs.astral.sh/uv/):
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
uv sync --group dev
|
|
223
|
+
uv run pytest
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Releasing
|
|
227
|
+
|
|
228
|
+
Releases are cut from `main` with the release script (maintainers only):
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
./bin/release.sh # patch bump
|
|
232
|
+
./bin/release.sh minor # or: major
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
The script bumps the version, builds sdist + wheel, validates them with
|
|
236
|
+
`twine check`, uploads to PyPI, then commits, tags `vX.Y.Z`, and creates a
|
|
237
|
+
GitHub release. It requires `uv`, `gh` (authenticated), and PyPI credentials
|
|
238
|
+
for `twine` (e.g. a `pypi-*` API token via `TWINE_USERNAME=__token__` /
|
|
239
|
+
`TWINE_PASSWORD` or `~/.pypirc`).
|
|
240
|
+
|
|
241
|
+
## License
|
|
242
|
+
|
|
243
|
+
MIT
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "fastapi-startkit-auth"
|
|
3
|
+
version = "0.3.0"
|
|
4
|
+
description = "Laravel-style authentication for FastAPI: OAuth2/JWT authorization server, cookie/session guard, SPA + CSRF protection, and Sanctum-style API tokens."
|
|
5
|
+
readme = "readme.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
keywords = [
|
|
9
|
+
"fastapi",
|
|
10
|
+
"oauth2",
|
|
11
|
+
"jwt",
|
|
12
|
+
"auth",
|
|
13
|
+
"passport",
|
|
14
|
+
"sanctum",
|
|
15
|
+
"authentication",
|
|
16
|
+
"authorization",
|
|
17
|
+
"security",
|
|
18
|
+
"tokens",
|
|
19
|
+
"session",
|
|
20
|
+
"cookie",
|
|
21
|
+
"csrf",
|
|
22
|
+
"spa",
|
|
23
|
+
"starter-kit",
|
|
24
|
+
]
|
|
25
|
+
authors = [
|
|
26
|
+
{name = "Bedram Tamang", email = "tmgbedu@gmail.com"}
|
|
27
|
+
]
|
|
28
|
+
requires-python = ">=3.9"
|
|
29
|
+
classifiers = [
|
|
30
|
+
"Development Status :: 4 - Beta",
|
|
31
|
+
"Framework :: FastAPI",
|
|
32
|
+
"Intended Audience :: Developers",
|
|
33
|
+
"Operating System :: OS Independent",
|
|
34
|
+
"Programming Language :: Python :: 3",
|
|
35
|
+
"Programming Language :: Python :: 3.9",
|
|
36
|
+
"Programming Language :: Python :: 3.10",
|
|
37
|
+
"Programming Language :: Python :: 3.11",
|
|
38
|
+
"Programming Language :: Python :: 3.12",
|
|
39
|
+
"Programming Language :: Python :: 3.13",
|
|
40
|
+
"Topic :: Internet :: WWW/HTTP :: Session",
|
|
41
|
+
"Topic :: Security",
|
|
42
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
43
|
+
]
|
|
44
|
+
dependencies = [
|
|
45
|
+
"fastapi>=0.110",
|
|
46
|
+
"pyjwt>=2.8",
|
|
47
|
+
"bcrypt>=4.1",
|
|
48
|
+
"pydantic>=2",
|
|
49
|
+
"python-multipart>=0.0.9",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
[project.urls]
|
|
53
|
+
Homepage = "https://github.com/fastapi-startkit/fastapi-startkit-auth"
|
|
54
|
+
Repository = "https://github.com/fastapi-startkit/fastapi-startkit-auth"
|
|
55
|
+
Issues = "https://github.com/fastapi-startkit/fastapi-startkit-auth/issues"
|
|
56
|
+
Changelog = "https://github.com/fastapi-startkit/fastapi-startkit-auth/releases"
|
|
57
|
+
|
|
58
|
+
[project.optional-dependencies]
|
|
59
|
+
masoniteorm = ["masonite-orm>=2.18"]
|
|
60
|
+
# The framework itself only supports Python >=3.12; on older interpreters the
|
|
61
|
+
# extra resolves to nothing and the package stays standalone.
|
|
62
|
+
startkit = ["fastapi-startkit>=0.51; python_version >= '3.12'"]
|
|
63
|
+
test = [
|
|
64
|
+
"pytest>=8",
|
|
65
|
+
"pytest-asyncio>=0.23",
|
|
66
|
+
"httpx>=0.27",
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
[dependency-groups]
|
|
70
|
+
dev = [
|
|
71
|
+
"pytest>=8",
|
|
72
|
+
"pytest-asyncio>=0.23",
|
|
73
|
+
"httpx>=0.27",
|
|
74
|
+
"ruff>=0.9.0",
|
|
75
|
+
"twine>=6.0.0",
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
[tool.uv]
|
|
79
|
+
# masonite-orm pins cleo<2.0 while fastapi-startkit needs cleo>=2.1: the two
|
|
80
|
+
# extras are mutually exclusive and cannot be locked together.
|
|
81
|
+
conflicts = [
|
|
82
|
+
[
|
|
83
|
+
{ extra = "masoniteorm" },
|
|
84
|
+
{ extra = "startkit" },
|
|
85
|
+
],
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
[tool.ruff]
|
|
89
|
+
line-length = 120
|
|
90
|
+
|
|
91
|
+
[tool.ruff.lint]
|
|
92
|
+
select = ["F401"]
|
|
93
|
+
fixable = ["F401"]
|
|
94
|
+
|
|
95
|
+
[tool.ruff.lint.per-file-ignores]
|
|
96
|
+
"__init__.py" = ["F401"]
|
|
97
|
+
|
|
98
|
+
[tool.pytest.ini_options]
|
|
99
|
+
testpaths = ["tests"]
|
|
100
|
+
pythonpath = ["src"]
|
|
101
|
+
asyncio_mode = "auto"
|
|
102
|
+
addopts = "-q"
|
|
103
|
+
norecursedirs = [".venv", "dist", "build", "__pycache__"]
|
|
104
|
+
|
|
105
|
+
[build-system]
|
|
106
|
+
requires = ["uv_build>=0.10.10,<0.11.0"]
|
|
107
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# fastapi-startkit-auth
|
|
2
|
+
|
|
3
|
+
Passport-style OAuth2 + JWT authentication for FastAPI.
|
|
4
|
+
|
|
5
|
+
`fastapi-startkit-auth` brings the ergonomics of [Laravel Passport](https://laravel.com/docs/13.x/passport)
|
|
6
|
+
to FastAPI: a config-driven guard/provider/passwords model layered on top of
|
|
7
|
+
OAuth2 grants and signed JWT access tokens (per the
|
|
8
|
+
[FastAPI security tutorial](https://fastapi.tiangolo.com/tutorial/security/oauth2-jwt/)).
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
|
|
12
|
+
| Area | What you get |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| **Config layer** | `AuthConfig` (`default` / `guards` / `providers` / `passwords`) + `AuthProvider` that registers into the app |
|
|
15
|
+
| **Password grant** | OAuth2 password grant → signed JWT access tokens with expiry (`/oauth/token`, `/token`) |
|
|
16
|
+
| **Refresh tokens** | Opaque refresh tokens with **rotation** and configurable TTL |
|
|
17
|
+
| **Personal access tokens** | Named, long-lived tokens with scopes/abilities |
|
|
18
|
+
| **Client credentials** | Machine-to-machine grant |
|
|
19
|
+
| **Authorization code + PKCE** | Browser/SPA flow with S256 & plain PKCE |
|
|
20
|
+
| **Clients** | Register / list / delete confidential & public clients |
|
|
21
|
+
| **Revocation & introspection** | RFC 7009 revoke + RFC 7662 introspect |
|
|
22
|
+
| **Password reset** | `password_reset_tokens` flow with configurable `expire` + `throttle` |
|
|
23
|
+
| **Guards & deps** | `current_user`, `optional_user`, `require_scopes` FastAPI dependencies |
|
|
24
|
+
| **Pluggable providers** | In-memory, generic ORM/masoniteorm model, or any custom `UserProvider` |
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install fastapi-startkit-auth
|
|
30
|
+
# optional ORM provider driver
|
|
31
|
+
pip install "fastapi-startkit-auth[masoniteorm]"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
> Uses `bcrypt` directly (not `passlib`, which imports the `crypt` stdlib module
|
|
35
|
+
> removed in Python 3.13+), so it runs on modern Python.
|
|
36
|
+
|
|
37
|
+
## Quickstart
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from fastapi import Depends
|
|
41
|
+
from fastapi_startkit_auth import Application, AuthProvider, AuthConfig, current_user, require_scopes
|
|
42
|
+
from myapp.models import User # any active-record-style model
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Config(AuthConfig):
|
|
46
|
+
key = "change-me-to-a-long-random-secret" # JWT signing key
|
|
47
|
+
|
|
48
|
+
default = {"guard": "api", "passwords": "users"}
|
|
49
|
+
guards = {"api": {"driver": "passport", "provider": "users"}}
|
|
50
|
+
providers = {"users": {"driver": "masoniteorm", "model": User}}
|
|
51
|
+
passwords = {
|
|
52
|
+
"users": {"provider": "users", "table": "password_reset_tokens",
|
|
53
|
+
"expire": 60, "throttle": 60},
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
app = Application([(AuthProvider, Config)])
|
|
58
|
+
api = app.api # the underlying FastAPI instance
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@api.get("/me")
|
|
62
|
+
def me(user=Depends(current_user)):
|
|
63
|
+
return user
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@api.get("/reports")
|
|
67
|
+
def reports(ctx=Depends(require_scopes("reports:read"))):
|
|
68
|
+
return {"ok": True}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Serve it:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
uvicorn myapp:app # Application is ASGI-callable
|
|
75
|
+
uvicorn myapp:app.api # or serve the FastAPI instance directly
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Configuration
|
|
79
|
+
|
|
80
|
+
`AuthConfig` mirrors Laravel's `config/auth.php` and adds JWT/token knobs:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
class AuthConfig:
|
|
84
|
+
default = {"guard": "api", "passwords": "users"}
|
|
85
|
+
guards = {"api": {"driver": "passport", "provider": "users"}}
|
|
86
|
+
providers = {"users": {"driver": "masoniteorm", "model": User}}
|
|
87
|
+
passwords = {"users": {"provider": "users", "table": "password_reset_tokens",
|
|
88
|
+
"expire": 60, "throttle": 60}}
|
|
89
|
+
|
|
90
|
+
# token settings (all optional, sensible defaults shown)
|
|
91
|
+
key = None # JWT secret (required in production)
|
|
92
|
+
algorithm = "HS256"
|
|
93
|
+
access_token_ttl = 3600 # seconds
|
|
94
|
+
refresh_token_ttl = 60 * 60 * 24 * 14
|
|
95
|
+
personal_access_token_ttl = 60 * 60 * 24 * 365
|
|
96
|
+
authorization_code_ttl = 600
|
|
97
|
+
bcrypt_rounds = 12
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Provider drivers
|
|
101
|
+
|
|
102
|
+
| driver | meaning |
|
|
103
|
+
| --- | --- |
|
|
104
|
+
| `masoniteorm` / `orm` / `model` | wrap a model exposing `find(id)` and `where(field, value).first()` |
|
|
105
|
+
| `memory` | in-memory dict store (`users=[...]`) — great for tests/demos |
|
|
106
|
+
| `instance` | pass a ready `UserProvider` via `{"instance": ...}` |
|
|
107
|
+
| `factory` | pass a zero-arg callable returning a `UserProvider` |
|
|
108
|
+
|
|
109
|
+
Any object implementing the `UserProvider` protocol
|
|
110
|
+
(`retrieve_by_id`, `retrieve_by_credentials`, `validate_credentials`,
|
|
111
|
+
`get_identifier`, `update_password`) is a valid provider.
|
|
112
|
+
|
|
113
|
+
## HTTP endpoints
|
|
114
|
+
|
|
115
|
+
| Method & path | Purpose |
|
|
116
|
+
| --- | --- |
|
|
117
|
+
| `POST /oauth/token` | Unified token endpoint: `password`, `refresh_token`, `client_credentials`, `authorization_code` |
|
|
118
|
+
| `POST /token` | Simple password grant (FastAPI-tutorial style) |
|
|
119
|
+
| `POST /oauth/authorize` | Approve an auth-code request (requires an authenticated user) → returns `code` |
|
|
120
|
+
| `POST /oauth/introspect` | RFC 7662 token introspection (**requires client authentication**) |
|
|
121
|
+
| `POST /oauth/revoke` | RFC 7009 access/refresh token revocation (**requires client authentication**) |
|
|
122
|
+
| `POST/GET /oauth/clients`, `DELETE /oauth/clients/{id}` | Client registration & management |
|
|
123
|
+
| `POST/GET /oauth/personal-access-tokens`, `DELETE .../{jti}` | Personal access tokens |
|
|
124
|
+
| `POST /password/email` | Trigger a password-reset token (delivered out-of-band; see below) |
|
|
125
|
+
| `POST /password/reset` | Reset the password with a token |
|
|
126
|
+
|
|
127
|
+
### Password resets
|
|
128
|
+
|
|
129
|
+
`POST /password/email` always returns the same generic response whether or not
|
|
130
|
+
the account exists (no user enumeration) and **never** puts the token in the
|
|
131
|
+
response body. Configure how the token reaches the user with a notifier:
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
class AuthConfig(BaseAuthConfig):
|
|
135
|
+
password_reset_notifier = staticmethod(lambda email, token: send_email(email, token))
|
|
136
|
+
# debug_expose_reset_token = True # DEV ONLY: echo the token in the response
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The `code_challenge` for public clients is mandatory — a public (secretless)
|
|
140
|
+
client cannot obtain an authorization code without PKCE, and codes are verified
|
|
141
|
+
against the `code_verifier` at exchange.
|
|
142
|
+
|
|
143
|
+
### Example: password grant
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
curl -X POST localhost:8000/oauth/token \
|
|
147
|
+
-d grant_type=password -d username=ada@example.com -d password=secret -d scope="read write"
|
|
148
|
+
# → { "access_token": "...", "token_type": "Bearer", "expires_in": 3600,
|
|
149
|
+
# "refresh_token": "...", "scope": "read write" }
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Example: authorization code + PKCE
|
|
153
|
+
|
|
154
|
+
1. `POST /oauth/authorize` with a bearer token and `code_challenge` → returns a single-use `code`.
|
|
155
|
+
2. `POST /oauth/token` with `grant_type=authorization_code`, the `code`, and the `code_verifier`.
|
|
156
|
+
|
|
157
|
+
## Scopes / abilities
|
|
158
|
+
|
|
159
|
+
Tokens carry scopes; enforce them with the `require_scopes` dependency:
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
require_scopes("posts:write") # must have this scope
|
|
163
|
+
require_scopes("a", "b") # must have all
|
|
164
|
+
require_scopes("a", "b", mode="any") # must have at least one
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
`*` is a wildcard scope that satisfies any check. Inside a handler you can also
|
|
168
|
+
inspect the `AuthContext` (`ctx.can(...)`, `ctx.can_any(...)`, `ctx.scopes`).
|
|
169
|
+
|
|
170
|
+
## Testing
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
pip install -e ".[test]"
|
|
174
|
+
pytest
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Or with [uv](https://docs.astral.sh/uv/):
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
uv sync --group dev
|
|
181
|
+
uv run pytest
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Releasing
|
|
185
|
+
|
|
186
|
+
Releases are cut from `main` with the release script (maintainers only):
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
./bin/release.sh # patch bump
|
|
190
|
+
./bin/release.sh minor # or: major
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
The script bumps the version, builds sdist + wheel, validates them with
|
|
194
|
+
`twine check`, uploads to PyPI, then commits, tags `vX.Y.Z`, and creates a
|
|
195
|
+
GitHub release. It requires `uv`, `gh` (authenticated), and PyPI credentials
|
|
196
|
+
for `twine` (e.g. a `pypi-*` API token via `TWINE_USERNAME=__token__` /
|
|
197
|
+
`TWINE_PASSWORD` or `~/.pypirc`).
|
|
198
|
+
|
|
199
|
+
## License
|
|
200
|
+
|
|
201
|
+
MIT
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Passport-style OAuth2 + JWT authentication for FastAPI.
|
|
2
|
+
|
|
3
|
+
Public API mirrors the config-driven guard/provider/passwords model of Laravel
|
|
4
|
+
Passport while staying idiomatic FastAPI:
|
|
5
|
+
|
|
6
|
+
from fastapi_startkit_auth import Application, AuthProvider, AuthConfig
|
|
7
|
+
|
|
8
|
+
Attributes are resolved lazily so importing a single submodule never forces the
|
|
9
|
+
whole package to load.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import importlib
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
__version__ = "0.2.0"
|
|
17
|
+
|
|
18
|
+
_EXPORTS = {
|
|
19
|
+
"AuthConfig": ("fastapi_startkit_auth.config", "AuthConfig"),
|
|
20
|
+
"AuthProvider": ("fastapi_startkit_auth.provider", "AuthProvider"),
|
|
21
|
+
"Application": ("fastapi_startkit_auth.application", "Application"),
|
|
22
|
+
"AuthManager": ("fastapi_startkit_auth.manager", "AuthManager"),
|
|
23
|
+
"current_user": ("fastapi_startkit_auth.dependencies", "current_user"),
|
|
24
|
+
"optional_user": ("fastapi_startkit_auth.dependencies", "optional_user"),
|
|
25
|
+
"require_scopes": ("fastapi_startkit_auth.dependencies", "require_scopes"),
|
|
26
|
+
"require_abilities": ("fastapi_startkit_auth.dependencies", "require_abilities"),
|
|
27
|
+
"AuthError": ("fastapi_startkit_auth.exceptions", "AuthError"),
|
|
28
|
+
"InvalidGrant": ("fastapi_startkit_auth.exceptions", "InvalidGrant"),
|
|
29
|
+
"InvalidClient": ("fastapi_startkit_auth.exceptions", "InvalidClient"),
|
|
30
|
+
"InvalidToken": ("fastapi_startkit_auth.exceptions", "InvalidToken"),
|
|
31
|
+
"InsufficientScope": ("fastapi_startkit_auth.exceptions", "InsufficientScope"),
|
|
32
|
+
"InvalidSession": ("fastapi_startkit_auth.exceptions", "InvalidSession"),
|
|
33
|
+
"CsrfTokenMismatch": ("fastapi_startkit_auth.exceptions", "CsrfTokenMismatch"),
|
|
34
|
+
"Auth": ("fastapi_startkit_auth.facade", "Auth"),
|
|
35
|
+
"SessionGuard": ("fastapi_startkit_auth.guards.session", "SessionGuard"),
|
|
36
|
+
"SessionMiddleware": ("fastapi_startkit_auth.middleware.session", "SessionMiddleware"),
|
|
37
|
+
"CsrfMiddleware": ("fastapi_startkit_auth.middleware.csrf", "CsrfMiddleware"),
|
|
38
|
+
"AuthServiceProvider": ("fastapi_startkit_auth.startkit", "AuthServiceProvider"),
|
|
39
|
+
"SessionRecord": ("fastapi_startkit_auth.sessions.models", "SessionRecord"),
|
|
40
|
+
"SessionStore": ("fastapi_startkit_auth.sessions.store", "SessionStore"),
|
|
41
|
+
"InMemorySessionStore": ("fastapi_startkit_auth.sessions.store", "InMemorySessionStore"),
|
|
42
|
+
"SqlSessionStore": ("fastapi_startkit_auth.sessions.sql", "SqlSessionStore"),
|
|
43
|
+
"TokenGuard": ("fastapi_startkit_auth.guards.token", "TokenGuard"),
|
|
44
|
+
"ApiTokenManager": ("fastapi_startkit_auth.apitokens.manager", "ApiTokenManager"),
|
|
45
|
+
"NewApiToken": ("fastapi_startkit_auth.apitokens.manager", "NewApiToken"),
|
|
46
|
+
"ApiTokenRecord": ("fastapi_startkit_auth.apitokens.models", "ApiTokenRecord"),
|
|
47
|
+
"ApiTokenRepository": ("fastapi_startkit_auth.apitokens.repository", "ApiTokenRepository"),
|
|
48
|
+
"InMemoryApiTokenRepository": (
|
|
49
|
+
"fastapi_startkit_auth.apitokens.repository",
|
|
50
|
+
"InMemoryApiTokenRepository",
|
|
51
|
+
),
|
|
52
|
+
"SqlApiTokenRepository": ("fastapi_startkit_auth.apitokens.sql", "SqlApiTokenRepository"),
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
__all__ = list(_EXPORTS)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def __getattr__(name: str) -> Any:
|
|
59
|
+
try:
|
|
60
|
+
module_name, attr = _EXPORTS[name]
|
|
61
|
+
except KeyError:
|
|
62
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from None
|
|
63
|
+
return getattr(importlib.import_module(module_name), attr)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def __dir__() -> list[str]:
|
|
67
|
+
return sorted(__all__)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from .manager import ApiTokenManager, NewApiToken
|
|
2
|
+
from .models import ApiTokenRecord
|
|
3
|
+
from .repository import ApiTokenRepository, InMemoryApiTokenRepository
|
|
4
|
+
from .sql import SqlApiTokenRepository
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"ApiTokenManager",
|
|
8
|
+
"ApiTokenRecord",
|
|
9
|
+
"ApiTokenRepository",
|
|
10
|
+
"InMemoryApiTokenRepository",
|
|
11
|
+
"NewApiToken",
|
|
12
|
+
"SqlApiTokenRepository",
|
|
13
|
+
]
|