fastapi-fullauth 0.2.0__tar.gz → 0.4.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 (92) hide show
  1. fastapi_fullauth-0.4.0/CHANGELOG.md +109 -0
  2. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/PKG-INFO +95 -30
  3. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/README.md +92 -7
  4. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/examples/sqlmodel_app/auth.py +2 -2
  5. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/__init__.py +3 -3
  6. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/adapters/base.py +23 -1
  7. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/adapters/memory.py +33 -1
  8. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/adapters/sqlalchemy/adapter.py +85 -1
  9. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/adapters/sqlalchemy/models.py +19 -1
  10. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/adapters/sqlmodel/adapter.py +90 -2
  11. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/adapters/sqlmodel/models.py +19 -0
  12. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/config.py +5 -1
  13. fastapi_fullauth-0.4.0/fastapi_fullauth/core/crypto.py +47 -0
  14. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/core/tokens.py +20 -9
  15. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/dependencies/__init__.py +6 -8
  16. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/dependencies/current_user.py +24 -15
  17. fastapi_fullauth-0.4.0/fastapi_fullauth/dependencies/require_role.py +33 -0
  18. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/exceptions.py +17 -0
  19. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/flows/login.py +13 -9
  20. fastapi_fullauth-0.4.0/fastapi_fullauth/flows/oauth.py +123 -0
  21. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/flows/password_reset.py +3 -0
  22. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/flows/register.py +8 -0
  23. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/fullauth.py +107 -30
  24. fastapi_fullauth-0.4.0/fastapi_fullauth/hooks.py +50 -0
  25. fastapi_fullauth-0.4.0/fastapi_fullauth/oauth/__init__.py +9 -0
  26. fastapi_fullauth-0.4.0/fastapi_fullauth/oauth/base.py +55 -0
  27. fastapi_fullauth-0.4.0/fastapi_fullauth/oauth/github.py +79 -0
  28. fastapi_fullauth-0.4.0/fastapi_fullauth/oauth/google.py +65 -0
  29. fastapi_fullauth-0.4.0/fastapi_fullauth/protection/ratelimit.py +166 -0
  30. fastapi_fullauth-0.4.0/fastapi_fullauth/router/auth.py +505 -0
  31. fastapi_fullauth-0.4.0/fastapi_fullauth/router/oauth.py +173 -0
  32. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/types.py +42 -15
  33. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/pyproject.toml +3 -17
  34. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/tests/test_customization.py +2 -5
  35. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/tests/test_dx_improvements.py +9 -14
  36. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/tests/test_email_verify.py +2 -5
  37. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/tests/test_middleware.py +83 -0
  38. fastapi_fullauth-0.4.0/tests/test_oauth.py +270 -0
  39. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/tests/test_tokens.py +7 -5
  40. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/uv.lock +6 -377
  41. fastapi_fullauth-0.2.0/CHANGELOG.md +0 -47
  42. fastapi_fullauth-0.2.0/fastapi_fullauth/core/crypto.py +0 -19
  43. fastapi_fullauth-0.2.0/fastapi_fullauth/dependencies/require_role.py +0 -78
  44. fastapi_fullauth-0.2.0/fastapi_fullauth/hooks.py +0 -16
  45. fastapi_fullauth-0.2.0/fastapi_fullauth/protection/ratelimit.py +0 -86
  46. fastapi_fullauth-0.2.0/fastapi_fullauth/router/auth.py +0 -486
  47. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/.github/workflows/ci.yml +0 -0
  48. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/.github/workflows/publish.yml +0 -0
  49. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/.gitignore +0 -0
  50. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/.python-version +0 -0
  51. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/LICENSE +0 -0
  52. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/examples/memory_app/__init__.py +0 -0
  53. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/examples/memory_app/auth.py +0 -0
  54. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/examples/memory_app/main.py +0 -0
  55. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/examples/memory_app/routes.py +0 -0
  56. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/examples/sqlmodel_app/__init__.py +0 -0
  57. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/examples/sqlmodel_app/config.py +0 -0
  58. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/examples/sqlmodel_app/main.py +0 -0
  59. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/examples/sqlmodel_app/models.py +0 -0
  60. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/examples/sqlmodel_app/routes.py +0 -0
  61. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/adapters/__init__.py +0 -0
  62. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/adapters/sqlalchemy/__init__.py +0 -0
  63. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/adapters/sqlmodel/__init__.py +0 -0
  64. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/backends/__init__.py +0 -0
  65. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/backends/base.py +0 -0
  66. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/backends/bearer.py +0 -0
  67. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/backends/cookie.py +0 -0
  68. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/core/__init__.py +0 -0
  69. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/core/redis_blacklist.py +0 -0
  70. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/flows/__init__.py +0 -0
  71. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/flows/email_verify.py +0 -0
  72. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/flows/logout.py +0 -0
  73. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/middleware/__init__.py +0 -0
  74. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/middleware/csrf.py +0 -0
  75. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/middleware/ratelimit.py +0 -0
  76. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/middleware/security_headers.py +0 -0
  77. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/migrations/__init__.py +0 -0
  78. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/migrations/helpers.py +0 -0
  79. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/protection/__init__.py +0 -0
  80. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/protection/lockout.py +0 -0
  81. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/rbac/__init__.py +0 -0
  82. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/router/__init__.py +0 -0
  83. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/utils.py +0 -0
  84. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/fastapi_fullauth/validators.py +0 -0
  85. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/tests/__init__.py +0 -0
  86. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/tests/conftest.py +0 -0
  87. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/tests/test_auth_flows.py +0 -0
  88. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/tests/test_crypto.py +0 -0
  89. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/tests/test_lockout.py +0 -0
  90. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/tests/test_new_endpoints.py +0 -0
  91. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/tests/test_refresh_tokens.py +0 -0
  92. {fastapi_fullauth-0.2.0 → fastapi_fullauth-0.4.0}/tests/test_roles.py +0 -0
@@ -0,0 +1,109 @@
1
+ # Changelog
2
+
3
+ ## 0.4.0
4
+
5
+ ### Added
6
+
7
+ - **OAuth2 social login** — Google and GitHub out of the box, extensible for custom providers
8
+ - `GET /oauth/{provider}/authorize` — get authorization URL
9
+ - `POST /oauth/{provider}/callback` — exchange code for JWT tokens
10
+ - `GET /oauth/providers` — list configured providers
11
+ - `GET /oauth/accounts` — list linked OAuth accounts
12
+ - `DELETE /oauth/accounts/{provider}` — unlink a provider (with lockout prevention)
13
+ - `OAuthProvider` abstract base class for implementing custom providers
14
+ - `OAuthAccount` and `OAuthUserInfo` types
15
+ - `OAuthAccountRecord` / `OAuthAccountModel` for SQLModel and SQLAlchemy adapters
16
+ - OAuth adapter methods on all adapters (memory, SQLModel, SQLAlchemy)
17
+ - `OAUTH_PROVIDERS`, `OAUTH_STATE_EXPIRE_SECONDS`, `OAUTH_AUTO_LINK_BY_EMAIL` config fields
18
+ - `after_oauth_login` hook event
19
+ - `oauth` optional dependency group (`pip install fastapi-fullauth[oauth]`)
20
+ - Auto-link OAuth to existing user by email (configurable)
21
+ - Auto-verify email when provider confirms it
22
+ - Lockout prevention — can't unlink last login method
23
+ - Multiple `redirect_uris` per OAuth provider — supports web, mobile, and production frontends from one config. Client passes `?redirect_uri=` on authorize, validated against allowed list.
24
+
25
+ ## 0.3.0
26
+
27
+ ### Breaking changes
28
+
29
+ - **`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`.
30
+ - **`create_token_pair` returns `tuple[str, RefreshTokenMeta]`** — second element is now `RefreshTokenMeta` instead of `str`.
31
+ - **`revoke_all_user_refresh_tokens` is now required** on custom adapters — new abstract method on `AbstractUserAdapter`.
32
+
33
+ ### Added
34
+
35
+ - `current_superuser` dependency and `SuperUser` annotated type
36
+ - `CurrentUser`, `VerifiedUser`, `SuperUser` annotated types in `dependencies.current_user` for cleaner route signatures
37
+ - `RefreshTokenMeta` named tuple — avoids decoding freshly created tokens just to read `expires_at` and `family_id`
38
+ - `FullAuth.get_custom_claims(user)` — moved custom claims logic from router into the class, with validation against reserved JWT keys (`sub`, `exp`, `type`, etc.)
39
+ - `revoke_all_user_refresh_tokens(user_id)` on all adapters — bulk session revocation
40
+ - Session revocation on password reset, password change, and account deletion
41
+ - `configure_hasher()` — wires `PASSWORD_HASH_ALGORITHM` config to the actual hasher; supports `argon2id` and `bcrypt`
42
+ - Automatic password rehash on login when hash algorithm or params have changed
43
+ - Register now checks uniqueness on `login_field` (not just email) when `login_field != "email"`
44
+ - `InMemoryBlacklist` now respects `ttl_seconds` — expired entries are evicted on lookup
45
+ - `RateLimiter` evicts keys with empty timestamp lists to prevent unbounded dict growth
46
+ - `description` parameter on all route decorators for Swagger docs
47
+
48
+ ### Fixed
49
+
50
+ - `current_active_verified_user` was missing `payload.type != "access"` check — refresh tokens could pass through
51
+ - Purpose tokens (password reset, email verify) could be used as regular access tokens — `current_user` now rejects tokens with `extra.purpose`
52
+ - Duplicate token decode + user lookup across dependencies, router endpoints, and admin routes — consolidated into reusable `current_user` dependency chain
53
+ - Duplicate `roles` + `extra_claims` fetch in refresh route — pulled above the if/else branch
54
+ - Login flow fetched the user from DB twice (once in router, once in `login()`) — now accepts pre-fetched user
55
+ - Unused `request: Request` parameters in dependencies and routes
56
+ - Removed duplicate docstrings on routes (kept `description=` on decorators)
57
+ - `require_permission` was a full copy of `require_role` — now delegates to it
58
+
59
+ ### Internal
60
+
61
+ - Route order follows auth lifecycle: register → login → refresh → logout → user → email/password → admin
62
+ - `require_role` / `require_permission` use `Depends(current_user)` instead of duplicating token logic
63
+ - Removed `_get_custom_claims` module-level function from router
64
+
65
+ ## 0.2.0
66
+
67
+ ### Breaking changes
68
+
69
+ - **JSON login** — `POST /login` now accepts `{"email": "...", "password": "..."}` instead of form data. Swagger auth uses bearer token input instead of username/password form.
70
+ - **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.
71
+ - **`user_model` is required** — `SQLModelAdapter(session_maker, user_model=MyUser)` — no default.
72
+ - **Removed `min_length=8`** from `CreateUserSchema` — password length is now fully controlled by `PasswordValidator` and `PASSWORD_MIN_LENGTH` config.
73
+ - **`SQLAlchemyAdapter` renamed `UserModel` to `UserBase`** — import `UserBase` instead.
74
+
75
+ ### Added
76
+
77
+ - `POST /auth/change-password` — verifies current password, validates new
78
+ - `PATCH /auth/me` — update profile with protected field filtering
79
+ - `DELETE /auth/me` — self-deletion
80
+ - `expires_in` in login/refresh responses
81
+ - Per-IP auth rate limiting on login, register, password-reset (`AUTH_RATE_LIMIT_*` config)
82
+ - `LOGIN_FIELD` config — login by email, username, phone, or any model field
83
+ - `get_user_by_field()` on all adapters for generic field lookups
84
+ - Structured example apps (`examples/memory_app/`, `examples/sqlmodel_app/`)
85
+
86
+ ### Fixed
87
+
88
+ - `InMemoryAdapter.update_user` returning base `UserSchema` instead of custom schema
89
+ - Stale `User.id` / `UserModel` references in adapter queries after model removal
90
+ - Parameter ordering in adapter constructors (required params before optional)
91
+
92
+ ## 0.1.0
93
+
94
+ Initial release.
95
+
96
+ - JWT access/refresh tokens with rotation and blacklisting
97
+ - Argon2id password hashing
98
+ - Auth flows: register, login, logout, password reset, email verification
99
+ - Brute-force lockout, per-IP rate limiting, CSRF, security headers
100
+ - Bearer and cookie backends
101
+ - SQLAlchemy, SQLModel, and InMemory adapters
102
+ - Redis blacklist backend
103
+ - Refresh token persistence with family tracking and reuse detection
104
+ - Flat config (`secret_key=...`) or full `FullAuthConfig` object
105
+ - Auto-derive schemas from ORM model fields
106
+ - Auto-wire middleware from config flags
107
+ - Route enum, event hooks, email hooks
108
+ - `current_user`, `current_active_verified_user`, `require_role` dependencies
109
+ - 97 tests
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-fullauth
3
- Version: 0.2.0
3
+ Version: 0.4.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,15 @@ 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'
31
+ Requires-Dist: httpx>=0.25; extra == 'all'
37
32
  Requires-Dist: redis>=5.0; extra == 'all'
38
33
  Requires-Dist: sqlalchemy[asyncio]>=2.0; extra == 'all'
39
34
  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
35
  Provides-Extra: oauth
48
- Requires-Dist: httpx-oauth>=0.13; extra == 'oauth'
49
- Provides-Extra: passkeys
50
- Requires-Dist: webauthn>=2.0; extra == 'passkeys'
36
+ Requires-Dist: httpx>=0.25; extra == 'oauth'
51
37
  Provides-Extra: redis
52
38
  Requires-Dist: redis>=5.0; extra == 'redis'
53
39
  Provides-Extra: sqlalchemy
@@ -56,12 +42,6 @@ Requires-Dist: sqlalchemy[asyncio]>=2.0; extra == 'sqlalchemy'
56
42
  Provides-Extra: sqlmodel
57
43
  Requires-Dist: alembic>=1.13; extra == 'sqlmodel'
58
44
  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
45
  Description-Content-Type: text/markdown
66
46
 
67
47
  # fastapi-fullauth
@@ -82,6 +62,8 @@ pip install fastapi-fullauth[sqlmodel]
82
62
  pip install fastapi-fullauth[sqlalchemy]
83
63
  # with redis for token blacklisting:
84
64
  pip install fastapi-fullauth[sqlmodel,redis]
65
+ # with OAuth2 social login:
66
+ pip install fastapi-fullauth[sqlmodel,oauth]
85
67
  ```
86
68
 
87
69
  ## Quick start
@@ -131,16 +113,31 @@ No need to create separate schema classes or subclass the adapter. Registration
131
113
 
132
114
  ## Protected routes
133
115
 
116
+ Use the `Annotated` types for clean route signatures:
117
+
134
118
  ```python
135
- from fastapi import Depends
136
- from fastapi_fullauth.dependencies import current_user, require_role
119
+ from fastapi_fullauth.dependencies import CurrentUser, VerifiedUser, SuperUser, require_role
137
120
 
138
121
  @app.get("/profile")
139
- async def profile(user=Depends(current_user)):
122
+ async def profile(user: CurrentUser):
140
123
  return user
141
124
 
125
+ @app.get("/dashboard")
126
+ async def dashboard(user: VerifiedUser):
127
+ # only email-verified users
128
+ return {"email": user.email}
129
+
142
130
  @app.delete("/admin/users/{id}")
143
- async def delete_user(user=Depends(require_role("admin"))):
131
+ async def delete_user(user: SuperUser):
132
+ # only superusers
133
+ ...
134
+
135
+ # or use require_role for custom roles
136
+ from fastapi import Depends
137
+ from fastapi_fullauth.dependencies import require_role
138
+
139
+ @app.get("/editor")
140
+ async def editor_panel(user=Depends(require_role("editor"))):
144
141
  ...
145
142
  ```
146
143
 
@@ -190,15 +187,83 @@ fullauth.hooks.on("after_register", welcome)
190
187
 
191
188
  Events: `after_register`, `after_login`, `after_logout`, `after_password_change`, `after_password_reset`, `after_email_verify`, `send_verification_email`, `send_password_reset_email`
192
189
 
193
- ## Route control
190
+ ## Custom token claims
191
+
192
+ Embed app-specific data into JWTs (available in `payload.extra`):
193
+
194
+ ```python
195
+ async def add_claims(user):
196
+ return {"tenant_id": "acme", "plan": "pro"}
197
+
198
+ fullauth = FullAuth(
199
+ secret_key="...",
200
+ adapter=adapter,
201
+ on_create_token_claims=add_claims,
202
+ )
203
+ ```
204
+
205
+ Reserved keys (`sub`, `exp`, `iat`, `jti`, `type`, `roles`, `extra`, `family_id`) are rejected to prevent accidental overwrites.
206
+
207
+ ## Password hashing
208
+
209
+ Argon2id by default. Switch to bcrypt via config:
210
+
211
+ ```python
212
+ fullauth = FullAuth(
213
+ secret_key="...",
214
+ adapter=adapter,
215
+ password_hash_algorithm="bcrypt", # requires: pip install bcrypt
216
+ )
217
+ ```
218
+
219
+ When switching algorithms, existing users are transparently rehashed on their next login.
220
+
221
+ ## OAuth2 social login
222
+
223
+ Add Google and/or GitHub login with a few config lines:
194
224
 
195
225
  ```python
196
- from fastapi_fullauth import Route
226
+ fullauth = FullAuth(
227
+ secret_key="...",
228
+ adapter=adapter,
229
+ oauth_providers={
230
+ "google": {
231
+ "client_id": "your-google-client-id",
232
+ "client_secret": "your-google-secret",
233
+ "redirect_uris": [
234
+ "http://localhost:3000/auth/callback",
235
+ "https://myapp.com/auth/callback",
236
+ "myapp://auth/callback", # Flutter deep link
237
+ ],
238
+ },
239
+ "github": {
240
+ "client_id": "your-github-client-id",
241
+ "client_secret": "your-github-secret",
242
+ "redirect_uri": "http://localhost:3000/auth/callback", # single URI also works
243
+ },
244
+ },
245
+ )
246
+ ```
247
+
248
+ This registers these routes automatically:
249
+
250
+ - `GET /auth/oauth/providers` — list configured providers
251
+ - `GET /auth/oauth/{provider}/authorize?redirect_uri=...` — get the authorization URL (optional `redirect_uri` param, validated against allowed list, defaults to first)
252
+ - `POST /auth/oauth/{provider}/callback` — exchange code for JWT tokens
253
+ - `GET /auth/oauth/accounts` — list linked OAuth accounts
254
+ - `DELETE /auth/oauth/accounts/{provider}` — unlink a provider
197
255
 
256
+ Users can link multiple providers and keep email/password login alongside OAuth. New users are auto-created on first OAuth login, and existing users are auto-linked by email.
257
+
258
+ Requires `httpx`: `pip install fastapi-fullauth[oauth]`
259
+
260
+ ## Route control
261
+
262
+ ```python
198
263
  fullauth = FullAuth(
199
264
  secret_key="...",
200
265
  adapter=adapter,
201
- enabled_routes=[Route.LOGIN, Route.LOGOUT, Route.REFRESH],
266
+ enabled_routes=["login", "logout", "refresh"],
202
267
  )
203
268
  ```
204
269
 
@@ -16,6 +16,8 @@ pip install fastapi-fullauth[sqlmodel]
16
16
  pip install fastapi-fullauth[sqlalchemy]
17
17
  # with redis for token blacklisting:
18
18
  pip install fastapi-fullauth[sqlmodel,redis]
19
+ # with OAuth2 social login:
20
+ pip install fastapi-fullauth[sqlmodel,oauth]
19
21
  ```
20
22
 
21
23
  ## Quick start
@@ -65,16 +67,31 @@ No need to create separate schema classes or subclass the adapter. Registration
65
67
 
66
68
  ## Protected routes
67
69
 
70
+ Use the `Annotated` types for clean route signatures:
71
+
68
72
  ```python
69
- from fastapi import Depends
70
- from fastapi_fullauth.dependencies import current_user, require_role
73
+ from fastapi_fullauth.dependencies import CurrentUser, VerifiedUser, SuperUser, require_role
71
74
 
72
75
  @app.get("/profile")
73
- async def profile(user=Depends(current_user)):
76
+ async def profile(user: CurrentUser):
74
77
  return user
75
78
 
79
+ @app.get("/dashboard")
80
+ async def dashboard(user: VerifiedUser):
81
+ # only email-verified users
82
+ return {"email": user.email}
83
+
76
84
  @app.delete("/admin/users/{id}")
77
- async def delete_user(user=Depends(require_role("admin"))):
85
+ async def delete_user(user: SuperUser):
86
+ # only superusers
87
+ ...
88
+
89
+ # or use require_role for custom roles
90
+ from fastapi import Depends
91
+ from fastapi_fullauth.dependencies import require_role
92
+
93
+ @app.get("/editor")
94
+ async def editor_panel(user=Depends(require_role("editor"))):
78
95
  ...
79
96
  ```
80
97
 
@@ -124,15 +141,83 @@ fullauth.hooks.on("after_register", welcome)
124
141
 
125
142
  Events: `after_register`, `after_login`, `after_logout`, `after_password_change`, `after_password_reset`, `after_email_verify`, `send_verification_email`, `send_password_reset_email`
126
143
 
127
- ## Route control
144
+ ## Custom token claims
145
+
146
+ Embed app-specific data into JWTs (available in `payload.extra`):
147
+
148
+ ```python
149
+ async def add_claims(user):
150
+ return {"tenant_id": "acme", "plan": "pro"}
151
+
152
+ fullauth = FullAuth(
153
+ secret_key="...",
154
+ adapter=adapter,
155
+ on_create_token_claims=add_claims,
156
+ )
157
+ ```
158
+
159
+ Reserved keys (`sub`, `exp`, `iat`, `jti`, `type`, `roles`, `extra`, `family_id`) are rejected to prevent accidental overwrites.
160
+
161
+ ## Password hashing
162
+
163
+ Argon2id by default. Switch to bcrypt via config:
164
+
165
+ ```python
166
+ fullauth = FullAuth(
167
+ secret_key="...",
168
+ adapter=adapter,
169
+ password_hash_algorithm="bcrypt", # requires: pip install bcrypt
170
+ )
171
+ ```
172
+
173
+ When switching algorithms, existing users are transparently rehashed on their next login.
174
+
175
+ ## OAuth2 social login
176
+
177
+ Add Google and/or GitHub login with a few config lines:
128
178
 
129
179
  ```python
130
- from fastapi_fullauth import Route
180
+ fullauth = FullAuth(
181
+ secret_key="...",
182
+ adapter=adapter,
183
+ oauth_providers={
184
+ "google": {
185
+ "client_id": "your-google-client-id",
186
+ "client_secret": "your-google-secret",
187
+ "redirect_uris": [
188
+ "http://localhost:3000/auth/callback",
189
+ "https://myapp.com/auth/callback",
190
+ "myapp://auth/callback", # Flutter deep link
191
+ ],
192
+ },
193
+ "github": {
194
+ "client_id": "your-github-client-id",
195
+ "client_secret": "your-github-secret",
196
+ "redirect_uri": "http://localhost:3000/auth/callback", # single URI also works
197
+ },
198
+ },
199
+ )
200
+ ```
201
+
202
+ This registers these routes automatically:
203
+
204
+ - `GET /auth/oauth/providers` — list configured providers
205
+ - `GET /auth/oauth/{provider}/authorize?redirect_uri=...` — get the authorization URL (optional `redirect_uri` param, validated against allowed list, defaults to first)
206
+ - `POST /auth/oauth/{provider}/callback` — exchange code for JWT tokens
207
+ - `GET /auth/oauth/accounts` — list linked OAuth accounts
208
+ - `DELETE /auth/oauth/accounts/{provider}` — unlink a provider
131
209
 
210
+ Users can link multiple providers and keep email/password login alongside OAuth. New users are auto-created on first OAuth login, and existing users are auto-linked by email.
211
+
212
+ Requires `httpx`: `pip install fastapi-fullauth[oauth]`
213
+
214
+ ## Route control
215
+
216
+ ```python
132
217
  fullauth = FullAuth(
133
218
  secret_key="...",
134
219
  adapter=adapter,
135
- enabled_routes=[Route.LOGIN, Route.LOGOUT, Route.REFRESH],
220
+ enabled_routes=["login", "logout", "refresh"],
136
221
  )
137
222
  ```
138
223
 
@@ -21,8 +21,8 @@ async def add_custom_claims(user: UserSchema) -> dict:
21
21
  fullauth = FullAuth(
22
22
  secret_key="change-me-use-a-32-byte-key-here",
23
23
  adapter=SQLModelAdapter(session_maker=session_maker, user_model=User),
24
- on_send_verification_email=send_verification_email,
25
- on_send_password_reset_email=send_password_reset_email,
26
24
  on_create_token_claims=add_custom_claims,
27
25
  include_user_in_login=True,
28
26
  )
27
+ fullauth.hooks.on("send_verification_email", send_verification_email)
28
+ fullauth.hooks.on("send_password_reset_email", send_password_reset_email)
@@ -1,8 +1,8 @@
1
- __version__ = "0.2.0"
1
+ __version__ = "0.4.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
  ]
@@ -1,7 +1,7 @@
1
1
  from abc import ABC, abstractmethod
2
2
  from typing import Any
3
3
 
4
- from fastapi_fullauth.types import CreateUserSchema, RefreshToken, UserSchema
4
+ from fastapi_fullauth.types import CreateUserSchema, OAuthAccount, RefreshToken, UserSchema
5
5
 
6
6
 
7
7
  class AbstractUserAdapter(ABC):
@@ -55,6 +55,9 @@ class AbstractUserAdapter(ABC):
55
55
  @abstractmethod
56
56
  async def revoke_refresh_token_family(self, family_id: str) -> None: ...
57
57
 
58
+ @abstractmethod
59
+ async def revoke_all_user_refresh_tokens(self, user_id: str) -> None: ...
60
+
58
61
  @abstractmethod
59
62
  async def set_user_verified(self, user_id: str) -> None: ...
60
63
 
@@ -63,3 +66,22 @@ class AbstractUserAdapter(ABC):
63
66
 
64
67
  @abstractmethod
65
68
  async def remove_role(self, user_id: str, role_name: str) -> None: ...
69
+
70
+ # ── OAuth (optional — override when using OAuth) ─────────────────
71
+
72
+ async def get_oauth_account(self, provider: str, provider_user_id: str) -> OAuthAccount | None:
73
+ raise NotImplementedError("Implement OAuth adapter methods to use OAuth")
74
+
75
+ async def get_user_oauth_accounts(self, user_id: str) -> list[OAuthAccount]:
76
+ raise NotImplementedError("Implement OAuth adapter methods to use OAuth")
77
+
78
+ async def create_oauth_account(self, data: OAuthAccount) -> OAuthAccount:
79
+ raise NotImplementedError("Implement OAuth adapter methods to use OAuth")
80
+
81
+ async def update_oauth_account(
82
+ self, provider: str, provider_user_id: str, data: dict[str, Any]
83
+ ) -> OAuthAccount | None:
84
+ raise NotImplementedError("Implement OAuth adapter methods to use OAuth")
85
+
86
+ async def delete_oauth_account(self, provider: str, provider_user_id: str) -> None:
87
+ raise NotImplementedError("Implement OAuth adapter methods to use OAuth")
@@ -3,7 +3,7 @@ from typing import Any
3
3
  from uuid_utils import uuid7
4
4
 
5
5
  from fastapi_fullauth.adapters.base import AbstractUserAdapter
6
- from fastapi_fullauth.types import CreateUserSchema, RefreshToken, UserSchema
6
+ from fastapi_fullauth.types import CreateUserSchema, OAuthAccount, RefreshToken, UserSchema
7
7
 
8
8
 
9
9
  class InMemoryAdapter(AbstractUserAdapter):
@@ -13,6 +13,7 @@ class InMemoryAdapter(AbstractUserAdapter):
13
13
  self._passwords: dict[str, str] = {}
14
14
  self._refresh_tokens: dict[str, RefreshToken] = {}
15
15
  self._roles: dict[str, list[str]] = {}
16
+ self._oauth_accounts: dict[tuple[str, str], OAuthAccount] = {}
16
17
 
17
18
  async def get_user_by_id(self, user_id: str) -> UserSchema | None:
18
19
  data = self._users.get(user_id)
@@ -82,6 +83,11 @@ class InMemoryAdapter(AbstractUserAdapter):
82
83
  if tok.family_id == family_id:
83
84
  tok.revoked = True
84
85
 
86
+ async def revoke_all_user_refresh_tokens(self, user_id: str) -> None:
87
+ for tok in self._refresh_tokens.values():
88
+ if tok.user_id == user_id:
89
+ tok.revoked = True
90
+
85
91
  async def set_user_verified(self, user_id: str) -> None:
86
92
  if user_id in self._users:
87
93
  self._users[user_id]["is_verified"] = True
@@ -99,3 +105,29 @@ class InMemoryAdapter(AbstractUserAdapter):
99
105
  roles.remove(role_name)
100
106
  if user_id in self._users:
101
107
  self._users[user_id]["roles"] = roles
108
+
109
+ # ── OAuth ────────────────────────────────────────────────────────
110
+
111
+ async def get_oauth_account(self, provider: str, provider_user_id: str) -> OAuthAccount | None:
112
+ return self._oauth_accounts.get((provider, provider_user_id))
113
+
114
+ async def get_user_oauth_accounts(self, user_id: str) -> list[OAuthAccount]:
115
+ return [a for a in self._oauth_accounts.values() if a.user_id == user_id]
116
+
117
+ async def create_oauth_account(self, data: OAuthAccount) -> OAuthAccount:
118
+ self._oauth_accounts[(data.provider, data.provider_user_id)] = data
119
+ return data
120
+
121
+ async def update_oauth_account(
122
+ self, provider: str, provider_user_id: str, data: dict[str, Any]
123
+ ) -> OAuthAccount | None:
124
+ key = (provider, provider_user_id)
125
+ account = self._oauth_accounts.get(key)
126
+ if account is None:
127
+ return None
128
+ updated = account.model_copy(update=data)
129
+ self._oauth_accounts[key] = updated
130
+ return updated
131
+
132
+ async def delete_oauth_account(self, provider: str, provider_user_id: str) -> None:
133
+ self._oauth_accounts.pop((provider, provider_user_id), None)
@@ -6,11 +6,12 @@ from sqlalchemy.orm import selectinload
6
6
 
7
7
  from fastapi_fullauth.adapters.base import AbstractUserAdapter
8
8
  from fastapi_fullauth.adapters.sqlalchemy.models import (
9
+ OAuthAccountModel,
9
10
  RefreshTokenModel,
10
11
  RoleModel,
11
12
  UserBase,
12
13
  )
13
- from fastapi_fullauth.types import CreateUserSchema, RefreshToken, UserSchema
14
+ from fastapi_fullauth.types import CreateUserSchema, OAuthAccount, RefreshToken, UserSchema
14
15
 
15
16
  # Map SQLAlchemy column types to Python types for schema auto-derivation
16
17
  _SA_TYPE_MAP: dict[type, type] = {}
@@ -203,6 +204,15 @@ class SQLAlchemyAdapter(AbstractUserAdapter):
203
204
  )
204
205
  await session.commit()
205
206
 
207
+ async def revoke_all_user_refresh_tokens(self, user_id: str) -> None:
208
+ async with self._session_maker() as session:
209
+ await session.execute(
210
+ update(RefreshTokenModel)
211
+ .where(RefreshTokenModel.user_id == user_id)
212
+ .values(revoked=True)
213
+ )
214
+ await session.commit()
215
+
206
216
  async def set_user_verified(self, user_id: str) -> None:
207
217
  async with self._session_maker() as session:
208
218
  await session.execute(
@@ -244,3 +254,77 @@ class SQLAlchemyAdapter(AbstractUserAdapter):
244
254
  if user:
245
255
  user.roles = [r for r in user.roles if r.name != role_name]
246
256
  await session.commit()
257
+
258
+ # ── OAuth ────────────────────────────────────────────────────────
259
+
260
+ def _to_oauth_account(self, row: OAuthAccountModel) -> OAuthAccount:
261
+ return OAuthAccount(
262
+ provider=row.provider,
263
+ provider_user_id=row.provider_user_id,
264
+ user_id=str(row.user_id),
265
+ provider_email=row.provider_email,
266
+ access_token=row.access_token,
267
+ refresh_token=row.refresh_token,
268
+ expires_at=row.expires_at,
269
+ )
270
+
271
+ async def get_oauth_account(self, provider: str, provider_user_id: str) -> OAuthAccount | None:
272
+ async with self._session_maker() as session:
273
+ result = await session.execute(
274
+ select(OAuthAccountModel).where(
275
+ OAuthAccountModel.provider == provider,
276
+ OAuthAccountModel.provider_user_id == provider_user_id,
277
+ )
278
+ )
279
+ row = result.scalars().first()
280
+ return self._to_oauth_account(row) if row else None
281
+
282
+ async def get_user_oauth_accounts(self, user_id: str) -> list[OAuthAccount]:
283
+ async with self._session_maker() as session:
284
+ result = await session.execute(
285
+ select(OAuthAccountModel).where(OAuthAccountModel.user_id == user_id)
286
+ )
287
+ return [self._to_oauth_account(row) for row in result.scalars().all()]
288
+
289
+ async def create_oauth_account(self, data: OAuthAccount) -> OAuthAccount:
290
+ async with self._session_maker() as session:
291
+ record = OAuthAccountModel(
292
+ provider=data.provider,
293
+ provider_user_id=data.provider_user_id,
294
+ user_id=data.user_id,
295
+ provider_email=data.provider_email,
296
+ access_token=data.access_token,
297
+ refresh_token=data.refresh_token,
298
+ expires_at=data.expires_at,
299
+ )
300
+ session.add(record)
301
+ await session.commit()
302
+ return data
303
+
304
+ async def update_oauth_account(
305
+ self, provider: str, provider_user_id: str, data: dict[str, Any]
306
+ ) -> OAuthAccount | None:
307
+ async with self._session_maker() as session:
308
+ await session.execute(
309
+ update(OAuthAccountModel)
310
+ .where(
311
+ OAuthAccountModel.provider == provider,
312
+ OAuthAccountModel.provider_user_id == provider_user_id,
313
+ )
314
+ .values(**data)
315
+ )
316
+ await session.commit()
317
+ return await self.get_oauth_account(provider, provider_user_id)
318
+
319
+ async def delete_oauth_account(self, provider: str, provider_user_id: str) -> None:
320
+ async with self._session_maker() as session:
321
+ result = await session.execute(
322
+ select(OAuthAccountModel).where(
323
+ OAuthAccountModel.provider == provider,
324
+ OAuthAccountModel.provider_user_id == provider_user_id,
325
+ )
326
+ )
327
+ row = result.scalars().first()
328
+ if row:
329
+ await session.delete(row)
330
+ await session.commit()