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.
GET /health for uptime monitoring# 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.ts
The 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.
{ "email": "user@example.com", "password": "securePass1" }201
{ "tokens": { "accessToken": "...", "refreshToken": "..." } }
Errors: 400 INVALID_EMAIL |
400 WEAK_PASSWORD |
400 INVALID_INPUT |
409 DUPLICATE_EMAIL |
Authenticate and receive tokens.
{ "email": "user@example.com", "password": "securePass1" }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).
{ "refreshToken": "..." }200
{ "tokens": { "accessToken": "...", "refreshToken": "..." } }
Errors: 401 INVALID_TOKEN |
401 TOKEN_EXPIRED |
404 USER_NOT_FOUND |
400 INVALID_INPUT |
Revoke a single refresh token.
{ "refreshToken": "..." }200
{ "message": "Logged out successfully" }
Revoke all refresh tokens for the authenticated user.
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.
{ "currentPassword": "oldPass1", "newPassword": "newPass1" }200
{ "message": "Password changed successfully" }
Errors: 401 MISSING_TOKEN |
401 INVALID_CREDENTIALS |
400 WEAK_PASSWORD |
400 INVALID_INPUT |
Get the authenticated user’s profile.
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.
{ "newEmail": "new@example.com", "password": "currentPass1" }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.
{ "password": "currentPass1" }200
{ "message": "Account deleted successfully" }
Errors: 401 MISSING_TOKEN |
401 INVALID_CREDENTIALS |
400 INVALID_INPUT |
Health check endpoint.
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)
| 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 |
# 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.ts
src/
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
AuthService interface in src/api/app.tssrc/auth/auth-service.tssrc/api/validation.tssrc/api/auth-router.tsAuthErrorCode in src/auth/errors.ts and to ERROR_STATUS_MAP in src/api/auth-router.tsSee ARCHITECTURE.md for detailed architecture documentation with diagrams.
MIT