Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
242 changes: 121 additions & 121 deletions CHANGELOG.md

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@ Both must pass before submitting a PR. CI enforces this.

## Branch naming

- `feat/description` new features
- `fix/description` bug fixes
- `refactor/description` code improvements
- `docs/description` documentation changes
- `feat/description` = new features
- `fix/description` = bug fixes
- `refactor/description` = code improvements
- `docs/description` = documentation changes

## Commit messages

Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ fix:
format:
uv run ruff format .

# Run all checks (format + lint + tests) run before committing
# Run all checks (format + lint + tests) = run before committing
check: format-check lint test

# Fix all issues then verify
Expand Down
34 changes: 17 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,27 +24,27 @@

---

Add a complete authentication and authorization system to your **FastAPI** project. FastAPI FullAuth is designed to be production-ready, async-native, and pluggable handling JWT tokens, refresh rotation, password hashing, email verification, OAuth2 social login, and role-based access out of the box.
Add a complete authentication and authorization system to your **FastAPI** project. FastAPI FullAuth is designed to be production-ready, async-native, and pluggable = handling JWT tokens, refresh rotation, password hashing, email verification, OAuth2 social login, and role-based access out of the box.

## Features

- **JWT access + refresh tokens** with configurable expiry
- **Refresh token rotation** with reuse detection revokes entire session family on replay
- **Refresh token rotation** with reuse detection = revokes entire session family on replay
- **Password hashing** via Argon2id (default) or bcrypt, with transparent rehashing
- **Email verification** and **password reset** flows with event hooks
- **Passkey (WebAuthn)** passwordless login with fingerprint, Face ID, security keys
- **OAuth2 social login** Google and GitHub, with multi-redirect-URI support
- **Role-based access control** `CurrentUser`, `VerifiedUser`, `SuperUser`, `require_role()`
- **Rate limiting** per-route auth limits + global middleware (memory or Redis)
- **Passkey (WebAuthn)** = passwordless login with fingerprint, Face ID, security keys
- **OAuth2 social login** = Google and GitHub, with multi-redirect-URI support
- **Role-based access control** = `CurrentUser`, `VerifiedUser`, `SuperUser`, `require_role()`
- **Rate limiting** = per-route auth limits + global middleware (memory or Redis)
- **CSRF protection** and **security headers** middleware, auto-wired
- **Pluggable adapters** SQLModel or SQLAlchemy
- **Generic type parameters** define your own schemas with full IDE support and type safety
- **Composable routers** include only the route groups you need
- **Event hooks** `after_register`, `after_login`, `send_verification_email`, etc.
- **Custom JWT claims** embed app-specific data in tokens
- **Structured logging** all auth events, security violations, and failures logged
- **Redis support** token blacklist and rate limiter backends
- **Python 3.10 3.14** supported
- **Pluggable adapters** = SQLModel or SQLAlchemy
- **Generic type parameters** = define your own schemas with full IDE support and type safety
- **Composable routers** = include only the route groups you need
- **Event hooks** = `after_register`, `after_login`, `send_verification_email`, etc.
- **Custom JWT claims** = embed app-specific data in tokens
- **Structured logging** = all auth events, security violations, and failures logged
- **Redis support** = token blacklist and rate limiter backends
- **Python 3.10 - 3.14** supported

## Installation

Expand Down Expand Up @@ -98,7 +98,7 @@ fullauth = FullAuth(
fullauth.init_app(app)
```

That's it all auth routes are registered under `/api/v1/auth/` automatically.
That's it = all auth routes are registered under `/api/v1/auth/` automatically.

Omit `config` in dev and a random secret key is generated (tokens won't survive restarts).

Expand Down Expand Up @@ -327,8 +327,8 @@ See [Configuration docs](https://mdfarhankc.github.io/fastapi-fullauth/configura

Using an AI coding assistant? Point it at our LLM-optimized docs:

- **[llms.txt](https://mdfarhankc.github.io/fastapi-fullauth/llms.txt)** concise overview with links to all doc pages
- **[llms-full.txt](https://mdfarhankc.github.io/fastapi-fullauth/llms-full.txt)** full documentation in a single file
- **[llms.txt](https://mdfarhankc.github.io/fastapi-fullauth/llms.txt)** = concise overview with links to all doc pages
- **[llms-full.txt](https://mdfarhankc.github.io/fastapi-fullauth/llms-full.txt)** = full documentation in a single file

Works with Claude, Cursor, Copilot, and any tool that accepts a docs URL.

Expand Down
6 changes: 3 additions & 3 deletions docs/adapters/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ Adapters are the database layer for fastapi-fullauth. They implement `AbstractUs

## Choosing an adapter

- **SQLModel** recommended for most projects. Clean model definitions, good type support. Use SQLite for prototyping.
- **SQLAlchemy** use if your project already uses SQLAlchemy's declarative base.
- **SQLModel** = recommended for most projects. Clean model definitions, good type support. Use SQLite for prototyping.
- **SQLAlchemy** = use if your project already uses SQLAlchemy's declarative base.

## Custom adapters

Expand All @@ -26,7 +26,7 @@ from fastapi_fullauth.adapters.base import (
OAuthAdapterMixin,
)

# Minimal just auth
# Minimal = just auth
class MyAdapter(AbstractUserAdapter):
async def get_user_by_id(self, user_id): ...
async def get_user_by_email(self, email): ...
Expand Down
6 changes: 3 additions & 3 deletions docs/adapters/sqlalchemy.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# SQLAlchemy Adapter

Use this adapter if your project already uses SQLAlchemy's declarative base. Bring your own `DeclarativeBase` the library doesn't ship one.
Use this adapter if your project already uses SQLAlchemy's declarative base. Bring your own `DeclarativeBase` = the library doesn't ship one.

## Installation

Expand Down Expand Up @@ -49,11 +49,11 @@ class User(UserMixin, Base):
refresh_tokens: Mapped[list[RefreshToken]] = relationship(lazy="noload")
```

`UserMixin` provides `id`, `email`, `hashed_password` (nullable `NULL` for OAuth-only users), `is_active`, `is_verified`, `is_superuser`, `created_at`.
`UserMixin` provides `id`, `email`, `hashed_password` (nullable = `NULL` for OAuth-only users), `is_active`, `is_verified`, `is_superuser`, `created_at`.

### 2. Create the adapter

Pass each concrete model class you defined required for the features you use:
Pass each concrete model class you defined = required for the features you use:

```python
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
Expand Down
2 changes: 1 addition & 1 deletion docs/adapters/sqlmodel.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ You can use either SQLAlchemy's `AsyncSession` or SQLModel's `AsyncSession`:
session_maker = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
```

Then create the adapter pass every concrete class you defined:
Then create the adapter = pass every concrete class you defined:

```python
from fastapi_fullauth.adapters import SQLModelAdapter
Expand Down
8 changes: 4 additions & 4 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ The main auth manager. Central entry point for the library.
from fastapi_fullauth import FullAuth, FullAuthConfig

fullauth = FullAuth(
adapter=adapter, # required database adapter
adapter=adapter, # required = database adapter
config=FullAuthConfig(...), # FullAuthConfig object (see Configuration)
providers=None, # list of OAuthProvider instances
backends=None, # [BearerBackend()] by default
Expand Down Expand Up @@ -114,9 +114,9 @@ class TokenPayload(BaseModel):

```python
from fastapi_fullauth.dependencies import (
CurrentUser, # Annotated type any authenticated user
VerifiedUser, # Annotated type verified email required
SuperUser, # Annotated type superuser required
CurrentUser, # Annotated type = any authenticated user
VerifiedUser, # Annotated type = verified email required
SuperUser, # Annotated type = superuser required
current_user, # function form of CurrentUser
require_role, # require_role("admin", "editor")
require_permission, # require_permission("posts:edit", "posts:delete")
Expand Down
6 changes: 3 additions & 3 deletions docs/auth/custom-claims.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ If your callback returns any of these, a `ValueError` is raised at token creatio

Custom claims are generated on:

- **Login** embedded in the access token
- **Token refresh** regenerated from the current user state
- **OAuth callback** embedded after OAuth user creation/linking
- **Login** = embedded in the access token
- **Token refresh** = regenerated from the current user state
- **OAuth callback** = embedded after OAuth user creation/linking

This means claims stay fresh on each refresh. If a user's plan changes, the next token refresh picks it up.
6 changes: 3 additions & 3 deletions docs/auth/dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,15 @@ from fastapi_fullauth.dependencies import require_role
async def editor_panel(user=Depends(require_role("editor"))):
return {"msg": "welcome, editor"}

# multiple roles user needs at least one
# multiple roles = user needs at least one
@app.get("/content")
async def content(user=Depends(require_role("editor", "author"))):
return {"msg": "welcome"}
```

### require_permission

Check that the user has at least one of the specified permissions. Permissions are resolved through roles a user with role `"editor"` gets all permissions assigned to that role.
Check that the user has at least one of the specified permissions. Permissions are resolved through roles = a user with role `"editor"` gets all permissions assigned to that role.

```python
from fastapi import Depends
Expand All @@ -74,7 +74,7 @@ from fastapi_fullauth.dependencies import require_permission
async def delete_post(id: str, user=Depends(require_permission("posts:delete"))):
...

# multiple permissions user needs at least one
# multiple permissions = user needs at least one
@app.put("/posts/{id}")
async def edit_post(id: str, user=Depends(require_permission("posts:edit", "posts:admin"))):
...
Expand Down
22 changes: 11 additions & 11 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,14 @@ FULLAUTH_BLACKLIST_BACKEND=redis
FULLAUTH_REDIS_URL=redis://localhost:6379/0
```

Then `FullAuthConfig()` picks it up no extra wiring needed.
Then `FullAuthConfig()` picks it up = no extra wiring needed.

### Precedence

pydantic-settings resolves values in this order, first wins:

1. Init kwargs `FullAuthConfig(SECRET_KEY="...")`
2. Process environment `os.environ["FULLAUTH_SECRET_KEY"]`
1. Init kwargs = `FullAuthConfig(SECRET_KEY="...")`
2. Process environment = `os.environ["FULLAUTH_SECRET_KEY"]`
3. `.env` file
4. Field defaults

Expand Down Expand Up @@ -85,9 +85,9 @@ class AppFullAuthConfig(FullAuthConfig):

### Cloud / container deployments

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.
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.

If you want to be defensively explicit that no file is ever read, pass `FullAuthConfig(_env_file=None)` but it's not required.
If you want to be defensively explicit that no file is ever read, pass `FullAuthConfig(_env_file=None)` = but it's not required.

## Reference

Expand All @@ -106,7 +106,7 @@ If you want to be defensively explicit that no file is ever read, pass `FullAuth
| `REFRESH_TOKEN_EXPIRE_DAYS` | `int` | `30` | Refresh token lifetime. |
| `REFRESH_TOKEN_ROTATION` | `bool` | `True` | Issue new refresh token on each refresh. |
| `JWT_LEEWAY_SECONDS` | `int` | `30` | Tolerance (seconds) for clock drift between client and server when validating `exp`/`iat`. |
| `PASSWORD_RESET_EXPIRE_MINUTES` | `int` | `15` | Password-reset token lifetime. Kept short independent of `ACCESS_TOKEN_EXPIRE_MINUTES`. |
| `PASSWORD_RESET_EXPIRE_MINUTES` | `int` | `15` | Password-reset token lifetime. Kept short = independent of `ACCESS_TOKEN_EXPIRE_MINUTES`. |
| `EMAIL_VERIFY_EXPIRE_MINUTES` | `int` | `1440` | Email-verification token lifetime (24 h). |

### Passwords
Expand All @@ -129,12 +129,12 @@ If you want to be defensively explicit that no file is ever read, pass `FullAuth
### Rate Limiting

Per-route auth rate limits are baked into the routers. Global request-rate
middleware (`RateLimitMiddleware`) is opt-in import it from
middleware (`RateLimitMiddleware`) is opt-in = import it from
`fastapi_fullauth.middleware` and call `app.add_middleware(...)` yourself.

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `RATE_LIMIT_BACKEND` | `"memory" \| "redis"` | `"memory"` | Backend used by `AuthRateLimiter` and `create_rate_limiter()`. Use `"redis"` in production `"memory"` is per-process, so the effective limit is multiplied by the worker count. |
| `RATE_LIMIT_BACKEND` | `"memory" \| "redis"` | `"memory"` | Backend used by `AuthRateLimiter` and `create_rate_limiter()`. Use `"redis"` in production = `"memory"` is per-process, so the effective limit is multiplied by the worker count. |
| `TRUSTED_PROXY_HEADERS` | `list[str]` | `[]` | Headers to read real client IP from (e.g. `["X-Forwarded-For"]`). |
| `AUTH_RATE_LIMIT_ENABLED` | `bool` | `True` | Enable per-route auth rate limits. |
| `AUTH_RATE_LIMIT_LOGIN` | `int` | `5` | Max login attempts per window. |
Expand All @@ -154,7 +154,7 @@ middleware (`RateLimitMiddleware`) is opt-in — import it from
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `BLACKLIST_ENABLED` | `bool` | `True` | Check blacklist on token decode. |
| `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). |
| `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). |

### Middleware

Expand Down Expand Up @@ -182,7 +182,7 @@ middleware (`RateLimitMiddleware`) is opt-in — import it from
|--------|------|---------|-------------|
| `OAUTH_STATE_EXPIRE_SECONDS` | `int` | `300` | OAuth state token TTL (5 min). |
| `OAUTH_AUTO_LINK_BY_EMAIL` | `bool` | `True` | Auto-link OAuth accounts to existing users by email. |
| `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. |
| `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. |

### Routing

Expand All @@ -200,6 +200,6 @@ middleware (`RateLimitMiddleware`) is opt-in — import it from
| `PASSKEY_RP_ID` | `str \| None` | `None` | Relying Party ID (your domain, e.g. `"example.com"`). |
| `PASSKEY_RP_NAME` | `str \| None` | `None` | Relying Party display name (e.g. `"My App"`). |
| `PASSKEY_ORIGINS` | `list[str]` | `[]` | Allowed origins (e.g. `["https://example.com", "https://m.example.com"]`). |
| `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). |
| `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). |
| `PASSKEY_CHALLENGE_TTL` | `int` | `60` | Challenge expiry in seconds. |
| `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. |
14 changes: 7 additions & 7 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ pip install fastapi-fullauth[sqlmodel]

## 1. Define your tables

Each library table is a **mixin** you combine with `table=True` (SQLModel) or your own `DeclarativeBase` (SQLAlchemy). Subclass only the ones you need features you don't opt into never register a table.
Each library table is a **mixin** you combine with `table=True` (SQLModel) or your own `DeclarativeBase` (SQLAlchemy). Subclass only the ones you need = features you don't opt into never register a table.

```python
# models.py
Expand Down Expand Up @@ -39,7 +39,7 @@ class User(UserMixin, table=True):
refresh_tokens: list[RefreshToken] = Relationship()
```

`UserMixin` provides `id`, `email`, `hashed_password` (nullable `NULL` for OAuth-only users), `is_active`, `is_verified`, `is_superuser`, and `created_at`. Add any extra fields you need.
`UserMixin` provides `id`, `email`, `hashed_password` (nullable = `NULL` for OAuth-only users), `is_active`, `is_verified`, `is_superuser`, and `created_at`. Add any extra fields you need.

!!! note
Define your own schemas extending `UserSchema` and `CreateUserSchema` to include custom fields like `display_name` and `phone`, then pass them to the adapter. See [Custom User Schemas](#custom-user-schemas) below or the [API Reference](api-reference.md).
Expand Down Expand Up @@ -131,7 +131,7 @@ app.include_router(fullauth.profile_router, prefix="/api/v1/auth")

### Middleware

`init_app()` does not add any middleware import what you need from `fastapi_fullauth.middleware` and add it yourself:
`init_app()` does not add any middleware = import what you need from `fastapi_fullauth.middleware` and add it yourself:

```python
from fastapi_fullauth.middleware import (
Expand Down Expand Up @@ -215,7 +215,7 @@ See [Protected Routes](auth/dependencies.md) for all dependency types.

## Next steps

- [Configuration](configuration.md) all config options
- [OAuth2 Social Login](oauth.md) add Google/GitHub login
- [Event Hooks](auth/hooks.md) send emails, log events
- [Rate Limiting](security/rate-limiting.md) protect your endpoints
- [Configuration](configuration.md) = all config options
- [OAuth2 Social Login](oauth.md) = add Google/GitHub login
- [Event Hooks](auth/hooks.md) = send emails, log events
- [Rate Limiting](security/rate-limiting.md) = protect your endpoints
Loading
Loading