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