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.
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/CHANGELOG.md +67 -5
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/PKG-INFO +22 -9
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/README.md +17 -7
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/docs/adapters/index.md +31 -14
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/docs/adapters/sqlalchemy.md +5 -4
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/docs/adapters/sqlmodel.md +14 -13
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/docs/api-reference.md +4 -2
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/docs/configuration.md +73 -2
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/docs/getting-started.md +15 -6
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/docs/index.md +11 -4
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/docs/llms-full.txt +157 -53
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/docs/migrations.md +33 -12
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/docs/security/middleware.md +4 -1
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/docs/security/rate-limiting.md +23 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/examples/sqlalchemy_app/models.py +2 -2
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/examples/sqlmodel_app/models.py +2 -6
- fastapi_fullauth-0.9.0/fastapi_fullauth/.agents/skills/fastapi-fullauth/SKILL.md +84 -0
- fastapi_fullauth-0.9.0/fastapi_fullauth/.agents/skills/fastapi-fullauth/references/adapters.md +182 -0
- fastapi_fullauth-0.9.0/fastapi_fullauth/.agents/skills/fastapi-fullauth/references/api-reference.md +385 -0
- fastapi_fullauth-0.9.0/fastapi_fullauth/.agents/skills/fastapi-fullauth/references/composable-design.md +116 -0
- fastapi_fullauth-0.9.0/fastapi_fullauth/.agents/skills/fastapi-fullauth/references/getting-started.md +180 -0
- fastapi_fullauth-0.9.0/fastapi_fullauth/.agents/skills/fastapi-fullauth/references/hooks.md +121 -0
- fastapi_fullauth-0.9.0/fastapi_fullauth/.agents/skills/fastapi-fullauth/references/migrations.md +137 -0
- fastapi_fullauth-0.9.0/fastapi_fullauth/.agents/skills/fastapi-fullauth/references/oauth.md +156 -0
- fastapi_fullauth-0.9.0/fastapi_fullauth/.agents/skills/fastapi-fullauth/references/passkeys.md +169 -0
- fastapi_fullauth-0.9.0/fastapi_fullauth/.agents/skills/fastapi-fullauth/references/production.md +147 -0
- fastapi_fullauth-0.9.0/fastapi_fullauth/.agents/skills/fastapi-fullauth/references/rbac.md +130 -0
- fastapi_fullauth-0.9.0/fastapi_fullauth/.agents/skills/fastapi-fullauth/references/testing.md +239 -0
- fastapi_fullauth-0.9.0/fastapi_fullauth/.agents/skills/fastapi-fullauth/references/troubleshooting.md +129 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/__init__.py +1 -1
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/adapters/__init__.py +15 -11
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/adapters/base.py +82 -29
- fastapi_fullauth-0.9.0/fastapi_fullauth/adapters/sqlalchemy/__init__.py +3 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/adapters/sqlalchemy/adapter.py +198 -16
- fastapi_fullauth-0.9.0/fastapi_fullauth/adapters/sqlalchemy/models/__init__.py +49 -0
- fastapi_fullauth-0.9.0/fastapi_fullauth/adapters/sqlalchemy/models/base.py +51 -0
- fastapi_fullauth-0.9.0/fastapi_fullauth/adapters/sqlalchemy/models/oauth.py +29 -0
- fastapi_fullauth-0.9.0/fastapi_fullauth/adapters/sqlalchemy/models/passkey.py +29 -0
- fastapi_fullauth-0.9.0/fastapi_fullauth/adapters/sqlalchemy/models/permission.py +26 -0
- fastapi_fullauth-0.9.0/fastapi_fullauth/adapters/sqlalchemy/models/role.py +25 -0
- fastapi_fullauth-0.9.0/fastapi_fullauth/adapters/sqlmodel/__init__.py +3 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/adapters/sqlmodel/adapter.py +194 -18
- fastapi_fullauth-0.9.0/fastapi_fullauth/adapters/sqlmodel/models/__init__.py +42 -0
- fastapi_fullauth-0.9.0/fastapi_fullauth/adapters/sqlmodel/models/base.py +46 -0
- fastapi_fullauth-0.9.0/fastapi_fullauth/adapters/sqlmodel/models/oauth.py +28 -0
- fastapi_fullauth-0.9.0/fastapi_fullauth/adapters/sqlmodel/models/passkey.py +26 -0
- fastapi_fullauth-0.9.0/fastapi_fullauth/adapters/sqlmodel/models/permission.py +19 -0
- fastapi_fullauth-0.9.0/fastapi_fullauth/adapters/sqlmodel/models/role.py +18 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/backends/bearer.py +1 -1
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/backends/cookie.py +14 -8
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/config.py +49 -1
- fastapi_fullauth-0.9.0/fastapi_fullauth/core/challenges.py +102 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/core/crypto.py +11 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/core/tokens.py +6 -1
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/dependencies/require_role.py +3 -1
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/flows/__init__.py +8 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/flows/email_verify.py +2 -2
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/flows/login.py +1 -1
- fastapi_fullauth-0.9.0/fastapi_fullauth/flows/oauth.py +198 -0
- fastapi_fullauth-0.9.0/fastapi_fullauth/flows/passkey.py +248 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/flows/password_reset.py +1 -0
- fastapi_fullauth-0.9.0/fastapi_fullauth/flows/set_password.py +39 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/fullauth.py +154 -36
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/hooks.py +15 -1
- fastapi_fullauth-0.9.0/fastapi_fullauth/migrations.py +101 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/protection/__init__.py +9 -1
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/protection/lockout.py +35 -9
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/protection/ratelimit.py +65 -5
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/router/_models.py +4 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/router/auth.py +37 -24
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/router/oauth.py +2 -1
- fastapi_fullauth-0.9.0/fastapi_fullauth/router/passkey.py +254 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/router/profile.py +35 -1
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/types.py +22 -2
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/utils.py +4 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/pyproject.toml +8 -1
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/tests/conftest.py +11 -9
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/tests/test_auth.py +75 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/tests/test_config.py +83 -0
- fastapi_fullauth-0.9.0/tests/test_cookie_backend.py +56 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/tests/test_oauth.py +45 -5
- fastapi_fullauth-0.9.0/tests/test_passkey.py +180 -0
- fastapi_fullauth-0.9.0/tests/test_polish.py +181 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/tests/test_rbac.py +14 -4
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/tests/test_sqlalchemy_adapter.py +26 -5
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/tests/test_sqlmodel_adapter.py +68 -3
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/uv.lock +149 -2
- fastapi_fullauth-0.7.0/fastapi_fullauth/adapters/sqlalchemy/__init__.py +0 -21
- fastapi_fullauth-0.7.0/fastapi_fullauth/adapters/sqlalchemy/models.py +0 -105
- fastapi_fullauth-0.7.0/fastapi_fullauth/adapters/sqlmodel/__init__.py +0 -21
- fastapi_fullauth-0.7.0/fastapi_fullauth/adapters/sqlmodel/models.py +0 -92
- fastapi_fullauth-0.7.0/fastapi_fullauth/flows/oauth.py +0 -143
- fastapi_fullauth-0.7.0/fastapi_fullauth/migrations.py +0 -61
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/.github/pull_request_template.md +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/.github/workflows/ci.yml +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/.github/workflows/docs.yml +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/.github/workflows/publish.yml +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/.gitignore +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/.python-version +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/CONTRIBUTING.md +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/LICENSE +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/Makefile +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/docs/auth/custom-claims.md +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/docs/auth/dependencies.md +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/docs/auth/hooks.md +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/docs/auth/passwords.md +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/docs/contributing.md +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/docs/llms.txt +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/docs/oauth.md +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/docs/stylesheets/home.css +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/examples/sqlalchemy_app/__init__.py +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/examples/sqlalchemy_app/auth.py +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/examples/sqlalchemy_app/config.py +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/examples/sqlalchemy_app/main.py +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/examples/sqlalchemy_app/routes.py +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/examples/sqlmodel_app/__init__.py +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/examples/sqlmodel_app/auth.py +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/examples/sqlmodel_app/config.py +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/examples/sqlmodel_app/main.py +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/examples/sqlmodel_app/routes.py +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/backends/__init__.py +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/backends/base.py +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/core/__init__.py +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/core/redis_blacklist.py +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/dependencies/__init__.py +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/dependencies/current_user.py +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/exceptions.py +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/flows/change_password.py +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/flows/logout.py +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/flows/register.py +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/flows/update_profile.py +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/middleware/__init__.py +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/middleware/csrf.py +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/middleware/ratelimit.py +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/middleware/security_headers.py +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/oauth/__init__.py +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/oauth/base.py +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/oauth/github.py +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/oauth/google.py +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/router/__init__.py +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/router/admin.py +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/router/verify.py +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/fastapi_fullauth/validators.py +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/mkdocs.yml +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/tests/__init__.py +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/tests/test_crypto.py +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/tests/test_hooks.py +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/tests/test_profile.py +0 -0
- {fastapi_fullauth-0.7.0 → fastapi_fullauth-0.9.0}/tests/test_security.py +0 -0
- {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`
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
100
|
+
Tables are created based on which model groups you import:
|
|
100
101
|
|
|
101
|
-
|
|
|
102
|
-
|
|
103
|
-
| `fullauth_users` |
|
|
104
|
-
| `fullauth_roles` |
|
|
105
|
-
| `
|
|
106
|
-
| `
|
|
107
|
-
| `
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|