JWT Authentication Module
Enterprise-grade JWT authentication built with Express & TypeScript. Zero-database, fully stateless, production-ready.
Quick Start
1. Clone & Install
git clone https://github.com/hoangsonww/jwt-module.git
cd jwt-module
npm install
2. Configure Environment
PORT=5001
JWT_ACCESS_SECRET=your-access-secret-min-32-chars
JWT_REFRESH_SECRET=your-refresh-secret-min-32-chars
CORS_ORIGIN=http://localhost:3000
NODE_ENV=development
3. Run
# Development
PORT=5001 npx ts-node src/server.ts
# Production (build first)
npm run build
NODE_ENV=production node dist/server.js
# Docker
docker compose up --build
4. Test It
# Register
curl -X POST http://localhost:5001/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","password":"Secret123"}'
# Login
curl -X POST http://localhost:5001/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","password":"Secret123"}'
# Access protected route
curl http://localhost:5001/auth/me \
-H "Authorization: Bearer <access_token>"
Architecture Overview
The module follows a clean layered architecture with strict separation between the HTTP API layer and the authentication business logic.
helmet, cors, json"] RL["Rate Limiter
Per-IP sliding window"] Val["Zod Validation
Schema-based input check"] MW["Auth Middleware
Bearer token verification"] Router["Auth Router
9 endpoints"] end subgraph Auth["Auth Layer (src/auth/)"] AS["Auth Service
register, login, refresh
logout, password, email"] TM["Token Manager
JWT sign/verify
Revocation blacklist"] PW["Password Utils
bcrypt hash/verify
Strength validation"] ERR["Error System
AuthError + codes"] end subgraph Storage["In-Memory Storage"] Users["Users Map<id, User>"] Tokens["Revoked Tokens Map"] Sessions["User Tokens Map"] Attempts["Login Attempts Map"] end Browser --> Express CLI --> Express Express --> RL RL --> Val Val --> MW MW --> Router Router --> AS AS --> TM AS --> PW AS --> ERR AS --> Users TM --> Tokens AS --> Sessions AS --> Attempts style Client fill:#161b22,stroke:#30363d,color:#e6edf3 style API fill:#161b22,stroke:#1f6feb,color:#e6edf3 style Auth fill:#161b22,stroke:#bc8cff,color:#e6edf3 style Storage fill:#161b22,stroke:#3fb950,color:#e6edf3
Key Design Decisions
- No database: All state is in-memory Maps. This keeps the module self-contained and eliminates infrastructure dependencies.
-
Interface-driven: The API layer consumes auth via the
AuthServiceinterface, making the auth implementation swappable. -
Layer separation: Business logic in
src/auth/never imports Express. HTTP concerns stay insrc/api/. -
Error-first: Typed
AuthErrorcodes propagate from auth layer to HTTP viaERROR_STATUS_MAP.
Authentication Flows
Complete Auth Lifecycle
Account Lockout Flow
Token Management
Token Lifecycle States
Token Rotation
Token Configuration
| Token Type | Algorithm | Expiry | Payload | Storage |
|---|---|---|---|---|
| Access Token | HS256 | 15 minutes | userId, email |
Client only |
| Refresh Token | HS256 | 7 days | userId, email |
Client + server tracking |
Error Handling
Error Architecture
Error Code Reference
| Code | HTTP Status | Description | Trigger |
|---|---|---|---|
DUPLICATE_EMAIL |
409 | Email already registered | register, updateEmail |
INVALID_CREDENTIALS |
401 | Wrong email or password | login, changePassword, updateEmail, deleteAccount |
INVALID_TOKEN |
401 | Token is malformed or revoked | refresh, any authenticated route |
TOKEN_EXPIRED |
401 | Token TTL exceeded | refresh, any authenticated route |
MISSING_SECRET |
500 | JWT secret env var not set | Any token operation |
INVALID_EMAIL |
400 | Email format validation failed | register, updateEmail |
WEAK_PASSWORD |
400 | Password doesn't meet policy | register, changePassword |
USER_NOT_FOUND |
404 | No user with given ID | getUserById, changePassword, etc. |
MISSING_TOKEN |
401 | No Authorization header | Any authenticated route |
ACCOUNT_LOCKED |
423 | Too many failed login attempts | login (after 5 failures) |
Response Shape
All error responses follow a consistent structure:
{
"error": {
"code": "INVALID_CREDENTIALS",
"message": "Invalid email or password"
}
}
API Reference
All endpoints accept and return JSON. The base URL is
http://localhost:5001.
Register a new user account. Returns access and refresh tokens.
{
"email": "user@example.com",
"password": "Secret123"
}
{
"tokens": {
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs..."
}
}
| Status | Code | When |
|---|---|---|
| 400 | WEAK_PASSWORD |
Password < 8 chars or missing letter/digit |
| 400 | INVALID_EMAIL |
Email format invalid |
| 409 | DUPLICATE_EMAIL |
Email already registered |
| 429 | Rate limited | Too many requests from IP |
Authenticate with email and password. Returns a token pair. Account locks after 5 failed attempts for 15 minutes.
{
"email": "user@example.com",
"password": "Secret123"
}
{
"tokens": {
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs..."
}
}
| Status | Code | When |
|---|---|---|
| 401 | INVALID_CREDENTIALS |
Wrong email or password |
| 423 | ACCOUNT_LOCKED |
5+ failed attempts in window |
| 429 | Rate limited | Too many requests from IP |
Exchange a valid refresh token for a new token pair. The old refresh token is revoked (rotation).
{ "refreshToken": "eyJhbGciOiJIUzI1NiIs..." }
{
"tokens": {
"accessToken": "new-access-token...",
"refreshToken": "new-refresh-token..."
}
}
| Status | Code | When |
|---|---|---|
| 401 | INVALID_TOKEN |
Token revoked or malformed |
| 401 | TOKEN_EXPIRED |
Refresh token TTL exceeded |
Get the current authenticated user's profile.
Authorization: Bearer <access_token>
{
"id": "clx9abc123def456",
"email": "user@example.com",
"createdAt": "2024-06-15T10:30:00.000Z"
}
Update the authenticated user's email. Requires password confirmation.
{
"newEmail": "newemail@example.com",
"password": "Secret123"
}
{ "message": "Email updated successfully" }
Permanently delete the authenticated user's account. Revokes all sessions. Requires password confirmation.
{ "password": "Secret123" }
{ "message": "Account deleted successfully" }
Revoke a specific refresh token, ending that session.
{ "refreshToken": "eyJhbGciOiJIUzI1NiIs..." }
{ "message": "Logged out successfully" }
Revoke all refresh tokens for the authenticated user, ending all sessions everywhere.
{ "message": "All sessions revoked successfully" }
Change the authenticated user's password. All existing sessions are revoked after change.
{
"currentPassword": "OldSecret123",
"newPassword": "NewSecret456"
}
{ "message": "Password changed successfully" }
Health check endpoint for load balancers and orchestrators.
{ "status": "ok" }
Rate Limiting
The API uses a per-IP sliding window rate limiter to prevent brute-force and abuse.
Security Headers"] HELMET --> CORS["CORS
Origin Check"] CORS --> LOG["Request Logger"] LOG --> JSON["JSON Parser
10kb limit"] JSON --> ROUTE{"Route Match?"} ROUTE -->|"Rate limited route"| RL["Rate Limiter
Per-IP window"] RL --> RLCHECK{"Under limit?"} RLCHECK -->|No| R429["429 Too Many Requests"] RLCHECK -->|Yes| ZOD["Zod Validation"] ROUTE -->|"Auth required"| MW["Auth Middleware
Verify Bearer token"] MW --> MWCHECK{"Valid token?"} MWCHECK -->|No| R401["401 Unauthorized"] MWCHECK -->|Yes| ZOD ROUTE -->|"Health check"| HEALTH["200 OK"] ZOD --> ZODCHECK{"Valid body?"} ZODCHECK -->|No| R400["400 Validation Error"] ZODCHECK -->|Yes| HANDLER["Route Handler"] HANDLER --> RES["JSON Response"] style R429 fill:#d29922,stroke:#d29922,color:#fff style R401 fill:#f85149,stroke:#f85149,color:#fff style R400 fill:#f85149,stroke:#f85149,color:#fff style HEALTH fill:#238636,stroke:#238636,color:#fff style RES fill:#238636,stroke:#238636,color:#fff
Rate Limited Endpoints
| Endpoint | Protected | Notes |
|---|---|---|
POST /auth/register |
Yes | Prevents mass account creation |
POST /auth/login |
Yes | Works with account lockout |
POST /auth/refresh |
Yes | Prevents token farming |
POST /auth/change-password |
Yes | Prevents brute-force on current password |
| All other endpoints | No | Protected by auth middleware instead |
Security Features
X-Frame-Options, CSP, HSTS"] CORS["CORS - Origin Whitelist"] TLS["TLS Termination (via LB)"] end subgraph L2["Layer 2: Input"] RL["Rate Limiter - Per-IP Window"] JSON["JSON Body Limit - 10kb"] ZOD["Zod Schema Validation"] end subgraph L3["Layer 3: Authentication"] JWT["JWT Verification - HS256"] ROT["Token Rotation on Refresh"] REV["Token Revocation Blacklist"] LOCK["Account Lockout - 5 attempts / 15min"] end subgraph L4["Layer 4: Data"] BCRYPT["bcrypt - 12 salt rounds"] PWPOL["Password Policy - 8+ chars, letter+digit"] NOPII["No PII in tokens beyond email"] end subgraph L5["Layer 5: Runtime"] NONROOT["Non-root Docker user"] READONLY["Read-only container filesystem"] RESLIM["Resource limits (CPU/Memory)"] end L1 --> L2 L2 --> L3 L3 --> L4 L4 -.-> L5 style L1 fill:#161b22,stroke:#58a6ff,color:#e6edf3 style L2 fill:#161b22,stroke:#d29922,color:#e6edf3 style L3 fill:#161b22,stroke:#bc8cff,color:#e6edf3 style L4 fill:#161b22,stroke:#3fb950,color:#e6edf3 style L5 fill:#161b22,stroke:#f85149,color:#e6edf3
This module uses in-memory storage. All user data is lost on restart. For production
with persistence, implement the AuthService interface with a real
database backend.
Password Security
Password Policy
- Minimum 8 characters
- At least one letter (a-z or A-Z)
- At least one digit (0-9)
- Hashed with bcrypt using 12 salt rounds (~250ms per hash)
- Plaintext password never stored or logged
Deployment
ALB / App Gateway
TLS termination, WAF"] subgraph K8s["Kubernetes Cluster (ECS / AKS)"] Ingress["Ingress Controller
nginx / AGIC"] subgraph Pods["jwt-module Pods"] P1["Pod 1
:5001"] P2["Pod 2
:5001"] P3["Pod 3
:5001"] end HPA["HPA
Auto-scale 2-20"] SVC["ClusterIP Service"] end Registry["Container Registry
ECR / ACR"] Secrets["Secrets Manager
SSM / Key Vault"] Logs["Log Aggregation
CloudWatch / Log Analytics"] Alerts["Monitoring & Alerts
CloudWatch / Azure Monitor"] end U --> LB LB --> Ingress Ingress --> SVC SVC --> P1 SVC --> P2 SVC --> P3 HPA -.-> Pods Registry -.->|pull image| Pods Secrets -.->|inject env| Pods Pods -.->|logs| Logs Logs -.->|trigger| Alerts style Internet fill:#161b22,stroke:#58a6ff,color:#e6edf3 style Cloud fill:#0d1117,stroke:#30363d,color:#e6edf3 style K8s fill:#161b22,stroke:#bc8cff,color:#e6edf3 style Pods fill:#21262d,stroke:#3fb950,color:#e6edf3
Docker
# Build and run with Docker Compose
export JWT_ACCESS_SECRET="your-secret-here"
export JWT_REFRESH_SECRET="your-secret-here"
docker compose up --build -d
# Check health
curl http://localhost:5001/health
Kubernetes (Kustomize)
# Dev
kubectl apply -k k8s/overlays/dev
# Staging
kubectl apply -k k8s/overlays/staging
# Production
kubectl apply -k k8s/overlays/prod
Kubernetes (Helm)
# Install
helm install jwt-module k8s/helm -f k8s/helm/values-prod.yaml
# Upgrade
helm upgrade jwt-module k8s/helm -f k8s/helm/values-prod.yaml
AWS (Terraform + ECS Fargate)
cd aws/terraform
terraform init
terraform plan -var-file=environments/prod.tfvars
terraform apply -var-file=environments/prod.tfvars
Azure (Terraform + AKS)
cd azure/terraform
terraform init
terraform plan -var-file=environments/prod.tfvars
terraform apply -var-file=environments/prod.tfvars
Configuration Reference
| Variable | Required | Default | Description |
|---|---|---|---|
PORT |
No | 3000 |
Server listen port |
JWT_ACCESS_SECRET |
Yes* | Dev fallback | Secret for signing access tokens (HS256) |
JWT_REFRESH_SECRET |
Yes* | Dev fallback | Secret for signing refresh tokens (HS256) |
CORS_ORIGIN |
No | * |
Allowed CORS origin |
NODE_ENV |
No | development |
Runtime environment |
In production, always set JWT_ACCESS_SECRET and
JWT_REFRESH_SECRET to strong, unique random strings (minimum 32
characters). The development fallback secrets in server.ts should never
be used in production.
Testing
# Run all tests
npm test
# Run with coverage report
npm run test:coverage
# Run specific test file
npx jest --config jest.config.ts src/tests/auth-service.test.ts
Test Suite Overview
| File | Coverage | Description |
|---|---|---|
auth-service.test.ts |
Auth Service | Register, login, refresh, logout, password change, email update, delete, lockout |
token.test.ts |
Token Utils | Generate, verify, revoke, prune expired tokens |
password.test.ts |
Password Utils | Hash, verify, strength validation edge cases |
middleware.test.ts |
Middleware | Auth middleware, missing/invalid/expired tokens |
api.test.ts |
Integration | Full HTTP endpoint tests with supertest |
Every test file calls clearRevokedTokens(),
clearLoginAttempts(), clearUserTokens(), and
clearRateLimitWindows() in afterEach to prevent cross-test
contamination.
Module Dependency Graph
(entry point)"] app["api/app.ts"] router["api/auth-router.ts"] mw["api/middleware.ts"] rl["api/rate-limiter.ts"] val["api/validation.ts"] as["auth/auth-service.ts"] token["auth/token.ts"] pw["auth/password.ts"] err["auth/errors.ts"] types["auth/types.ts"] server --> app server --> as app --> router app --> mw app --> types router --> err router --> mw router --> rl router --> val mw --> token as --> err as --> pw as --> token as --> types token --> err token --> types pw --> err style server fill:#1f6feb,stroke:#58a6ff,color:#fff style app fill:#21262d,stroke:#58a6ff,color:#e6edf3 style router fill:#21262d,stroke:#58a6ff,color:#e6edf3 style as fill:#21262d,stroke:#bc8cff,color:#e6edf3 style token fill:#21262d,stroke:#bc8cff,color:#e6edf3 style pw fill:#21262d,stroke:#bc8cff,color:#e6edf3 style err fill:#21262d,stroke:#f85149,color:#e6edf3 style types fill:#21262d,stroke:#3fb950,color:#e6edf3
Contributing
Development Setup
git clone https://github.com/hoangsonww/jwt-module.git
cd jwt-module
npm install
npm test # Verify everything passes
PORT=5001 npx ts-node src/server.ts # Start dev server
Project Conventions
- All auth business logic goes in
src/auth/— never in the router -
New error codes: add to
AuthErrorCodeunion inerrors.tsAND toERROR_STATUS_MAPinauth-router.ts - New input schemas go in
src/api/validation.tsusing Zod - Tests must clean up state in
afterEach(see Testing section) - No database or external service dependencies without an interface abstraction
- TypeScript strict mode — no
any, no@ts-ignore
Adding a New Endpoint
-
Add the method to the
AuthServiceinterface insrc/api/app.ts - Implement the logic in
src/auth/auth-service.ts - Add Zod validation schema in
src/api/validation.ts - Add the route in
src/api/auth-router.ts -
Add any new error codes to both
errors.tsandERROR_STATUS_MAP - Write tests covering happy path, edge cases, and error cases