A production-ready, reusable authentication REST API built with Node.js, TypeScript, Express, and MySQL. Features dual-token JWT authentication, email verification, password reset, refresh token rotation with reuse detection, and rate limiting on all sensitive endpoints.
This API is designed as a reusable authentication foundation to eliminate the need to rebuild auth for each new project. It targets standard web applications—SaaS platforms, client dashboards, and consumer apps—and implements common, production-ready patterns for session and token management.
Tokens are stored as one-way hashes in the database, so a database leak does not expose usable credentials.
- Dual-Token Auth — Short-lived JWT access token + opaque HttpOnly refresh token cookie
- Refresh Token Rotation — Reuse detection: a consumed token being replayed immediately revokes all sessions for that user
- Email Verification — Opaque token-based verification flow with resend support
- Password Reset — Single-use time-limited token via email; invalidates all active sessions on success
- Rate Limiting — Per-endpoint limits on all auth routes (register, login, refresh, password reset, etc.)
- Scheduled Cleanup — Daily cron job purges expired refresh, verification, and reset tokens
- Type Safety — Full TypeScript with strict typing and Zod request validation
- Security Hardening — Helmet headers, CORS, bcrypt hashing, timing-safe token comparisons
Backend: Node.js | TypeScript | Express
Database: MySQL | mysql2 (raw SQL, no ORM)
Authentication: JWT | bcryptjs
Validation: Zod
Email: Nodemailer
Security: Helmet | express-rate-limit
Logging: Winston
Scheduler: node-cron
graph TB
Client[Client / Frontend]
Routes[Routes Layer]
MW[Middleware Layer<br/>Auth · Validation · Rate Limit]
Controllers[Controllers Layer]
Services[Services Layer]
Models[Models Layer]
DB[(MySQL Database)]
Email[Email Service<br/>Nodemailer]
Client -->|HTTP Request| Routes
Routes -->|Validation + Auth| MW
MW -->|Processed Request| Controllers
Controllers -->|Business Logic| Services
Services -->|Raw SQL| Models
Models -->|mysql2 Pool| DB
Services -->|Transactional Emails| Email
Controllers -->|HTTP Response| Client
style Client fill:#e1f5ff
style Routes fill:#fff4e1
style MW fill:#ffe1f5
style Controllers fill:#e1ffe1
style Services fill:#f5e1ff
style Models fill:#ffe1e1
style DB fill:#333,color:#fff
style Email fill:#e1e1ff
Design Pattern: Layered architecture — routes handle HTTP, controllers parse requests and delegate, services own business logic, models execute raw SQL against the database.
secure-auth-system/
├── database/
│ └── schema.sql # Full MySQL schema
├── src/
│ ├── @types/ # Global TypeScript type definitions
│ ├── config/
│ │ ├── adapters/ # Thin wrappers: JWT, bcrypt, env-var
│ │ └── db/ # mysql2 pool setup
│ ├── constants/ # HTTP status codes, auth constants
│ ├── controllers/
│ │ ├── auth/ # Auth controller
│ │ └── user/ # User controller
│ ├── data/
│ │ ├── models/ # Raw SQL models (User, RefreshToken, etc.)
│ │ ├── schemas/ # Zod validation schemas
│ │ └── entities/ # Entity type definitions
│ ├── middlewares/ # authenticate, errorHandler, rateLimiter, validation
│ ├── routes/
│ │ ├── auth/ # /api/v1/auth routes
│ │ └── user/ # /api/v1/users routes
│ ├── services/
│ │ ├── auth/ # AuthService, TokenService, EmailVerificationService, PasswordResetService
│ │ ├── cleanup/ # Expired token cleanup
│ │ ├── cron/ # Scheduled job definitions
│ │ ├── email/ # Email sending
│ │ └── user/ # UserService
│ ├── templates/email/ # HTML email templates
│ ├── utils/ # CustomError, ApiResponse, logger, time parser, token helpers
│ ├── app.ts # Express app setup + middleware registration
│ └── server.ts # Entry point — connects DB, starts server
├── .env.template
├── package.json
├── tsconfig.json
└── README.md
Base path: /api/v1
| Method | Endpoint | Description |
|---|---|---|
| POST | /auth/register |
Register — triggers verification email |
| POST | /auth/login |
Login — returns access token, sets refresh cookie |
| POST | /auth/logout |
Revoke refresh token, clear cookie |
| POST | /auth/refresh |
Rotate refresh token, return new access token |
| GET | /auth/me |
Restore session from refresh cookie |
| POST | /auth/verify-email |
Verify email with token from link |
| POST | /auth/resend-verification |
Resend verification email |
| POST | /auth/forgot-password |
Send password reset email |
| POST | /auth/reset-password |
Reset password using token from link |
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| GET | /users/me |
Get authenticated user profile | Bearer token |
| Method | Endpoint | Description |
|---|---|---|
| GET | /health |
Health check |
- Node.js v18+
- MySQL v8+
- SMTP credentials (Gmail app password or similar)
# Clone repository
git clone https://github.com/jarolthecoder/secure-auth-system.git
cd secure-auth-system
# Install dependencies
npm install
# Configure environment
cp .env.template .env
# Edit .env with your credentials
# Create database
mysql -u root -p < database/schema.sql
# Start development server
npm run devServer runs at http://localhost:<PORT>
| Variable | Description |
|---|---|
NODE_ENV |
development or production |
PORT |
Server port |
FRONTEND_URL |
Frontend origin — used in email links |
ALLOWED_ORIGINS |
Comma-separated CORS origins |
DATABASE_HOST |
MySQL host |
DATABASE_PORT |
MySQL port |
DATABASE_USER |
MySQL user |
DATABASE_PASSWORD |
MySQL password |
DATABASE_NAME |
Database name |
DATABASE_SSL |
true when DB is on a remote/managed host |
JWT_SECRET |
Min 32 random chars — openssl rand -hex 32 |
ACCESS_TOKEN_EXP |
e.g. 15m |
REFRESH_TOKEN_EXP |
e.g. 7d |
EMAIL_VERIFICATION_TOKEN_EXP |
e.g. 30m |
PASSWORD_RESET_TOKEN_EXP |
e.g. 15m |
MAILER_SERVICE |
Nodemailer service, e.g. gmail |
MAILER_EMAIL |
Sender address |
MAILER_SECRET_KEY |
App password or API key |
Dual-token system with opaque refresh tokens:
- Access Token (JWT, short-lived e.g. 15m) — sent as
Authorization: Bearerheader for protected routes - Refresh Token (opaque, long-lived e.g. 7d) — stored as an HttpOnly cookie; only a bcrypt hash is persisted in the DB
# Authenticate requests
Authorization: Bearer <ACCESS_TOKEN>
# Rotate tokens when access token expires
POST /api/v1/auth/refresh (refresh cookie sent automatically)
Token reuse detection: if a previously-rotated refresh token is presented again, all sessions for that user are immediately revoked — this limits the blast radius of a stolen token.
npm run dev # Start with hot reload (ts-node-dev)
npm run build # Compile TypeScript to dist/
npm start # Build then run dist/app.js- Layered Architecture — Routes, controllers, services, and models each own a single responsibility with no cross-layer leakage
- Opaque Refresh Tokens — Format
jti.rawSecret; only the bcrypt hash ofrawSecretis stored — a DB leak exposes nothing replayable - Token Reuse Detection — Replaying a rotated refresh token triggers full session revocation for that user
- Timing-Safe Comparisons — All token and credential validations are constant-time to prevent timing oracle attacks
- Centralized Error Handling — Throw
CustomError.<type>()anywhere; globalerrorHandlermiddleware formats the response consistently - Adapter Pattern —
JwtAdapter,bcryptAdapter, andenvswrap third-party libraries so the rest of the codebase never imports them directly - Fast-fail Config — All env vars validated at startup via
env-var; missing vars crash immediately with a clear message - Scheduled Cleanup —
CronServiceruns at 6 AM daily to purge expired tokens viaCleanupService
- Deploy behind a reverse proxy (nginx, Caddy) — required for accurate IP-based rate limiting
- Set
DATABASE_SSL=truefor any managed or remote database - Set
NODE_ENV=productionto enable secure cookies and suppress stack traces in error responses - Rate limiting uses an in-memory store — add
rate-limit-redisbefore scaling to multiple instances
Jarol Riera
- GitHub: @jarolthecoder
- LinkedIn: Jarol Riera
This project is licensed under the MIT License — see the LICENSE file for details.