# FastAPI FullAuth > Production-grade, async-native authentication and authorization for FastAPI. FastAPI FullAuth is a pluggable authentication and authorization library for FastAPI. It provides JWT access and refresh tokens with rotation and reuse detection, password hashing (Argon2id or bcrypt), email verification and password reset flows, OAuth2 social login (Google, GitHub), role-based access control with permissions, rate limiting, CSRF protection, and security headers middleware. The library uses a layered architecture: adapters handle database storage (SQLModel, SQLAlchemy, or in-memory), backends handle token extraction, flows implement auth logic, and composable routers expose API endpoints. Configuration is managed through Pydantic Settings with environment variable support. Generic type parameters allow custom user schemas with full IDE support. - Documentation: https://mdfarhankc.github.io/fastapi-fullauth/ - Source: https://github.com/mdfarhankc/fastapi-fullauth - PyPI: https://pypi.org/project/fastapi-fullauth/ - License: MIT - Python: 3.10 -- 3.14 --- # README FastAPI FullAuth Production-grade, async-native authentication and authorization for FastAPI. Documentation: https://mdfarhankc.github.io/fastapi-fullauth Source Code: https://github.com/mdfarhankc/fastapi-fullauth --- 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 - **Password hashing** via Argon2id (default) or bcrypt, with transparent rehashing - **Email verification** and **password reset** flows with event hooks - **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 ## Installation ```bash pip install fastapi-fullauth # with an ORM adapter pip install fastapi-fullauth[sqlmodel] pip install fastapi-fullauth[sqlalchemy] # with Redis for token blacklisting pip install fastapi-fullauth[sqlmodel,redis] # with OAuth2 social login pip install fastapi-fullauth[sqlmodel,oauth] # everything pip install fastapi-fullauth[all] ``` ## Quick start ```python from fastapi import FastAPI from fastapi_fullauth import FullAuth, FullAuthConfig from fastapi_fullauth.adapters.sqlmodel import SQLModelAdapter app = FastAPI() fullauth = FullAuth( adapter=SQLModelAdapter(session_maker=session_maker, user_model=User), config=FullAuthConfig(SECRET_KEY="your-secret-key"), ) fullauth.init_app(app) ``` 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). ### Composable routers Exclude routers you don't need: ```python fullauth.init_app(app, exclude_routers=["admin"]) ``` Or wire routers manually for full control: ```python app = FastAPI() fullauth.bind(app) # required for dependencies to work app.include_router(fullauth.auth_router, prefix="/api/v1/auth") app.include_router(fullauth.profile_router, prefix="/api/v1/auth") fullauth.init_middleware(app) ``` | Router | Routes | |--------|--------| | `auth_router` | register, login, logout, refresh | | `profile_router` | me, verified-me, update profile, delete account, change password | | `verify_router` | email verification, password reset | | `admin_router` | assign/remove roles and permissions (superuser) | | `oauth_router` | OAuth provider routes (only if configured) | `fullauth.init_app(app)` includes all of them. Use `exclude_routers` or individual routers for granular control. ## Routes | Method | Path | Description | |--------|------|-------------| | `POST` | `/auth/register` | Create a new user | | `POST` | `/auth/login` | Authenticate, get tokens | | `POST` | `/auth/logout` | Blacklist token | | `POST` | `/auth/refresh` | Rotate token pair | | `GET` | `/auth/me` | Get current user | | `GET` | `/auth/me/verified` | Verified users only | | `PATCH` | `/auth/me` | Update profile | | `DELETE` | `/auth/me` | Delete account | | `POST` | `/auth/change-password` | Change password | | `POST` | `/auth/verify-email/request` | Request verification email | | `POST` | `/auth/verify-email/confirm` | Confirm email | | `POST` | `/auth/password-reset/request` | Request password reset | | `POST` | `/auth/password-reset/confirm` | Reset password | | `POST` | `/auth/admin/assign-role` | Assign role (superuser) | | `POST` | `/auth/admin/remove-role` | Remove role (superuser) | | `POST` | `/auth/admin/assign-permission` | Assign permission to role (superuser) | | `POST` | `/auth/admin/remove-permission` | Remove permission from role (superuser) | | `GET` | `/auth/admin/role-permissions/{role}` | List role's permissions (superuser) | With OAuth enabled, additional routes are registered under `/auth/oauth/`. All routes are prefixed with `/api/v1` by default. ## Custom user schemas Define your model and schemas — pass them explicitly to the adapter: ```python from sqlmodel import Field, Relationship from fastapi_fullauth import FullAuth, FullAuthConfig, UserSchema, CreateUserSchema from fastapi_fullauth.adapters.sqlmodel import SQLModelAdapter from fastapi_fullauth.adapters.sqlmodel.models.base import UserBase, RefreshTokenRecord from fastapi_fullauth.adapters.sqlmodel.models.role import Role, UserRoleLink class User(UserBase, table=True): __tablename__ = "fullauth_users" display_name: str = Field(default="", max_length=100) phone: str = Field(default="", max_length=20) roles: list[Role] = Relationship(link_model=UserRoleLink) refresh_tokens: list[RefreshTokenRecord] = Relationship() class MyUserSchema(UserSchema): display_name: str = "" phone: str = "" class MyCreateSchema(CreateUserSchema): display_name: str = "" fullauth = FullAuth( adapter=SQLModelAdapter( session_maker, user_model=User, user_schema=MyUserSchema, create_user_schema=MyCreateSchema, ), config=FullAuthConfig(SECRET_KEY="..."), ) ``` Full IDE autocompletion and type checking on custom fields. Use `get_current_user_dependency()` for typed dependencies: ```python from typing import Annotated from fastapi import Depends from fastapi_fullauth.dependencies import get_current_user_dependency MyCurrentUser = Annotated[MyUserSchema, Depends(get_current_user_dependency(MyUserSchema))] @app.get("/profile") async def profile(user: MyCurrentUser): return {"name": user.display_name} # IDE knows this field exists ``` ## Protected routes ```python from fastapi import Depends from fastapi_fullauth.dependencies import CurrentUser, VerifiedUser, SuperUser, require_role @app.get("/profile") async def profile(user: CurrentUser): return user @app.get("/dashboard") async def dashboard(user: VerifiedUser): return {"email": user.email} @app.delete("/admin/users/{id}") async def delete_user(user: SuperUser): ... @app.get("/editor") async def editor_panel(user=Depends(require_role("editor"))): ... ``` ## OAuth2 social login ```python from fastapi_fullauth import FullAuth, FullAuthConfig from fastapi_fullauth.oauth.google import GoogleOAuthProvider from fastapi_fullauth.oauth.github import GitHubOAuthProvider fullauth = FullAuth( adapter=adapter, config=FullAuthConfig(SECRET_KEY="..."), providers=[ GoogleOAuthProvider( client_id="your-google-client-id", client_secret="your-google-secret", redirect_uris=[ "http://localhost:3000/auth/callback", "https://myapp.com/auth/callback", ], ), GitHubOAuthProvider( client_id="your-github-client-id", client_secret="your-github-secret", redirect_uris=["http://localhost:3000/auth/callback"], ), ], ) ``` Requires `httpx`: `pip install fastapi-fullauth[oauth]` ## Event hooks ```python async def welcome(user): await send_email(user.email, "Welcome!") async def send_verify(email, token): await send_email(email, f"Verify: https://myapp.com/verify?token={token}") fullauth.hooks.on("after_register", welcome) fullauth.hooks.on("send_verification_email", send_verify) ``` Events: `after_register`, `after_login`, `after_logout`, `after_password_change`, `after_password_reset`, `after_email_verify`, `send_verification_email`, `send_password_reset_email`, `after_oauth_login` ## Configuration Pass a `FullAuthConfig` object or set env vars with `FULLAUTH_` prefix. ```python fullauth = FullAuth( adapter=adapter, config=FullAuthConfig( SECRET_KEY="...", ACCESS_TOKEN_EXPIRE_MINUTES=60, API_PREFIX="/api/v2", LOGIN_FIELD="username", PASSWORD_HASH_ALGORITHM="bcrypt", BLACKLIST_BACKEND="redis", REDIS_URL="redis://localhost:6379/0", AUTH_RATE_LIMIT_ENABLED=True, TRUSTED_PROXY_HEADERS=["X-Forwarded-For"], ), ) ``` See [Configuration docs](https://mdfarhankc.github.io/fastapi-fullauth/configuration/) for all options. ## AI-friendly docs 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 Works with Claude, Cursor, Copilot, and any tool that accepts a docs URL. ## Development ```bash git clone https://github.com/mdfarhankc/fastapi-fullauth.git cd fastapi-fullauth uv sync --dev --extra sqlalchemy --extra sqlmodel uv run pytest tests/ -v # run examples uv run uvicorn examples.sqlmodel_app.main:app --reload ``` ## License MIT --- # Getting Started # Getting Started This guide walks through setting up fastapi-fullauth from scratch. ## Installation ```bash pip install fastapi-fullauth[sqlmodel] ``` ## 1. Define your user model ```python # models.py from sqlmodel import Field, Relationship from fastapi_fullauth.adapters.sqlmodel import ( UserBase, Role, UserRoleLink, RefreshTokenRecord, ) class User(UserBase, table=True): __tablename__ = "fullauth_users" display_name: str = Field(default="", max_length=100) phone: str = Field(default="", max_length=20) roles: list[Role] = Relationship(link_model=UserRoleLink) refresh_tokens: list[RefreshTokenRecord] = Relationship() ``` `UserBase` provides `id`, `email`, `hashed_password`, `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). ## 2. Set up the database ```python # config.py from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine DATABASE_URL = "sqlite+aiosqlite:///app.db" engine = create_async_engine(DATABASE_URL) session_maker = async_sessionmaker(engine, expire_on_commit=False) ``` ## 3. Configure FullAuth ```python # auth.py from fastapi_fullauth import FullAuth, FullAuthConfig from fastapi_fullauth.adapters.sqlmodel import SQLModelAdapter from .config import session_maker from .models import User fullauth = FullAuth( adapter=SQLModelAdapter(session_maker=session_maker, user_model=User), config=FullAuthConfig( SECRET_KEY="your-secret-key-at-least-32-bytes", ), ) ``` !!! tip Omit `SECRET_KEY` during development and a random one is generated automatically. Tokens won't survive restarts, but it's convenient for dev. ## 4. Wire it into FastAPI ```python # main.py from contextlib import asynccontextmanager from fastapi import FastAPI from sqlmodel import SQLModel from .auth import fullauth from .config import engine @asynccontextmanager async def lifespan(app: FastAPI): async with engine.begin() as conn: await conn.run_sync(SQLModel.metadata.create_all) yield await engine.dispose() app = FastAPI(lifespan=lifespan) fullauth.init_app(app) ``` That's it. Start the server and you have a full auth system: ```bash uvicorn main:app --reload ``` ### Composable routers `init_app()` registers all routes. To exclude specific routers, use `exclude_routers`: ```python fullauth.init_app(app, exclude_routers=["admin"]) ``` For full manual control, wire routers and middleware yourself: ```python app = FastAPI(lifespan=lifespan) fullauth.bind(app) # required for dependencies to work app.include_router(fullauth.auth_router, prefix="/api/v1/auth") app.include_router(fullauth.profile_router, prefix="/api/v1/auth") fullauth.init_middleware(app) ``` | Router | Routes | |--------|--------| | `auth_router` | register, login, logout, refresh | | `profile_router` | me, verified-me, update profile, delete account, change password | | `verify_router` | email verification, password reset | | `admin_router` | assign/remove roles and permissions (superuser) | | `oauth_router` | OAuth provider routes (only if configured) | ## 5. Try it out **Register:** ```bash curl -X POST http://localhost:8000/api/v1/auth/register \ -H "Content-Type: application/json" \ -d '{"email": "user@example.com", "password": "securepass123"}' ``` **Login:** ```bash curl -X POST http://localhost:8000/api/v1/auth/login \ -H "Content-Type: application/json" \ -d '{"email": "user@example.com", "password": "securepass123"}' ``` Response: ```json { "access_token": "eyJ...", "refresh_token": "eyJ...", "token_type": "bearer", "expires_in": 1800, "user": null } ``` When `INCLUDE_USER_IN_LOGIN=True`, `user` contains the full user object instead of `null`. **Get current user:** ```bash curl http://localhost:8000/api/v1/auth/me \ -H "Authorization: Bearer eyJ..." ``` ## 6. Add protected routes ```python from fastapi_fullauth.dependencies import CurrentUser, VerifiedUser, require_role @app.get("/profile") async def profile(user: CurrentUser): return user @app.get("/dashboard") async def dashboard(user: VerifiedUser): return {"email": user.email} @app.get("/admin") async def admin(user=Depends(require_role("admin"))): return {"msg": "admin area"} ``` 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 All configuration is managed through `FullAuthConfig`, a [Pydantic Settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/) class. Every option can be set via environment variables with the `FULLAUTH_` prefix. ## Usage Pass config inline or as an object: === "Config object" ```python from fastapi_fullauth import FullAuth, FullAuthConfig fullauth = FullAuth( adapter=adapter, config=FullAuthConfig( SECRET_KEY="...", ACCESS_TOKEN_EXPIRE_MINUTES=60, API_PREFIX="/api/v2", ), ) ``` === "Environment variables" ```bash export FULLAUTH_SECRET_KEY="your-secret-key" export FULLAUTH_ACCESS_TOKEN_EXPIRE_MINUTES=60 ``` ```python from fastapi_fullauth import FullAuth # reads from env automatically fullauth = FullAuth(adapter=adapter) ``` ## Reference ### Core | Option | Type | Default | Description | |--------|------|---------|-------------| | `SECRET_KEY` | `str \| None` | `None` | JWT signing key. Auto-generated in dev if not set. | | `ALGORITHM` | `str` | `"HS256"` | JWT signing algorithm. | ### Tokens | Option | Type | Default | Description | |--------|------|---------|-------------| | `ACCESS_TOKEN_EXPIRE_MINUTES` | `int` | `30` | Access token lifetime. | | `REFRESH_TOKEN_EXPIRE_DAYS` | `int` | `30` | Refresh token lifetime. | | `REFRESH_TOKEN_ROTATION` | `bool` | `True` | Issue new refresh token on each refresh. | ### Passwords | Option | Type | Default | Description | |--------|------|---------|-------------| | `PASSWORD_HASH_ALGORITHM` | `"argon2id" \| "bcrypt"` | `"argon2id"` | Hashing algorithm. | | `PASSWORD_MIN_LENGTH` | `int` | `8` | Minimum password length. | ### Login | Option | Type | Default | Description | |--------|------|---------|-------------| | `LOGIN_FIELD` | `str` | `"email"` | Field used for login (`"email"`, `"username"`, etc.). | | `INCLUDE_USER_IN_LOGIN` | `bool` | `False` | Include user object in login/OAuth callback response. | | `LOCKOUT_ENABLED` | `bool` | `True` | Enable account lockout after failed login attempts. | | `LOCKOUT_BACKEND` | `"memory" \| "redis"` | `"memory"` | Lockout storage backend. Use `"redis"` for multi-worker deployments. | | `MAX_LOGIN_ATTEMPTS` | `int` | `5` | Failed attempts before account lockout. | | `LOCKOUT_DURATION_MINUTES` | `int` | `15` | Lockout duration after max attempts. | ### Rate Limiting | Option | Type | Default | Description | |--------|------|---------|-------------| | `RATE_LIMIT_ENABLED` | `bool` | `False` | Enable global rate limit middleware. | | `RATE_LIMIT_BACKEND` | `"memory" \| "redis"` | `"memory"` | Rate limiter storage backend. | | `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. | | `AUTH_RATE_LIMIT_REGISTER` | `int` | `3` | Max registrations per window. | | `AUTH_RATE_LIMIT_PASSWORD_RESET` | `int` | `3` | Max password reset requests per window. | | `AUTH_RATE_LIMIT_WINDOW_SECONDS` | `int` | `60` | Rate limit window in seconds. | ### Redis | Option | Type | Default | Description | |--------|------|---------|-------------| | `REDIS_URL` | `str \| None` | `None` | Redis connection URL. Required when using Redis backends. | ### Token Blacklist | Option | Type | Default | Description | |--------|------|---------|-------------| | `BLACKLIST_ENABLED` | `bool` | `True` | Check blacklist on token decode. | | `BLACKLIST_BACKEND` | `"memory" \| "redis"` | `"memory"` | Blacklist storage backend. | ### Middleware | Option | Type | Default | Description | |--------|------|---------|-------------| | `INJECT_SECURITY_HEADERS` | `bool` | `True` | Auto-add security headers middleware. | | `CSRF_ENABLED` | `bool` | `False` | Auto-add CSRF middleware. | | `CSRF_SECRET` | `str \| None` | `None` | CSRF signing secret. Falls back to `SECRET_KEY`. | ### Cookies | Option | Type | Default | Description | |--------|------|---------|-------------| | `COOKIE_NAME` | `str` | `"fullauth_access"` | Access token cookie name. | | `COOKIE_SECURE` | `bool` | `True` | Set Secure flag on cookies. | | `COOKIE_HTTPONLY` | `bool` | `True` | Set HttpOnly flag on cookies. | | `COOKIE_SAMESITE` | `"lax" \| "strict" \| "none"` | `"lax"` | SameSite cookie policy. | | `COOKIE_DOMAIN` | `str \| None` | `None` | Cookie domain. | ### OAuth | Option | Type | Default | Description | |--------|------|---------|-------------| | `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. | ### Routing | Option | Type | Default | Description | |--------|------|---------|-------------| | `API_PREFIX` | `str` | `"/api/v1"` | URL prefix for all routes. | | `AUTH_ROUTER_PREFIX` | `str` | `"/auth"` | Auth router sub-prefix. | | `ROUTER_TAGS` | `list[str]` | `["Auth"]` | OpenAPI tags for auth routes. | --- # Adapters Overview # Adapters Adapters are the database layer for fastapi-fullauth. They implement `AbstractUserAdapter`, which defines how users, refresh tokens, roles, and OAuth accounts are stored and retrieved. ## Available adapters | Adapter | Backend | Install | |---------|---------|---------| | [SQLModel](sqlmodel.md) | Any SQLAlchemy-supported DB | `pip install fastapi-fullauth[sqlmodel]` | | [SQLAlchemy](sqlalchemy.md) | Any SQLAlchemy-supported DB | `pip install fastapi-fullauth[sqlalchemy]` | ## 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. ## Custom adapters Subclass `AbstractUserAdapter` for core auth. Add mixins for roles, permissions, or OAuth: ```python from fastapi_fullauth.adapters.base import ( AbstractUserAdapter, RoleAdapterMixin, PermissionAdapterMixin, OAuthAdapterMixin, ) # Minimal — just auth class MyAdapter(AbstractUserAdapter): async def get_user_by_id(self, user_id): ... async def get_user_by_email(self, email): ... async def create_user(self, data, hashed_password): ... # ... core methods only # With roles and permissions class MyFullAdapter(AbstractUserAdapter, RoleAdapterMixin, PermissionAdapterMixin): # ... core + role + permission methods pass ``` | Mixin | Methods | When to use | |-------|---------|-------------| | `RoleAdapterMixin` | `assign_role`, `remove_role`, `get_user_roles` | Role management | | `PermissionAdapterMixin` | `get_role_permissions`, `assign/remove_permission_to_role` | RBAC permissions | | `OAuthAdapterMixin` | 5 OAuth account methods | OAuth providers | See the [source of AbstractUserAdapter](https://github.com/mdfarhankc/fastapi-fullauth/blob/main/fastapi_fullauth/adapters/base.py) for the full interface. ## Custom schemas Define your own user schemas by extending `UserSchema` and `CreateUserSchema`, then pass them to the adapter: ```python from fastapi_fullauth import UserSchema, CreateUserSchema class MyUserSchema(UserSchema): display_name: str = "" class MyCreateSchema(CreateUserSchema): display_name: str = "" adapter = SQLModelAdapter( session_maker=session_maker, user_model=User, user_schema=MyUserSchema, create_user_schema=MyCreateSchema, ) ``` If your app uses roles, add `roles` to your custom schema: ```python class MyUserSchema(UserSchema): roles: list[str] = Field(default_factory=list) ``` --- # SQLModel Adapter # SQLModel Adapter The recommended adapter for most projects. ## Installation ```bash pip install fastapi-fullauth[sqlmodel] ``` ## Setup ### 1. Define your user model ```python from sqlmodel import Field, Relationship from fastapi_fullauth.adapters.sqlmodel import ( UserBase, Role, UserRoleLink, RefreshTokenRecord, ) class User(UserBase, table=True): __tablename__ = "fullauth_users" # add your custom fields display_name: str = Field(default="", max_length=100) phone: str = Field(default="", max_length=20) # required relationships roles: list[Role] = Relationship(link_model=UserRoleLink) refresh_tokens: list[RefreshTokenRecord] = Relationship() ``` `UserBase` provides these fields: | Field | Type | Description | |-------|------|-------------| | `id` | `UUID` (UUID7) | Primary key, auto-generated | | `email` | `str` | Unique, indexed | | `hashed_password` | `str` | Password hash | | `is_active` | `bool` | Account active flag | | `is_verified` | `bool` | Email verified flag | | `is_superuser` | `bool` | Superuser flag | | `created_at` | `datetime` | UTC creation timestamp | ### 2. Create the adapter ```python from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine engine = create_async_engine("sqlite+aiosqlite:///app.db") session_maker = async_sessionmaker(engine, expire_on_commit=False) ``` You can use either SQLAlchemy's `AsyncSession` or SQLModel's `AsyncSession`: === "SQLAlchemy AsyncSession" ```python from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine engine = create_async_engine("sqlite+aiosqlite:///app.db") session_maker = async_sessionmaker(engine, expire_on_commit=False) ``` === "SQLModel AsyncSession" ```python from sqlalchemy.ext.asyncio import create_async_engine from sqlalchemy.ext.asyncio import async_sessionmaker from sqlmodel.ext.asyncio.session import AsyncSession engine = create_async_engine("sqlite+aiosqlite:///app.db") session_maker = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) ``` Then create the adapter: ```python from fastapi_fullauth.adapters.sqlmodel import SQLModelAdapter adapter = SQLModelAdapter(session_maker=session_maker, user_model=User) ``` ### 3. Wire into FullAuth ```python from fastapi_fullauth import FullAuth, FullAuthConfig fullauth = FullAuth( adapter=adapter, config=FullAuthConfig( SECRET_KEY="your-secret-key", ), ) ``` ## Tables created The SQLModel adapter uses these tables: | Table | Purpose | |-------|---------| | `fullauth_users` | User accounts (your model) | | `fullauth_roles` | Role definitions | | `fullauth_user_roles` | User-role link table | | `fullauth_refresh_tokens` | Stored refresh tokens | | `fullauth_oauth_accounts` | Linked OAuth provider accounts | ## Custom schemas Define your own schemas and pass them to the adapter: ```python from fastapi_fullauth import UserSchema, CreateUserSchema class MyUserSchema(UserSchema): display_name: str = "" phone: str = "" class MyCreateSchema(CreateUserSchema): display_name: str = "" adapter = SQLModelAdapter( session_maker=session_maker, user_model=User, user_schema=MyUserSchema, create_user_schema=MyCreateSchema, ) ``` If you don't pass custom schemas, the base `UserSchema` and `CreateUserSchema` are used. ## OAuth support The SQLModel adapter implements `OAuthAdapterMixin`. Import `OAuthAccountRecord` from `models.oauth` to register the table. --- # SQLAlchemy Adapter # SQLAlchemy Adapter Use this adapter if your project already uses SQLAlchemy's declarative base. ## Installation ```bash pip install fastapi-fullauth[sqlalchemy] ``` ## Setup ### 1. Define your user model ```python from sqlalchemy import Boolean, Column, DateTime, String, Table, ForeignKey from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship from fastapi_fullauth.adapters.sqlalchemy.models.base import FullAuthBase, UserBase from fastapi_fullauth.adapters.sqlalchemy.models.role import RoleModel class User(UserBase): __tablename__ = "fullauth_users" # add your custom fields display_name: Mapped[str] = mapped_column(String(100), default="") phone: Mapped[str] = mapped_column(String(20), default="") # required relationships roles: Mapped[list[RoleModel]] = relationship( secondary="fullauth_user_roles", lazy="selectin", ) ``` `UserBase` provides the same core fields as the SQLModel version: `id`, `email`, `hashed_password`, `is_active`, `is_verified`, `is_superuser`, `created_at`. ### 2. Create the adapter ```python from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine from fastapi_fullauth.adapters.sqlalchemy import SQLAlchemyAdapter engine = create_async_engine("sqlite+aiosqlite:///app.db") session_maker = async_sessionmaker(engine, expire_on_commit=False) adapter = SQLAlchemyAdapter(session_maker=session_maker, user_model=User) ``` ### 3. Wire into FullAuth ```python from fastapi_fullauth import FullAuth, FullAuthConfig fullauth = FullAuth( adapter=adapter, config=FullAuthConfig( SECRET_KEY="your-secret-key", ), ) ``` ## Table creation Use your existing Alembic setup or create tables directly: ```python async with engine.begin() as conn: await conn.run_sync(FullAuthBase.metadata.create_all) ``` ## Custom schemas Define your own schemas and pass them to the adapter: ```python from fastapi_fullauth import UserSchema, CreateUserSchema class MyUserSchema(UserSchema): display_name: str = "" class MyCreateSchema(CreateUserSchema): display_name: str adapter = SQLAlchemyAdapter( session_maker=session_maker, user_model=User, user_schema=MyUserSchema, create_user_schema=MyCreateSchema, ) ``` If you don't pass custom schemas, the base `UserSchema` and `CreateUserSchema` are used. --- # Protected Routes # Protected Routes fastapi-fullauth provides FastAPI dependencies to protect your routes. Use them with `Depends()` or the `Annotated` type aliases. ## Dependency types ### CurrentUser Any authenticated user (active account required). ```python from fastapi_fullauth.dependencies import CurrentUser @app.get("/profile") async def profile(user: CurrentUser): return {"email": user.email, "roles": user.roles} ``` ### VerifiedUser Authenticated user with a verified email address. ```python from fastapi_fullauth.dependencies import VerifiedUser @app.get("/dashboard") async def dashboard(user: VerifiedUser): return {"email": user.email} ``` Returns `403 Forbidden` if the user's email is not verified. ### SuperUser Authenticated user with `is_superuser=True`. ```python from fastapi_fullauth.dependencies import SuperUser @app.delete("/admin/users/{user_id}") async def delete_user(user_id: str, admin: SuperUser): ... ``` Returns `403 Forbidden` if the user is not a superuser. ### require_role Check that the user has at least one of the specified roles. Superusers bypass all role checks. ```python from fastapi import Depends from fastapi_fullauth.dependencies import require_role @app.get("/editor") async def editor_panel(user=Depends(require_role("editor"))): return {"msg": "welcome, editor"} # 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. ```python from fastapi import Depends from fastapi_fullauth.dependencies import require_permission @app.delete("/posts/{id}") async def delete_post(id: str, user=Depends(require_permission("posts:delete"))): ... # 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"))): ... ``` Superusers bypass all permission checks. #### Setting up permissions Permissions are assigned to roles, not directly to users: ```bash # Assign permissions to a role (superuser only) curl -X POST http://localhost:8000/api/v1/auth/admin/assign-permission \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"role": "editor", "permission": "posts:create"}' curl -X POST http://localhost:8000/api/v1/auth/admin/assign-permission \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"role": "editor", "permission": "posts:edit"}' # List permissions for a role curl http://localhost:8000/api/v1/auth/admin/role-permissions/editor \ -H "Authorization: Bearer " # → ["posts:create", "posts:edit"] ``` Or programmatically: ```python await adapter.assign_permission_to_role("editor", "posts:create") await adapter.assign_permission_to_role("editor", "posts:edit") await adapter.remove_permission_from_role("editor", "posts:create") # resolve all permissions for a user (through their roles) perms = await adapter.get_user_permissions(user.id) # → ["posts:edit"] ``` #### require_role vs require_permission | | `require_role` | `require_permission` | |---|---|---| | Checks | Role names on the user | Permissions resolved through roles | | Setup | Just assign roles | Assign roles + map permissions to roles | | Use case | Simple apps ("admin vs user") | Fine-grained access ("can edit posts?") | | Change access | Modify code | Update DB mappings | ## How it works All dependencies follow the same flow: 1. Extract the JWT from the `Authorization: Bearer ` header (or cookie backend) 2. Decode and validate the token (expiry, blacklist, signature) 3. Look up the user by `sub` (user ID) from the token payload 4. Apply additional checks (verified, superuser, roles) If any step fails, a `401 Unauthorized` or `403 Forbidden` response is returned automatically. ## Typed dependencies for custom schemas If you use custom user schemas, the default `CurrentUser` type resolves to the base `UserSchema`. Use the factory functions for full type safety: ```python from typing import Annotated from fastapi import Depends from fastapi_fullauth.dependencies import ( get_current_user_dependency, get_verified_user_dependency, get_superuser_dependency, ) # your custom schema from myapp.schemas import MyUserSchema MyCurrentUser = Annotated[MyUserSchema, Depends(get_current_user_dependency(MyUserSchema))] MyVerifiedUser = Annotated[MyUserSchema, Depends(get_verified_user_dependency(MyUserSchema))] MySuperUser = Annotated[MyUserSchema, Depends(get_superuser_dependency(MyUserSchema))] @app.get("/profile") async def profile(user: MyCurrentUser): return {"name": user.display_name} # IDE knows this field exists ``` ## Using with the function form If you prefer the function form over `Annotated` types: ```python from fastapi import Depends from fastapi_fullauth.dependencies import current_user, current_active_verified_user, current_superuser @app.get("/profile") async def profile(user=Depends(current_user)): return user ``` ## Role management Roles are managed through the admin endpoints (superuser only): ```bash # Assign a role curl -X POST http://localhost:8000/api/v1/auth/admin/assign-role \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"user_id": "...", "role": "editor"}' # Remove a role curl -X POST http://localhost:8000/api/v1/auth/admin/remove-role \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"user_id": "...", "role": "editor"}' ``` You can also manage roles programmatically through the adapter: ```python await adapter.assign_role(user_id, "editor") await adapter.remove_role(user_id, "editor") roles = await adapter.get_user_roles(user_id) ``` --- # Event Hooks # Event Hooks Hooks let you run custom logic when auth events happen - send emails, log analytics, sync with external systems - without modifying the core auth flows. ## Registering hooks ```python fullauth = FullAuth(adapter=adapter, config=FullAuthConfig(SECRET_KEY="...")) async def on_register(user): print(f"New user: {user.email}") fullauth.hooks.on("after_register", on_register) ``` ## Available events ### User lifecycle | Event | Callback signature | When | |-------|-------------------|------| | `after_register` | `async def(user: UserSchema)` | After successful registration | | `after_login` | `async def(user: UserSchema)` | After successful login | | `after_logout` | `async def(user_id: str)` | After logout | | `after_password_change` | `async def(user: UserSchema)` | After password change | | `after_password_reset` | `async def(user: UserSchema)` | After password reset | | `after_email_verify` | `async def(user: UserSchema)` | After email verification | ### Email events | Event | Callback signature | When | |-------|-------------------|------| | `send_verification_email` | `async def(email: str, token: str)` | When verification is requested | | `send_password_reset_email` | `async def(email: str, token: str)` | When password reset is requested | ### OAuth events | Event | Callback signature | When | |-------|-------------------|------| | `after_oauth_login` | `async def(user: UserSchema, provider: str, is_new_user: bool)` | After OAuth callback | ## Example: email verification ```python async def send_verification_email(email: str, token: str): # build your verification URL verify_url = f"https://myapp.com/verify?token={token}" await my_email_service.send( to=email, subject="Verify your email", body=f"Click here to verify: {verify_url}", ) async def send_password_reset_email(email: str, token: str): reset_url = f"https://myapp.com/reset-password?token={token}" await my_email_service.send( to=email, subject="Reset your password", body=f"Click here to reset: {reset_url}", ) fullauth.hooks.on("send_verification_email", send_verification_email) fullauth.hooks.on("send_password_reset_email", send_password_reset_email) ``` !!! note If you don't register a `send_verification_email` hook, the verification token is still generated but never delivered. Same for password reset. ## Example: audit logging ```python import logging logger = logging.getLogger("auth") async def log_login(user): logger.info(f"Login: {user.email} (id={user.id})") async def log_failed_logout(user_id): logger.info(f"Logout: user_id={user_id}") fullauth.hooks.on("after_login", log_login) fullauth.hooks.on("after_logout", log_failed_logout) ``` ## Multiple hooks per event You can register multiple callbacks for the same event. They run in registration order: ```python fullauth.hooks.on("after_register", send_welcome_email) fullauth.hooks.on("after_register", create_default_workspace) fullauth.hooks.on("after_register", track_signup_analytics) ``` --- # Password Validation # Password Validation fastapi-fullauth includes a configurable password validator that checks passwords on registration, password change, and password reset. ## Default behavior By default, only minimum length is enforced (8 characters, configurable via `PASSWORD_MIN_LENGTH`). ## Custom rules ```python from fastapi_fullauth import FullAuth, FullAuthConfig from fastapi_fullauth.validators import PasswordValidator validator = PasswordValidator( min_length=10, require_uppercase=True, require_lowercase=True, require_digit=True, require_special=True, blocked_passwords=["password123", "qwerty123"], ) fullauth = FullAuth( adapter=adapter, config=FullAuthConfig( SECRET_KEY="...", ), password_validator=validator, ) ``` ## Validation rules | Rule | Default | Description | |------|---------|-------------| | `min_length` | `8` | Minimum password length | | `require_uppercase` | `False` | Must contain `[A-Z]` | | `require_lowercase` | `False` | Must contain `[a-z]` | | `require_digit` | `False` | Must contain `[0-9]` | | `require_special` | `False` | Must contain `[!@#$%^&*(),.?":{}|<>]` | | `blocked_passwords` | `[]` | List of disallowed passwords (case-insensitive) | When validation fails, a `422 Unprocessable Entity` response is returned with all violated rules: ```json { "detail": "Password must be at least 10 characters; Password must contain at least one uppercase letter" } ``` ## Password hashing Passwords are hashed with **Argon2id** by default. Switch to bcrypt via config: ```python fullauth = FullAuth( adapter=adapter, config=FullAuthConfig( SECRET_KEY="...", PASSWORD_HASH_ALGORITHM="bcrypt", # requires: pip install bcrypt ), ) ``` When switching algorithms, existing hashes are transparently detected by prefix (`$2b$` for bcrypt, `$argon2` for Argon2id). Users are rehashed on their next successful login. --- # Custom Token Claims # Custom Token Claims Embed app-specific data into JWT tokens. Custom claims are available in the `extra` field of decoded token payloads. ## Setup Pass an async callback to `on_create_token_claims`: ```python from fastapi_fullauth import FullAuthConfig from fastapi_fullauth.types import UserSchema async def add_claims(user: UserSchema) -> dict: return { "tenant_id": "acme", "plan": "pro", } fullauth = FullAuth( adapter=adapter, config=FullAuthConfig( SECRET_KEY="...", ), on_create_token_claims=add_claims, ) ``` The returned dict is embedded in the `extra` field of every access token. ## Accessing claims Custom claims are available when decoding tokens: ```python payload = await fullauth.token_engine.decode_token(token) tenant_id = payload.extra.get("tenant_id") plan = payload.extra.get("plan") ``` ## Reserved keys The following keys cannot be used in custom claims (they're used by the JWT structure): `sub`, `exp`, `iat`, `jti`, `type`, `roles`, `extra`, `family_id` If your callback returns any of these, a `ValueError` is raised at token creation time. ## When claims are generated 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 This means claims stay fresh on each refresh. If a user's plan changes, the next token refresh picks it up. --- # Middleware # Middleware fastapi-fullauth includes three middleware components. By default, `init_app()` auto-wires them based on config flags. Pass `auto_middleware=False` to manage them yourself, or use `init_middleware()` when wiring routers manually: ```python fullauth.init_app(app, auto_middleware=False) # or, when using composable routers: fullauth.init_middleware(app) ``` ## Security Headers Enabled by default (`INJECT_SECURITY_HEADERS=True`). Adds standard security headers to every response: | Header | Value | |--------|-------| | `X-Content-Type-Options` | `nosniff` | | `X-Frame-Options` | `DENY` | | `X-XSS-Protection` | `1; mode=block` | | `Strict-Transport-Security` | `max-age=31536000; includeSubDomains` | | `Referrer-Policy` | `strict-origin-when-cross-origin` | | `Permissions-Policy` | `geolocation=(), camera=(), microphone=()` | ### Custom headers Override or add headers: ```python from fastapi_fullauth.middleware import SecurityHeadersMiddleware app.add_middleware( SecurityHeadersMiddleware, custom_headers={ "X-Frame-Options": "SAMEORIGIN", # override default "X-Custom-Header": "value", # add new }, ) ``` ## CSRF Protection Disabled by default (`CSRF_ENABLED=False`). Enable it for cookie-based auth where the frontend and backend share a domain: ```python from fastapi_fullauth import FullAuth, FullAuthConfig fullauth = FullAuth( adapter=adapter, config=FullAuthConfig( SECRET_KEY="...", CSRF_ENABLED=True, CSRF_SECRET="optional-separate-secret", # falls back to SECRET_KEY ), ) ``` ### How it works Uses the **double-submit cookie** pattern: 1. On `GET` requests, a signed CSRF cookie (`fullauth_csrf`) is set 2. On state-changing requests (`POST`, `PUT`, `PATCH`, `DELETE`), the client must send the cookie value in the `X-CSRF-Token` header 3. The middleware verifies the cookie signature and compares cookie vs header ### Frontend integration ```javascript // read the CSRF cookie const csrfToken = document.cookie .split('; ') .find(row => row.startsWith('fullauth_csrf=')) ?.split('=')[1]; // include it in requests fetch('/api/v1/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken, }, credentials: 'include', body: JSON.stringify({ email, password }), }); ``` ### Exempt paths ```python from fastapi_fullauth.middleware import CSRFMiddleware app.add_middleware( CSRFMiddleware, secret="your-secret", exempt_paths=["/api/v1/webhooks"], ) ``` ## Rate Limiting See [Rate Limiting](rate-limiting.md) for full details. --- # Rate Limiting # Rate Limiting fastapi-fullauth provides two levels of rate limiting: 1. **Auth rate limits** — per-route limits on login, register, and password reset (enabled by default) 2. **Global rate limit middleware** — limits all requests per IP (disabled by default) Both support in-memory and Redis backends. ## Auth rate limits Enabled by default. Protects auth endpoints from brute force: | Route | Default limit | Config | |-------|--------------|--------| | Login | 5 per minute | `AUTH_RATE_LIMIT_LOGIN` | | Register | 3 per minute | `AUTH_RATE_LIMIT_REGISTER` | | Password reset | 3 per minute | `AUTH_RATE_LIMIT_PASSWORD_RESET` | ```python from fastapi_fullauth import FullAuth, FullAuthConfig fullauth = FullAuth( adapter=adapter, config=FullAuthConfig( SECRET_KEY="...", AUTH_RATE_LIMIT_LOGIN=10, # 10 login attempts per window AUTH_RATE_LIMIT_REGISTER=5, # 5 registrations per window AUTH_RATE_LIMIT_WINDOW_SECONDS=120, # 2-minute window ), ) ``` Disable entirely: ```python fullauth = FullAuth( adapter=adapter, config=FullAuthConfig( SECRET_KEY="...", AUTH_RATE_LIMIT_ENABLED=False, ), ) ``` ## Global rate limit middleware Limits all requests per client IP. Disabled by default: ```python fullauth = FullAuth( adapter=adapter, config=FullAuthConfig( SECRET_KEY="...", RATE_LIMIT_ENABLED=True, ), ) fullauth.init_app(app) # middleware is auto-added ``` Default: 60 requests per 60 seconds per IP. Response headers are included on every response: ``` X-RateLimit-Limit: 60 X-RateLimit-Remaining: 57 X-RateLimit-Reset: 45 ``` When the limit is exceeded, a `429 Too Many Requests` response is returned. ## Proxy support Behind a reverse proxy (Nginx, Cloudflare, AWS ALB), `request.client.host` is the proxy's IP, not the real user's. Configure trusted proxy headers so rate limiting works correctly: ```python fullauth = FullAuth( adapter=adapter, config=FullAuthConfig( SECRET_KEY="...", TRUSTED_PROXY_HEADERS=["X-Forwarded-For"], ), ) ``` !!! warning Only list headers you trust. If your server is directly exposed to the internet (no proxy), leave this empty — otherwise users can spoof their IP via the header. When `X-Forwarded-For` contains a chain (e.g. `1.2.3.4, 10.0.0.1`), the first IP (original client) is used. This setting applies to both auth rate limits and the global rate limit middleware. ## Redis backend For multi-process or multi-server deployments, use the Redis backend: ```python fullauth = FullAuth( adapter=adapter, config=FullAuthConfig( SECRET_KEY="...", RATE_LIMIT_ENABLED=True, RATE_LIMIT_BACKEND="redis", REDIS_URL="redis://localhost:6379/0", ), ) ``` The in-memory backend is per-process. Redis shares state across all workers/servers. ## Custom backends Register your own lockout or rate limiter backend: ```python from fastapi_fullauth.protection.lockout import LockoutManager, register_lockout_backend class DatabaseLockoutManager(LockoutManager): def __init__(self, max_attempts, lockout_seconds, **kwargs): super().__init__(max_attempts, lockout_seconds) async def is_locked(self, key: str) -> bool: ... async def record_failure(self, key: str) -> None: ... async def clear(self, key: str) -> None: ... register_lockout_backend("database", DatabaseLockoutManager) # Then set LOCKOUT_BACKEND="database" in config ``` Same pattern for rate limiters with `register_rate_limiter_backend()`. ## Manual middleware setup If you need more control, disable auto-middleware and add it yourself: ```python from fastapi_fullauth.protection.ratelimit import RateLimitMiddleware, RateLimiter fullauth.init_app(app, auto_middleware=False) app.add_middleware( RateLimitMiddleware, max_requests=100, window_seconds=60, exempt_paths=["/health", "/metrics"], trusted_proxy_headers=["X-Forwarded-For"], ) ``` --- # OAuth2 Social Login # OAuth2 Social Login Add Google and GitHub login with a few config lines. Users can link multiple providers alongside email/password login. ## Installation ```bash pip install fastapi-fullauth[oauth] ``` ## Configuration ```python from fastapi_fullauth import FullAuth, FullAuthConfig from fastapi_fullauth.oauth.google import GoogleOAuthProvider from fastapi_fullauth.oauth.github import GitHubOAuthProvider fullauth = FullAuth( adapter=adapter, config=FullAuthConfig(SECRET_KEY="..."), providers=[ GoogleOAuthProvider( client_id="your-google-client-id", client_secret="your-google-secret", redirect_uris=[ "http://localhost:3000/auth/callback", "https://myapp.com/auth/callback", ], ), GitHubOAuthProvider( client_id="your-github-client-id", client_secret="your-github-secret", redirect_uris=["http://localhost:3000/auth/callback"], ), ], ) ``` !!! tip `redirect_uris` is the list of allowed callback URLs. The client must pass `redirect_uri` as a query parameter in the authorize request — the library validates it against this list. ## Routes When OAuth providers are configured, these routes are registered automatically: | Method | Path | Description | |--------|------|-------------| | GET | `/auth/oauth/providers` | List configured providers | | GET | `/auth/oauth/{provider}/authorize` | Get authorization URL | | POST | `/auth/oauth/{provider}/callback` | Exchange code for tokens | | GET | `/auth/oauth/accounts` | List linked OAuth accounts | | DELETE | `/auth/oauth/accounts/{provider}` | Unlink a provider | ## How the flow works ### 1. Get the authorization URL ``` GET /api/v1/auth/oauth/google/authorize?redirect_uri=http://localhost:3000/auth/callback ``` Response: ```json { "authorization_url": "https://accounts.google.com/o/oauth2/v2/auth?client_id=...&state=..." } ``` The `redirect_uri` parameter is optional. If omitted, the first URI in your `redirect_uris` list is used. The value is validated against the allowed list. ### 2. Redirect the user Your frontend redirects the user to the `authorization_url`. The user authenticates with Google/GitHub. ### 3. Handle the callback The provider redirects back to your `redirect_uri` with `code` and `state` query parameters. Your frontend sends these to the callback endpoint: ``` POST /api/v1/auth/oauth/google/callback { "code": "4/0AX4XfW...", "state": "eyJ..." } ``` Response: ```json { "access_token": "eyJ...", "refresh_token": "eyJ...", "token_type": "bearer", "expires_in": 1800, "user": null } ``` From this point on, the session works exactly like email/password login. The user can call `/me`, `/refresh`, `/logout`, etc. with the JWT tokens. ## What happens on callback 1. **State token is verified** (CSRF protection, 5-minute TTL) 2. **Authorization code is exchanged** for provider tokens 3. **User info is fetched** from the provider (email, name, picture) 4. **Account linking logic** runs: - If this provider account is already linked → update tokens, return existing user - If an account with the same email exists → link the OAuth account to it - Otherwise → create a new user with a random password 5. **JWT tokens are issued** (same as regular login) !!! note If the provider reports the email as verified, the user's `is_verified` flag is set to `True` automatically. ## Auto-linking by email By default, if a user registers with `user@example.com` via email/password, then later logs in with Google using the same email, the accounts are linked automatically. Disable this with: ```python fullauth = FullAuth( adapter=adapter, config=FullAuthConfig( ..., OAUTH_AUTO_LINK_BY_EMAIL=False, ), ) ``` ## Unlinking providers Users can unlink an OAuth provider: ``` DELETE /api/v1/auth/oauth/accounts/google ``` This is blocked if the OAuth account is the user's only login method (no password set, no other OAuth providers). The user must set a password first. ## Event hooks ```python async def on_oauth_login(user, provider, is_new_user): if is_new_user: print(f"New user via {provider}: {user.email}") else: print(f"Returning user via {provider}: {user.email}") fullauth.hooks.on("after_oauth_login", on_oauth_login) ``` The `after_register` hook also fires for new OAuth users. ## Provider setup guides ### Google 1. Go to [Google Cloud Console](https://console.cloud.google.com/) 2. Create a project (or select existing) 3. Go to **APIs & Services > Credentials** 4. Create an **OAuth 2.0 Client ID** (Web application) 5. Add your redirect URIs under **Authorized redirect URIs** 6. Copy the Client ID and Client Secret Default scopes: `openid`, `email`, `profile` ### GitHub 1. Go to [GitHub Developer Settings](https://github.com/settings/developers) 2. Click **New OAuth App** 3. Set the **Authorization callback URL** to your redirect URI 4. Copy the Client ID and Client Secret Default scopes: `read:user`, `user:email` --- # Database Migrations # Database Migrations fastapi-fullauth provides Alembic integration helpers for managing database schema migrations. ## Quick start (without Alembic) For development or simple projects, create tables directly: === "SQLModel" ```python from sqlmodel import SQLModel async with engine.begin() as conn: await conn.run_sync(SQLModel.metadata.create_all) ``` === "SQLAlchemy" ```python from fastapi_fullauth.adapters.sqlalchemy.models.base import FullAuthBase async with engine.begin() as conn: await conn.run_sync(FullAuthBase.metadata.create_all) ``` ## Alembic integration For production, use Alembic for proper migration management. ### 1. Initialize Alembic ```bash alembic init alembic ``` ### 2. Update env.py Import fullauth models so Alembic detects them during autogenerate: === "All tables" ```python # alembic/env.py from fastapi_fullauth.migrations import include_fullauth_models from sqlmodel import SQLModel include_fullauth_models("sqlmodel") from your_app.models import User # noqa: F401 target_metadata = SQLModel.metadata ``` === "Selective tables" ```python # alembic/env.py — only core + roles, no permissions/oauth from fastapi_fullauth.migrations import include_fullauth_models from sqlmodel import SQLModel include_fullauth_models("sqlmodel", include=["base", "role"]) from your_app.models import User # noqa: F401 target_metadata = SQLModel.metadata ``` === "SQLAlchemy" ```python # alembic/env.py from fastapi_fullauth.migrations import include_fullauth_models from your_app.models import Base # your declarative base include_fullauth_models("sqlalchemy") target_metadata = Base.metadata ``` ### 3. Generate migrations ```bash alembic revision --autogenerate -m "add fullauth tables" alembic upgrade head ``` ## Model groups Models are split into groups. Import only what you need: | Group | Tables | When to include | |-------|--------|-----------------| | `base` | `fullauth_users`, `fullauth_refresh_tokens` | Always (core auth) | | `role` | `fullauth_roles`, `fullauth_user_roles` | When using roles | | `permission` | `fullauth_permissions`, `fullauth_role_permissions` | When using RBAC permissions | | `oauth` | `fullauth_oauth_accounts` | When using OAuth providers | ## Helper functions ### `include_fullauth_models(adapter, include=None)` Imports fullauth model classes so Alembic's autogenerate detects them. Call this in `env.py` before setting `target_metadata`. ```python from fastapi_fullauth.migrations import include_fullauth_models # all tables include_fullauth_models("sqlmodel") # selective — only core + roles include_fullauth_models("sqlmodel", include=["base", "role"]) ``` ### `get_fullauth_metadata(adapter)` Returns the SQLAlchemy `MetaData` object containing fullauth table definitions. Useful if you need to merge metadata from multiple sources. ```python from fastapi_fullauth.migrations import get_fullauth_metadata metadata = get_fullauth_metadata("sqlalchemy") ``` --- # API Reference # API Reference Quick reference for the main classes, types, and functions. ## FullAuth The main auth manager. Central entry point for the library. ```python from fastapi_fullauth import FullAuth, FullAuthConfig fullauth = FullAuth( adapter=adapter, # required — database adapter config=FullAuthConfig(...), # FullAuthConfig object (see Configuration) providers=None, # list of OAuthProvider instances backends=None, # [BearerBackend()] by default password_validator=None, # PasswordValidator instance on_create_token_claims=None, # async callback for custom JWT claims ) ``` ### Methods | Method | Description | |--------|-------------| | `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. | | `bind(app)` | Bind FullAuth to a FastAPI app (sets `app.state.fullauth`). Required when using composable routers without `init_app()`. | | `init_middleware(app)` | Wire up middleware from config. Also calls `bind()` if not already done. | | `hooks.on(event, callback)` | Register an event hook | ### Properties | Property | Type | Description | |----------|------|-------------| | `config` | `FullAuthConfig` | Active configuration | | `adapter` | `AbstractUserAdapter` | Database adapter | | `token_engine` | `TokenEngine` | JWT creation/validation engine | | `auth_router` | `APIRouter` | Login, logout, register, refresh routes | | `profile_router` | `APIRouter` | Me, update profile, change password, delete account routes | | `verify_router` | `APIRouter` | Email verification and password reset routes | | `admin_router` | `APIRouter` | Role/permission management routes (superuser) | | `oauth_router` | `APIRouter` | OAuth provider routes | ## FullAuthConfig ```python from fastapi_fullauth import FullAuthConfig ``` Pydantic Settings class. See [Configuration](configuration.md) for all options. ## Types ```python from fastapi_fullauth.types import ( UserSchema, # base user response model CreateUserSchema, # base registration model (email + password) TokenPair, # access_token + refresh_token + token_type + expires_in TokenPayload, # decoded JWT payload RefreshToken, # stored refresh token record OAuthAccount, # linked OAuth provider account OAuthUserInfo, # user info from OAuth provider ) ``` ### UserSchema ```python class UserSchema(BaseModel): id: UUID email: EmailStr is_active: bool = True is_verified: bool = False is_superuser: bool = False PROTECTED_FIELDS: ClassVar[set[str]] = { "id", "email", "hashed_password", "is_active", "is_verified", "is_superuser", "roles", "password", "created_at", "refresh_tokens", } ``` Extend `PROTECTED_FIELDS` in subclasses to protect custom sensitive fields from profile updates. ### TokenPair ```python class TokenPair(BaseModel): access_token: str refresh_token: str token_type: str = "bearer" expires_in: int | None = None ``` ### TokenPayload ```python class TokenPayload(BaseModel): sub: str # user ID exp: datetime # expiry iat: datetime # issued at jti: str # unique token ID type: str # "access" or "refresh" roles: list[str] # user roles extra: dict[str, Any] # custom claims family_id: str | None # refresh token family ``` ## Dependencies ```python from fastapi_fullauth.dependencies import ( 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") ) ``` ## Exceptions ```python from fastapi_fullauth.exceptions import ( FullAuthError, # base exception AuthenticationError, # login failed AuthorizationError, # insufficient permissions TokenError, # invalid token TokenExpiredError, # token expired TokenBlacklistedError, # token was revoked UserAlreadyExistsError, # duplicate registration UserNotFoundError, # user not found InvalidPasswordError, # password validation failed AccountLockedError, # too many failed attempts NoValidFieldsError, # all profile update fields are protected UnknownFieldsError, # profile update contains unknown fields OAuthError, # OAuth base error OAuthProviderError, # provider-specific error ) ``` ## Utilities ```python from fastapi_fullauth import generate_secret_key, create_superuser # generate a cryptographically secure secret key key = generate_secret_key() # create a superuser programmatically user = await create_superuser(adapter, "admin@example.com", "password") ``` ## Validators ```python from fastapi_fullauth import PasswordValidator validator = PasswordValidator( min_length=10, require_uppercase=True, require_lowercase=True, require_digit=True, require_special=True, blocked_passwords=["password123"], ) ``` --- # Contributing # Contributing Thanks for your interest in contributing to FastAPI FullAuth! ## Development setup ```bash git clone https://github.com/mdfarhankc/fastapi-fullauth.git cd fastapi-fullauth uv sync --dev --extra sqlalchemy --extra sqlmodel --extra redis --extra oauth ``` ## Running tests ```bash uv run pytest tests/ -v ``` ## Linting and formatting ```bash uv run ruff check . uv run ruff format . ``` Both must pass before submitting a PR. CI enforces this. ## Making changes 1. Fork the repo and create a branch from `main` 2. Make your changes 3. Add tests for new functionality 4. Ensure all tests pass and lint is clean 5. Submit a pull request ## Branch naming | Prefix | Use | |--------|-----| | `feat/` | New features | | `fix/` | Bug fixes | | `refactor/` | Code improvements | | `docs/` | Documentation | ## What to contribute - Bug fixes - New OAuth providers (Apple, Discord, Microsoft, etc.) - Adapter implementations (MongoDB, Tortoise ORM, etc.) - Documentation improvements - Test coverage improvements - Performance improvements ## Reporting bugs Use the [bug report template](https://github.com/mdfarhankc/fastapi-fullauth/issues/new?template=bug_report.yml) on GitHub Issues. ## Requesting features Use the [feature request template](https://github.com/mdfarhankc/fastapi-fullauth/issues/new?template=feature_request.yml) on GitHub Issues. ## Code style - Follow existing patterns in the codebase - Use type annotations - Keep functions focused and small - Log security-sensitive events via `logging.getLogger("fastapi_fullauth.*")` - Don't add docstrings/comments unless the logic isn't self-evident ## License By contributing, you agree that your contributions will be licensed under the MIT License.