Maintenance notice: FastAPI Users is in maintenance mode — no new features, only security and dependency updates. It is stable for production use.
fastapi-users provides ready-to-use user registration, login, password reset, and email verification. It integrates natively with async SQLAlchemy and JWT.
| Feature | Endpoint |
|---|---|
| Register | POST /auth/register |
| Login (JWT) | POST /auth/jwt/login |
| Logout | POST /auth/jwt/logout |
| Forgot password | POST /auth/forgot-password |
| Reset password | POST /auth/reset-password |
| Request verification | POST /auth/request-verify-token |
| Verify email | POST /auth/verify |
| Get current user | GET /users/me |
| Update current user | PATCH /users/me |
| Get user by ID (admin) | GET /users/{id} |
| Update user (admin) | PATCH /users/{id} |
| Delete user (admin) | DELETE /users/{id} |
Add to pyproject.toml dependencies:
fastapi-users[sqlalchemy]
Install:
uv add "fastapi-users[sqlalchemy]"
asyncpgandsqlalchemy[asyncio]are already inpyproject.toml— no additional driver install needed.
FastAPI Users needs separate secrets for password reset and verification tokens. Add these to Settings:
# app/config.py — add inside the Settings class
# FastAPI Users
auth_reset_password_token_secret: str
auth_verification_token_secret: strAdd to .env and .env.example:
# FastAPI Users — generate each with: openssl rand -hex 32
AUTH_RESET_PASSWORD_TOKEN_SECRET=your-reset-secret-here
AUTH_VERIFICATION_TOKEN_SECRET=your-verification-secret-hereFastAPI Users provides SQLAlchemyBaseUserTableUUID which includes all required auth fields. Extend it and register it with the shared Base from app/database.py.
# app/users/models.py
from fastapi_users.db import SQLAlchemyBaseUserTableUUID
from app.database import Base
class User(SQLAlchemyBaseUserTableUUID, Base):
"""
Inherits from SQLAlchemyBaseUserTableUUID which provides:
- id (UUID, primary key)
- email (str, unique, indexed)
- hashed_password (str)
- is_active (bool, default True)
- is_superuser (bool, default False)
- is_verified (bool, default False)
Add custom columns below as needed:
# first_name: Mapped[str] = mapped_column(String(100), nullable=True)
"""
passThese schemas handle request/response validation for all user endpoints.
# app/users/schemas.py
import uuid
from fastapi_users import schemas
class UserRead(schemas.BaseUser[uuid.UUID]):
"""Schema for reading user data (returned in API responses)."""
pass
class UserCreate(schemas.BaseUserCreate):
"""Schema for user registration requests."""
pass
class UserUpdate(schemas.BaseUserUpdate):
"""Schema for user update requests (all fields optional)."""
passTo expose custom model fields in responses, add them to UserRead. To allow users to set them on registration, add to UserCreate.
This bridges FastAPI Users with the existing async session from app/database.py.
# app/users/dependencies.py
from fastapi import Depends
from fastapi_users.db import SQLAlchemyUserDatabase
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.users.models import User
async def get_user_db(session: AsyncSession = Depends(get_db)):
yield SQLAlchemyUserDatabase(session, User)UserManager is where all user lifecycle logic lives. Secrets are loaded from settings — never hardcoded.
The
on_after_forgot_passwordandon_after_request_verifyhooks below have
# app/users/manager.py
import uuid
from typing import Optional
from fastapi import Depends, Request, Response
from fastapi_users import BaseUserManager, UUIDIDMixin
from fastapi_users.db import SQLAlchemyUserDatabase
from app.config import settings
from app.users.dependencies import get_user_db
from app.users.models import User
class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
reset_password_token_secret = settings.auth_reset_password_token_secret
verification_token_secret = settings.auth_verification_token_secret
async def on_after_register(self, user: User, request: Optional[Request] = None):
print(f"User {user.id} has registered.")
# TODO: send welcome email
async def on_after_login(
self,
user: User,
request: Optional[Request] = None,
response: Optional[Response] = None,
):
print(f"User {user.id} logged in.")
async def on_after_forgot_password(
self, user: User, token: str, request: Optional[Request] = None
):
print(f"User {user.id} requested password reset. Token: {token}")
# TODO: send password reset email with token
async def on_after_reset_password(self, user: User, request: Optional[Request] = None):
print(f"User {user.id} has reset their password.")
async def on_after_request_verify(
self, user: User, token: str, request: Optional[Request] = None
):
print(f"Verification requested for user {user.id}. Token: {token}")
# TODO: send verification email with token
async def on_after_verify(self, user: User, request: Optional[Request] = None):
print(f"User {user.id} has been verified.")
async def on_before_delete(self, user: User, request: Optional[Request] = None):
print(f"User {user.id} is about to be deleted.")
async def on_after_delete(self, user: User, request: Optional[Request] = None):
print(f"User {user.id} has been deleted.")
async def get_user_manager(user_db: SQLAlchemyUserDatabase = Depends(get_user_db)):
yield UserManager(user_db)Create a dedicated app/auth/ directory for the JWT backend and fastapi-users instance — mirroring the app/auth/ pattern used for custom auth logic.
app/auth/backend.pyalso houses the RBAC dependency factory (require_role) and the ready-to-use role dependencies (current_student,current_instructor,current_admin). See RBAC Setup for the full guide.
# app/auth/backend.py
import uuid
from fastapi_users import FastAPIUsers, models
from fastapi_users.authentication import AuthenticationBackend, BearerTransport, JWTStrategy
from app.config import settings
from app.users.manager import get_user_manager
from app.users.models import User
bearer_transport = BearerTransport(tokenUrl="api/auth/jwt/login")
def get_jwt_strategy() -> JWTStrategy[models.UP, models.ID]:
return JWTStrategy(
secret=settings.jwt_secret_key,
lifetime_seconds=settings.jwt_access_token_expire_minutes * 60,
)
auth_backend = AuthenticationBackend(
name="jwt",
transport=bearer_transport,
get_strategy=get_jwt_strategy,
)
fastapi_users = FastAPIUsers[User, uuid.UUID](get_user_manager, [auth_backend])
current_active_user = fastapi_users.current_user(active=True)
current_superuser = fastapi_users.current_user(active=True, superuser=True)All fastapi-users auth routes live here, keeping app/api/router.py clean.
# app/auth/router.py
from fastapi import APIRouter
from app.auth.backend import auth_backend, fastapi_users
from app.users.schemas import UserCreate, UserRead
router = APIRouter()
router.include_router(
fastapi_users.get_auth_router(auth_backend),
prefix="/jwt",
tags=["Auth"],
)
router.include_router(
fastapi_users.get_register_router(UserRead, UserCreate),
tags=["Auth"],
)
router.include_router(
fastapi_users.get_reset_password_router(),
tags=["Auth"],
)
router.include_router(
fastapi_users.get_verify_router(UserRead),
tags=["Auth"],
)User management routes (me, by ID, update, delete).
# app/users/router.py
from fastapi import APIRouter
from app.auth.backend import fastapi_users
from app.users.schemas import UserRead, UserUpdate
router = APIRouter()
router.include_router(
fastapi_users.get_users_router(UserRead, UserUpdate),
tags=["Users"],
)Plug both routers into the central aggregator — same pattern as all other modules.
# app/api/router.py
from fastapi import APIRouter
from app.auth.router import router as auth_router
from app.users.router import router as users_router
router = APIRouter()
router.include_router(auth_router, prefix="/auth", tags=["Auth"])
router.include_router(users_router, prefix="/users", tags=["Users"])The central router is already wired into main.py with the /api prefix:
# app/main.py (existing line — no change needed)
app.include_router(api_router, prefix="/api", tags=["API"])| Method | Path | Content-Type |
|---|---|---|
POST |
/api/auth/jwt/login |
application/x-www-form-urlencoded |
POST |
/api/auth/jwt/logout |
— |
POST |
/api/auth/register |
application/json |
POST |
/api/auth/forgot-password |
application/json |
POST |
/api/auth/reset-password |
application/json |
POST |
/api/auth/request-verify-token |
application/json |
POST |
/api/auth/verify |
application/json |
GET |
/api/users/me |
— |
PATCH |
/api/users/me |
application/json |
GET |
/api/users/{id} |
— |
PATCH |
/api/users/{id} |
application/json |
DELETE |
/api/users/{id} |
— |
Common mistake:
POST /api/auth/jwt/loginusesapplication/x-www-form-urlencodedwith fieldusername(notapplication/json.
Self-deletion protection:
DELETE /api/users/{id}returns403if the requesting admin attempts to delete their own account.
PATCH /api/users/me— allowed fields: Onlypasswordis accepted.role,is_active,is_superuser, andis_verifiedare all stripped from the request and hidden from the OpenAPI schema. Email changes are blocked until email verification is enabled — see Email Setup. Role changes are admin-only viaPATCH /api/users/{id}.
Add the import to alembic/env.py so autogenerate detects the user table:
# alembic/env.py — add after existing model imports
from app.users import models as users_models # noqa: F401Then generate and apply the migration:
alembic revision --autogenerate -m "create users table"
alembic upgrade headImport current_active_user or current_superuser from app.auth.backend:
from fastapi import Depends
from app.auth.backend import current_active_user, current_superuser
from app.users.models import User
# Requires a valid JWT — returns 401 if not authenticated
@router.get("/me")
async def get_me(user: User = Depends(current_active_user)):
return {"id": str(user.id), "email": user.email}
# Requires superuser flag — returns 403 if not superuser
@router.delete("/{item_id}")
async def delete_item(item_id: int, user: User = Depends(current_superuser)):
...app/
├── auth/
│ ├── __init__.py
│ ├── backend.py ← auth_backend, fastapi_users, current_active_user, current_superuser
│ └── router.py ← login, logout, register, password reset, verify routes
├── users/
│ ├── __init__.py
│ ├── models.py ← User ORM model (SQLAlchemyBaseUserTableUUID + Base)
│ ├── schemas.py ← UserRead, UserCreate, UserUpdate
│ ├── dependencies.py ← get_user_db (bridges get_db → SQLAlchemyUserDatabase)
│ ├── manager.py ← UserManager + get_user_manager dependency
│ └── router.py ← /me, /{id} user management routes
└── api/
└── router.py ← central aggregator (auth + users + future modules)
- Add
fastapi-users[sqlalchemy]topyproject.tomland runuv sync - Add
AUTH_RESET_PASSWORD_TOKEN_SECRETandAUTH_VERIFICATION_TOKEN_SECRETto.env,.env.example, andapp/config.py - Create
app/users/models.pywithUsermodel - Create
app/users/schemas.pywithUserRead,UserCreate,UserUpdate - Create
app/users/dependencies.pywithget_user_db - Create
app/users/manager.pywithUserManagerandget_user_manager - Create
app/auth/backend.pywithauth_backend,fastapi_users,current_active_user, and RBAC dependencies — see RBAC Setup - Create
app/auth/router.pywith all auth routes - Create
app/users/router.pywith user management routes - Wire both routers into
app/api/router.py - Import
users_modelsinalembic/env.py - Run
alembic revision --autogenerate -m "create users table"andalembic upgrade head - Set up email sending — see Email Setup