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.
Files changed (57) hide show
  1. fastapi_startkit_auth-0.3.0/LICENSE +21 -0
  2. fastapi_startkit_auth-0.3.0/PKG-INFO +243 -0
  3. fastapi_startkit_auth-0.3.0/pyproject.toml +107 -0
  4. fastapi_startkit_auth-0.3.0/readme.md +201 -0
  5. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/__init__.py +67 -0
  6. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/apitokens/__init__.py +13 -0
  7. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/apitokens/manager.py +122 -0
  8. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/apitokens/models.py +29 -0
  9. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/apitokens/repository.py +120 -0
  10. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/apitokens/sql.py +152 -0
  11. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/application.py +38 -0
  12. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/clients/__init__.py +4 -0
  13. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/clients/models.py +29 -0
  14. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/clients/repository.py +69 -0
  15. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/config.py +103 -0
  16. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/dependencies.py +93 -0
  17. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/exceptions.py +90 -0
  18. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/facade.py +107 -0
  19. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/grants/__init__.py +13 -0
  20. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/grants/authorization_code.py +83 -0
  21. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/grants/client_credentials.py +19 -0
  22. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/grants/password.py +32 -0
  23. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/grants/pkce.py +27 -0
  24. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/grants/refresh.py +13 -0
  25. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/guards/__init__.py +5 -0
  26. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/guards/guard.py +85 -0
  27. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/guards/session.py +81 -0
  28. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/guards/token.py +58 -0
  29. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/manager.py +320 -0
  30. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/middleware/__init__.py +4 -0
  31. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/middleware/csrf.py +160 -0
  32. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/middleware/session.py +117 -0
  33. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/passwords/__init__.py +4 -0
  34. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/passwords/broker.py +69 -0
  35. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/passwords/repository.py +44 -0
  36. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/provider.py +69 -0
  37. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/providers/__init__.py +4 -0
  38. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/providers/base.py +38 -0
  39. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/providers/memory.py +69 -0
  40. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/providers/model.py +70 -0
  41. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/publishable/cors.py +44 -0
  42. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/py.typed +0 -0
  43. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/routes.py +362 -0
  44. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/routes_spa.py +34 -0
  45. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/security/__init__.py +4 -0
  46. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/security/hashing.py +43 -0
  47. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/security/jwt.py +49 -0
  48. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/sessions/__init__.py +5 -0
  49. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/sessions/models.py +29 -0
  50. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/sessions/sql.py +158 -0
  51. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/sessions/state.py +11 -0
  52. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/sessions/store.py +100 -0
  53. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/startkit.py +38 -0
  54. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/tokens/__init__.py +9 -0
  55. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/tokens/models.py +68 -0
  56. fastapi_startkit_auth-0.3.0/src/fastapi_startkit_auth/tokens/repository.py +129 -0
  57. 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
+ ]