fastapi-fullauth 0.1.0__tar.gz → 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_fullauth-0.3.0/CHANGELOG.md +87 -0
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/PKG-INFO +72 -34
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/README.md +71 -10
- fastapi_fullauth-0.3.0/examples/memory_app/auth.py +8 -0
- fastapi_fullauth-0.3.0/examples/memory_app/main.py +13 -0
- fastapi_fullauth-0.3.0/examples/memory_app/routes.py +15 -0
- fastapi_fullauth-0.3.0/examples/sqlmodel_app/auth.py +28 -0
- fastapi_fullauth-0.3.0/examples/sqlmodel_app/config.py +5 -0
- fastapi_fullauth-0.3.0/examples/sqlmodel_app/main.py +26 -0
- fastapi_fullauth-0.3.0/examples/sqlmodel_app/models.py +18 -0
- fastapi_fullauth-0.3.0/examples/sqlmodel_app/routes.py +15 -0
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/__init__.py +3 -3
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/adapters/base.py +11 -0
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/adapters/memory.py +9 -1
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/adapters/sqlalchemy/__init__.py +2 -2
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/adapters/sqlalchemy/adapter.py +30 -15
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/adapters/sqlalchemy/models.py +13 -16
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/adapters/sqlmodel/__init__.py +0 -2
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/adapters/sqlmodel/adapter.py +23 -8
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/adapters/sqlmodel/models.py +7 -23
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/config.py +2 -0
- fastapi_fullauth-0.3.0/fastapi_fullauth/core/crypto.py +47 -0
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/core/tokens.py +20 -9
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/dependencies/__init__.py +6 -7
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/dependencies/current_user.py +32 -28
- fastapi_fullauth-0.3.0/fastapi_fullauth/dependencies/require_role.py +33 -0
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/flows/login.py +22 -18
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/flows/password_reset.py +3 -0
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/flows/register.py +8 -0
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/fullauth.py +58 -23
- fastapi_fullauth-0.3.0/fastapi_fullauth/hooks.py +50 -0
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/protection/ratelimit.py +8 -4
- fastapi_fullauth-0.3.0/fastapi_fullauth/router/__init__.py +0 -0
- fastapi_fullauth-0.3.0/fastapi_fullauth/router/auth.py +505 -0
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/types.py +21 -16
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/pyproject.toml +1 -17
- fastapi_fullauth-0.3.0/tests/__init__.py +0 -0
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/tests/conftest.py +2 -2
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/tests/test_auth_flows.py +5 -5
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/tests/test_customization.py +7 -10
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/tests/test_dx_improvements.py +6 -16
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/tests/test_email_verify.py +3 -6
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/tests/test_new_endpoints.py +5 -5
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/tests/test_refresh_tokens.py +7 -7
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/tests/test_roles.py +2 -2
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/tests/test_tokens.py +7 -5
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/uv.lock +2 -379
- fastapi_fullauth-0.1.0/CHANGELOG.md +0 -38
- fastapi_fullauth-0.1.0/examples/memory_app.py +0 -62
- fastapi_fullauth-0.1.0/examples/sqlalchemy_app.py +0 -86
- fastapi_fullauth-0.1.0/examples/sqlmodel_app.py +0 -91
- fastapi_fullauth-0.1.0/fastapi_fullauth/core/crypto.py +0 -19
- fastapi_fullauth-0.1.0/fastapi_fullauth/dependencies/require_role.py +0 -78
- fastapi_fullauth-0.1.0/fastapi_fullauth/hooks.py +0 -16
- fastapi_fullauth-0.1.0/fastapi_fullauth/router/auth.py +0 -477
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/.github/workflows/ci.yml +0 -0
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/.github/workflows/publish.yml +0 -0
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/.gitignore +0 -0
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/.python-version +0 -0
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/LICENSE +0 -0
- {fastapi_fullauth-0.1.0/fastapi_fullauth/rbac → fastapi_fullauth-0.3.0/examples/memory_app}/__init__.py +0 -0
- {fastapi_fullauth-0.1.0/fastapi_fullauth/router → fastapi_fullauth-0.3.0/examples/sqlmodel_app}/__init__.py +0 -0
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/adapters/__init__.py +0 -0
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/backends/__init__.py +0 -0
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/backends/base.py +0 -0
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/backends/bearer.py +0 -0
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/backends/cookie.py +0 -0
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/core/__init__.py +0 -0
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/core/redis_blacklist.py +0 -0
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/exceptions.py +0 -0
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/flows/__init__.py +0 -0
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/flows/email_verify.py +0 -0
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/flows/logout.py +0 -0
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/middleware/__init__.py +0 -0
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/middleware/csrf.py +0 -0
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/middleware/ratelimit.py +0 -0
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/middleware/security_headers.py +0 -0
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/migrations/__init__.py +0 -0
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/migrations/helpers.py +0 -0
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/protection/__init__.py +0 -0
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/protection/lockout.py +0 -0
- {fastapi_fullauth-0.1.0/tests → fastapi_fullauth-0.3.0/fastapi_fullauth/rbac}/__init__.py +0 -0
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/utils.py +0 -0
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/validators.py +0 -0
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/tests/test_crypto.py +0 -0
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/tests/test_lockout.py +0 -0
- {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/tests/test_middleware.py +0 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Breaking changes
|
|
6
|
+
|
|
7
|
+
- **`create_refresh_token` returns `RefreshTokenMeta`** — previously returned a plain `str`. Now returns a `NamedTuple` with `.token`, `.expires_at`, `.family_id`. Callers that used the raw string must access `.token`.
|
|
8
|
+
- **`create_token_pair` returns `tuple[str, RefreshTokenMeta]`** — second element is now `RefreshTokenMeta` instead of `str`.
|
|
9
|
+
- **`revoke_all_user_refresh_tokens` is now required** on custom adapters — new abstract method on `AbstractUserAdapter`.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `current_superuser` dependency and `SuperUser` annotated type
|
|
14
|
+
- `CurrentUser`, `VerifiedUser`, `SuperUser` annotated types in `dependencies.current_user` for cleaner route signatures
|
|
15
|
+
- `RefreshTokenMeta` named tuple — avoids decoding freshly created tokens just to read `expires_at` and `family_id`
|
|
16
|
+
- `FullAuth.get_custom_claims(user)` — moved custom claims logic from router into the class, with validation against reserved JWT keys (`sub`, `exp`, `type`, etc.)
|
|
17
|
+
- `revoke_all_user_refresh_tokens(user_id)` on all adapters — bulk session revocation
|
|
18
|
+
- Session revocation on password reset, password change, and account deletion
|
|
19
|
+
- `configure_hasher()` — wires `PASSWORD_HASH_ALGORITHM` config to the actual hasher; supports `argon2id` and `bcrypt`
|
|
20
|
+
- Automatic password rehash on login when hash algorithm or params have changed
|
|
21
|
+
- Register now checks uniqueness on `login_field` (not just email) when `login_field != "email"`
|
|
22
|
+
- `InMemoryBlacklist` now respects `ttl_seconds` — expired entries are evicted on lookup
|
|
23
|
+
- `RateLimiter` evicts keys with empty timestamp lists to prevent unbounded dict growth
|
|
24
|
+
- `description` parameter on all route decorators for Swagger docs
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- `current_active_verified_user` was missing `payload.type != "access"` check — refresh tokens could pass through
|
|
29
|
+
- Purpose tokens (password reset, email verify) could be used as regular access tokens — `current_user` now rejects tokens with `extra.purpose`
|
|
30
|
+
- Duplicate token decode + user lookup across dependencies, router endpoints, and admin routes — consolidated into reusable `current_user` dependency chain
|
|
31
|
+
- Duplicate `roles` + `extra_claims` fetch in refresh route — pulled above the if/else branch
|
|
32
|
+
- Login flow fetched the user from DB twice (once in router, once in `login()`) — now accepts pre-fetched user
|
|
33
|
+
- Unused `request: Request` parameters in dependencies and routes
|
|
34
|
+
- Removed duplicate docstrings on routes (kept `description=` on decorators)
|
|
35
|
+
- `require_permission` was a full copy of `require_role` — now delegates to it
|
|
36
|
+
|
|
37
|
+
### Internal
|
|
38
|
+
|
|
39
|
+
- Route order follows auth lifecycle: register → login → refresh → logout → user → email/password → admin
|
|
40
|
+
- `require_role` / `require_permission` use `Depends(current_user)` instead of duplicating token logic
|
|
41
|
+
- Removed `_get_custom_claims` module-level function from router
|
|
42
|
+
|
|
43
|
+
## 0.2.0
|
|
44
|
+
|
|
45
|
+
### Breaking changes
|
|
46
|
+
|
|
47
|
+
- **JSON login** — `POST /login` now accepts `{"email": "...", "password": "..."}` instead of form data. Swagger auth uses bearer token input instead of username/password form.
|
|
48
|
+
- **No default User model** — SQLModel and SQLAlchemy adapters no longer ship a concrete `User`/`UserModel` table class. Users must define their own model from `UserBase`. This eliminates relationship conflicts when subclassing.
|
|
49
|
+
- **`user_model` is required** — `SQLModelAdapter(session_maker, user_model=MyUser)` — no default.
|
|
50
|
+
- **Removed `min_length=8`** from `CreateUserSchema` — password length is now fully controlled by `PasswordValidator` and `PASSWORD_MIN_LENGTH` config.
|
|
51
|
+
- **`SQLAlchemyAdapter` renamed `UserModel` to `UserBase`** — import `UserBase` instead.
|
|
52
|
+
|
|
53
|
+
### Added
|
|
54
|
+
|
|
55
|
+
- `POST /auth/change-password` — verifies current password, validates new
|
|
56
|
+
- `PATCH /auth/me` — update profile with protected field filtering
|
|
57
|
+
- `DELETE /auth/me` — self-deletion
|
|
58
|
+
- `expires_in` in login/refresh responses
|
|
59
|
+
- Per-IP auth rate limiting on login, register, password-reset (`AUTH_RATE_LIMIT_*` config)
|
|
60
|
+
- `LOGIN_FIELD` config — login by email, username, phone, or any model field
|
|
61
|
+
- `get_user_by_field()` on all adapters for generic field lookups
|
|
62
|
+
- Structured example apps (`examples/memory_app/`, `examples/sqlmodel_app/`)
|
|
63
|
+
|
|
64
|
+
### Fixed
|
|
65
|
+
|
|
66
|
+
- `InMemoryAdapter.update_user` returning base `UserSchema` instead of custom schema
|
|
67
|
+
- Stale `User.id` / `UserModel` references in adapter queries after model removal
|
|
68
|
+
- Parameter ordering in adapter constructors (required params before optional)
|
|
69
|
+
|
|
70
|
+
## 0.1.0
|
|
71
|
+
|
|
72
|
+
Initial release.
|
|
73
|
+
|
|
74
|
+
- JWT access/refresh tokens with rotation and blacklisting
|
|
75
|
+
- Argon2id password hashing
|
|
76
|
+
- Auth flows: register, login, logout, password reset, email verification
|
|
77
|
+
- Brute-force lockout, per-IP rate limiting, CSRF, security headers
|
|
78
|
+
- Bearer and cookie backends
|
|
79
|
+
- SQLAlchemy, SQLModel, and InMemory adapters
|
|
80
|
+
- Redis blacklist backend
|
|
81
|
+
- Refresh token persistence with family tracking and reuse detection
|
|
82
|
+
- Flat config (`secret_key=...`) or full `FullAuthConfig` object
|
|
83
|
+
- Auto-derive schemas from ORM model fields
|
|
84
|
+
- Auto-wire middleware from config flags
|
|
85
|
+
- Route enum, event hooks, email hooks
|
|
86
|
+
- `current_user`, `current_active_verified_user`, `require_role` dependencies
|
|
87
|
+
- 97 tests
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-fullauth
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Production-grade, async-native authentication and authorization library for FastAPI
|
|
5
5
|
Project-URL: Homepage, https://github.com/mdfarhankc/fastapi-fullauth
|
|
6
6
|
Project-URL: Documentation, https://github.com/mdfarhankc/fastapi-fullauth
|
|
@@ -25,29 +25,12 @@ Requires-Dist: fastapi>=0.110
|
|
|
25
25
|
Requires-Dist: pydantic-settings>=2.0
|
|
26
26
|
Requires-Dist: pydantic[email]>=2.0
|
|
27
27
|
Requires-Dist: pyjwt>=2.8
|
|
28
|
-
Requires-Dist: python-multipart>=0.0.22
|
|
29
28
|
Requires-Dist: uuid-utils>=0.14.1
|
|
30
29
|
Provides-Extra: all
|
|
31
30
|
Requires-Dist: alembic>=1.13; extra == 'all'
|
|
32
|
-
Requires-Dist: beanie>=1.25; extra == 'all'
|
|
33
|
-
Requires-Dist: httpx-oauth>=0.13; extra == 'all'
|
|
34
|
-
Requires-Dist: itsdangerous>=2.1; extra == 'all'
|
|
35
|
-
Requires-Dist: pyotp>=2.9; extra == 'all'
|
|
36
|
-
Requires-Dist: qrcode>=7.4; extra == 'all'
|
|
37
31
|
Requires-Dist: redis>=5.0; extra == 'all'
|
|
38
32
|
Requires-Dist: sqlalchemy[asyncio]>=2.0; extra == 'all'
|
|
39
33
|
Requires-Dist: sqlmodel>=0.0.16; extra == 'all'
|
|
40
|
-
Requires-Dist: tortoise-orm>=0.21; extra == 'all'
|
|
41
|
-
Requires-Dist: webauthn>=2.0; extra == 'all'
|
|
42
|
-
Provides-Extra: audit
|
|
43
|
-
Provides-Extra: beanie
|
|
44
|
-
Requires-Dist: beanie>=1.25; extra == 'beanie'
|
|
45
|
-
Provides-Extra: magic-link
|
|
46
|
-
Requires-Dist: itsdangerous>=2.1; extra == 'magic-link'
|
|
47
|
-
Provides-Extra: oauth
|
|
48
|
-
Requires-Dist: httpx-oauth>=0.13; extra == 'oauth'
|
|
49
|
-
Provides-Extra: passkeys
|
|
50
|
-
Requires-Dist: webauthn>=2.0; extra == 'passkeys'
|
|
51
34
|
Provides-Extra: redis
|
|
52
35
|
Requires-Dist: redis>=5.0; extra == 'redis'
|
|
53
36
|
Provides-Extra: sqlalchemy
|
|
@@ -56,12 +39,6 @@ Requires-Dist: sqlalchemy[asyncio]>=2.0; extra == 'sqlalchemy'
|
|
|
56
39
|
Provides-Extra: sqlmodel
|
|
57
40
|
Requires-Dist: alembic>=1.13; extra == 'sqlmodel'
|
|
58
41
|
Requires-Dist: sqlmodel>=0.0.16; extra == 'sqlmodel'
|
|
59
|
-
Provides-Extra: tenants
|
|
60
|
-
Provides-Extra: tortoise
|
|
61
|
-
Requires-Dist: tortoise-orm>=0.21; extra == 'tortoise'
|
|
62
|
-
Provides-Extra: totp
|
|
63
|
-
Requires-Dist: pyotp>=2.9; extra == 'totp'
|
|
64
|
-
Requires-Dist: qrcode>=7.4; extra == 'totp'
|
|
65
42
|
Description-Content-Type: text/markdown
|
|
66
43
|
|
|
67
44
|
# fastapi-fullauth
|
|
@@ -114,13 +91,12 @@ from sqlmodel import Field, Relationship
|
|
|
114
91
|
|
|
115
92
|
class MyUser(UserBase, table=True):
|
|
116
93
|
__tablename__ = "fullauth_users"
|
|
117
|
-
__table_args__ = {"extend_existing": True}
|
|
118
94
|
|
|
119
95
|
display_name: str = Field(default="", max_length=100)
|
|
120
96
|
phone: str = Field(default="", max_length=20)
|
|
121
97
|
|
|
122
|
-
roles: list[Role] = Relationship(
|
|
123
|
-
refresh_tokens: list[RefreshTokenRecord] = Relationship(
|
|
98
|
+
roles: list[Role] = Relationship(link_model=UserRoleLink)
|
|
99
|
+
refresh_tokens: list[RefreshTokenRecord] = Relationship()
|
|
124
100
|
|
|
125
101
|
fullauth = FullAuth(
|
|
126
102
|
secret_key="...",
|
|
@@ -132,16 +108,31 @@ No need to create separate schema classes or subclass the adapter. Registration
|
|
|
132
108
|
|
|
133
109
|
## Protected routes
|
|
134
110
|
|
|
111
|
+
Use the `Annotated` types for clean route signatures:
|
|
112
|
+
|
|
135
113
|
```python
|
|
136
|
-
from
|
|
137
|
-
from fastapi_fullauth.dependencies import current_user, require_role
|
|
114
|
+
from fastapi_fullauth.dependencies import CurrentUser, VerifiedUser, SuperUser, require_role
|
|
138
115
|
|
|
139
116
|
@app.get("/profile")
|
|
140
|
-
async def profile(user
|
|
117
|
+
async def profile(user: CurrentUser):
|
|
141
118
|
return user
|
|
142
119
|
|
|
120
|
+
@app.get("/dashboard")
|
|
121
|
+
async def dashboard(user: VerifiedUser):
|
|
122
|
+
# only email-verified users
|
|
123
|
+
return {"email": user.email}
|
|
124
|
+
|
|
143
125
|
@app.delete("/admin/users/{id}")
|
|
144
|
-
async def delete_user(user
|
|
126
|
+
async def delete_user(user: SuperUser):
|
|
127
|
+
# only superusers
|
|
128
|
+
...
|
|
129
|
+
|
|
130
|
+
# or use require_role for custom roles
|
|
131
|
+
from fastapi import Depends
|
|
132
|
+
from fastapi_fullauth.dependencies import require_role
|
|
133
|
+
|
|
134
|
+
@app.get("/editor")
|
|
135
|
+
async def editor_panel(user=Depends(require_role("editor"))):
|
|
145
136
|
...
|
|
146
137
|
```
|
|
147
138
|
|
|
@@ -191,15 +182,44 @@ fullauth.hooks.on("after_register", welcome)
|
|
|
191
182
|
|
|
192
183
|
Events: `after_register`, `after_login`, `after_logout`, `after_password_change`, `after_password_reset`, `after_email_verify`, `send_verification_email`, `send_password_reset_email`
|
|
193
184
|
|
|
194
|
-
##
|
|
185
|
+
## Custom token claims
|
|
186
|
+
|
|
187
|
+
Embed app-specific data into JWTs (available in `payload.extra`):
|
|
195
188
|
|
|
196
189
|
```python
|
|
197
|
-
|
|
190
|
+
async def add_claims(user):
|
|
191
|
+
return {"tenant_id": "acme", "plan": "pro"}
|
|
198
192
|
|
|
199
193
|
fullauth = FullAuth(
|
|
200
194
|
secret_key="...",
|
|
201
195
|
adapter=adapter,
|
|
202
|
-
|
|
196
|
+
on_create_token_claims=add_claims,
|
|
197
|
+
)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Reserved keys (`sub`, `exp`, `iat`, `jti`, `type`, `roles`, `extra`, `family_id`) are rejected to prevent accidental overwrites.
|
|
201
|
+
|
|
202
|
+
## Password hashing
|
|
203
|
+
|
|
204
|
+
Argon2id by default. Switch to bcrypt via config:
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
fullauth = FullAuth(
|
|
208
|
+
secret_key="...",
|
|
209
|
+
adapter=adapter,
|
|
210
|
+
password_hash_algorithm="bcrypt", # requires: pip install bcrypt
|
|
211
|
+
)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
When switching algorithms, existing users are transparently rehashed on their next login.
|
|
215
|
+
|
|
216
|
+
## Route control
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
fullauth = FullAuth(
|
|
220
|
+
secret_key="...",
|
|
221
|
+
adapter=adapter,
|
|
222
|
+
enabled_routes=["login", "logout", "refresh"],
|
|
203
223
|
)
|
|
204
224
|
```
|
|
205
225
|
|
|
@@ -211,6 +231,20 @@ SecurityHeaders, CSRF, and rate limiting are auto-wired from config flags. Pass
|
|
|
211
231
|
|
|
212
232
|
Login, register, and password-reset have per-IP rate limits enabled by default (5/3/3 per minute). Configure via `AUTH_RATE_LIMIT_*` settings.
|
|
213
233
|
|
|
234
|
+
## Login field
|
|
235
|
+
|
|
236
|
+
By default, login uses `email`. Change it to any field on your user model:
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
# username login: POST /login {"username": "john", "password": "..."}
|
|
240
|
+
fullauth = FullAuth(secret_key="...", adapter=adapter, login_field="username")
|
|
241
|
+
|
|
242
|
+
# phone login: POST /login {"phone": "+1234567890", "password": "..."}
|
|
243
|
+
fullauth = FullAuth(secret_key="...", adapter=adapter, login_field="phone")
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
The Swagger UI and request body update automatically. The adapter looks up users by that field.
|
|
247
|
+
|
|
214
248
|
## Development
|
|
215
249
|
|
|
216
250
|
```bash
|
|
@@ -218,6 +252,10 @@ git clone https://github.com/mdfarhankc/fastapi-fullauth.git
|
|
|
218
252
|
cd fastapi-fullauth
|
|
219
253
|
uv sync --dev --extra sqlalchemy --extra sqlmodel
|
|
220
254
|
uv run pytest tests/ -v
|
|
255
|
+
|
|
256
|
+
# run examples
|
|
257
|
+
uv run uvicorn examples.memory_app.main:app --reload
|
|
258
|
+
uv run uvicorn examples.sqlmodel_app.main:app --reload
|
|
221
259
|
```
|
|
222
260
|
|
|
223
261
|
## License
|
|
@@ -48,13 +48,12 @@ from sqlmodel import Field, Relationship
|
|
|
48
48
|
|
|
49
49
|
class MyUser(UserBase, table=True):
|
|
50
50
|
__tablename__ = "fullauth_users"
|
|
51
|
-
__table_args__ = {"extend_existing": True}
|
|
52
51
|
|
|
53
52
|
display_name: str = Field(default="", max_length=100)
|
|
54
53
|
phone: str = Field(default="", max_length=20)
|
|
55
54
|
|
|
56
|
-
roles: list[Role] = Relationship(
|
|
57
|
-
refresh_tokens: list[RefreshTokenRecord] = Relationship(
|
|
55
|
+
roles: list[Role] = Relationship(link_model=UserRoleLink)
|
|
56
|
+
refresh_tokens: list[RefreshTokenRecord] = Relationship()
|
|
58
57
|
|
|
59
58
|
fullauth = FullAuth(
|
|
60
59
|
secret_key="...",
|
|
@@ -66,16 +65,31 @@ No need to create separate schema classes or subclass the adapter. Registration
|
|
|
66
65
|
|
|
67
66
|
## Protected routes
|
|
68
67
|
|
|
68
|
+
Use the `Annotated` types for clean route signatures:
|
|
69
|
+
|
|
69
70
|
```python
|
|
70
|
-
from
|
|
71
|
-
from fastapi_fullauth.dependencies import current_user, require_role
|
|
71
|
+
from fastapi_fullauth.dependencies import CurrentUser, VerifiedUser, SuperUser, require_role
|
|
72
72
|
|
|
73
73
|
@app.get("/profile")
|
|
74
|
-
async def profile(user
|
|
74
|
+
async def profile(user: CurrentUser):
|
|
75
75
|
return user
|
|
76
76
|
|
|
77
|
+
@app.get("/dashboard")
|
|
78
|
+
async def dashboard(user: VerifiedUser):
|
|
79
|
+
# only email-verified users
|
|
80
|
+
return {"email": user.email}
|
|
81
|
+
|
|
77
82
|
@app.delete("/admin/users/{id}")
|
|
78
|
-
async def delete_user(user
|
|
83
|
+
async def delete_user(user: SuperUser):
|
|
84
|
+
# only superusers
|
|
85
|
+
...
|
|
86
|
+
|
|
87
|
+
# or use require_role for custom roles
|
|
88
|
+
from fastapi import Depends
|
|
89
|
+
from fastapi_fullauth.dependencies import require_role
|
|
90
|
+
|
|
91
|
+
@app.get("/editor")
|
|
92
|
+
async def editor_panel(user=Depends(require_role("editor"))):
|
|
79
93
|
...
|
|
80
94
|
```
|
|
81
95
|
|
|
@@ -125,15 +139,44 @@ fullauth.hooks.on("after_register", welcome)
|
|
|
125
139
|
|
|
126
140
|
Events: `after_register`, `after_login`, `after_logout`, `after_password_change`, `after_password_reset`, `after_email_verify`, `send_verification_email`, `send_password_reset_email`
|
|
127
141
|
|
|
128
|
-
##
|
|
142
|
+
## Custom token claims
|
|
143
|
+
|
|
144
|
+
Embed app-specific data into JWTs (available in `payload.extra`):
|
|
129
145
|
|
|
130
146
|
```python
|
|
131
|
-
|
|
147
|
+
async def add_claims(user):
|
|
148
|
+
return {"tenant_id": "acme", "plan": "pro"}
|
|
132
149
|
|
|
133
150
|
fullauth = FullAuth(
|
|
134
151
|
secret_key="...",
|
|
135
152
|
adapter=adapter,
|
|
136
|
-
|
|
153
|
+
on_create_token_claims=add_claims,
|
|
154
|
+
)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Reserved keys (`sub`, `exp`, `iat`, `jti`, `type`, `roles`, `extra`, `family_id`) are rejected to prevent accidental overwrites.
|
|
158
|
+
|
|
159
|
+
## Password hashing
|
|
160
|
+
|
|
161
|
+
Argon2id by default. Switch to bcrypt via config:
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
fullauth = FullAuth(
|
|
165
|
+
secret_key="...",
|
|
166
|
+
adapter=adapter,
|
|
167
|
+
password_hash_algorithm="bcrypt", # requires: pip install bcrypt
|
|
168
|
+
)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
When switching algorithms, existing users are transparently rehashed on their next login.
|
|
172
|
+
|
|
173
|
+
## Route control
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
fullauth = FullAuth(
|
|
177
|
+
secret_key="...",
|
|
178
|
+
adapter=adapter,
|
|
179
|
+
enabled_routes=["login", "logout", "refresh"],
|
|
137
180
|
)
|
|
138
181
|
```
|
|
139
182
|
|
|
@@ -145,6 +188,20 @@ SecurityHeaders, CSRF, and rate limiting are auto-wired from config flags. Pass
|
|
|
145
188
|
|
|
146
189
|
Login, register, and password-reset have per-IP rate limits enabled by default (5/3/3 per minute). Configure via `AUTH_RATE_LIMIT_*` settings.
|
|
147
190
|
|
|
191
|
+
## Login field
|
|
192
|
+
|
|
193
|
+
By default, login uses `email`. Change it to any field on your user model:
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
# username login: POST /login {"username": "john", "password": "..."}
|
|
197
|
+
fullauth = FullAuth(secret_key="...", adapter=adapter, login_field="username")
|
|
198
|
+
|
|
199
|
+
# phone login: POST /login {"phone": "+1234567890", "password": "..."}
|
|
200
|
+
fullauth = FullAuth(secret_key="...", adapter=adapter, login_field="phone")
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
The Swagger UI and request body update automatically. The adapter looks up users by that field.
|
|
204
|
+
|
|
148
205
|
## Development
|
|
149
206
|
|
|
150
207
|
```bash
|
|
@@ -152,6 +209,10 @@ git clone https://github.com/mdfarhankc/fastapi-fullauth.git
|
|
|
152
209
|
cd fastapi-fullauth
|
|
153
210
|
uv sync --dev --extra sqlalchemy --extra sqlmodel
|
|
154
211
|
uv run pytest tests/ -v
|
|
212
|
+
|
|
213
|
+
# run examples
|
|
214
|
+
uv run uvicorn examples.memory_app.main:app --reload
|
|
215
|
+
uv run uvicorn examples.sqlmodel_app.main:app --reload
|
|
155
216
|
```
|
|
156
217
|
|
|
157
218
|
## License
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Run: uv run uvicorn examples.memory_app.main:app --reload
|
|
3
|
+
No database needed — everything in memory.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from fastapi import FastAPI
|
|
7
|
+
|
|
8
|
+
from .auth import fullauth
|
|
9
|
+
from .routes import router
|
|
10
|
+
|
|
11
|
+
app = FastAPI(title="FullAuth Memory Demo")
|
|
12
|
+
fullauth.init_app(app)
|
|
13
|
+
app.include_router(router)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from fastapi import APIRouter, Depends
|
|
2
|
+
|
|
3
|
+
from fastapi_fullauth.dependencies import current_user, require_role
|
|
4
|
+
|
|
5
|
+
router = APIRouter(prefix="/api/v1")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@router.get("/dashboard")
|
|
9
|
+
async def dashboard(user=Depends(current_user)):
|
|
10
|
+
return {"msg": "welcome", "user": user}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@router.get("/admin")
|
|
14
|
+
async def admin_only(user=Depends(require_role("admin"))):
|
|
15
|
+
return {"msg": "admin area", "user": user}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from fastapi_fullauth import FullAuth
|
|
2
|
+
from fastapi_fullauth.adapters.sqlmodel import SQLModelAdapter
|
|
3
|
+
from fastapi_fullauth.types import UserSchema
|
|
4
|
+
|
|
5
|
+
from .config import session_maker
|
|
6
|
+
from .models import User
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def send_verification_email(email: str, token: str):
|
|
10
|
+
print(f"\n[VERIFY] To: {email}\n[VERIFY] Token: {token}\n")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
async def send_password_reset_email(email: str, token: str):
|
|
14
|
+
print(f"\n[RESET] To: {email}\n[RESET] Token: {token}\n")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def add_custom_claims(user: UserSchema) -> dict:
|
|
18
|
+
return {"display_name": getattr(user, "display_name", "")}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
fullauth = FullAuth(
|
|
22
|
+
secret_key="change-me-use-a-32-byte-key-here",
|
|
23
|
+
adapter=SQLModelAdapter(session_maker=session_maker, user_model=User),
|
|
24
|
+
on_create_token_claims=add_custom_claims,
|
|
25
|
+
include_user_in_login=True,
|
|
26
|
+
)
|
|
27
|
+
fullauth.hooks.on("send_verification_email", send_verification_email)
|
|
28
|
+
fullauth.hooks.on("send_password_reset_email", send_password_reset_email)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Run: uv run uvicorn examples.sqlmodel_app.main:app --reload
|
|
3
|
+
Requires: uv add fastapi-fullauth[sqlmodel] aiosqlite
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from contextlib import asynccontextmanager
|
|
7
|
+
|
|
8
|
+
from fastapi import FastAPI
|
|
9
|
+
from sqlmodel import SQLModel
|
|
10
|
+
|
|
11
|
+
from .auth import fullauth
|
|
12
|
+
from .config import engine
|
|
13
|
+
from .routes import router
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@asynccontextmanager
|
|
17
|
+
async def lifespan(app: FastAPI):
|
|
18
|
+
async with engine.begin() as conn:
|
|
19
|
+
await conn.run_sync(SQLModel.metadata.create_all)
|
|
20
|
+
yield
|
|
21
|
+
await engine.dispose()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
app = FastAPI(title="FullAuth SQLModel Demo", lifespan=lifespan)
|
|
25
|
+
fullauth.init_app(app)
|
|
26
|
+
app.include_router(router)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from sqlmodel import Field, Relationship
|
|
2
|
+
|
|
3
|
+
from fastapi_fullauth.adapters.sqlmodel import (
|
|
4
|
+
RefreshTokenRecord,
|
|
5
|
+
Role,
|
|
6
|
+
UserBase,
|
|
7
|
+
UserRoleLink,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class User(UserBase, table=True):
|
|
12
|
+
__tablename__ = "fullauth_users"
|
|
13
|
+
|
|
14
|
+
display_name: str = Field(default="", max_length=100)
|
|
15
|
+
phone: str = Field(default="", max_length=20)
|
|
16
|
+
|
|
17
|
+
roles: list[Role] = Relationship(link_model=UserRoleLink)
|
|
18
|
+
refresh_tokens: list[RefreshTokenRecord] = Relationship()
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from fastapi import APIRouter, Depends
|
|
2
|
+
|
|
3
|
+
from fastapi_fullauth.dependencies import current_user, require_role
|
|
4
|
+
|
|
5
|
+
router = APIRouter(prefix="/api/v1")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@router.get("/dashboard")
|
|
9
|
+
async def dashboard(user=Depends(current_user)):
|
|
10
|
+
return {"msg": "welcome", "user": user}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@router.get("/admin")
|
|
14
|
+
async def admin_only(user=Depends(require_role("admin"))):
|
|
15
|
+
return {"msg": "admin area", "user": user}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
__version__ = "0.
|
|
1
|
+
__version__ = "0.3.0"
|
|
2
2
|
|
|
3
3
|
from fastapi_fullauth.config import FullAuthConfig
|
|
4
4
|
from fastapi_fullauth.fullauth import FullAuth
|
|
5
|
-
from fastapi_fullauth.types import
|
|
5
|
+
from fastapi_fullauth.types import RouteName
|
|
6
6
|
from fastapi_fullauth.utils import create_superuser, generate_secret_key
|
|
7
7
|
from fastapi_fullauth.validators import PasswordValidator
|
|
8
8
|
|
|
@@ -10,7 +10,7 @@ __all__ = [
|
|
|
10
10
|
"FullAuth",
|
|
11
11
|
"FullAuthConfig",
|
|
12
12
|
"PasswordValidator",
|
|
13
|
-
"
|
|
13
|
+
"RouteName",
|
|
14
14
|
"create_superuser",
|
|
15
15
|
"generate_secret_key",
|
|
16
16
|
]
|
|
@@ -17,6 +17,14 @@ class AbstractUserAdapter(ABC):
|
|
|
17
17
|
@abstractmethod
|
|
18
18
|
async def get_user_by_email(self, email: str) -> UserSchema | None: ...
|
|
19
19
|
|
|
20
|
+
async def get_user_by_field(self, field: str, value: str) -> UserSchema | None:
|
|
21
|
+
"""Look up a user by an arbitrary field. Override for non-email login."""
|
|
22
|
+
if field == "email":
|
|
23
|
+
return await self.get_user_by_email(value)
|
|
24
|
+
raise NotImplementedError(
|
|
25
|
+
f"Lookup by '{field}' not implemented — override get_user_by_field()"
|
|
26
|
+
)
|
|
27
|
+
|
|
20
28
|
@abstractmethod
|
|
21
29
|
async def create_user(self, data: CreateUserSchema, hashed_password: str) -> UserSchema: ...
|
|
22
30
|
|
|
@@ -47,6 +55,9 @@ class AbstractUserAdapter(ABC):
|
|
|
47
55
|
@abstractmethod
|
|
48
56
|
async def revoke_refresh_token_family(self, family_id: str) -> None: ...
|
|
49
57
|
|
|
58
|
+
@abstractmethod
|
|
59
|
+
async def revoke_all_user_refresh_tokens(self, user_id: str) -> None: ...
|
|
60
|
+
|
|
50
61
|
@abstractmethod
|
|
51
62
|
async def set_user_verified(self, user_id: str) -> None: ...
|
|
52
63
|
|
|
@@ -21,8 +21,11 @@ class InMemoryAdapter(AbstractUserAdapter):
|
|
|
21
21
|
return self._user_schema(**data)
|
|
22
22
|
|
|
23
23
|
async def get_user_by_email(self, email: str) -> UserSchema | None:
|
|
24
|
+
return await self.get_user_by_field("email", email)
|
|
25
|
+
|
|
26
|
+
async def get_user_by_field(self, field: str, value: str) -> UserSchema | None:
|
|
24
27
|
for data in self._users.values():
|
|
25
|
-
if data
|
|
28
|
+
if data.get(field) == value:
|
|
26
29
|
return self._user_schema(**data)
|
|
27
30
|
return None
|
|
28
31
|
|
|
@@ -79,6 +82,11 @@ class InMemoryAdapter(AbstractUserAdapter):
|
|
|
79
82
|
if tok.family_id == family_id:
|
|
80
83
|
tok.revoked = True
|
|
81
84
|
|
|
85
|
+
async def revoke_all_user_refresh_tokens(self, user_id: str) -> None:
|
|
86
|
+
for tok in self._refresh_tokens.values():
|
|
87
|
+
if tok.user_id == user_id:
|
|
88
|
+
tok.revoked = True
|
|
89
|
+
|
|
82
90
|
async def set_user_verified(self, user_id: str) -> None:
|
|
83
91
|
if user_id in self._users:
|
|
84
92
|
self._users[user_id]["is_verified"] = True
|
{fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/adapters/sqlalchemy/__init__.py
RENAMED
|
@@ -3,7 +3,7 @@ from fastapi_fullauth.adapters.sqlalchemy.models import (
|
|
|
3
3
|
FullAuthBase,
|
|
4
4
|
RefreshTokenModel,
|
|
5
5
|
RoleModel,
|
|
6
|
-
|
|
6
|
+
UserBase,
|
|
7
7
|
UserRoleModel,
|
|
8
8
|
)
|
|
9
9
|
|
|
@@ -12,6 +12,6 @@ __all__ = [
|
|
|
12
12
|
"RefreshTokenModel",
|
|
13
13
|
"RoleModel",
|
|
14
14
|
"SQLAlchemyAdapter",
|
|
15
|
-
"
|
|
15
|
+
"UserBase",
|
|
16
16
|
"UserRoleModel",
|
|
17
17
|
]
|