A production-ready, standalone authentication microservice β register, login, OAuth 2.0, TOTP 2FA, RBAC, session management, and Redis rate limiting. Deploy once. Plug into every future project.
Features β’ Quick Start β’ API Endpoints β’ Integration Guide β’ Docker β’ Tests
- Overview
- Features
- Tech Stack
- Live Demo
- Project Structure
- Getting Started
- Environment Variables
- Database Schema
- API Endpoints
- End-to-End Flow Testing
- Running Tests
- Plugging Into Your Project
- Security Design
- Deployment Checklist
- Contributing
AuthShield is a complete, standalone authentication microservice you build once and reuse across every project. It handles everything auth β registration, email verification, JWT token lifecycle, OAuth 2.0 social login, TOTP two-factor authentication, role-based access control, session management, and rate limiting β so your application services never have to implement any of it again.
Downstream services share one JWT_SECRET_KEY, validate JWTs locally with zero runtime calls to AuthShield per request, and store the user_id UUID from the token as a foreign key in their own databases.
- β Standalone β one deployed service handles auth for any number of projects
- β
Zero auth code in your apps β a 40-line
auth.pyis all your downstream service needs - β Production-hardened β bcrypt, token rotation with theft detection, sliding-window rate limits, security headers on every response
- β 48 integration tests β real PostgreSQL and Redis, no infrastructure mocks
- β Full OAuth 2.0 β Google and GitHub with CSRF protection and account linking
- β TOTP 2FA β Google Authenticator / Authy compatible, QR code setup flow
- β Complete session control β list and revoke individual sessions per device
- β Structured logging β JSON logs ready for Datadog, CloudWatch, or Papertrail
- JWT access tokens (15-minute TTL) + opaque refresh tokens (7-day TTL)
- Refresh token rotation β every use issues a new pair and invalidates the old one
- Reuse detection β using a rotated token revokes the entire token family (theft signal β attacker and victim both logged out)
- Redis blacklist β
POST /auth/logouttakes effect on the very next request, not at natural JWT expiry - Email verification required before first login, with resend endpoint
- Google and GitHub sign-in via Authorization Code Flow
- CSRF protection β single-use state tokens stored in Redis; replayed callbacks rejected
- Account linking β OAuth login to an existing email/password account automatically merges them
- Auto-verified β OAuth users skip the email verification step entirely
- QR code PNG + raw secret returned on setup β scan with any TOTP app
- Single-use temporary token bridges the
login β TOTP verifystep (5-minute TTL) - Disabling 2FA requires a valid current TOTP code (prevents accidental lockout)
- Β±30-second clock skew tolerated (
valid_window=1in pyotp)
- Three built-in roles:
user,moderator,admin - Roles embedded in the JWT β your services check them with zero database calls
- Admin endpoints protected with a
require_roles(["admin"])dependency - Admin cannot remove their own admin role (last-admin lockout prevention)
- Every login creates a tracked session recording IP address, device info, and timestamps
- List all active sessions β current session flagged with
is_current: true - Revoke any specific session β remote sessions kill the refresh token; revoking the current session immediately blacklists the access token
- Admin can revoke all sessions for any user (used on account deactivation)
- Redis sliding-window algorithm β no fixed-window boundary to exploit
- Per-IP limits on all sensitive endpoints
Retry-After,X-RateLimit-Limit, andX-RateLimit-Windowheaders on every 429 response
X-Content-Type-Options: nosniffX-Frame-Options: DENYX-XSS-Protection: 1; mode=blockReferrer-Policy: strict-origin-when-cross-originStrict-Transport-Security+Content-Security-Policyin production modeServerheader stripped
- bcrypt hashing (12 rounds, ~250 ms per hash β brute-force resistant)
- Forgot/reset flow β time-limited single-use tokens via Redis; successful reset revokes all sessions
- Change password β requires current password, rejects same-as-current
- Email enumeration prevention β
/forgot-passwordalways returns200regardless of whether the email exists
- FastAPI β async HTTP framework with automatic OpenAPI docs
- Uvicorn β ASGI server (4 workers in production)
- PostgreSQL 15 β users, roles, sessions, tokens, login audit log
- SQLAlchemy 2.0 β async ORM with relationship loading
- Alembic β version-controlled schema migrations
- asyncpg β fastest async PostgreSQL driver for Python
- Redis 7 β token blacklist, rate limit windows, 2FA temp tokens, OAuth CSRF state, password reset tokens, email verification tokens
- PyJWT β JWT encode/decode (HS256)
- bcrypt β password hashing (12 rounds)
- pyotp β TOTP generation and verification
- qrcode + Pillow β QR code PNG generation for 2FA setup
- httpx β async HTTP for OAuth token exchange with Google/GitHub
- Pydantic v2 β request/response schema validation, typed settings
- pydantic-settings β typed environment variable loading with
.envsupport
- structlog β JSON-structured logs ready for production log aggregators
- pytest + pytest-asyncio β async test suite (48 tests)
- httpx β async test client via
ASGITransport - Real PostgreSQL and Redis β no infrastructure mocks; integration bugs that mocks hide are caught here
https://authshield-31lz.onrender.com/docs
- Swagger UI: https://authshield-31lz.onrender.com/docs
- ReDoc: https://authshield-31lz.onrender.com/redoc
curl https://authshield-31lz.onrender.com/api/v1/health
# {"status":"healthy","database":"ok","redis":"ok","version":"1.0.0"}# Register
curl -X POST https://authshield-31lz.onrender.com/api/v1/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","password":"TestPass123!","full_name":"Jane Doe"}'
# Login (after verifying email)
curl -X POST https://authshield-31lz.onrender.com/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","password":"TestPass123!"}'authshield/
βββ app/
β βββ main.py # App factory, middleware, exception handlers
β βββ config.py # Typed settings via pydantic-settings
β β
β βββ api/v1/endpoints/ # Route handlers (thin β delegate to services)
β β βββ auth.py # Register, login, refresh, logout, verify-email
β β βββ oauth.py # Google + GitHub OAuth flows + callbacks
β β βββ two_factor.py # 2FA enable, confirm, verify, disable
β β βββ sessions.py # List sessions, revoke session
β β βββ users.py # GET /users/me, PATCH /users/me
β β βββ admin.py # Admin user listing, roles, status management
β β
β βββ core/
β β βββ security.py # JWT encode/decode, bcrypt hash/verify
β β βββ rate_limiter.py # Redis sliding-window rate limiter
β β βββ exceptions.py # Custom exception hierarchy + error codes
β β βββ openapi.py # Custom OpenAPI schema (Swagger branding)
β β
β βββ db/
β β βββ session.py # SQLAlchemy async engine + get_db dependency
β β βββ redis.py # Redis connection pool + get_redis dependency
β β βββ base.py # SQLAlchemy declarative base
β β
β βββ middleware/
β β βββ security.py # Injects security headers on every response
β β
β βββ models/ # SQLAlchemy ORM models
β β βββ user.py # User (email, password_hash, OAuth fields, 2FA secret)
β β βββ role.py # Role (name, description)
β β βββ user_role.py # Many-to-many junction table
β β βββ refresh_token.py # Token (hash, family_id, rotation chain)
β β βββ session.py # Session (IP, device, is_active)
β β βββ login_history.py # Immutable audit log (append-only)
β β
β βββ repositories/ # DB query logic β no business logic here
β β βββ user_repository.py
β β βββ role_repository.py
β β βββ token_repository.py
β β βββ session_repository.py
β β
β βββ schemas/ # Pydantic request/response models
β β βββ auth.py # RegisterRequest, LoginRequest, TokenResponse
β β βββ user.py # UserResponse, UserUpdateRequest
β β βββ admin.py # AdminUserResponse, RoleUpdateRequest
β β βββ oauth.py # OAuthCallbackResponse
β β βββ two_factor.py # TwoFactorSetupResponse, TwoFactorVerifyRequest
β β βββ common.py # StandardResponse[T] wrapper
β β
β βββ services/ # Business logic layer
β βββ auth_service.py # Registration, login, logout, verification, refresh
β βββ oauth_service.py # Google + GitHub token exchange and user linking
β βββ totp_service.py # 2FA setup, confirmation, login verify, disable
β βββ session_service.py # Session listing and targeted revocation
β βββ password_service.py # Forgot/reset/change password flows
β βββ admin_service.py # User listing, role updates, status management
β
βββ alembic/ # Database migrations
β βββ versions/ # One file per schema change
β
βββ tests/
β βββ conftest.py # Fixtures: db (transaction rollback), client,
β β # regular_user, admin_user, rate_limit_client
β βββ test_auth.py # 18 tests β registration, verification, login, refresh, logout
β βββ test_passwords.py # 7 tests β forgot, reset, change password
β βββ test_sessions.py # 7 tests β listing, revocation, ownership checks
β βββ test_admin.py # 9 tests β RBAC enforcement, user management
β βββ test_rate_limiting.py # 3 tests β real rate limiter (bypassed in other tests)
β
βββ nginx/
β βββ nginx.conf # HTTPS, TLS 1.3, OCSP stapling, proxy config
β
βββ scripts/
β βββ seed_roles.py # Seeds user / moderator / admin roles
β
βββ Dockerfile # Multi-stage build (~180 MB final image)
βββ docker-compose.yml # authshield + postgres + redis, with health checks
βββ .env.example # All environment variables documented
βββ alembic.ini # Alembic configuration
βββ pytest.ini # pytest + asyncio config
βββ requirements.txt # Python dependencies
βββ README.md # This file
http://localhost:8000/api/v1
All protected endpoints require a JWT Bearer token in the Authorization header:
Authorization: Bearer <your_access_token>
Success:
{
"status": "success",
"message": "Operation successful",
"data": { ... }
}Error:
{
"status": "error",
"error_code": "AUTH_INVALID_CREDENTIALS",
"message": "Invalid email or password"
}| Header | Description |
|---|---|
X-RateLimit-Limit |
Max requests allowed in the window |
X-RateLimit-Window |
Window size in seconds |
Retry-After |
Seconds until the limit resets (on 429 only) |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
POST |
/auth/register |
β | Register with email + password |
POST |
/auth/verify-email |
β | Verify email with token from inbox |
POST |
/auth/resend-verification |
β | Re-send verification email |
POST |
/auth/login |
β | Login β returns access + refresh tokens |
POST |
/auth/refresh |
β | Rotate refresh token β returns new pair |
POST |
/auth/logout |
Bearer | Revoke current session |
POST |
/auth/logout-all |
Bearer | Revoke all sessions on all devices |
π Authentication Examples
Register:
curl -X POST http://localhost:8000/api/v1/auth/register \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "StrongPass123!",
"full_name": "Jane Doe"
}'{
"status": "success",
"message": "Registration successful. Please verify your email.",
"data": {
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com"
}
}Login:
curl -X POST http://localhost:8000/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com", "password": "StrongPass123!"}'{
"status": "success",
"data": {
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"refresh_token": "rt_a1b2c3d4e5f6...",
"token_type": "Bearer",
"expires_in": 900
}
}Refresh Token:
curl -X POST http://localhost:8000/api/v1/auth/refresh \
-H "Content-Type: application/json" \
-d '{"refresh_token": "rt_a1b2c3d4e5f6..."}'
# Old refresh_token is permanently invalidated β save the new pairLogout:
curl -X POST http://localhost:8000/api/v1/auth/logout \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{"refresh_token": "rt_a1b2c3d4e5f6..."}'
# Access token immediately blacklisted β 401 on very next request| Method | Endpoint | Auth | Description |
|---|---|---|---|
POST |
/auth/forgot-password |
β | Send reset email (always returns 200) |
POST |
/auth/reset-password |
β | Reset password with emailed token |
POST |
/auth/change-password |
Bearer | Change password while authenticated |
π Password Examples
Forgot Password:
curl -X POST http://localhost:8000/api/v1/auth/forgot-password \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com"}'
# Always returns 200 regardless of whether the email exists β prevents enumerationReset Password:
curl -X POST http://localhost:8000/api/v1/auth/reset-password \
-H "Content-Type: application/json" \
-d '{
"token": "token-copied-from-email",
"new_password": "NewStrongPass456!"
}'
# All existing sessions are revoked on successChange Password (authenticated):
curl -X POST http://localhost:8000/api/v1/auth/change-password \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"current_password": "OldPass123!",
"new_password": "NewPass456!"
}'
# Returns 400 AUTH_SAME_PASSWORD if new == current| Method | Endpoint | Description |
|---|---|---|
GET |
/auth/oauth/google |
Redirect browser to Google sign-in |
GET |
/auth/oauth/google/callback |
Google redirect callback (handled internally) |
GET |
/auth/oauth/github |
Redirect browser to GitHub sign-in |
GET |
/auth/oauth/github/callback |
GitHub redirect callback (handled internally) |
π OAuth Flow
Step 1 β Navigate the browser (full-page redirect, not a fetch call):
GET http://localhost:8000/api/v1/auth/oauth/google
β 302 to Google consent screen
Step 2 β User approves β Google calls the callback:
GET /auth/oauth/google/callback?code=xxx&state=yyy
Step 3 β AuthShield returns tokens:
{
"status": "success",
"data": {
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"refresh_token": "rt_a1b2c3d4...",
"token_type": "Bearer",
"is_new_user": true
}
}Account linking: If the OAuth email already exists in the database from a password registration, it is linked automatically. The user can then sign in via either method.
New user: No email verification required β the OAuth provider already verified the email.
| Method | Endpoint | Auth | Description |
|---|---|---|---|
POST |
/auth/2fa/enable |
Bearer | Begin 2FA setup β returns QR code and secret |
POST |
/auth/2fa/confirm |
Bearer | Confirm 2FA with the first code from the app |
POST |
/auth/2fa/verify |
β | Complete the 2FA login step with temp token |
POST |
/auth/2fa/disable |
Bearer | Disable 2FA (requires valid current TOTP code) |
π 2FA Flow
Step 1 β Enable:
curl -X POST http://localhost:8000/api/v1/auth/2fa/enable \
-H "Authorization: Bearer YOUR_TOKEN"{
"data": {
"secret": "JBSWY3DPEHPK3PXP",
"qr_code": "data:image/png;base64,iVBORw0KGgo...",
"qr_uri": "otpauth://totp/AuthShield:user@example.com?secret=JBSWY3DPEHPK3PXP&issuer=AuthShield"
}
}Step 2 β Scan QR code with Google Authenticator or Authy, then confirm:
curl -X POST http://localhost:8000/api/v1/auth/2fa/confirm \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"code": "123456"}'Step 3 β Login flow once 2FA is active:
# POST /auth/login now returns 403 instead of tokens:
{
"error_code": "AUTH_2FA_REQUIRED",
"details": { "temp_token": "tmp_abc123..." }
}
# Complete login with the TOTP code:
curl -X POST http://localhost:8000/api/v1/auth/2fa/verify \
-H "Content-Type: application/json" \
-d '{"temp_token": "tmp_abc123...", "code": "654321"}'
# Returns full access_token + refresh_token| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET |
/users/me |
Bearer | Get own profile, roles, and account status |
PATCH |
/users/me |
Bearer | Update full name or email |
π Profile Examples
Get Profile:
curl http://localhost:8000/api/v1/users/me \
-H "Authorization: Bearer YOUR_TOKEN"{
"data": {
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"full_name": "Jane Doe",
"roles": ["user"],
"is_verified": true,
"is_active": true,
"is_2fa_enabled": false,
"oauth_provider": null,
"avatar_url": null,
"created_at": "2025-03-01T10:00:00Z",
"updated_at": "2025-03-01T10:00:00Z"
}
}Update Profile:
curl -X PATCH http://localhost:8000/api/v1/users/me \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"full_name": "Jane Updated Doe"}'| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET |
/sessions |
Bearer | List all active sessions |
DELETE |
/sessions/{id} |
Bearer | Revoke a specific session |
π Session Examples
List Sessions:
curl http://localhost:8000/api/v1/sessions \
-H "Authorization: Bearer YOUR_TOKEN"{
"data": {
"sessions": [
{
"id": "aaa-111",
"ip_address": "192.168.1.1",
"device_info": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)...",
"created_at": "2025-03-01T08:00:00Z",
"last_used_at": "2025-03-01T10:30:00Z",
"is_current": true
},
{
"id": "bbb-222",
"ip_address": "10.0.0.5",
"device_info": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0)...",
"created_at": "2025-02-28T20:00:00Z",
"last_used_at": "2025-03-01T09:15:00Z",
"is_current": false
}
],
"total": 2
}
}Revoke a Session:
curl -X DELETE http://localhost:8000/api/v1/sessions/bbb-222 \
-H "Authorization: Bearer YOUR_TOKEN"
# Remote session β refresh token killed
# Current session β access token immediately blacklisted in Redis| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET |
/admin/users |
Admin | List all users (paginated + searchable) |
GET |
/admin/users/{id} |
Admin | Get full user details |
PATCH |
/admin/users/{id}/roles |
Admin | Replace user's entire role set |
PATCH |
/admin/users/{id}/status |
Admin | Activate or deactivate an account |
GET |
/admin/users/{id}/sessions |
Admin | View all sessions for a user |
DELETE |
/admin/users/{id}/sessions |
Admin | Revoke all sessions for a user |
π Admin Examples
List Users (with search):
curl "http://localhost:8000/api/v1/admin/users?search=jane&limit=20&skip=0" \
-H "Authorization: Bearer ADMIN_TOKEN"Update Roles:
curl -X PATCH http://localhost:8000/api/v1/admin/users/USER_ID/roles \
-H "Authorization: Bearer ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"roles": ["user", "moderator"]}'
# Admin cannot remove their own admin role β 400 ADMIN_CANNOT_REMOVE_OWN_ROLEDeactivate Account:
curl -X PATCH http://localhost:8000/api/v1/admin/users/USER_ID/status \
-H "Authorization: Bearer ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"is_active": false}'
# Immediately revokes ALL sessions and tokens for that user| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET |
/health |
β | Live connectivity check for DB and Redis |
curl http://localhost:8000/api/v1/health{
"status": "healthy",
"database": "ok",
"redis": "ok",
"version": "1.0.0"
}| Endpoint | Limit | Window |
|---|---|---|
POST /auth/login |
5 requests | 60 s |
POST /auth/register |
3 requests | 60 s |
POST /auth/forgot-password |
3 requests | 5 min |
POST /auth/2fa/verify |
5 requests | 60 s |
POST /auth/resend-verification |
3 requests | 5 min |
Algorithm: Redis sorted sets with a sliding window β unlike token-bucket or fixed-window implementations, there is no clock boundary to exploit. Keys expire automatically; no cleanup job needed.
- Python 3.12+
- PostgreSQL 15+
- Redis 7+
- Git
- Docker
git clone https://github.com/ravigupta97/authshield.git
cd authshieldWindows (PowerShell):
python -m venv venv
venv\Scripts\activatemacOS / Linux:
python3 -m venv venv
source venv/bin/activatepip install -r requirements.txtcp .env.example .env
# Open .env and fill in DATABASE_URL, REDIS_URL, JWT_SECRET_KEY, and SMTP credentialsGenerate a secure JWT_SECRET_KEY:
# Windows PowerShell
python -c "import secrets; print(secrets.token_hex(32))"# macOS / Linux
openssl rand -hex 32# Create the database
createdb authshield_db
# Apply all migrations
alembic upgrade head
# Seed default roles: user, moderator, admin
python scripts/seed_roles.pyuvicorn app.main:app --reload --port 8000Available at:
- Swagger UI: http://localhost:8000/docs β live endpoint testing with Bearer token auth button
- ReDoc: http://localhost:8000/redoc
- Health check: http://localhost:8000/api/v1/health
cp .env.example .env
# Fill ALL values β especially JWT_SECRET_KEY, POSTGRES_PASSWORD, SMTP / OAuth credentialsGenerate a secure JWT_SECRET_KEY:
python -c "import secrets; print(secrets.token_hex(32))"docker-compose up -d --buildThis starts three containers on an isolated bridge network:
| Container | Image | Role |
|---|---|---|
authshield_api |
Built from Dockerfile |
FastAPI app (Uvicorn) |
authshield_postgres |
postgres:15-alpine |
Database β data in named volume |
authshield_redis |
redis:7-alpine |
Token blacklist, rate limiting, cache |
docker-compose exec authshield alembic upgrade head
docker-compose exec authshield python scripts/seed_roles.pycurl http://localhost:8000/api/v1/health
# {"status":"healthy","database":"ok","redis":"ok","version":"1.0.0"}# Follow live logs
docker-compose logs -f authshield
# Open a shell inside the container
docker-compose exec authshield bash
# Restart a single service
docker-compose restart authshield
# Stop everything (data is preserved in named volumes)
docker-compose down
# Stop and wipe all data volumes (destructive!)
docker-compose down -v| Variable | Example | Notes |
|---|---|---|
APP_ENV |
production |
development | staging | production β enables HSTS + CSP in production |
JWT_SECRET_KEY |
64-char hex string | python -c "import secrets; print(secrets.token_hex(32))" β never commit |
DATABASE_URL |
postgresql+asyncpg://user:pw@postgres/db |
In Docker, use service name postgres |
REDIS_URL |
redis://redis:6379/0 |
In Docker, use service name redis |
ACCESS_TOKEN_EXPIRE_MINUTES |
15 |
Keep short in production |
REFRESH_TOKEN_EXPIRE_DAYS |
7 |
Balance security vs UX |
BCRYPT_ROUNDS |
12 |
Increase to 13β14 for extra security (~500 ms/hash) |
GOOGLE_CLIENT_ID |
xxx.apps.googleusercontent.com |
Google Cloud Console |
GOOGLE_CLIENT_SECRET |
GOCSPX-xxx |
Use secrets manager β never plain .env in git |
GITHUB_CLIENT_ID |
Iv1.xxx |
GitHub Developer Settings |
GITHUB_CLIENT_SECRET |
xxx |
Use secrets manager |
SMTP_HOST |
smtp.sendgrid.net |
Use SendGrid / SES / Postmark in production |
SMTP_PORT |
587 |
|
SMTP_USER |
apikey |
Provider-specific |
SMTP_PASSWORD |
SG.xxx |
Use secrets manager |
FRONTEND_URL |
https://app.yourdomain.com |
Used in email links and OAuth post-callback redirects |
ALLOWED_ORIGINS |
https://app.yourdomain.com |
Comma-separated CORS origins |
See .env.example for the complete annotated list.
ββββββββββββββββββββββββββββββββββββββββββββ
β USERS β
ββββββββββββββββββββββββββββββββββββββββββββ€
β id UUID (PK) β
β email unique, indexed β
β password_hash nullable (null=OAuth user)β
β full_name string β
β is_active bool default true β
β is_verified bool default false β
β oauth_provider nullable ("google"|"github")β
β oauth_id nullable β
β avatar_url nullable β
β is_2fa_enabled bool default false β
β totp_secret nullable (encrypted) β
β created_at timestamp β
β updated_at timestamp β
ββββββββββββββββββββ¬ββββββββββββββββββββββββ
β 1
ββββββββββββΌβββββββββββββββββββββββββββββββ
β N β N β N
βββββββββΌβββββββ βββΌβββββββββββββββββ ββββββββββββΌβββββββββββ
β USER_ROLES β β SESSIONS β β REFRESH_TOKENS β
ββββββββββββββββ€ ββββββββββββββββββββ€ βββββββββββββββββββββββ€
β user_id FK β β id UUID (PK) β β id UUID (PK) β
β role_id FK β β user_id FK β β token_hash SHA-256 β
ββββββββ¬ββββββββ β ip_address β β user_id FK β
β β device_info β β session_id FK β
β N β is_active bool β β family_id UUID ββββ rotation chain
ββββββββΌβββββββ β created_at β β is_used bool β
β ROLES β β last_used_at β β is_revoked bool β
βββββββββββββββ€ ββββββββββββββββββββ β expires_at β
β id UUID β βββββββββββββββββββββββ
β name string β ββββββββββββββββββββββββββββββββββββββββββββββ
β description β β LOGIN_HISTORY β
βββββββββββββββ ββββββββββββββββββββββββββββββββββββββββββββββ€
β id UUID (PK) β
β user_id FK (nullable) β
β ip_address string β
β status success | failed β
β failure_reason nullable β
β created_at timestamp (append-only) β
ββββββββββββββββββββββββββββββββββββββββββββββ
| Decision | Why |
|---|---|
password_hash is nullable |
OAuth users authenticate via provider β they have no password |
token_hash stores SHA-256, not the raw token |
DB breach exposes unusable hashes, not working tokens |
family_id on refresh tokens |
Groups a rotation chain β reuse of any token kills the entire family |
login_history is append-only |
Provides a tamper-evident audit trail β no updates, no deletes |
No users table in your apps |
Downstream services store user_id UUID as a FK β AuthShield owns user data |
# 1. Register
curl -X POST http://localhost:8000/api/v1/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","password":"TestPass123!","full_name":"Test User"}'
# 2. Check Mailtrap for the verification email β copy the token
# 3. Verify email
curl -X POST http://localhost:8000/api/v1/auth/verify-email \
-H "Content-Type: application/json" \
-d '{"token": "TOKEN_FROM_EMAIL"}'
# 4. Login β save both tokens
curl -X POST http://localhost:8000/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","password":"TestPass123!"}'
# 5. Call a protected endpoint
curl http://localhost:8000/api/v1/users/me \
-H "Authorization: Bearer ACCESS_TOKEN"
# 6. Rotate refresh token
curl -X POST http://localhost:8000/api/v1/auth/refresh \
-H "Content-Type: application/json" \
-d '{"refresh_token": "REFRESH_TOKEN"}'
# 7. Try the old refresh token β expect 401 AUTH_REFRESH_TOKEN_REUSED
# AND entire token family is revoked (token theft detection)# Login before email verification β 403 AUTH_EMAIL_NOT_VERIFIED
# Duplicate email registration β 409 AUTH_EMAIL_ALREADY_REGISTERED
# Weak password (no uppercase/digit/special) β 422
# Wrong password β 401 AUTH_INVALID_CREDENTIALS (same message as non-existent email)
# Tampered JWT signature β 401 AUTH_TOKEN_INVALID
# Expired access token β 401 AUTH_TOKEN_EXPIRED
# Logout then use old access token β 401 AUTH_TOKEN_REVOKED
# 6 POSTs to /auth/login β 429 SYS_RATE_LIMIT_EXCEEDED + Retry-After header
# Invalid verification token β 400 AUTH_INVALID_VERIFICATION_TOKEN
# Verification token used twice β 400 on second attempt
# Reset to same password β 400 AUTH_SAME_PASSWORD
# Admin removing own admin role β 400 ADMIN_CANNOT_REMOVE_OWN_ROLE# All 48 tests
pytest tests/ -v
# Specific test file
pytest tests/test_auth.py -v
# Specific test class
pytest tests/test_sessions.py::TestSessionRevocation -v
# Stop on first failure
pytest tests/ -v -x
# With captured stdout (useful for debugging)
pytest tests/ -v -s --tb=short
# Clear Redis rate-limit keys before a run (Windows PowerShell)
redis-cli --scan --pattern "rate:*" | ForEach-Object { redis-cli DEL $_ }tests/test_admin.py::TestRBACEnforcement::test_regular_user_cannot_access_admin PASSED
tests/test_admin.py::TestRBACEnforcement::test_admin_can_list_users PASSED
tests/test_admin.py::TestUserManagement::test_admin_can_deactivate_user PASSED
tests/test_admin.py::TestUserManagement::test_admin_cannot_remove_own_admin_role PASSED
tests/test_auth.py::TestRegistration::test_register_success PASSED
tests/test_auth.py::TestEmailVerification::test_verify_email_success PASSED
tests/test_auth.py::TestLogin::test_login_success PASSED
tests/test_auth.py::TestTokenRefresh::test_refresh_token_rotation PASSED
tests/test_auth.py::TestLogout::test_logout_success PASSED
tests/test_passwords.py::TestForgotPassword::test_forgot_password_always_returns_200 PASSED
tests/test_sessions.py::TestSessionRevocation::test_revoke_current_session PASSED
tests/test_rate_limiting.py::TestLoginRateLimit::test_login_rate_limit_enforced PASSED
... (36 more)
==================== 48 passed in 218.12s ====================
| Concern | Approach |
|---|---|
| DB isolation | Each test wraps operations in a transaction that rolls back - no persistent test data |
| Rate limiting | Disabled via TESTING=true env var in all tests except test_rate_limiting.py |
| Rate limit tests | rate_limit_client fixture temporarily unsets TESTING to enable real rate limiting |
| Infrastructure | Real PostgreSQL and Redis - integration bugs that mocks hide are caught |
| Windows compat | WindowsSelectorEventLoopPolicy set in root conftest.py for Redis/asyncio compatibility |
AuthShield is a standalone service. Your downstream projects need exactly one file and one shared environment variable.
π Full integration guide with multi-framework examples (FastAPI, Flask, Django, Express) and a complete E-Commerce walkthrough: INTEGRATION_GUIDE.md
Your Frontend
β
βββ auth requests βββββββΆ AuthShield :8000
β (login, register, (issues + manages JWTs)
β OAuth, 2FA, logout)
β
βββ business requests βββΆ Your API :8001
(tasks, orders, etc.) (validates JWT locally β zero calls to AuthShield)
# your_project/auth.py β copy verbatim, never changes
import os, jwt
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
SECRET_KEY = os.getenv("SECRET_KEY") # same value as AuthShield
ALGORITHM = "HS256"
bearer = HTTPBearer()
def get_current_user(
creds: HTTPAuthorizationCredentials = Depends(bearer)
) -> dict:
try:
payload = jwt.decode(creds.credentials, SECRET_KEY, algorithms=[ALGORITHM])
except jwt.ExpiredSignatureError:
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Token expired")
except jwt.InvalidTokenError:
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Invalid token")
if payload.get("type") != "access":
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Not an access token")
return payload # {sub, email, roles, session_id, jti, exp}
def require_roles(roles: list[str]):
"""Factory: require at least one of the given roles."""
def check(user: dict = Depends(get_current_user)) -> dict:
if not any(r in user["roles"] for r in roles):
raise HTTPException(status.HTTP_403_FORBIDDEN, f"Requires roles: {roles}")
return user
return check# your_project/.env
SECRET_KEY=same-value-as-authshield # that's the only AuthShield config neededfrom auth import get_current_user, require_roles
# Any authenticated user
@app.get("/tasks")
def list_tasks(user = Depends(get_current_user), db = Depends(get_db)):
return db.query(Task).filter_by(user_id=user["sub"]).all()
# β UUID from JWT β FK in your DB
# Admin only
@app.delete("/admin/tasks/{id}")
def delete_any_task(user = Depends(require_roles(["admin"]))):
...class Task(Base):
__tablename__ = "tasks"
id = Column(UUID, primary_key=True, default=uuid4)
user_id = Column(UUID, nullable=False, index=True)
# β This is user["sub"] from the JWT.
# No FK constraint to a users table β AuthShield owns users entirely.
title = Column(String, nullable=False){
"sub": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"roles": ["user"],
"session_id": "abc-123",
"jti": "unique-token-id",
"type": "access",
"iat": 1700000000,
"exp": 1700000900
}| Decision | Rationale |
|---|---|
| 15-minute access token TTL | Stolen token window is β€15 minutes |
| Refresh tokens stored as SHA-256 hash | DB breach exposes unusable hashes β not working tokens |
| Token family revocation on reuse | Reuse means possible theft β revoking the family kicks the attacker AND forces the victim to re-login, alerting them |
| Redis blacklist on logout | Immediate effect β no gap between logout and natural token expiry |
| bcrypt 12 rounds (~250 ms/hash) | Brute-forcing 10M passwords would take ~29 days on a 10-GPU rig |
| Single-use OAuth state tokens in Redis | Replayed OAuth callbacks are rejected (CSRF hardening) |
| Password reset revokes all sessions | If an attacker triggered the reset, they are kicked on completion |
| Forgot-password always returns 200 | Prevents discovering which emails are registered |
| Admin cannot remove own admin role | Prevents last-admin lockout |
| Sliding-window rate limits | No fixed-window boundary to exploit |
| Security headers on every response | Mitigates clickjacking, MIME sniffing, XSS, and referrer leakage |
- Generate
SECRET_KEYβopenssl rand -hex 32 - Set
ENVIRONMENT=production - Configure production SMTP (SendGrid / AWS SES / Postmark)
- Update Google and GitHub OAuth redirect URIs to your production domain
- Obtain SSL certificate (Let's Encrypt / Certbot β free)
- Set strong
POSTGRES_PASSWORDandREDIS_PASSWORD
-
docker-compose up -d --build -
docker-compose exec api alembic upgrade head -
docker-compose exec api python scripts/seed_roles.py -
curl https://auth.yourdomain.com/api/v1/healthβ{"status":"healthy"}
-
GET /docsreturns 404 (Swagger hidden in production) - Security headers present on all responses
- 6th login attempt in 60 s returns 429 with
Retry-Afterheader - Registration email arrives in inbox
- Full Google OAuth flow completes on production domain
- Ship JSON logs to Datadog / CloudWatch / Papertrail
- Alert on elevated
AUTH_INVALID_CREDENTIALSrate β brute-force signal - Alert on
AUTH_REFRESH_TOKEN_REUSEDβ possible token theft - Monitor Redis memory (rate-limit keys accumulate under sustained attack)
- Schedule
login_historytable cleanup (grows unbounded)
Contributions are welcome! Please follow these steps:
- Fork the repository
- Create a feature branch:
git checkout -b feature/amazing-feature- Make your changes and commit:
git commit -m "feat: add amazing feature"- Push to your fork:
git push origin feature/amazing-feature- Open a Pull Request
- Follow PEP 8 style guide
- Add tests for all new features β maintain 48+ passing
- Keep commits atomic and descriptive (
feat:,fix:,test:,docs:) - Update this README if endpoints or env vars change
Ravi Gupta
- GitHub: @ravigupta97
- LinkedIn: Ravi Gupta
- Email: gupta_ravi@outlook.in
- FastAPI β outstanding async web framework
- SQLAlchemy β powerful async ORM
- Redis β the backbone of blacklisting, rate limiting, and OAuth state
- pyotp β clean, standards-compliant TOTP implementation
- structlog β JSON-structured logging done right
- π¬ Issues: GitHub Issues
- π Swagger Docs: http://localhost:8000/docs
Built using FastAPI, PostgreSQL, and Redis