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.
Files changed (87) hide show
  1. fastapi_fullauth-0.3.0/CHANGELOG.md +87 -0
  2. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/PKG-INFO +72 -34
  3. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/README.md +71 -10
  4. fastapi_fullauth-0.3.0/examples/memory_app/auth.py +8 -0
  5. fastapi_fullauth-0.3.0/examples/memory_app/main.py +13 -0
  6. fastapi_fullauth-0.3.0/examples/memory_app/routes.py +15 -0
  7. fastapi_fullauth-0.3.0/examples/sqlmodel_app/auth.py +28 -0
  8. fastapi_fullauth-0.3.0/examples/sqlmodel_app/config.py +5 -0
  9. fastapi_fullauth-0.3.0/examples/sqlmodel_app/main.py +26 -0
  10. fastapi_fullauth-0.3.0/examples/sqlmodel_app/models.py +18 -0
  11. fastapi_fullauth-0.3.0/examples/sqlmodel_app/routes.py +15 -0
  12. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/__init__.py +3 -3
  13. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/adapters/base.py +11 -0
  14. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/adapters/memory.py +9 -1
  15. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/adapters/sqlalchemy/__init__.py +2 -2
  16. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/adapters/sqlalchemy/adapter.py +30 -15
  17. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/adapters/sqlalchemy/models.py +13 -16
  18. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/adapters/sqlmodel/__init__.py +0 -2
  19. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/adapters/sqlmodel/adapter.py +23 -8
  20. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/adapters/sqlmodel/models.py +7 -23
  21. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/config.py +2 -0
  22. fastapi_fullauth-0.3.0/fastapi_fullauth/core/crypto.py +47 -0
  23. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/core/tokens.py +20 -9
  24. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/dependencies/__init__.py +6 -7
  25. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/dependencies/current_user.py +32 -28
  26. fastapi_fullauth-0.3.0/fastapi_fullauth/dependencies/require_role.py +33 -0
  27. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/flows/login.py +22 -18
  28. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/flows/password_reset.py +3 -0
  29. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/flows/register.py +8 -0
  30. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/fullauth.py +58 -23
  31. fastapi_fullauth-0.3.0/fastapi_fullauth/hooks.py +50 -0
  32. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/protection/ratelimit.py +8 -4
  33. fastapi_fullauth-0.3.0/fastapi_fullauth/router/__init__.py +0 -0
  34. fastapi_fullauth-0.3.0/fastapi_fullauth/router/auth.py +505 -0
  35. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/types.py +21 -16
  36. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/pyproject.toml +1 -17
  37. fastapi_fullauth-0.3.0/tests/__init__.py +0 -0
  38. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/tests/conftest.py +2 -2
  39. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/tests/test_auth_flows.py +5 -5
  40. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/tests/test_customization.py +7 -10
  41. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/tests/test_dx_improvements.py +6 -16
  42. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/tests/test_email_verify.py +3 -6
  43. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/tests/test_new_endpoints.py +5 -5
  44. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/tests/test_refresh_tokens.py +7 -7
  45. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/tests/test_roles.py +2 -2
  46. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/tests/test_tokens.py +7 -5
  47. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/uv.lock +2 -379
  48. fastapi_fullauth-0.1.0/CHANGELOG.md +0 -38
  49. fastapi_fullauth-0.1.0/examples/memory_app.py +0 -62
  50. fastapi_fullauth-0.1.0/examples/sqlalchemy_app.py +0 -86
  51. fastapi_fullauth-0.1.0/examples/sqlmodel_app.py +0 -91
  52. fastapi_fullauth-0.1.0/fastapi_fullauth/core/crypto.py +0 -19
  53. fastapi_fullauth-0.1.0/fastapi_fullauth/dependencies/require_role.py +0 -78
  54. fastapi_fullauth-0.1.0/fastapi_fullauth/hooks.py +0 -16
  55. fastapi_fullauth-0.1.0/fastapi_fullauth/router/auth.py +0 -477
  56. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/.github/workflows/ci.yml +0 -0
  57. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/.github/workflows/publish.yml +0 -0
  58. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/.gitignore +0 -0
  59. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/.python-version +0 -0
  60. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/LICENSE +0 -0
  61. {fastapi_fullauth-0.1.0/fastapi_fullauth/rbac → fastapi_fullauth-0.3.0/examples/memory_app}/__init__.py +0 -0
  62. {fastapi_fullauth-0.1.0/fastapi_fullauth/router → fastapi_fullauth-0.3.0/examples/sqlmodel_app}/__init__.py +0 -0
  63. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/adapters/__init__.py +0 -0
  64. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/backends/__init__.py +0 -0
  65. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/backends/base.py +0 -0
  66. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/backends/bearer.py +0 -0
  67. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/backends/cookie.py +0 -0
  68. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/core/__init__.py +0 -0
  69. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/core/redis_blacklist.py +0 -0
  70. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/exceptions.py +0 -0
  71. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/flows/__init__.py +0 -0
  72. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/flows/email_verify.py +0 -0
  73. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/flows/logout.py +0 -0
  74. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/middleware/__init__.py +0 -0
  75. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/middleware/csrf.py +0 -0
  76. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/middleware/ratelimit.py +0 -0
  77. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/middleware/security_headers.py +0 -0
  78. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/migrations/__init__.py +0 -0
  79. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/migrations/helpers.py +0 -0
  80. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/protection/__init__.py +0 -0
  81. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/protection/lockout.py +0 -0
  82. {fastapi_fullauth-0.1.0/tests → fastapi_fullauth-0.3.0/fastapi_fullauth/rbac}/__init__.py +0 -0
  83. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/utils.py +0 -0
  84. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/fastapi_fullauth/validators.py +0 -0
  85. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/tests/test_crypto.py +0 -0
  86. {fastapi_fullauth-0.1.0 → fastapi_fullauth-0.3.0}/tests/test_lockout.py +0 -0
  87. {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.1.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(back_populates="users", link_model=UserRoleLink)
123
- refresh_tokens: list[RefreshTokenRecord] = Relationship(back_populates="user")
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 fastapi import Depends
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=Depends(current_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=Depends(require_role("admin"))):
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
- ## Route control
185
+ ## Custom token claims
186
+
187
+ Embed app-specific data into JWTs (available in `payload.extra`):
195
188
 
196
189
  ```python
197
- from fastapi_fullauth import Route
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
- enabled_routes=[Route.LOGIN, Route.LOGOUT, Route.REFRESH],
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(back_populates="users", link_model=UserRoleLink)
57
- refresh_tokens: list[RefreshTokenRecord] = Relationship(back_populates="user")
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 fastapi import Depends
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=Depends(current_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=Depends(require_role("admin"))):
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
- ## Route control
142
+ ## Custom token claims
143
+
144
+ Embed app-specific data into JWTs (available in `payload.extra`):
129
145
 
130
146
  ```python
131
- from fastapi_fullauth import Route
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
- enabled_routes=[Route.LOGIN, Route.LOGOUT, Route.REFRESH],
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,8 @@
1
+ from fastapi_fullauth import FullAuth
2
+ from fastapi_fullauth.adapters.memory import InMemoryAdapter
3
+
4
+ fullauth = FullAuth(
5
+ secret_key="change-me-use-a-32-byte-key-here",
6
+ adapter=InMemoryAdapter(),
7
+ include_user_in_login=True,
8
+ )
@@ -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,5 @@
1
+ from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
2
+
3
+ DATABASE_URL = "sqlite+aiosqlite:///fullauth_sqlmodel_demo.db"
4
+ engine = create_async_engine(DATABASE_URL)
5
+ session_maker = async_sessionmaker(engine, expire_on_commit=False)
@@ -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.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 Route
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
- "Route",
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["email"] == email:
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
@@ -3,7 +3,7 @@ from fastapi_fullauth.adapters.sqlalchemy.models import (
3
3
  FullAuthBase,
4
4
  RefreshTokenModel,
5
5
  RoleModel,
6
- UserModel,
6
+ UserBase,
7
7
  UserRoleModel,
8
8
  )
9
9
 
@@ -12,6 +12,6 @@ __all__ = [
12
12
  "RefreshTokenModel",
13
13
  "RoleModel",
14
14
  "SQLAlchemyAdapter",
15
- "UserModel",
15
+ "UserBase",
16
16
  "UserRoleModel",
17
17
  ]