A production-ready Authentication microservice built with FastAPI. This service solves the repetitive task of building secure authentication for every project by providing a robust, scalable, and highly configurable starter kit.
Building a secure auth system is hard. This project follows industry standards and best practices:
- Stateless & Scalable: Uses JWT with proper rotation and revocation.
- Security First: Includes MFA (TOTP), Brute-force protection, and secure cookie handling.
- Developer Friendly: Clean architecture, fully typed, and includes a comprehensive test suite.
- Multi-Factor Authentication (MFA): Secure TOTP-based authentication via Authenticator apps.
- Email/Password Auth: Secure hashing with
bcrypt/argon2. - Google OAuth 2.0: Integrated social login support.
- Session Management: Refresh token rotation with reuse detection and revocation.
- Device Tracking: Monitor active sessions with location and device info.
- Email Providers: Mailgun primary provider with SMTP fallback support.
- Production Logging: JSON stdout logs designed for Docker
json-filerotation. - Security: Rate limiting (Redis), account lockout, and hashed reset tokens.
- Architecture: Async SQLAlchemy 2.0, Alembic migrations, and Dependency Injection.
- Framework: FastAPI
- Database: SQLAlchemy 2.x (Async), Alembic
- Cache/Rate Limit: Redis (Async)
- Security: PyJWT, bcrypt, pyotp
- Email: Mailgun API or SMTP
- Logging: Python logging, colorized local logs, JSON production logs
- Validation: Pydantic 2.x
- Testing: Pytest, Coverage
| Method | Endpoint | Description | Auth |
|---|---|---|---|
POST |
/api/v1/auth/register |
Create a new user account | Public |
POST |
/api/v1/auth/login |
Login user and get access token, or MFA token when MFA is enabled | Public |
POST |
/api/v1/auth/admin/login |
Login admin and get access token, or MFA token when MFA is enabled | Public |
GET |
/api/v1/auth/google/callback |
Complete Google OAuth login callback | Public |
POST |
/api/v1/auth/refresh |
Rotate refresh token and issue a new access token | Refresh Cookie |
POST |
/api/v1/auth/logout |
Revoke current session | Refresh Cookie |
POST |
/api/v1/auth/logout-all |
Revoke all active sessions | Private |
GET |
/api/v1/auth/sessions |
Get active sessions for current user | Private |
DELETE |
/api/v1/auth/sessions/{session_id} |
Revoke a specific session | Private |
POST |
/api/v1/auth/forgot-password |
Request password reset token/email | Public |
POST |
/api/v1/auth/reset-password |
Reset password using reset token | Public |
POST |
/api/v1/auth/change-password |
Change current user's password | Private |
POST |
/api/v1/auth/email/verification/me |
Generate email verification token for current user | Private |
POST |
/api/v1/auth/verify-email |
Verify email using verification token | Public |
POST |
/api/v1/auth/mfa/setup |
Generate MFA secret & QR code | Private |
POST |
/api/v1/auth/mfa/enable |
Verify & enable MFA for account | Private |
POST |
/api/v1/auth/mfa/disable |
Disable MFA for current user | Private |
POST |
/api/v1/auth/mfa/verify-login |
Finalize login with OTP code | MFA Token |
This service implements a secure two-step MFA process:
- Setup: User calls
/mfa/setup(authenticated) to receive a secret and a QR code URL. - Activation: User scans the QR and provides the first code to
/mfa/enableto activate. - Login: If enabled,
/loginreturns astatus: "mfa_required"and a temporarymfa_token. - Verification: User submits the OTP and
mfa_tokento/mfa/verify-loginto receive final Access & Refresh tokens.
Run the entire stack (API + Redis + DB) with a single command:
docker-compose up --buildapp/
apis/ API route modules
cache/ Redis client
celery/ Background task modules
configs/ Settings, database, initialization
constants/ Enums and messages
core/ JWT, cookies, OAuth, security, lifespan
dependencies/ FastAPI dependencies
middleware/ Rate limiter
models/ SQLAlchemy models
repository/ Database repositories
schemas/ Pydantic schemas
services/ Business logic
migrations/ Alembic migration environment
requirements/ pip requirements files
scripts/ Helper scripts
tests/ Unit, integration, and e2e tests
- Python
3.12+ - Redis
- SQLite, MySQL, or another SQLAlchemy-supported database
uvorpip
Create a .env file from the example:
cp .env.example .envRequired secrets:
JWT_ACCESS_SECRET=change-me
JWT_REFRESH_SECRET=change-meGOOGLE_CLIENT_ID=change-me
GOOGLE_CLIENT_SECRET=change-me
GOOGLE_REDIRECT_URI=http://localhost:8000/api/v1/auth/google/callbackEMAIL_PROVIDER=mailgun
MAILGUN_API_URL=https://api.mailgun.net/v3
MAILGUN_API_KEY=change-me
MAILGUN_DOMAIN=change-me
MAILGUN_FROM_EMAIL=no-reply@example.comEMAIL_PROVIDER=smtp
SMTP_TLS=true
SMTP_SSL=false
SMTP_PORT=587
SMTP_HOST=smtp.example.com
SMTP_USERNAME=change-me
SMTP_PASSWORD=change-me
SMTP_FROM_EMAIL=no-reply@example.com
SMTP_FROM_NAME=Auth ServiceCommon local values:
ENVIRONMENT=local
DEBUG=true
LOG_LEVEL=DEBUG
LOG_QUEUE_SIZE=5000
DATABASE_URL=sqlite+aiosqlite:///./test.db
SYNC_DATABASE_URL=sqlite:///./test.db
REDIS_URL=redis://localhost:6379/0
API_ROOT=/api
API_V1_PREFIX=/v1
COOKIE_SECURE=false
COOKIE_SAMESITE=laxFor MySQL, use an async database URL:
DATABASE_URL=mysql+aiomysql://user:password@localhost:3306/auth_service
SYNC_DATABASE_URL=mysql+pymysql://user:password@localhost:3306/auth_serviceUsing uv:
uv sync --extra devUsing pip:
python -m venv .venv
source .venv/bin/activate
pip install -r requirements/dev.txtOn Windows PowerShell:
python -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -r requirements/dev.txtStart Redis first, then run the app:
python run_dev.pyOr run Uvicorn directly:
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000API docs:
http://localhost:8000/docs
http://localhost:8000/redoc
Create a migration:
alembic revision --autogenerate -m "create users tables"Apply migrations:
alembic upgrade headCheck current migration:
alembic currentHelper script:
bash scripts/alembic_migration.sh create -m "migration message"
bash scripts/alembic_migration.sh upgrade
bash scripts/alembic_migration.sh current- User logs in with email/password or Google OAuth.
- Service returns a short-lived JWT access token.
- Service stores a hashed refresh token in
user_sessions. - Refresh token is sent as an HTTP-only cookie.
- Refreshing rotates the refresh token and revokes the previous session.
- Reuse or mismatch detection revokes all user sessions.
The service uses app.configs.logging.get_logger(__name__) for application logs. setup_logging() runs during app startup from the FastAPI lifespan hook.
Logs are written to stdout only. This matches the production Docker setup where the container filesystem is read-only and Docker owns persistence/rotation through the json-file logging driver.
logger.info()
|
v
root logger
|
v
StreamHandler(stdout)
|
v
Docker json-file driver
|
v
host-managed container logs
Logging behavior:
- Local/staging console logs use colored output.
- Production logs are structured JSON on
stdout. - The app does not write
logs/app.login Docker production. - Docker rotates logs at
10 MB. - Docker keeps
3rotated log files. read_only: truekeeps the container filesystem immutable.tmpfs: /tmpprovides temporary writable space for runtime temp files.- Noisy libraries like
uvicorn,sqlalchemy,httpx, andhttpcoreare suppressed.
Docker compose logging config:
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
read_only: true
tmpfs:
- /tmpWhy stdout-only:
app writes stdout
|
v
Docker captures logs
|
v
Docker rotates log files
Do not add app-level file logging in production unless you also mount a writable volume for logs.
Example usage:
from app.configs.logging import get_logger
logger = get_logger(__name__)
logger.info("User logged in", extra={"user_id": user_id})The email service uses a provider interface so the app can switch between Mailgun and SMTP from environment settings.
EmailService
|
v
get_email_provider()
|
+----------------+
| |
v v
MailgunProvider SMTPProvider
| |
v v
Mailgun API SMTP server
Provider selection:
EMAIL_PROVIDER=mailgunor:
EMAIL_PROVIDER=smtpUsage:
from app.services.email import email_service
await email_service.send_email(
recipients=["user@example.com"],
subject="Verify your email",
html_body="<p>Hello</p>",
)Background sending:
await email_service.send_email_background(
recipients=["user@example.com"],
subject="Verify your email",
html_body="<p>Hello</p>",
background_tasks=background_tasks,
)Errors are logged with logger.exception(). Background emails use raise_on_error=False so failed email delivery does not crash the request.
FastAPI lifespan is used for app-level startup and shutdown work. It should manage shared infrastructure resources, not per-request objects.
Current startup flow:
app startup
|
v
setup_logging()
|
v
init_redis()
|
v
RateLimiter(app.state.redis)
Current shutdown flow:
app shutdown
|
v
close_redis(app.state.redis)
Good things to put in lifespan:
- Logging setup
- Redis connection setup and close
- Rate limiter initialization
- Database engine health check
- Background scheduler start/stop
- Cache warmup
- Metrics/tracing setup
Do not put these in lifespan:
- Per-request database sessions
- Current user loading
- Auth checks
- Request-specific service objects
- Request-specific transactions
Use FastAPI dependencies for per-request work:
async def get_async_db() -> AsyncGenerator[AsyncSession, None]:
async with AsyncSessionLocal() as session:
yield sessiondb_deps = Annotated[AsyncSession, Depends(get_async_db)]Rule:
lifespan = shared app resources
dependencies = per-request resources
- Access tokens are short-lived.
- Refresh tokens are stored only as hashes.
- Refresh tokens are rotated on every refresh.
- Session reuse detection revokes all sessions for the user.
- Failed login attempts can temporarily lock an account.
- Password reset tokens are hashed before storage.
- Refresh cookies are HTTP-only by default.
- Set
COOKIE_SECURE=truein production.
Main tables:
usersuser_sessionspassword_reset_tokens
User fields include:
- name, email, phone, profile image
- hashed password
- Google ID
- role
- mfa enable, mfa secret
- email verification status
- active/superuser flags
- login and lockout metadata
- soft-delete metadata
Session fields include:
- user ID
- refresh token JTI
- refresh token hash
- user agent and IP address
- expiry
- revocation metadata
- replacement session ID
Format:
ruff format .Lint:
ruff check . --fixType-check:
mypy appHelper script:
bash scripts/code_quality.sh allRun tests:
pytestRun with coverage:
pytest --cov=appCoverage is configured to require 80%.
- Use strong unique JWT secrets.
- Use HTTPS.
- Set
COOKIE_SECURE=true. - Restrict
ALLOWED_ORIGINS. - Use a production database.
- Run migrations before deployment.
- Run behind a process manager such as Gunicorn with Uvicorn workers.
- Configure Redis persistence/availability based on deployment needs.
Production dependencies are listed in:
requirements/prod.txt
MIT License. See LICENSE.