Skip to content

ravigupta97/authshield

Repository files navigation

πŸ›‘οΈ AuthShield

FastAPI Python PostgreSQL Redis Docker

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


πŸ“– Table of Contents


🌟 Overview

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.

Why AuthShield?

  • βœ… Standalone β€” one deployed service handles auth for any number of projects
  • βœ… Zero auth code in your apps β€” a 40-line auth.py is 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

✨ Features

πŸ” Authentication & Tokens

  • 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/logout takes effect on the very next request, not at natural JWT expiry
  • Email verification required before first login, with resend endpoint

🌐 OAuth 2.0

  • 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

πŸ”‘ Two-Factor Authentication (TOTP)

  • QR code PNG + raw secret returned on setup β€” scan with any TOTP app
  • Single-use temporary token bridges the login β†’ TOTP verify step (5-minute TTL)
  • Disabling 2FA requires a valid current TOTP code (prevents accidental lockout)
  • Β±30-second clock skew tolerated (valid_window=1 in pyotp)

πŸ‘₯ Role-Based Access Control (RBAC)

  • 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)

πŸ“± Session Management

  • 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)

🚦 Rate Limiting

  • Redis sliding-window algorithm β€” no fixed-window boundary to exploit
  • Per-IP limits on all sensitive endpoints
  • Retry-After, X-RateLimit-Limit, and X-RateLimit-Window headers on every 429 response

πŸ”’ Security Headers (every response)

  • X-Content-Type-Options: nosniff
  • X-Frame-Options: DENY
  • X-XSS-Protection: 1; mode=block
  • Referrer-Policy: strict-origin-when-cross-origin
  • Strict-Transport-Security + Content-Security-Policy in production mode
  • Server header stripped

πŸ”‘ Password Management

  • 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-password always returns 200 regardless of whether the email exists

πŸ› οΈ Tech Stack

Backend

  • FastAPI β€” async HTTP framework with automatic OpenAPI docs
  • Uvicorn β€” ASGI server (4 workers in production)

Database

  • 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

Cache & State

  • Redis 7 β€” token blacklist, rate limit windows, 2FA temp tokens, OAuth CSRF state, password reset tokens, email verification tokens

Authentication & Security

  • 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

Validation & Configuration

  • Pydantic v2 β€” request/response schema validation, typed settings
  • pydantic-settings β€” typed environment variable loading with .env support

Observability

  • structlog β€” JSON-structured logs ready for production log aggregators

Testing

  • 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

🌐 Live Demo

API Base URL

https://authshield-31lz.onrender.com/docs

Interactive Documentation

Health Check

curl https://authshield-31lz.onrender.com/api/v1/health
# {"status":"healthy","database":"ok","redis":"ok","version":"1.0.0"}

Quick Test

# 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!"}'

πŸ“ Project Structure

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

πŸ“š API Documentation

Base URL

http://localhost:8000/api/v1

Authentication

All protected endpoints require a JWT Bearer token in the Authorization header:

Authorization: Bearer <your_access_token>

Standard Response Format

Success:

{
  "status": "success",
  "message": "Operation successful",
  "data": { ... }
}

Error:

{
  "status": "error",
  "error_code": "AUTH_INVALID_CREDENTIALS",
  "message": "Invalid email or password"
}

Rate Limit Headers

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)

πŸ”Œ API Endpoints

πŸ” Authentication (/auth)

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 pair

Logout:

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

πŸ”‘ Password Management (/auth)

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 enumeration

Reset 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 success

Change 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

🌐 OAuth 2.0 (/auth/oauth)

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.


πŸ”‘ Two-Factor Authentication (/auth/2fa)

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

πŸ‘€ User Profile (/users)

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"}'

πŸ“± Sessions (/sessions)

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

πŸ”§ Admin (/admin) β€” requires admin role

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_ROLE

Deactivate 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

πŸ“Š Health

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"
}

🚦 Rate Limits

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.


πŸš€ Getting Started

Prerequisites

  • Python 3.12+
  • PostgreSQL 15+
  • Redis 7+
  • Git
  • Docker

Local Setup

1️⃣ Clone the repository

git clone https://github.com/ravigupta97/authshield.git
cd authshield

2️⃣ Create virtual environment

Windows (PowerShell):

python -m venv venv
venv\Scripts\activate

macOS / Linux:

python3 -m venv venv
source venv/bin/activate

3️⃣ Install dependencies

pip install -r requirements.txt

4️⃣ Configure environment

cp .env.example .env
# Open .env and fill in DATABASE_URL, REDIS_URL, JWT_SECRET_KEY, and SMTP credentials

Generate a secure JWT_SECRET_KEY:

# Windows PowerShell
python -c "import secrets; print(secrets.token_hex(32))"
# macOS / Linux
openssl rand -hex 32

5️⃣ Create the database and run migrations

# Create the database
createdb authshield_db

# Apply all migrations
alembic upgrade head

# Seed default roles: user, moderator, admin
python scripts/seed_roles.py

6️⃣ Start the development server

uvicorn app.main:app --reload --port 8000

Available at:


🐳 Docker & Production

1️⃣ Configure the environment file

cp .env.example .env
# Fill ALL values β€” especially JWT_SECRET_KEY, POSTGRES_PASSWORD, SMTP / OAuth credentials

Generate a secure JWT_SECRET_KEY:

python -c "import secrets; print(secrets.token_hex(32))"

2️⃣ Build and start all services

docker-compose up -d --build

This 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

3️⃣ Run migrations and seed roles

docker-compose exec authshield alembic upgrade head
docker-compose exec authshield python scripts/seed_roles.py

4️⃣ Verify the deployment

curl http://localhost:8000/api/v1/health
# {"status":"healthy","database":"ok","redis":"ok","version":"1.0.0"}

Useful Docker Commands

# 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

πŸ”§ Environment Variables

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.


πŸ—„οΈ Database Schema

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                  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)    β”‚
                 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Key Design Decisions

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

πŸ§ͺ End-to-End Flow Testing

Full Registration β†’ Login β†’ Refresh Flow

# 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)

Edge Cases to Verify

# 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

πŸ§ͺ Running Tests

# 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 $_ }

Test Results

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

Testing Strategy

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

πŸ”Œ Plugging Into Your Project

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

The Mental Model

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)

Step 1 β€” Copy auth.py into every new project

# 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

Step 2 β€” Add one env var to your project

# your_project/.env
SECRET_KEY=same-value-as-authshield   # that's the only AuthShield config needed

Step 3 β€” Protect your endpoints

from 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"]))):
    ...

Step 4 β€” Your database schema (no users table)

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)

JWT Payload Reference

{
  "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
}

πŸ”’ Security Design

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

βœ… Deployment Checklist

Before First Deploy

  • 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_PASSWORD and REDIS_PASSWORD

Deploy

  • 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"}

Post-Deploy Smoke Tests

  • GET /docs returns 404 (Swagger hidden in production)
  • Security headers present on all responses
  • 6th login attempt in 60 s returns 429 with Retry-After header
  • Registration email arrives in inbox
  • Full Google OAuth flow completes on production domain

Monitoring

  • Ship JSON logs to Datadog / CloudWatch / Papertrail
  • Alert on elevated AUTH_INVALID_CREDENTIALS rate β€” brute-force signal
  • Alert on AUTH_REFRESH_TOKEN_REUSED β€” possible token theft
  • Monitor Redis memory (rate-limit keys accumulate under sustained attack)
  • Schedule login_history table cleanup (grows unbounded)

🀝 Contributing

Contributions are welcome! Please follow these steps:

  1. Fork the repository
  2. Create a feature branch:
git checkout -b feature/amazing-feature
  1. Make your changes and commit:
git commit -m "feat: add amazing feature"
  1. Push to your fork:
git push origin feature/amazing-feature
  1. Open a Pull Request

Contribution Guidelines

  • 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

πŸ‘¨β€πŸ’» Author

Ravi Gupta


πŸ™ Acknowledgments

  • 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

πŸ“ž Support


Built using FastAPI, PostgreSQL, and Redis

⬆ Back to Top

About

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.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages