Warming up the neural circuits...
By the end of this project you will have shipped:
This is the auth system every production app needs and most tutorials skip. Not just "hash a password and return a JWT" — the whole pipeline. Email verification. Refresh token rotation. RBAC. Rate limiting. Security headers. When you finish this, you can drop it into any project and have enterprise-grade auth.
This project applies every L3 chapter:
Register → Verify email → Login → Access protected resources → Refresh token → Logout
↓
Password reset (forgot → email → reset)POST /api/auth/register body: { email, password, name }
POST /api/auth/verify-email body: { token }
POST /api/auth/login body: { email, password }
POST /api/auth/refresh body: { refreshToken }
POST /api/auth/logout body: { refreshToken }
POST /api/auth/forgot-password body: { email }
POST /api/auth/reset-password body: { token, newPassword }
GET /api/users/me → current user profile
GET /api/users → admin only: list users
PATCH /api/users/:id/role → admin only: change role
DELETE /api/users/:id → admin only: delete userCREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
name VARCHAR(255) NOT NULL,
email_verified BOOLEAN DEFAULT FALSE,
avatar_url TEXT,
created_at TIMESTAMPTZ DEFAULT now()
auth-system/
├── prisma/
│ └── schema.prisma
├── src/
│ ├── server.ts
│ ├── app.ts
│ ├── config.ts # env vars, pepper, JWT secret
│ ├── lib/
│ │ ├── prisma.ts
│ │ ├── jwt.ts # sign, verify, refresh
│ │ ├── password.ts # hash, verify, pepper
│ │ ├── email.ts # send verification + reset emails
│ │ └── errors.ts # AppError class with status codes
│ ├── middleware/
│ │ ├── authenticate.ts # JWT verification → req.user
│ │ ├── authorize.ts # RBAC checker
│ │ ├── rateLimiter.ts # sliding window via Redis
│ │ ├── validate.ts # Zod validation
│ │ └── errorHandler.ts
│ ├── modules/
│ │ ├── auth/
│ │ │ ├── auth.routes.ts
│ │ │ ├── auth.service.ts # register, login, refresh, logout, verify, reset
│ │ │ └── auth.schemas.ts
│ │ └── users/
│ │ ├── users.routes.ts
│ │ ├── users.service.ts
│ │ └── users.schemas.ts
│ └── utils/
│ └── tokens.ts # generate random tokens
├── .env.example
├── package.json
└── README.mdcan(userId, resource, action) functionauthorize(resource, action) middlewareasync function refreshTokens(refreshToken: string) {
const stored = await db.refreshToken.findUnique({ where: { token: refreshToken } });
if (!stored || stored.
sk_live, password, secret).env in .gitignore ✅*) ✅| Mistake | Why it's wrong | How to fix |
|---|---|---|
| No refresh token rotation | A stolen refresh token gives permanent access until expiry | Rotate on every use. Detect reuse → revoke family. |
| Email verification token never expires | An old verification link remains valid forever | Set 24-hour expiry. Delete expired tokens via cron. |
| Password reset doesn't invalidate sessions | Attacker resets password but existing sessions (refresh tokens) still work | Delete all refresh tokens for the user on password reset |
| Rate limit on login but not on forgot-password | Forgot-password can be used to enumerate users (different responses for valid vs invalid email) | Rate limit forgot-password: 3 req/hour per IP. Always return "If that email exists, we sent a reset link." |
admin role hardcoded as string | if (user.role === 'admin') scattered everywhere | Use can(userId, 'users', 'delete') — centralized |
Walk me through the login flow in your auth system. User sends email + password → server hashes password with pepper, compares to stored bcrypt hash → if valid, generates JWT access token (15 min) and opaque refresh token (7 days, stored in DB) → returns both to client. Client sends access token in Authorization header. On expiry, client sends refresh token to /refresh → server rotates refresh token, returns new access + refresh tokens.
Why do you need both access and refresh tokens? Access token is short-lived (15 min) and stateless (verified without DB query). Refresh token is long-lived (7 days) and stateful (stored in DB, revocable). This balances performance (no DB hit on every request) with security (limited window if token stolen, instant revocation via refresh token).
How does your refresh token rotation detect token theft? Each refresh token belongs to a "family." On first use, the token is marked as used and a new token in the same family is issued. If an already-used token is presented, it means the legitimate user's token was used by someone else — a theft. The entire family is revoked, forcing re-authentication for all devices.
What would you change if this auth system needed to support 1 million concurrent users? Move refresh tokens from PostgreSQL to Redis (faster reads, built-in TTL). Use RS256 for JWT so services can verify without sharing a secret. Add a JWKS endpoint for key rotation. Deploy rate limiting as a separate service (e.g., Envoy proxy). Move email sending to a background job (BullMQ). Add a layer for RBAC permissions (Redis, 5 min TTL).
This project implements a complete, production-grade auth system: JWT access + refresh tokens with rotation, email verification, password reset, RBAC with three roles, rate limiting, and security headers. The refresh token rotation with family reuse detection is the standout feature — it detects token theft and automatically revokes all compromised sessions. Follow the security audit checklist before merging. This is the auth system you can drop into any future project.
can(userId, resource, action).