A standalone JWT authentication module built with Express and TypeScript.
jwt-module is a self-contained authentication service providing user registration, login, JWT-based access/refresh tokens with rotation, and full account management through a REST API. It uses in-memory storage, making it ideal for development, prototyping, and learning.
graph LR
Client([Client]) -->|HTTP| API[Express API Layer]
API -->|AuthService interface| Auth[Auth Service Core]
Auth -->|read/write| Store[(In-Memory Store)]
Important
Note: All data is lost on restart. For production use, implement a persistent backing store behind the existing interfaces.
- 👤 User Registration -- email/password signup with duplicate detection
- 🔑 Login -- credential verification with JWT token pair issuance
- 🔄 Refresh Token Rotation -- old tokens revoked on every refresh, new pair issued
- 🛡️ Account Lockout -- automatic lock after 5 failed login attempts (15 min cooldown)
- 🚥 Rate Limiting -- per-IP sliding window (20 requests / 15 min) on sensitive endpoints
- 📤 Logout / Logout All -- single-session and all-session token revocation
- 🔒 Change Password -- requires current password, revokes all sessions
- ✉️ Update Email -- requires password confirmation, duplicate check
- 🗑️ Delete Account -- requires password confirmation, full cleanup
- 🔍 Get Profile -- returns user ID, email, and creation date
- ✅ Health Check --
GET /healthfor uptime monitoring - 🛡️ Helmet -- security headers on all responses
- 🌐 CORS -- configurable allowed origins
- 📝 Zod Validation -- schema validation on all request bodies
# Clone the repository
git clone https://github.com/hoangsonww/JWT-Module.git
cd JWT-Module
# Install dependencies
npm install
# Set environment variables (or use dev defaults)
cp .env.example .env
# Build
npm run build
# Start development server
npx ts-node src/server.tsThe server starts on http://localhost:3000 by default. A test UI is served at the root URL.
| Variable | Description | Default | Required |
|---|---|---|---|
JWT_ACCESS_SECRET |
Secret for signing access tokens | Dev fallback | Yes (in production) |
JWT_REFRESH_SECRET |
Secret for signing refresh tokens | Dev fallback | Yes (in production) |
PORT |
Server listen port | 3000 |
No |
NODE_ENV |
Runtime environment | -- | No |
CORS_ORIGIN |
Allowed origins (comma-separated or *) |
* |
No |
In development, fallback secrets are applied automatically. Always set real secrets in production.
All endpoints return JSON. Error responses use the shape:
{ "error": { "code": "ERROR_CODE", "message": "Human-readable message" } }Create a new account.
- Auth: None
- Body:
{ "email": "user@example.com", "password": "securePass1" } - Success:
201{ "tokens": { "accessToken": "...", "refreshToken": "..." } } - Errors:
400 INVALID_EMAIL|400 WEAK_PASSWORD|400 INVALID_INPUT|409 DUPLICATE_EMAIL
Authenticate and receive tokens.
- Auth: None
- Body:
{ "email": "user@example.com", "password": "securePass1" } - Success:
200{ "tokens": { "accessToken": "...", "refreshToken": "..." } } - Errors:
401 INVALID_CREDENTIALS|423 ACCOUNT_LOCKED|400 INVALID_INPUT
Exchange a refresh token for a new token pair. The old refresh token is revoked (rotation).
- Auth: None
- Body:
{ "refreshToken": "..." } - Success:
200{ "tokens": { "accessToken": "...", "refreshToken": "..." } } - Errors:
401 INVALID_TOKEN|401 TOKEN_EXPIRED|404 USER_NOT_FOUND|400 INVALID_INPUT
Revoke a single refresh token.
- Auth: None
- Body:
{ "refreshToken": "..." } - Success:
200{ "message": "Logged out successfully" }
Revoke all refresh tokens for the authenticated user.
- Auth: Bearer token
- Body: None
- Success:
200{ "message": "All sessions revoked successfully" } - Errors:
401 MISSING_TOKEN|401 INVALID_TOKEN|401 TOKEN_EXPIRED
Change password for the authenticated user. Revokes all sessions.
- Auth: Bearer token
- Body:
{ "currentPassword": "oldPass1", "newPassword": "newPass1" } - Success:
200{ "message": "Password changed successfully" } - Errors:
401 MISSING_TOKEN|401 INVALID_CREDENTIALS|400 WEAK_PASSWORD|400 INVALID_INPUT
Get the authenticated user's profile.
- Auth: Bearer token
- Success:
200{ "id": "...", "email": "user@example.com", "createdAt": "2026-01-01T00:00:00.000Z" } - Errors:
401 MISSING_TOKEN|401 INVALID_TOKEN|404 USER_NOT_FOUND
Update the authenticated user's email.
- Auth: Bearer token
- Body:
{ "newEmail": "new@example.com", "password": "currentPass1" } - Success:
200{ "message": "Email updated successfully" } - Errors:
401 MISSING_TOKEN|401 INVALID_CREDENTIALS|400 INVALID_EMAIL|409 DUPLICATE_EMAIL|400 INVALID_INPUT
Delete the authenticated user's account.
- Auth: Bearer token
- Body:
{ "password": "currentPass1" } - Success:
200{ "message": "Account deleted successfully" } - Errors:
401 MISSING_TOKEN|401 INVALID_CREDENTIALS|400 INVALID_INPUT
Health check endpoint.
- Auth: None
- Success:
200{ "status": "ok" }
sequenceDiagram
participant C as Client
participant A as API
participant S as Auth Service
C->>A: POST /auth/register {email, password}
A->>S: register(input)
S-->>A: {accessToken, refreshToken}
A-->>C: 201 {tokens}
C->>A: POST /auth/login {email, password}
A->>S: login(input)
S-->>A: {accessToken, refreshToken}
A-->>C: 200 {tokens}
C->>A: GET /auth/me [Bearer accessToken]
A->>A: authenticateToken middleware
A->>S: getUserById(userId)
S-->>A: user
A-->>C: 200 {id, email, createdAt}
Note over C,S: Access token expires after 15 min
C->>A: POST /auth/refresh {refreshToken}
A->>S: refreshTokens(refreshToken)
S->>S: Revoke old token, issue new pair
S-->>A: {newAccessToken, newRefreshToken}
A-->>C: 200 {tokens}
C->>A: POST /auth/logout {refreshToken}
A->>S: logout(refreshToken)
S->>S: Revoke token
A-->>C: 200 {message}
Every incoming request passes through multiple security layers before reaching the auth service:
flowchart TD
REQ([Incoming Request]) --> HELMET[Helmet Security Headers]
HELMET --> CORS[CORS Check]
CORS --> LOGGER[Request Logger]
LOGGER --> BODY[Body Parser - 10kb limit]
BODY --> ROUTE{Route Match}
ROUTE -->|Public| RATE[Rate Limiter]
ROUTE -->|Protected| AUTH[Bearer Token Check]
AUTH -->|Valid| RATE2[Rate Limiter]
AUTH -->|Invalid| R401([401 Unauthorized])
RATE --> ZOD[Zod Schema Validation]
RATE2 --> ZOD
RATE -->|Exceeded| R429([429 Too Many Requests])
RATE2 -->|Exceeded| R429
ZOD -->|Valid| SERVICE[Auth Service]
ZOD -->|Invalid| R400([400 Invalid Input])
SERVICE -->|Lockout check| LOCK{Account Locked?}
LOCK -->|No| PROCESS[Process Request]
LOCK -->|Yes| R423([423 Locked])
PROCESS --> RESP([Success Response])
| Layer | Mechanism | Details |
|---|---|---|
| Transport | Helmet | Security headers (X-Content-Type-Options, X-Frame-Options, etc.) |
| Transport | CORS | Configurable allowed origins |
| Transport | Body size limit | 10kb max JSON payload |
| Rate control | Per-IP rate limiter | 20 requests per 15-minute sliding window |
| Input validation | Zod schemas | Type-safe validation on all request bodies |
| Authentication | JWT HS256 | Algorithm pinning prevents substitution attacks |
| Authorization | Bearer middleware | Access token verification on protected routes |
| Brute force | Account lockout | 5 failed attempts triggers 15-minute lock |
| Token security | Refresh rotation | Old tokens revoked on each refresh |
| Password | bcrypt (12 rounds) | Adaptive hashing with salt |
stateDiagram-v2
[*] --> Normal
Normal --> FailedAttempt: Wrong password
FailedAttempt --> FailedAttempt: Wrong password (count < 5)
FailedAttempt --> Locked: 5th failed attempt
FailedAttempt --> Normal: Successful login (resets count)
Locked --> Normal: 15 minutes elapsed
Locked --> Locked: Login attempt (rejected)
- Threshold: 5 consecutive failed attempts per email
- Lockout duration: 15 minutes
- Reset: Successful login clears the failure counter
| Error Code | HTTP Status | Description |
|---|---|---|
INVALID_INPUT |
400 | Request body failed Zod validation |
INVALID_EMAIL |
400 | Email format is invalid |
WEAK_PASSWORD |
400 | Password does not meet strength requirements |
INVALID_CREDENTIALS |
401 | Wrong email or password |
MISSING_TOKEN |
401 | Authorization header missing or malformed |
INVALID_TOKEN |
401 | Token is invalid or revoked |
TOKEN_EXPIRED |
401 | Token has expired |
USER_NOT_FOUND |
404 | User does not exist |
DUPLICATE_EMAIL |
409 | Email already registered |
ACCOUNT_LOCKED |
423 | Too many failed login attempts |
RATE_LIMITED |
429 | IP exceeded request limit |
MISSING_SECRET |
500 | JWT secret environment variable not set |
INTERNAL_ERROR |
500 | Unexpected server error |
- 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
# Run tests
npm test
# Run tests with coverage
npm run test:coverage
# Build TypeScript
npm run build
# Start dev server (with default secrets)
npx ts-node src/server.ts
# Start on custom port
PORT=5001 npx ts-node src/server.tssrc/
auth/ # Core auth logic (no HTTP dependency)
auth-service.ts # Registration, login, refresh, account management
errors.ts # AuthError class and AuthErrorCode union type
password.ts # bcrypt hashing and password strength validation
token.ts # JWT generation, verification, revocation blacklist
types.ts # Shared interfaces (User, AuthTokens, TokenPayload, etc.)
index.ts # Barrel export for auth module
api/ # HTTP layer
app.ts # Express app factory, AuthService interface
auth-router.ts # Route handlers, error-to-HTTP-status mapping
middleware.ts # authenticateToken, requestLogger
rate-limiter.ts # Per-IP sliding window rate limiter
validation.ts # Zod schemas for all request bodies
server.ts # Entry point -- wires auth-service into Express app
public/ # Static test UI
- Add the method to the
AuthServiceinterface insrc/api/app.ts - Implement the logic in
src/auth/auth-service.ts - Add a Zod schema in
src/api/validation.ts - Add the route handler in
src/api/auth-router.ts - If a new error code is needed, add it to
AuthErrorCodeinsrc/auth/errors.tsand toERROR_STATUS_MAPinsrc/api/auth-router.ts
See ARCHITECTURE.md for detailed architecture documentation with diagrams.
MIT
