fastapi-fullauth 0.7.0__tar.gz → 0.9.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 (152) hide show
  1. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/CHANGELOG.md +67 -5
  2. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/PKG-INFO +22 -9
  3. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/README.md +17 -7
  4. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/docs/adapters/index.md +31 -14
  5. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/docs/adapters/sqlalchemy.md +5 -4
  6. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/docs/adapters/sqlmodel.md +14 -13
  7. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/docs/api-reference.md +4 -2
  8. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/docs/configuration.md +73 -2
  9. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/docs/getting-started.md +15 -6
  10. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/docs/index.md +11 -4
  11. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/docs/llms-full.txt +157 -53
  12. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/docs/migrations.md +33 -12
  13. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/docs/security/middleware.md +4 -1
  14. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/docs/security/rate-limiting.md +23 -0
  15. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/examples/sqlalchemy_app/models.py +2 -2
  16. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/examples/sqlmodel_app/models.py +2 -6
  17. fastapi_fullauth-0.9.0/fastapi_fullauth/.agents/skills/fastapi-fullauth/SKILL.md +84 -0
  18. fastapi_fullauth-0.9.0/fastapi_fullauth/.agents/skills/fastapi-fullauth/references/adapters.md +182 -0
  19. fastapi_fullauth-0.9.0/fastapi_fullauth/.agents/skills/fastapi-fullauth/references/api-reference.md +385 -0
  20. fastapi_fullauth-0.9.0/fastapi_fullauth/.agents/skills/fastapi-fullauth/references/composable-design.md +116 -0
  21. fastapi_fullauth-0.9.0/fastapi_fullauth/.agents/skills/fastapi-fullauth/references/getting-started.md +180 -0
  22. fastapi_fullauth-0.9.0/fastapi_fullauth/.agents/skills/fastapi-fullauth/references/hooks.md +121 -0
  23. fastapi_fullauth-0.9.0/fastapi_fullauth/.agents/skills/fastapi-fullauth/references/migrations.md +137 -0
  24. fastapi_fullauth-0.9.0/fastapi_fullauth/.agents/skills/fastapi-fullauth/references/oauth.md +156 -0
  25. fastapi_fullauth-0.9.0/fastapi_fullauth/.agents/skills/fastapi-fullauth/references/passkeys.md +169 -0
  26. fastapi_fullauth-0.9.0/fastapi_fullauth/.agents/skills/fastapi-fullauth/references/production.md +147 -0
  27. fastapi_fullauth-0.9.0/fastapi_fullauth/.agents/skills/fastapi-fullauth/references/rbac.md +130 -0
  28. fastapi_fullauth-0.9.0/fastapi_fullauth/.agents/skills/fastapi-fullauth/references/testing.md +239 -0
  29. fastapi_fullauth-0.9.0/fastapi_fullauth/.agents/skills/fastapi-fullauth/references/troubleshooting.md +129 -0
  30. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/__init__.py +1 -1
  31. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/adapters/__init__.py +15 -11
  32. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/adapters/base.py +82 -29
  33. fastapi_fullauth-0.9.0/fastapi_fullauth/adapters/sqlalchemy/__init__.py +3 -0
  34. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/adapters/sqlalchemy/adapter.py +198 -16
  35. fastapi_fullauth-0.9.0/fastapi_fullauth/adapters/sqlalchemy/models/__init__.py +49 -0
  36. fastapi_fullauth-0.9.0/fastapi_fullauth/adapters/sqlalchemy/models/base.py +51 -0
  37. fastapi_fullauth-0.9.0/fastapi_fullauth/adapters/sqlalchemy/models/oauth.py +29 -0
  38. fastapi_fullauth-0.9.0/fastapi_fullauth/adapters/sqlalchemy/models/passkey.py +29 -0
  39. fastapi_fullauth-0.9.0/fastapi_fullauth/adapters/sqlalchemy/models/permission.py +26 -0
  40. fastapi_fullauth-0.9.0/fastapi_fullauth/adapters/sqlalchemy/models/role.py +25 -0
  41. fastapi_fullauth-0.9.0/fastapi_fullauth/adapters/sqlmodel/__init__.py +3 -0
  42. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/adapters/sqlmodel/adapter.py +194 -18
  43. fastapi_fullauth-0.9.0/fastapi_fullauth/adapters/sqlmodel/models/__init__.py +42 -0
  44. fastapi_fullauth-0.9.0/fastapi_fullauth/adapters/sqlmodel/models/base.py +46 -0
  45. fastapi_fullauth-0.9.0/fastapi_fullauth/adapters/sqlmodel/models/oauth.py +28 -0
  46. fastapi_fullauth-0.9.0/fastapi_fullauth/adapters/sqlmodel/models/passkey.py +26 -0
  47. fastapi_fullauth-0.9.0/fastapi_fullauth/adapters/sqlmodel/models/permission.py +19 -0
  48. fastapi_fullauth-0.9.0/fastapi_fullauth/adapters/sqlmodel/models/role.py +18 -0
  49. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/backends/bearer.py +1 -1
  50. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/backends/cookie.py +14 -8
  51. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/config.py +49 -1
  52. fastapi_fullauth-0.9.0/fastapi_fullauth/core/challenges.py +102 -0
  53. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/core/crypto.py +11 -0
  54. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/core/tokens.py +6 -1
  55. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/dependencies/require_role.py +3 -1
  56. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/flows/__init__.py +8 -0
  57. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/flows/email_verify.py +2 -2
  58. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/flows/login.py +1 -1
  59. fastapi_fullauth-0.9.0/fastapi_fullauth/flows/oauth.py +198 -0
  60. fastapi_fullauth-0.9.0/fastapi_fullauth/flows/passkey.py +248 -0
  61. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/flows/password_reset.py +1 -0
  62. fastapi_fullauth-0.9.0/fastapi_fullauth/flows/set_password.py +39 -0
  63. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/fullauth.py +154 -36
  64. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/hooks.py +15 -1
  65. fastapi_fullauth-0.9.0/fastapi_fullauth/migrations.py +101 -0
  66. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/protection/__init__.py +9 -1
  67. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/protection/lockout.py +35 -9
  68. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/protection/ratelimit.py +65 -5
  69. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/router/_models.py +4 -0
  70. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/router/auth.py +37 -24
  71. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/router/oauth.py +2 -1
  72. fastapi_fullauth-0.9.0/fastapi_fullauth/router/passkey.py +254 -0
  73. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/router/profile.py +35 -1
  74. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/types.py +22 -2
  75. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/utils.py +4 -0
  76. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/pyproject.toml +8 -1
  77. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/tests/conftest.py +11 -9
  78. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/tests/test_auth.py +75 -0
  79. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/tests/test_config.py +83 -0
  80. fastapi_fullauth-0.9.0/tests/test_cookie_backend.py +56 -0
  81. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/tests/test_oauth.py +45 -5
  82. fastapi_fullauth-0.9.0/tests/test_passkey.py +180 -0
  83. fastapi_fullauth-0.9.0/tests/test_polish.py +181 -0
  84. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/tests/test_rbac.py +14 -4
  85. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/tests/test_sqlalchemy_adapter.py +26 -5
  86. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/tests/test_sqlmodel_adapter.py +68 -3
  87. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/uv.lock +149 -2
  88. fastapi_fullauth-0.7.0/fastapi_fullauth/adapters/sqlalchemy/__init__.py +0 -21
  89. fastapi_fullauth-0.7.0/fastapi_fullauth/adapters/sqlalchemy/models.py +0 -105
  90. fastapi_fullauth-0.7.0/fastapi_fullauth/adapters/sqlmodel/__init__.py +0 -21
  91. fastapi_fullauth-0.7.0/fastapi_fullauth/adapters/sqlmodel/models.py +0 -92
  92. fastapi_fullauth-0.7.0/fastapi_fullauth/flows/oauth.py +0 -143
  93. fastapi_fullauth-0.7.0/fastapi_fullauth/migrations.py +0 -61
  94. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  95. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  96. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/.github/pull_request_template.md +0 -0
  97. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/.github/workflows/ci.yml +0 -0
  98. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/.github/workflows/docs.yml +0 -0
  99. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/.github/workflows/publish.yml +0 -0
  100. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/.gitignore +0 -0
  101. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/.python-version +0 -0
  102. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/CONTRIBUTING.md +0 -0
  103. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/LICENSE +0 -0
  104. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/Makefile +0 -0
  105. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/docs/auth/custom-claims.md +0 -0
  106. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/docs/auth/dependencies.md +0 -0
  107. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/docs/auth/hooks.md +0 -0
  108. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/docs/auth/passwords.md +0 -0
  109. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/docs/contributing.md +0 -0
  110. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/docs/llms.txt +0 -0
  111. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/docs/oauth.md +0 -0
  112. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/docs/stylesheets/home.css +0 -0
  113. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/examples/sqlalchemy_app/__init__.py +0 -0
  114. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/examples/sqlalchemy_app/auth.py +0 -0
  115. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/examples/sqlalchemy_app/config.py +0 -0
  116. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/examples/sqlalchemy_app/main.py +0 -0
  117. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/examples/sqlalchemy_app/routes.py +0 -0
  118. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/examples/sqlmodel_app/__init__.py +0 -0
  119. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/examples/sqlmodel_app/auth.py +0 -0
  120. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/examples/sqlmodel_app/config.py +0 -0
  121. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/examples/sqlmodel_app/main.py +0 -0
  122. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/examples/sqlmodel_app/routes.py +0 -0
  123. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/backends/__init__.py +0 -0
  124. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/backends/base.py +0 -0
  125. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/core/__init__.py +0 -0
  126. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/core/redis_blacklist.py +0 -0
  127. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/dependencies/__init__.py +0 -0
  128. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/dependencies/current_user.py +0 -0
  129. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/exceptions.py +0 -0
  130. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/flows/change_password.py +0 -0
  131. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/flows/logout.py +0 -0
  132. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/flows/register.py +0 -0
  133. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/flows/update_profile.py +0 -0
  134. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/middleware/__init__.py +0 -0
  135. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/middleware/csrf.py +0 -0
  136. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/middleware/ratelimit.py +0 -0
  137. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/middleware/security_headers.py +0 -0
  138. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/oauth/__init__.py +0 -0
  139. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/oauth/base.py +0 -0
  140. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/oauth/github.py +0 -0
  141. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/oauth/google.py +0 -0
  142. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/router/__init__.py +0 -0
  143. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/router/admin.py +0 -0
  144. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/router/verify.py +0 -0
  145. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/validators.py +0 -0
  146. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/mkdocs.yml +0 -0
  147. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/tests/__init__.py +0 -0
  148. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/tests/test_crypto.py +0 -0
  149. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/tests/test_hooks.py +0 -0
  150. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/tests/test_profile.py +0 -0
  151. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/tests/test_security.py +0 -0
  152. {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/tests/test_tokens.py +0 -0
@@ -1,5 +1,71 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.0
4
+
5
+ ### Breaking changes
6
+
7
+ - **Lockout now returns `401`** instead of `423 Locked`. Clients that branched on `423` to render a "your account is locked" UI will silently fall into the generic credentials-error path. The change is deliberate — see Security below.
8
+ - **Email lookup is now case-insensitive.** On case-sensitive database collations (MySQL default, SQL Server), rows registered with mixed-case emails (`Alice@X.com`) will stop matching logins submitted in a different case. Run a one-off `UPDATE fullauth_users SET email = LOWER(TRIM(email))` before upgrading. PostgreSQL/SQLite with default collations are unaffected.
9
+
10
+ ### Security
11
+
12
+ - Emails are now normalised (stripped + lowercased) on create, update, and lookup in both built-in adapters. Previously `Alice@X.com` and `alice@X.com` could register as separate accounts on case-sensitive collations (MySQL default, SQL Server).
13
+ - Login now returns the same generic `401 Could not validate credentials` response for a locked account as for a wrong password. Previously a `423 Locked` status let an attacker distinguish "email exists and is locked out" from "wrong password" — an enumeration signal once they'd exhausted the lockout counter on a target email. The `AccountLockedError` message no longer includes the identifier (cleaner logs too).
14
+ - Opt-in `PREVENT_REGISTRATION_ENUMERATION` setting (default `False`). When `True`, `/register` always responds `202` + `{"detail": "If this email isn't already registered, a verification email has been sent."}` whether the email was taken or not — attackers can't probe the user table through the registration endpoint. Off by default to keep the `201` + user / `409` conflict shape that most client apps expect.
15
+
16
+ ### Fixed
17
+
18
+ - `BearerBackend` accepts any case of the `Bearer` auth scheme (`Bearer`, `bearer`, `BEARER`, mixed) per RFC 7235. Clients that sent a lowercase scheme were previously rejected with a 401.
19
+ - `require_role` tolerates a `UserSchema` subclass with no `roles` field — returns a clean `403` instead of `AttributeError` / `500`. The default schema doesn't ship with `roles`; apps using RBAC still need to add it to their custom schema.
20
+ - `hash_password(..., algorithm="bcrypt")` rejects passwords over 72 UTF-8 bytes with `InvalidPasswordError` instead of silently truncating. bcrypt's built-in truncation would otherwise cause subtle lockouts if an app later migrated to argon2id.
21
+ - SQLModel `UserBase.hashed_password` column is now `Text`. Argon2id hashes are ~97 characters; MySQL / MSSQL default `VARCHAR(255)` was still fine but the column type is explicit now, matching the SQLAlchemy adapter.
22
+ - `FullAuthConfig` validates passkey settings at construction time when `PASSKEY_ENABLED=True` — empty `PASSKEY_RP_ID` / `PASSKEY_ORIGINS`, RP ID with scheme or path, origin without scheme, and Redis backend without `REDIS_URL` all raise at config creation instead of surfacing as 500s at first request.
23
+
24
+ ## 0.8.0
25
+
26
+ ### Security
27
+
28
+ - OAuth auto-link-by-email now requires `info.email_verified=True` from the provider when an account with that email already exists. Without this gate, any provider that returns an unverified email (e.g. GitHub secondary addresses) could be used to hijack an existing account by registering the provider with the victim's email.
29
+ - Cookie backend's `delete_token` now matches the same `secure`/`samesite`/`path`/`domain` attributes used on set. Browsers ignore (or reject, for `SameSite=None`) a deletion that doesn't match — logout previously left the cookie in place on some setups.
30
+ - Refresh-token revocation is now an atomic compare-and-swap (`UPDATE ... WHERE revoked=false`). Two concurrent refresh calls with the same token can no longer both succeed by racing the old stored-state check. `AbstractUserAdapter.revoke_refresh_token` now returns `bool` — custom adapters should honour the CAS semantics.
31
+ - `create_user` catches `IntegrityError` from duplicate-email races and raises `UserAlreadyExistsError`. The register flow's pre-check only guards the common case; concurrent signups used to surface as 500s.
32
+ - OAuth account table now has a composite unique constraint on `(provider, provider_user_id)`. Existing SQL users should autogenerate an Alembic migration to add it. `create_oauth_account` now returns the existing row on concurrent-insert collisions instead of erroring.
33
+ - Password-reset and email-verification tokens now use their own TTLs (`PASSWORD_RESET_EXPIRE_MINUTES`, default 15; `EMAIL_VERIFY_EXPIRE_MINUTES`, default 1440) instead of inheriting `ACCESS_TOKEN_EXPIRE_MINUTES`. A production tweak to access-token lifetime for mobile clients no longer silently extends the window in which a stolen password-reset email grants an account takeover.
34
+
35
+ ### Breaking changes
36
+
37
+ - **Models split into packages** — `adapters/sqlmodel/models.py` and `adapters/sqlalchemy/models.py` are now `models/` directories with `base.py`, `role.py`, `permission.py`, `oauth.py`. Old import paths (`from fastapi_fullauth.adapters.sqlmodel.models import ...`) still work via `__init__.py` re-exports. New selective imports: `from fastapi_fullauth.adapters.sqlmodel.models.base import UserBase, RefreshTokenRecord`.
38
+ - **`roles` removed from default `UserSchema`** — apps that use roles should extend `UserSchema` with `roles: list[str] = Field(default_factory=list)`. Apps without roles are unaffected.
39
+ - **Admin router auto-skipped** when adapter doesn't implement `RoleAdapterMixin`. OAuth/passkey routers auto-skipped similarly.
40
+ - **`AbstractUserAdapter.revoke_refresh_token` now returns `bool`** — custom adapters need to return `True` only when the token actually transitioned from not-revoked to revoked (CAS semantics).
41
+
42
+ ### Added
43
+
44
+ - **Composable models** — only imported model groups register tables. Apps that don't need roles/permissions/oauth skip those tables entirely.
45
+ - **Selective migration helper** — `include_fullauth_models("sqlmodel", include=["base", "role"])` imports only specified model groups for Alembic.
46
+ - **`exclude_routers` param on `init_app()`** — `fullauth.init_app(app, exclude_routers=["admin"])` to skip routers you don't need.
47
+ - **`bind(app)` method** — bind FullAuth to a FastAPI app for composable router usage. Called automatically by `init_app()` and `init_middleware()`.
48
+ - **`init_middleware()` method** — wire up middleware independently when using composable routers.
49
+ - **`RouterName` type** — `Literal["auth", "profile", "verify", "admin", "oauth"]` for type-safe router exclusion.
50
+ - **`AuthRateLimiter` class** — per-route auth rate limiting extracted from FullAuth into its own class.
51
+ - **`exchange_oauth_code()`, `link_or_create_user()`, `issue_oauth_tokens()`** — OAuth callback split into composable flow functions. `oauth_callback()` still works as before (delegates to the three).
52
+ - **`register_lockout_backend()`** — register custom lockout backends for `create_lockout()` factory.
53
+ - **`register_rate_limiter_backend()`** — register custom rate limiter backends for `create_rate_limiter()` factory.
54
+ - **Passkey (WebAuthn) authentication** — passwordless login with fingerprint, Face ID, security keys. Register, authenticate, list, and delete passkeys. Requires `pip install fastapi-fullauth[passkey]` and `PASSKEY_ENABLED=True`.
55
+ - **`ChallengeStore`** — abstract challenge store with InMemory and Redis backends for WebAuthn flows.
56
+ - **`PasskeyAdapterMixin`** — adapter mixin for passkey credential persistence.
57
+ - **Adapter mixins** — `AbstractUserAdapter` split into composable interfaces: `RoleAdapterMixin`, `PermissionAdapterMixin`, `OAuthAdapterMixin`, `PasskeyAdapterMixin`. Custom adapters implement only what they need. Built-in adapters inherit all mixins (backward compatible).
58
+
59
+ ### Changed
60
+
61
+ - Adapter model imports are lazy — importing the adapter no longer registers role/permission/oauth tables
62
+ - Rate limiting extracted from FullAuth `__init__` into `AuthRateLimiter`
63
+ - SQLModelAdapter `session_maker` type hint accepts both session types cleanly
64
+ - `TokenClaimsBuilder` and `RouterName` moved to `types.py`
65
+ - `init_app()` and `init_middleware()` are now idempotent. Calling either twice on the same FastAPI app emits a `UserWarning` and is a no-op. Previously a second call (e.g. `init_app(app)` followed by a stray `init_middleware(app)`) doubled the middleware stack — duplicate security headers, two rate-limiter instances halving the effective limit, and a CSRF layer validating another CSRF layer's cookies.
66
+ - JWT decode now tolerates clock drift between services via `JWT_LEEWAY_SECONDS` (default 30). Eliminates sporadic 401s caused by ±30 s skew between client and server clocks or across load-balanced instances.
67
+ - `FullAuthConfig` reads `.env` in the current working directory by default (`env_file=".env"`), and ignores unknown `FULLAUTH_*` vars instead of erroring (`extra="ignore"`). Local dev "just works" without passing `_env_file=".env"` explicitly. Cloud deployments are unaffected — pydantic-settings' precedence is init kwargs → `os.environ` → `.env` → defaults, so platform-injected env vars always win, and a missing `.env` is a silent no-op. Use `FullAuthConfig(_env_file="…")` or a `SettingsConfigDict` subclass to read a different file.
68
+
3
69
  ## 0.7.0
4
70
 
5
71
  ### Breaking changes
@@ -9,9 +75,6 @@
9
75
  - **OAuth providers passed as objects** — `FullAuth(providers=[GoogleOAuthProvider(...)])` replaces `OAUTH_PROVIDERS` dict in config. `OAuthProviderConfig` removed.
10
76
  - **`OAuthProvider` simplified** — only `redirect_uris: list[str]` (removed singular `redirect_uri`). `get_redirect_uri()` removed.
11
77
  - **`redirect_uri` required in authorize URL** — clients must pass `?redirect_uri=` in the OAuth authorize request.
12
-
13
- ### Breaking changes (audit cleanup)
14
-
15
78
  - **`include_user_in_login` moved to config** — use `FullAuthConfig(INCLUDE_USER_IN_LOGIN=True)` or `FULLAUTH_INCLUDE_USER_IN_LOGIN=true` env var instead of `FullAuth(include_user_in_login=True)`.
16
79
  - **Login response always includes `user` field** — when `INCLUDE_USER_IN_LOGIN=False`, `user` is `null` (previously the key was absent). When `True`, `user` contains the full user schema object.
17
80
 
@@ -34,9 +97,8 @@
34
97
  - `InMemoryLockoutManager` replaces the old sync `LockoutManager`
35
98
  - `migrations/` package flattened to single `migrations.py` module (import paths unchanged)
36
99
  - 4 `type: ignore` comments fixed (replaced with `getattr`, assertions, `model_validate`)
37
- - Misplaced `type: ignore` comments moved inline
38
100
  - 204 routes (`delete_me`, `unlink_oauth_account`) no longer return unnecessary `Response` objects
39
- - Logout route return type corrected to `Response` (needs it for cookie deletion)
101
+ - Logout route return type corrected to `Response`
40
102
  - All tests migrated from InMemory to SQLModel + SQLite
41
103
  - Tests regrouped: `test_auth`, `test_profile`, `test_config`, `test_hooks`, `test_security`, `test_rbac`
42
104
  - `UUID(payload.sub)` conversion at token boundaries (dependencies, router, flows)
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-fullauth
3
- Version: 0.7.0
3
+ Version: 0.9.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://mdfarhankc.github.io/fastapi-fullauth
7
7
  Project-URL: Repository, https://github.com/mdfarhankc/fastapi-fullauth
8
8
  License-Expression: MIT
9
9
  License-File: LICENSE
10
- Keywords: argon2,async,authentication,authorization,bcrypt,csrf,fastapi,jwt,oauth2,password-hashing,pydantic,rbac,redis,refresh-token,role-based-access-control,security,sqlalchemy,sqlmodel
10
+ Keywords: argon2,async,authentication,authorization,bcrypt,csrf,fastapi,jwt,oauth2,passkey,password-hashing,pydantic,rbac,redis,refresh-token,role-based-access-control,security,sqlalchemy,sqlmodel,webauthn
11
11
  Classifier: Development Status :: 3 - Alpha
12
12
  Classifier: Framework :: FastAPI
13
13
  Classifier: Intended Audience :: Developers
@@ -32,8 +32,11 @@ Requires-Dist: httpx>=0.25; extra == 'all'
32
32
  Requires-Dist: redis>=5.0; extra == 'all'
33
33
  Requires-Dist: sqlalchemy[asyncio]>=2.0; extra == 'all'
34
34
  Requires-Dist: sqlmodel>=0.0.16; extra == 'all'
35
+ Requires-Dist: webauthn>=2.0; extra == 'all'
35
36
  Provides-Extra: oauth
36
37
  Requires-Dist: httpx>=0.25; extra == 'oauth'
38
+ Provides-Extra: passkey
39
+ Requires-Dist: webauthn>=2.0; extra == 'passkey'
37
40
  Provides-Extra: redis
38
41
  Requires-Dist: redis>=5.0; extra == 'redis'
39
42
  Provides-Extra: sqlalchemy
@@ -78,6 +81,7 @@ Add a complete authentication and authorization system to your **FastAPI** proje
78
81
  - **Refresh token rotation** with reuse detection — revokes entire session family on replay
79
82
  - **Password hashing** via Argon2id (default) or bcrypt, with transparent rehashing
80
83
  - **Email verification** and **password reset** flows with event hooks
84
+ - **Passkey (WebAuthn)** — passwordless login with fingerprint, Face ID, security keys
81
85
  - **OAuth2 social login** — Google and GitHub, with multi-redirect-URI support
82
86
  - **Role-based access control** — `CurrentUser`, `VerifiedUser`, `SuperUser`, `require_role()`
83
87
  - **Rate limiting** — per-route auth limits + global middleware (memory or Redis)
@@ -106,6 +110,9 @@ pip install fastapi-fullauth[sqlmodel,redis]
106
110
  # with OAuth2 social login
107
111
  pip install fastapi-fullauth[sqlmodel,oauth]
108
112
 
113
+ # with passkey/WebAuthn
114
+ pip install fastapi-fullauth[sqlmodel,passkey]
115
+
109
116
  # everything
110
117
  pip install fastapi-fullauth[all]
111
118
  ```
@@ -132,16 +139,21 @@ Omit `config` in dev and a random secret key is generated (tokens won't survive
132
139
 
133
140
  ### Composable routers
134
141
 
135
- Include only the route groups you need:
142
+ Exclude routers you don't need:
143
+
144
+ ```python
145
+ fullauth.init_app(app, exclude_routers=["admin"])
146
+ ```
147
+
148
+ Or wire routers manually for full control:
136
149
 
137
150
  ```python
138
151
  app = FastAPI()
139
- app.state.fullauth = fullauth
152
+ fullauth.bind(app) # required for dependencies to work
140
153
 
141
- # pick what you want
142
154
  app.include_router(fullauth.auth_router, prefix="/api/v1/auth")
143
155
  app.include_router(fullauth.profile_router, prefix="/api/v1/auth")
144
- # skip verify, admin, oauth
156
+ fullauth.init_middleware(app)
145
157
  ```
146
158
 
147
159
  | Router | Routes |
@@ -151,6 +163,7 @@ app.include_router(fullauth.profile_router, prefix="/api/v1/auth")
151
163
  | `verify_router` | email verification, password reset |
152
164
  | `admin_router` | assign/remove roles and permissions (superuser) |
153
165
  | `oauth_router` | OAuth provider routes (only if configured) |
166
+ | `passkey_router` | Passkey register, authenticate, list, delete (only if enabled) |
154
167
 
155
168
  `fullauth.init_app(app)` includes all of them. Use individual routers for granular control.
156
169
 
@@ -186,9 +199,9 @@ Define your model and schemas — pass them explicitly to the adapter:
186
199
  ```python
187
200
  from sqlmodel import Field, Relationship
188
201
  from fastapi_fullauth import FullAuth, FullAuthConfig, UserSchema, CreateUserSchema
189
- from fastapi_fullauth.adapters.sqlmodel import (
190
- UserBase, Role, UserRoleLink, RefreshTokenRecord, SQLModelAdapter,
191
- )
202
+ from fastapi_fullauth.adapters.sqlmodel import SQLModelAdapter
203
+ from fastapi_fullauth.adapters.sqlmodel.models.base import UserBase, RefreshTokenRecord
204
+ from fastapi_fullauth.adapters.sqlmodel.models.role import Role, UserRoleLink
192
205
 
193
206
  class User(UserBase, table=True):
194
207
  __tablename__ = "fullauth_users"
@@ -32,6 +32,7 @@ Add a complete authentication and authorization system to your **FastAPI** proje
32
32
  - **Refresh token rotation** with reuse detection — revokes entire session family on replay
33
33
  - **Password hashing** via Argon2id (default) or bcrypt, with transparent rehashing
34
34
  - **Email verification** and **password reset** flows with event hooks
35
+ - **Passkey (WebAuthn)** — passwordless login with fingerprint, Face ID, security keys
35
36
  - **OAuth2 social login** — Google and GitHub, with multi-redirect-URI support
36
37
  - **Role-based access control** — `CurrentUser`, `VerifiedUser`, `SuperUser`, `require_role()`
37
38
  - **Rate limiting** — per-route auth limits + global middleware (memory or Redis)
@@ -60,6 +61,9 @@ pip install fastapi-fullauth[sqlmodel,redis]
60
61
  # with OAuth2 social login
61
62
  pip install fastapi-fullauth[sqlmodel,oauth]
62
63
 
64
+ # with passkey/WebAuthn
65
+ pip install fastapi-fullauth[sqlmodel,passkey]
66
+
63
67
  # everything
64
68
  pip install fastapi-fullauth[all]
65
69
  ```
@@ -86,16 +90,21 @@ Omit `config` in dev and a random secret key is generated (tokens won't survive
86
90
 
87
91
  ### Composable routers
88
92
 
89
- Include only the route groups you need:
93
+ Exclude routers you don't need:
94
+
95
+ ```python
96
+ fullauth.init_app(app, exclude_routers=["admin"])
97
+ ```
98
+
99
+ Or wire routers manually for full control:
90
100
 
91
101
  ```python
92
102
  app = FastAPI()
93
- app.state.fullauth = fullauth
103
+ fullauth.bind(app) # required for dependencies to work
94
104
 
95
- # pick what you want
96
105
  app.include_router(fullauth.auth_router, prefix="/api/v1/auth")
97
106
  app.include_router(fullauth.profile_router, prefix="/api/v1/auth")
98
- # skip verify, admin, oauth
107
+ fullauth.init_middleware(app)
99
108
  ```
100
109
 
101
110
  | Router | Routes |
@@ -105,6 +114,7 @@ app.include_router(fullauth.profile_router, prefix="/api/v1/auth")
105
114
  | `verify_router` | email verification, password reset |
106
115
  | `admin_router` | assign/remove roles and permissions (superuser) |
107
116
  | `oauth_router` | OAuth provider routes (only if configured) |
117
+ | `passkey_router` | Passkey register, authenticate, list, delete (only if enabled) |
108
118
 
109
119
  `fullauth.init_app(app)` includes all of them. Use individual routers for granular control.
110
120
 
@@ -140,9 +150,9 @@ Define your model and schemas — pass them explicitly to the adapter:
140
150
  ```python
141
151
  from sqlmodel import Field, Relationship
142
152
  from fastapi_fullauth import FullAuth, FullAuthConfig, UserSchema, CreateUserSchema
143
- from fastapi_fullauth.adapters.sqlmodel import (
144
- UserBase, Role, UserRoleLink, RefreshTokenRecord, SQLModelAdapter,
145
- )
153
+ from fastapi_fullauth.adapters.sqlmodel import SQLModelAdapter
154
+ from fastapi_fullauth.adapters.sqlmodel.models.base import UserBase, RefreshTokenRecord
155
+ from fastapi_fullauth.adapters.sqlmodel.models.role import Role, UserRoleLink
146
156
 
147
157
  class User(UserBase, table=True):
148
158
  __tablename__ = "fullauth_users"
@@ -16,25 +16,35 @@ Adapters are the database layer for fastapi-fullauth. They implement `AbstractUs
16
16
 
17
17
  ## Custom adapters
18
18
 
19
- You can implement your own adapter for any database by subclassing `AbstractUserAdapter`:
19
+ Subclass `AbstractUserAdapter` for core auth. Add mixins for roles, permissions, or OAuth:
20
20
 
21
21
  ```python
22
- from fastapi_fullauth.adapters.base import AbstractUserAdapter
23
- from fastapi_fullauth.types import CreateUserSchema, RefreshToken, UserSchema
24
-
25
- class MongoAdapter(AbstractUserAdapter):
26
- async def get_user_by_id(self, user_id: str) -> UserSchema | None:
27
- ...
28
-
29
- async def get_user_by_email(self, email: str) -> UserSchema | None:
30
- ...
31
-
32
- async def create_user(self, data: CreateUserSchema, hashed_password: str) -> UserSchema:
33
- ...
22
+ from fastapi_fullauth.adapters.base import (
23
+ AbstractUserAdapter,
24
+ RoleAdapterMixin,
25
+ PermissionAdapterMixin,
26
+ OAuthAdapterMixin,
27
+ )
34
28
 
35
- # ... implement all abstract methods
29
+ # Minimal just auth
30
+ class MyAdapter(AbstractUserAdapter):
31
+ async def get_user_by_id(self, user_id): ...
32
+ async def get_user_by_email(self, email): ...
33
+ async def create_user(self, data, hashed_password): ...
34
+ # ... core methods only
35
+
36
+ # With roles and permissions
37
+ class MyFullAdapter(AbstractUserAdapter, RoleAdapterMixin, PermissionAdapterMixin):
38
+ # ... core + role + permission methods
39
+ pass
36
40
  ```
37
41
 
42
+ | Mixin | Methods | When to use |
43
+ |-------|---------|-------------|
44
+ | `RoleAdapterMixin` | `assign_role`, `remove_role`, `get_user_roles` | Role management |
45
+ | `PermissionAdapterMixin` | `get_role_permissions`, `assign_permission_to_role`, `remove_permission_from_role` | RBAC permissions |
46
+ | `OAuthAdapterMixin` | `get_oauth_account`, `create_oauth_account`, `update_oauth_account`, `delete_oauth_account`, `get_user_oauth_accounts` | OAuth providers |
47
+
38
48
  See the [source of AbstractUserAdapter](https://github.com/mdfarhankc/fastapi-fullauth/blob/main/fastapi_fullauth/adapters/base.py) for the full interface.
39
49
 
40
50
  ## Custom schemas
@@ -57,3 +67,10 @@ adapter = SQLModelAdapter(
57
67
  create_user_schema=MyCreateSchema,
58
68
  )
59
69
  ```
70
+
71
+ If your app uses roles, add `roles` to your custom schema:
72
+
73
+ ```python
74
+ class MyUserSchema(UserSchema):
75
+ roles: list[str] = Field(default_factory=list)
76
+ ```
@@ -15,9 +15,8 @@ pip install fastapi-fullauth[sqlalchemy]
15
15
  ```python
16
16
  from sqlalchemy import Boolean, Column, DateTime, String, Table, ForeignKey
17
17
  from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
18
- from fastapi_fullauth.adapters.sqlalchemy.models import (
19
- FullAuthBase, RoleModel, UserBase, user_role_table,
20
- )
18
+ from fastapi_fullauth.adapters.sqlalchemy.models.base import FullAuthBase, UserBase
19
+ from fastapi_fullauth.adapters.sqlalchemy.models.role import RoleModel
21
20
 
22
21
  class User(UserBase):
23
22
  __tablename__ = "fullauth_users"
@@ -27,7 +26,9 @@ class User(UserBase):
27
26
  phone: Mapped[str] = mapped_column(String(20), default="")
28
27
 
29
28
  # required relationships
30
- roles: Mapped[list[RoleModel]] = relationship(secondary=user_role_table)
29
+ roles: Mapped[list[RoleModel]] = relationship(
30
+ secondary="fullauth_user_roles", lazy="selectin",
31
+ )
31
32
  ```
32
33
 
33
34
  `UserBase` provides the same core fields as the SQLModel version: `id`, `email`, `hashed_password`, `is_active`, `is_verified`, `is_superuser`, `created_at`.
@@ -14,9 +14,8 @@ pip install fastapi-fullauth[sqlmodel]
14
14
 
15
15
  ```python
16
16
  from sqlmodel import Field, Relationship
17
- from fastapi_fullauth.adapters.sqlmodel import (
18
- UserBase, Role, UserRoleLink, RefreshTokenRecord,
19
- )
17
+ from fastapi_fullauth.adapters.sqlmodel.models.base import UserBase, RefreshTokenRecord
18
+ from fastapi_fullauth.adapters.sqlmodel.models.role import Role, UserRoleLink
20
19
 
21
20
  class User(UserBase, table=True):
22
21
  __tablename__ = "fullauth_users"
@@ -25,11 +24,13 @@ class User(UserBase, table=True):
25
24
  display_name: str = Field(default="", max_length=100)
26
25
  phone: str = Field(default="", max_length=20)
27
26
 
28
- # required relationships
27
+ # relationships — import only what you need
29
28
  roles: list[Role] = Relationship(link_model=UserRoleLink)
30
29
  refresh_tokens: list[RefreshTokenRecord] = Relationship()
31
30
  ```
32
31
 
32
+ Only tables for imported models are created. Skip `role` imports for apps that don't need roles.
33
+
33
34
  `UserBase` provides these fields:
34
35
 
35
36
  | Field | Type | Description |
@@ -96,15 +97,15 @@ fullauth = FullAuth(
96
97
 
97
98
  ## Tables created
98
99
 
99
- The SQLModel adapter uses these tables:
100
+ Tables are created based on which model groups you import:
100
101
 
101
- | Table | Purpose |
102
- |-------|---------|
103
- | `fullauth_users` | User accounts (your model) |
104
- | `fullauth_roles` | Role definitions |
105
- | `fullauth_user_roles` | User-role link table |
106
- | `fullauth_refresh_tokens` | Stored refresh tokens |
107
- | `fullauth_oauth_accounts` | Linked OAuth provider accounts |
102
+ | Group | Tables | Import from |
103
+ |-------|--------|-------------|
104
+ | Core (always) | `fullauth_users`, `fullauth_refresh_tokens` | `models.base` |
105
+ | Roles | `fullauth_roles`, `fullauth_user_roles` | `models.role` |
106
+ | Permissions | `fullauth_permissions`, `fullauth_role_permissions` | `models.permission` |
107
+ | OAuth | `fullauth_oauth_accounts` | `models.oauth` |
108
+ | Passkeys | `fullauth_passkeys` | `models.passkey` |
108
109
 
109
110
  ## Custom schemas
110
111
 
@@ -132,4 +133,4 @@ If you don't pass custom schemas, the base `UserSchema` and `CreateUserSchema` a
132
133
 
133
134
  ## OAuth support
134
135
 
135
- The SQLModel adapter includes full OAuth support. The `OAuthAccountRecord` model is included and the adapter implements all OAuth methods from `AbstractUserAdapter`.
136
+ The SQLModel adapter implements `OAuthAdapterMixin`. Import `OAuthAccountRecord` from `models.oauth` to register the table.
@@ -23,7 +23,9 @@ fullauth = FullAuth(
23
23
 
24
24
  | Method | Description |
25
25
  |--------|-------------|
26
- | `init_app(app, auto_middleware=True)` | Mount routes and middleware on a FastAPI app |
26
+ | `init_app(app, *, auto_middleware=True, exclude_routers=None)` | Mount routes and middleware on a FastAPI app. Pass `exclude_routers=["admin"]` to skip specific routers. |
27
+ | `bind(app)` | Bind FullAuth to a FastAPI app (sets `app.state.fullauth`). Required when using composable routers without `init_app()`. |
28
+ | `init_middleware(app)` | Wire up middleware from config. Also calls `bind()` if not already done. |
27
29
  | `hooks.on(event, callback)` | Register an event hook |
28
30
 
29
31
  ### Properties
@@ -38,6 +40,7 @@ fullauth = FullAuth(
38
40
  | `verify_router` | `APIRouter` | Email verification and password reset routes |
39
41
  | `admin_router` | `APIRouter` | Role/permission management routes (superuser) |
40
42
  | `oauth_router` | `APIRouter` | OAuth provider routes |
43
+ | `passkey_router` | `APIRouter` | Passkey WebAuthn routes |
41
44
 
42
45
  ## FullAuthConfig
43
46
 
@@ -70,7 +73,6 @@ class UserSchema(BaseModel):
70
73
  is_active: bool = True
71
74
  is_verified: bool = False
72
75
  is_superuser: bool = False
73
- roles: list[str] = []
74
76
 
75
77
  PROTECTED_FIELDS: ClassVar[set[str]] = {
76
78
  "id", "email", "hashed_password", "is_active",
@@ -35,6 +35,60 @@ Pass config inline or as an object:
35
35
  fullauth = FullAuth(adapter=adapter)
36
36
  ```
37
37
 
38
+ ## Reading from a `.env` file
39
+
40
+ `FullAuthConfig` reads a `.env` file in the current working directory by default (via pydantic-settings). Drop a `.env` next to your app entry point:
41
+
42
+ ```bash
43
+ # .env
44
+ FULLAUTH_SECRET_KEY=replace-me-with-32-random-bytes
45
+ FULLAUTH_ACCESS_TOKEN_EXPIRE_MINUTES=15
46
+ FULLAUTH_BLACKLIST_BACKEND=redis
47
+ FULLAUTH_REDIS_URL=redis://localhost:6379/0
48
+ ```
49
+
50
+ Then `FullAuthConfig()` picks it up — no extra wiring needed.
51
+
52
+ ### Precedence
53
+
54
+ pydantic-settings resolves values in this order, first wins:
55
+
56
+ 1. Init kwargs — `FullAuthConfig(SECRET_KEY="...")`
57
+ 2. Process environment — `os.environ["FULLAUTH_SECRET_KEY"]`
58
+ 3. `.env` file
59
+ 4. Field defaults
60
+
61
+ So anything you export in your shell, in `uvicorn --env-file`, or in Docker `env_file:` overrides the dotfile. The dotfile is only read if the variable isn't already in `os.environ`.
62
+
63
+ ### Using a different file
64
+
65
+ Pass `_env_file` at construction:
66
+
67
+ ```python
68
+ FullAuthConfig(_env_file=".env.production")
69
+ ```
70
+
71
+ Or subclass once in your app:
72
+
73
+ ```python
74
+ from fastapi_fullauth import FullAuthConfig
75
+ from pydantic_settings import SettingsConfigDict
76
+
77
+ class AppFullAuthConfig(FullAuthConfig):
78
+ model_config = SettingsConfigDict(
79
+ env_prefix="FULLAUTH_",
80
+ case_sensitive=True,
81
+ env_file=".env.local",
82
+ extra="ignore",
83
+ )
84
+ ```
85
+
86
+ ### Cloud / container deployments
87
+
88
+ You don't need to change anything. Managed platforms (FastAPI Cloud, Fly, Railway, Render), Docker, and Kubernetes inject config as real environment variables — those end up in `os.environ` inside the container. The `.env` default simply doesn't find a file to read and falls through to the process env. No overhead, no surprises.
89
+
90
+ If you want to be defensively explicit that no file is ever read, pass `FullAuthConfig(_env_file=None)` — but it's not required.
91
+
38
92
  ## Reference
39
93
 
40
94
  ### Core
@@ -51,6 +105,9 @@ Pass config inline or as an object:
51
105
  | `ACCESS_TOKEN_EXPIRE_MINUTES` | `int` | `30` | Access token lifetime. |
52
106
  | `REFRESH_TOKEN_EXPIRE_DAYS` | `int` | `30` | Refresh token lifetime. |
53
107
  | `REFRESH_TOKEN_ROTATION` | `bool` | `True` | Issue new refresh token on each refresh. |
108
+ | `JWT_LEEWAY_SECONDS` | `int` | `30` | Tolerance (seconds) for clock drift between client and server when validating `exp`/`iat`. |
109
+ | `PASSWORD_RESET_EXPIRE_MINUTES` | `int` | `15` | Password-reset token lifetime. Kept short — independent of `ACCESS_TOKEN_EXPIRE_MINUTES`. |
110
+ | `EMAIL_VERIFY_EXPIRE_MINUTES` | `int` | `1440` | Email-verification token lifetime (24 h). |
54
111
 
55
112
  ### Passwords
56
113
 
@@ -75,12 +132,13 @@ Pass config inline or as an object:
75
132
  | Option | Type | Default | Description |
76
133
  |--------|------|---------|-------------|
77
134
  | `RATE_LIMIT_ENABLED` | `bool` | `False` | Enable global rate limit middleware. |
78
- | `RATE_LIMIT_BACKEND` | `"memory" \| "redis"` | `"memory"` | Rate limiter storage backend. |
135
+ | `RATE_LIMIT_BACKEND` | `"memory" \| "redis"` | `"memory"` | Rate limiter storage backend. Use `"redis"` in production — `"memory"` is per-process, so the effective limit is multiplied by the worker count. |
79
136
  | `TRUSTED_PROXY_HEADERS` | `list[str]` | `[]` | Headers to read real client IP from (e.g. `["X-Forwarded-For"]`). |
80
137
  | `AUTH_RATE_LIMIT_ENABLED` | `bool` | `True` | Enable per-route auth rate limits. |
81
138
  | `AUTH_RATE_LIMIT_LOGIN` | `int` | `5` | Max login attempts per window. |
82
139
  | `AUTH_RATE_LIMIT_REGISTER` | `int` | `3` | Max registrations per window. |
83
140
  | `AUTH_RATE_LIMIT_PASSWORD_RESET` | `int` | `3` | Max password reset requests per window. |
141
+ | `AUTH_RATE_LIMIT_PASSKEY_AUTH` | `int` | `10` | Max passkey authenticate/begin requests per window. |
84
142
  | `AUTH_RATE_LIMIT_WINDOW_SECONDS` | `int` | `60` | Rate limit window in seconds. |
85
143
 
86
144
  ### Redis
@@ -94,7 +152,7 @@ Pass config inline or as an object:
94
152
  | Option | Type | Default | Description |
95
153
  |--------|------|---------|-------------|
96
154
  | `BLACKLIST_ENABLED` | `bool` | `True` | Check blacklist on token decode. |
97
- | `BLACKLIST_BACKEND` | `"memory" \| "redis"` | `"memory"` | Blacklist storage backend. |
155
+ | `BLACKLIST_BACKEND` | `"memory" \| "redis"` | `"memory"` | Blacklist storage backend. Use `"redis"` in production — `"memory"` is per-process, so a token revoked on one worker remains usable on others (logout won't actually revoke). |
98
156
 
99
157
  ### Middleware
100
158
 
@@ -120,6 +178,7 @@ Pass config inline or as an object:
120
178
  |--------|------|---------|-------------|
121
179
  | `OAUTH_STATE_EXPIRE_SECONDS` | `int` | `300` | OAuth state token TTL (5 min). |
122
180
  | `OAUTH_AUTO_LINK_BY_EMAIL` | `bool` | `True` | Auto-link OAuth accounts to existing users by email. |
181
+ | `PREVENT_REGISTRATION_ENUMERATION` | `bool` | `False` | When `True`, `/register` always returns `202` + a generic message whether or not the email is already registered — an attacker can't use registration responses to probe the user table. Opt-in because the default `201` + user / `409` conflict behavior is simpler for client apps. |
123
182
 
124
183
  ### Routing
125
184
 
@@ -128,3 +187,15 @@ Pass config inline or as an object:
128
187
  | `API_PREFIX` | `str` | `"/api/v1"` | URL prefix for all routes. |
129
188
  | `AUTH_ROUTER_PREFIX` | `str` | `"/auth"` | Auth router sub-prefix. |
130
189
  | `ROUTER_TAGS` | `list[str]` | `["Auth"]` | OpenAPI tags for auth routes. |
190
+
191
+ ### Passkeys
192
+
193
+ | Option | Type | Default | Description |
194
+ |--------|------|---------|-------------|
195
+ | `PASSKEY_ENABLED` | `bool` | `False` | Enable passkey (WebAuthn) routes. |
196
+ | `PASSKEY_RP_ID` | `str \| None` | `None` | Relying Party ID (your domain, e.g. `"example.com"`). |
197
+ | `PASSKEY_RP_NAME` | `str \| None` | `None` | Relying Party display name (e.g. `"My App"`). |
198
+ | `PASSKEY_ORIGINS` | `list[str]` | `[]` | Allowed origins (e.g. `["https://example.com", "https://m.example.com"]`). |
199
+ | `PASSKEY_CHALLENGE_BACKEND` | `"memory" \| "redis"` | `"memory"` | Challenge store backend. Use `"redis"` in production — `"memory"` is per-process and breaks under `uvicorn --workers N` (begin and complete can land on different workers). |
200
+ | `PASSKEY_CHALLENGE_TTL` | `int` | `60` | Challenge expiry in seconds. |
201
+ | `PASSKEY_REQUIRE_USER_VERIFICATION` | `bool` | `True` | Require user verification (PIN/biometric) on register and authenticate. Set `False` only if you need to allow silent authenticators. |
@@ -13,9 +13,8 @@ pip install fastapi-fullauth[sqlmodel]
13
13
  ```python
14
14
  # models.py
15
15
  from sqlmodel import Field, Relationship
16
- from fastapi_fullauth.adapters.sqlmodel import (
17
- UserBase, Role, UserRoleLink, RefreshTokenRecord,
18
- )
16
+ from fastapi_fullauth.adapters.sqlmodel.models.base import UserBase, RefreshTokenRecord
17
+ from fastapi_fullauth.adapters.sqlmodel.models.role import Role, UserRoleLink
19
18
 
20
19
  class User(UserBase, table=True):
21
20
  __tablename__ = "fullauth_users"
@@ -94,15 +93,24 @@ uvicorn main:app --reload
94
93
 
95
94
  ### Composable routers
96
95
 
97
- `init_app()` registers all routes. If you want only specific route groups, include them manually:
96
+ `init_app()` registers all routes. To exclude specific routers, use `exclude_routers`:
97
+
98
+ ```python
99
+ # skip admin routes
100
+ fullauth.init_app(app, exclude_routers=["admin"])
101
+ ```
102
+
103
+ For full manual control, wire routers and middleware yourself:
98
104
 
99
105
  ```python
100
106
  app = FastAPI(lifespan=lifespan)
101
- app.state.fullauth = fullauth
107
+ fullauth.bind(app) # required for dependencies to work
102
108
 
103
- # only auth + profile, no verification/admin/oauth
104
109
  app.include_router(fullauth.auth_router, prefix="/api/v1/auth")
105
110
  app.include_router(fullauth.profile_router, prefix="/api/v1/auth")
111
+
112
+ # optionally wire middleware from config
113
+ fullauth.init_middleware(app) # also calls bind() if not done
106
114
  ```
107
115
 
108
116
  | Router | Routes |
@@ -112,6 +120,7 @@ app.include_router(fullauth.profile_router, prefix="/api/v1/auth")
112
120
  | `verify_router` | email verification, password reset |
113
121
  | `admin_router` | assign/remove roles and permissions (superuser) |
114
122
  | `oauth_router` | OAuth provider routes (only if configured) |
123
+ | `passkey_router` | Passkey register, authenticate, list, delete (only if enabled) |
115
124
 
116
125
  ## 5. Try it out
117
126
 
@@ -86,15 +86,21 @@ Omit `SECRET_KEY` in dev and a random one is generated (tokens won't survive res
86
86
 
87
87
  ### Composable routers
88
88
 
89
- Include only the route groups you need:
89
+ Exclude routers you don't need:
90
+
91
+ ```python
92
+ fullauth.init_app(app, exclude_routers=["admin"])
93
+ ```
94
+
95
+ Or wire routers manually for full control:
90
96
 
91
97
  ```python
92
98
  app = FastAPI()
93
- app.state.fullauth = fullauth
99
+ fullauth.bind(app) # required for dependencies to work
94
100
 
95
101
  app.include_router(fullauth.auth_router, prefix="/api/v1/auth")
96
102
  app.include_router(fullauth.profile_router, prefix="/api/v1/auth")
97
- # skip verify, admin, oauth
103
+ fullauth.init_middleware(app)
98
104
  ```
99
105
 
100
106
  | Router | Routes |
@@ -104,8 +110,9 @@ app.include_router(fullauth.profile_router, prefix="/api/v1/auth")
104
110
  | `verify_router` | email verification, password reset |
105
111
  | `admin_router` | assign/remove roles and permissions (superuser) |
106
112
  | `oauth_router` | OAuth provider routes (only if configured) |
113
+ | `passkey_router` | Passkey register, authenticate, list, delete (only if enabled) |
107
114
 
108
- `fullauth.init_app(app)` includes all of them.
115
+ `fullauth.init_app(app)` includes all of them. Use `exclude_routers` or individual routers for granular control.
109
116
 
110
117
  ## Routes
111
118