Clone. Configure. Deploy. A complete authentication system you can use in your production applications right now.
Stop building auth from scratch. This template includes everything you need:
- ✅ Secure by default - bcrypt, JWT, httpOnly cookies, rate limiting
- ✅ One-command setup - Just run
docker-compose up - ✅ Beautiful UI included - Login, Register, Dashboard ready to use
- ✅ Easy to customize - Clean code, well-documented, modular design
- ✅ Battle-tested patterns - Token rotation, MFA, audit logs
- Quick Start
- What You'll Learn
- System Architecture
- Security Concepts Explained
- API Reference
- Project Structure
- Configuration
- Deployment
- Docker Desktop installed and running
# 1. Clone the repository
git clone https://github.com/YOUR_USERNAME/authforge.git
cd authforge
# 2. Start everything with Docker
docker-compose up -d --build
# 3. Open the app
# Frontend: http://localhost:5173
# API: http://localhost:3000
# Emails: http://localhost:8025That's it! 🎉 No Node.js installation needed. Everything runs in Docker.
This is a complete, production-ready authentication system with:
| Feature | What It Does |
|---|---|
| Password Security | bcrypt hashing + unique salt per user prevents rainbow table attacks |
| JWT Access Tokens | Stateless 15-min tokens that don't require database lookups |
| Refresh Token Rotation | 7-day tokens with automatic theft detection |
| OAuth 2.0 | "Login with Google" using industry-standard OAuth flow |
| Email MFA | Optional 2-factor authentication via email OTP |
| Rate Limiting | Blocks brute force attacks (5 attempts/15 min) |
| Account Lockout | Locks account after 5 failed login attempts |
| Session Management | View and revoke active sessions from any device |
| Audit Logging | Every security event recorded in the database |
| Role-Based Access | USER, MODERATOR, ADMIN, SUPER_ADMIN roles |
┌─────────────────────────────────────────────────────────────────┐
│ FRONTEND (React) │
│ http://localhost:5173 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
│ │ Login │ │ Register │ │ Verify │ │ Dashboard │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────────┬─────────┘ │
└───────┼─────────────┼─────────────┼─────────────────┼───────────┘
│ │ │ │
└─────────────┴─────────────┴─────────────────┘
│
HTTP (JSON) + Cookies
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ BACKEND (Express.js) │
│ http://localhost:3000 │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
│ │ Routes │ │ Middleware │ │ Controllers │ │
│ │ /auth/* │──│ Rate Limit │──│ Login, Register, etc │ │
│ │ /oauth/* │ │ JWT Verify │ │ │ │
│ │ /user/* │ │ RBAC │ │ │ │
│ └─────────────┘ └─────────────┘ └───────────┬─────────────┘ │
│ │ │
│ ┌─────────────────────────────────────────────┴─────────────┐ │
│ │ SERVICES │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ Hash │ │ Token │ │ OTP │ │ Email │ │ │
│ │ │ Service │ │ Service │ │ Service │ │ Service │ │ │
│ │ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ │ │
│ └───────┼──────────┼───────────┼───────────┼────────────────┘ │
└──────────┼──────────┼───────────┼───────────┼───────────────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌──────────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ PostgreSQL │ │ Redis │ │ Redis │ │ SMTP │
│ (Users, │ │ (Tokens, │ │ (OTPs, │ │ (MailHog)│
│ Sessions) │ │ Cache) │ │ Attempts)│ │ │
└──────────────┘ └──────────┘ └──────────┘ └──────────┘
The Problem: If we store passwords as-is, a database breach exposes all passwords.
The Solution: Hash passwords with bcrypt + unique salt per user.
Password: "mypassword123"
│
▼
┌─────────────────────────────────────────────┐
│ Salt: "a1b2c3d4e5f6..." (random, unique) │
│ │
│ Combined: "mypassword123" + salt │
│ │ │
│ ▼ │
│ bcrypt(combined, 12) │
│ │ │
│ ▼ │
│ Hash: "$2b$12$xyz..." (stored in DB) │
└─────────────────────────────────────────────┘
Why Salt?
- Without salt: Same password = same hash (vulnerable to rainbow tables)
- With unique salt: Same password = different hash (secure!)
Code Location: src/services/hashService.js
The Problem: Checking the database for every API request is slow and doesn't scale.
The Solution: Issue a self-contained token that the server can verify without database lookups.
┌──────────────────────────────────────────────────────────────────┐
│ JWT STRUCTURE │
├──────────────────────────────────────────────────────────────────┤
│ │
│ Header.Payload.Signature │
│ │
│ ┌─────────────┐ ┌─────────────────┐ ┌─────────────────────┐ │
│ │ HEADER │ │ PAYLOAD │ │ SIGNATURE │ │
│ │ │ │ │ │ │ │
│ │ { │ │ { │ │ HMAC-SHA256( │ │
│ │ "alg": │ │ "userId": 1, │ │ header + "." + │ │
│ │ "HS256", │ │ "email": "x", │ │ payload, │ │
│ │ "typ": │ │ "role": "user"│ │ SECRET_KEY │ │
│ │ "JWT" │ │ "exp": 12345 │ │ ) │ │
│ │ } │ │ } │ │ │ │
│ └─────────────┘ └─────────────────┘ └─────────────────────┘ │
│ │ │ │ │
│ └───────────────────┴──────────────────────┘ │
│ │ │
│ Base64 Encoded │
│ │ │
│ eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjF9.dBjftJeZ4CVP-mB92K27uhbUJU1p1r │
└──────────────────────────────────────────────────────────────────┘
How Verification Works (No Database!):
- Server receives JWT
- Server uses SECRET_KEY to verify signature
- If valid → trust the payload data
- If invalid → reject the request
Why Short Expiry (15 min)?
- If stolen, attacker has limited time
- Refresh token handles getting new access tokens
Code Location: src/services/tokenService.js
The Problem: Short-lived access tokens = user logs out frequently. Long-lived tokens = security risk.
The Solution: Two-token system with rotation.
┌─────────────────────────────────────────────────────────────────────────┐
│ REFRESH TOKEN ROTATION FLOW │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ LOGIN │
│ ───── │
│ User → [email + password] → Server │
│ │ │
│ ┌────────────┴────────────┐ │
│ ▼ ▼ │
│ Access Token (JWT) Refresh Token │
│ ├─ 15 min expiry ├─ 7 day expiry │
│ ├─ Stored in memory ├─ Stored in httpOnly cookie │
│ └─ Used for API calls └─ Hash stored in database │
│ │
│ ───────────────────────────────────────────────────────────────────── │
│ │
│ REFRESH (Token Rotation) │
│ ─────────────────────── │
│ │
│ ┌─────────┐ Old Refresh Token ┌─────────┐ │
│ │ Client │ ───────────────────────►│ Server │ │
│ └─────────┘ └────┬────┘ │
│ ▲ │ │
│ │ 1. Verify hash in DB │
│ │ 2. INVALIDATE old token ← Key step! │
│ │ 3. Generate NEW tokens │
│ │ │ │
│ │ New Access + Refresh Tokens │ │
│ └───────────────────────────────────┘ │
│ │
│ ───────────────────────────────────────────────────────────────────── │
│ │
│ THEFT DETECTION │
│ ─────────────── │
│ │
│ If attacker uses STOLEN refresh token: │
│ │
│ 1. Attacker uses stolen token → Gets new tokens → Old token invalid │
│ 2. Real user uses OLD token → Token already used! → ALERT! │
│ 3. Server revokes ALL tokens for that user → Forces re-login │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Code Location: src/services/tokenService.js → rotateRefreshToken()
The Problem: Passwords can be phished or leaked. We need a second factor.
The Solution: Send a temporary code via email that expires quickly.
┌─────────────────────────────────────────────────────────────────┐
│ OTP FLOW │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. GENERATE │
│ ───────── │
│ • Generate 6-digit code: 847293 │
│ • Hash before storing (security!) │
│ │
│ 2. STORE IN REDIS │
│ ──────────────── │
│ ┌────────────────────────────────────────────┐ │
│ │ Key: "otp:user@example.com:verify_email" │ │
│ │ Value: "hashed_847293" │ │
│ │ TTL: 300 seconds (5 minutes) │ ← Auto-expire│
│ └────────────────────────────────────────────┘ │
│ │
│ 3. SEND EMAIL │
│ ────────── │
│ 📧 "Your code is: 847293" │
│ │
│ 4. VERIFY │
│ ────── │
│ User enters: 847293 │
│ Server: hash(847293) == stored_hash? ✓ │
│ Delete from Redis (one-time use) │
│ │
│ WHY REDIS? │
│ ────────── │
│ • Auto-expiration (TTL) - no cleanup needed │
│ • Fast read/write - no DB overhead │
│ • Temporary data - perfect for in-memory cache │
│ │
└─────────────────────────────────────────────────────────────────┘
Code Location: src/services/otpService.js
The Problem: Users hate creating new passwords. They want "Login with Google."
The Solution: OAuth 2.0 - let Google verify the user, then create our own session.
┌─────────────────────────────────────────────────────────────────────────┐
│ GOOGLE OAUTH 2.0 FLOW │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ USER AUTHFORGE GOOGLE │
│ │ │ │ │
│ │ │ │ │
│ 1. │──Click "Login │ │ │
│ │ with Google"──►│ │ │
│ │ │ │ │
│ 2. │ │──Redirect to────────► │
│ │ │ Google consent │ │
│ │ │ screen │ │
│ │ │ │ │
│ 3. │◄──────────────────────────────────── │ │
│ │ "AuthForge wants to access your │ │
│ │ email and profile. Allow?" │ │
│ │ │ │ │
│ 4. │──Click "Allow"──│─────────────────────► │
│ │ │ │ │
│ 5. │ │◄──Redirect with ────│ │
│ │ │ ?code=abc123 │ │
│ │ │ │ │
│ 6. │ │──Exchange code──────► (Server-to-server) │
│ │ │ for tokens │ │
│ │ │ │ │
│ 7. │ │◄──{access_token, ───│ │
│ │ │ id_token} │ │
│ │ │ │ │
│ 8. │ │──Decode id_token────│ │
│ │ │ Get: email, name │ │
│ │ │ │ │
│ 9. │ │──Find/Create user ──│ │
│ │ │ in OUR database │ │
│ │ │ │ │
│ 10. │ │──Issue OUR JWT ─────│ │
│ │ │ (not Google's!) │ │
│ │ │ │ │
│ 11. │◄──Logged in!────│ │ │
│ │ w/ AuthForge │ │ │
│ │ session │ │ │
│ │
│ KEY INSIGHT: We use Google ONLY for identity verification. │
│ We issue OUR OWN tokens - keeping us independent of Google. │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Code Location: src/controllers/oauthController.js
The Problem: Attackers can try thousands of passwords per second.
The Solution: Limit attempts and lock accounts after failures.
┌─────────────────────────────────────────────────────────────────┐
│ PROTECTION LAYERS │
├─────────────────────────────────────────────────────────────────┤
│ │
│ LAYER 1: IP-BASED RATE LIMITING │
│ ─────────────────────────────── │
│ │
│ ┌────────────────┐ │
│ │ 192.168.1.100 │──► 5 login attempts / 15 min │
│ └────────────────┘ │ │
│ ├─ Attempt 1: ✓ │
│ ├─ Attempt 2: ✓ │
│ ├─ Attempt 3: ✓ │
│ ├─ Attempt 4: ✓ │
│ ├─ Attempt 5: ✓ │
│ └─ Attempt 6: ❌ BLOCKED (429 Error) │
│ │
│ ───────────────────────────────────────────────────────────── │
│ │
│ LAYER 2: ACCOUNT LOCKOUT │
│ ──────────────────────── │
│ │
│ ┌─────────────────────┐ │
│ │ user@example.com │──► Track failed attempts in Redis │
│ └─────────────────────┘ │
│ │
│ Attempt 1: Wrong password → attempts: 1 │
│ Attempt 2: Wrong password → attempts: 2 │
│ Attempt 3: Wrong password → attempts: 3 │
│ Attempt 4: Wrong password → attempts: 4 │
│ Attempt 5: Wrong password → attempts: 5 → 🔒 LOCKED │
│ │
│ Account locked for 15 minutes │
│ Even correct password won't work! │
│ │
│ ───────────────────────────────────────────────────────────── │
│ │
│ LAYER 3: GENERAL API RATE LIMIT │
│ ─────────────────────────────── │
│ │
│ All endpoints: 100 requests / 15 min per IP │
│ Prevents DDoS and API abuse │
│ │
└─────────────────────────────────────────────────────────────────┘
Code Location: src/middleware/rateLimiter.js
| Method | Endpoint | Description | Auth Required |
|---|---|---|---|
| POST | /auth/register |
Create new account | ❌ |
| POST | /auth/login |
Login with email/password | ❌ |
| POST | /auth/verify-email |
Verify email with OTP | ❌ |
| POST | /auth/refresh |
Get new access token | 🍪 Cookie |
| POST | /auth/logout |
Logout & revoke tokens | ✅ |
| POST | /auth/forgot-password |
Request password reset | ❌ |
| POST | /auth/reset-password |
Reset with OTP | ❌ |
| POST | /auth/change-password |
Change password | ✅ |
| GET | /auth/me |
Get current user | ✅ |
| Method | Endpoint | Description |
|---|---|---|
| GET | /oauth/google |
Start Google login |
| GET | /oauth/google/callback |
Google callback |
| Method | Endpoint | Description | Auth Required |
|---|---|---|---|
| GET | /user/profile |
Get profile | ✅ |
| PATCH | /user/profile |
Update profile | ✅ |
| GET | /user/sessions |
List active sessions | ✅ |
| DELETE | /user/sessions/:id |
Revoke a session | ✅ |
| POST | /user/mfa/enable |
Enable MFA | ✅ |
| POST | /user/mfa/disable |
Disable MFA | ✅ |
| GET | /user/activity |
Get activity log | ✅ |
authforge/
├── docker-compose.yml # All services configuration
├── Dockerfile # Production API image
├── Dockerfile.dev # Development API image
│
├── frontend/ # React UI
│ ├── src/
│ │ ├── pages/ # Login, Register, Dashboard
│ │ ├── App.jsx # Routes & AuthContext
│ │ └── index.css # Styling
│ └── Dockerfile
│
├── prisma/
│ └── schema.prisma # Database schema
│
└── src/ # Backend
├── app.js # Express app
├── config/
│ ├── database.js # Prisma client
│ ├── redis.js # Redis client
│ └── env.js # Environment config
├── controllers/
│ ├── authController.js # Login, register, etc.
│ ├── oauthController.js # Google OAuth
│ └── userController.js # Profile, sessions
├── middleware/
│ ├── authenticate.js # JWT verification
│ ├── authorize.js # Role-based access
│ ├── rateLimiter.js # Rate limiting
│ └── errorHandler.js # Error handling
├── services/
│ ├── hashService.js # Password hashing
│ ├── tokenService.js # JWT management
│ ├── otpService.js # OTP generation
│ ├── emailService.js # Send emails
│ └── auditService.js # Audit logging
└── routes/
├── auth.routes.js
├── oauth.routes.js
└── user.routes.js
All configuration is in docker-compose.yml environment variables.
# Generate secure secrets:
# node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
- JWT_ACCESS_SECRET=<64-char-random-secret>
- JWT_REFRESH_SECRET=<64-char-random-secret>
- COOKIE_SECRET=<random-secret>
# Get from https://console.cloud.google.com/
- GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
- GOOGLE_CLIENT_SECRET=your-client-secret
# Use real SMTP (SendGrid, AWS SES, etc.)
- SMTP_HOST=smtp.sendgrid.net
- SMTP_PORT=587
- SMTP_USER=apikey
- SMTP_PASS=your-api-key- Push code to GitHub
- Connect repository to Railway/Render
- Set environment variables
- Deploy!
# Build production images
docker-compose -f docker-compose.prod.yml build
# Run in production
docker-compose -f docker-compose.prod.yml up -d- Register: http://localhost:5173/register
- Check email: http://localhost:8025 (MailHog)
- Enter OTP: Complete verification
- Dashboard: Explore sessions and MFA
- Enable MFA: Toggle 2FA
- Logout & Login: See MFA in action!
MIT © Piyush Sahoo
Built with ❤️ by Piyush