User Authentication Example#
This example demonstrates a complete spec-driven workflow for building JWT-based user authentication. It covers login, signup, password reset, and security best practices.
Phase 1: Initialize#
/spec-init user-authentication
Initial description:
JWT-based user authentication system with email/password login, user registration, password reset, and protected routes. Must include rate limiting and secure password storage.
Generated spec structure:
.specs/user-authentication/
├── spec.json
└── requirements.md (template)
Phase 2: Requirements#
Running /spec-requirements user-authentication generates:
# User Authentication - Requirements
## Purpose
Implement secure user authentication with JWT tokens to protect API endpoints
and enable personalized user experiences.
## Target Users
- End users accessing the application
- System administrators managing user accounts
## Functional Requirements
### FR-1: User Registration
When a new user provides valid email and password,
the system shall create an account and return a JWT token
within 500ms.
**Acceptance Criteria:**
- Email must be unique
- Password must be 8+ characters with uppercase, lowercase, and number
- Returns JWT token on success
- Sends welcome email
### FR-2: User Login
When a registered user provides correct credentials,
the system shall authenticate and return a JWT token
with 24-hour expiration.
**Acceptance Criteria:**
- Validates email exists
- Verifies password with bcrypt
- Returns JWT token on success
- Returns generic error on failure (security)
### FR-3: Password Reset
When a user requests password reset,
the system shall send a secure reset link valid for 1 hour.
**Acceptance Criteria:**
- Reset link contains secure token
- Token expires after 1 hour
- Old password remains valid until reset
- Email sent asynchronously
### FR-4: Protected Routes
When accessing protected endpoints without valid JWT,
the system shall reject the request with 401 status.
**Acceptance Criteria:**
- Validates JWT signature
- Checks token expiration
- Extracts user ID from token
- Returns 401 for invalid/missing tokens
## Non-Functional Requirements
### Security
- Passwords hashed with bcrypt (cost factor 12)
- JWT secret minimum 256 bits
- Rate limiting: 5 login attempts per 15 minutes per IP
- HTTPS only in production
### Performance
- Login response time < 500ms
- Token validation < 10ms
- Support 1000 concurrent users
### Compliance
- GDPR compliant (data deletion)
- Audit logs for authentication events
Phase 3: Design#
Running /spec-design user-authentication generates:
# User Authentication - Design
## Architecture
┌─────────────┐ ┌──────────────┐ ┌─────────────┐ │ Client │─────▶│ Auth API │─────▶│ Database │ └─────────────┘ └──────────────┘ └─────────────┘ │ ▼ ┌──────────────┐ │ JWT Tokens │ └──────────────┘
## API Design
### POST /api/auth/register
**Request:**
```json
{
"email": "user@example.com",
"password": "SecurePass123"
}
Response (201):
{
"token": "eyJhbGciOiJIUzI1NiIs...",
"user": {
"id": "uuid",
"email": "user@example.com"
}
}
Response (400):
{
"error": "Email already exists"
}
POST /api/auth/login#
Request:
{
"email": "user@example.com",
"password": "SecurePass123"
}
Response (200):
{
"token": "eyJhbGciOiJIUzI1NiIs...",
"user": {
"id": "uuid",
"email": "user@example.com"
}
}
Response (401):
{
"error": "Invalid credentials"
}
POST /api/auth/forgot-password#
Request:
{
"email": "user@example.com"
}
Response (200):
{
"message": "Reset email sent"
}
Database Schema#
-- Users table
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- Password reset tokens
CREATE TABLE password_resets (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
token_hash VARCHAR(255) NOT NULL,
expires_at TIMESTAMP NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
-- Indexes
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_password_resets_token ON password_resets(token_hash);
Security Measures#
-
Password Hashing
- bcrypt with cost factor 12
- Never store plain text passwords
-
JWT Configuration
- HS256 algorithm
- 24-hour expiration
- Secure secret from environment
-
Rate Limiting
- 5 attempts per 15 minutes
- IP-based tracking
- Returns 429 status when exceeded
-
Input Validation
- Email format validation
- Password strength requirements
- SQL injection prevention (parameterized queries)
Error Handling#
| Scenario | Status | Message |
|---|---|---|
| Invalid email format | 400 | "Invalid email format" |
| Weak password | 400 | "Password does not meet requirements" |
| Email exists | 400 | "Email already registered" |
| Invalid credentials | 401 | "Invalid credentials" |
| Missing token | 401 | "Authentication required" |
| Expired token | 401 | "Token expired" |
| Rate limited | 429 | "Too many attempts" |
---
## Phase 4: Tasks
Running `/spec-tasks user-authentication` generates:
```markdown
# User Authentication - Sprint Plan
**Estimated Time:** 6 hours
## TASK 1: Database Setup (45 min)
**Priority:** P0 (Blocker)
**Dependencies:** None
**Input:** Design document schema
**OUTPUT:**
- Migration files for users and password_resets tables
- Indexes created
- Database connection configured
**VERIFY:**
```bash
npm run db:migrate
# Check tables exist
TASK 2: Password Hashing Utility (30 min)#
Priority: P0 (Blocker) Dependencies: None
OUTPUT:
src/lib/password.tswith hashPassword() and verifyPassword()- Unit tests
VERIFY:
npm test src/lib/password.test.ts
TASK 3: JWT Utility (30 min)#
Priority: P0 (Blocker) Dependencies: None
OUTPUT:
src/lib/jwt.tswith signToken() and verifyToken()- Unit tests
VERIFY:
npm test src/lib/jwt.test.ts
TASK 4: Register Endpoint (1 hour)#
Priority: P0 (Blocker) Dependencies: TASK 1, TASK 2
OUTPUT:
src/app/api/auth/register/route.ts- Input validation
- Password hashing
- JWT generation
- Error handling
VERIFY:
curl -X POST http://localhost:3000/api/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","password":"TestPass123"}'
# Returns 201 with token
TASK 5: Login Endpoint (45 min)#
Priority: P0 (Blocker) Dependencies: TASK 1, TASK 2, TASK 3
OUTPUT:
src/app/api/auth/login/route.ts- Credential validation
- Rate limiting middleware
- JWT generation
VERIFY:
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","password":"TestPass123"}'
# Returns 200 with token
TASK 6: Forgot Password Endpoint (1 hour)#
Priority: P1 (High) Dependencies: TASK 1, TASK 3
OUTPUT:
src/app/api/auth/forgot-password/route.ts- Token generation and storage
- Email service integration
- Expiration handling
VERIFY:
- Check email sent (use test email catcher)
- Verify token stored in database
TASK 7: Auth Middleware (45 min)#
Priority: P0 (Blocker) Dependencies: TASK 3
OUTPUT:
src/middleware/auth.ts- JWT verification
- User extraction
- 401 responses for invalid tokens
VERIFY:
- Test with valid token → passes
- Test with invalid token → 401
TASK 8: Protected Route Example (30 min)#
Priority: P1 (High) Dependencies: TASK 7
OUTPUT:
src/app/api/user/profile/route.ts- Demonstrates auth middleware usage
- Returns user data from token
TASK 9: Integration Tests (1 hour)#
Priority: P1 (High) Dependencies: All above tasks
OUTPUT:
src/tests/auth.integration.test.ts- Register → Login → Access protected route flow
- Error case testing
VERIFY:
npm run test:integration
---
## Phase 5: Implementation
Run `/code user-authentication` to implement the next pending task, then `/test` and `/review`.
### Key Implementation Details
#### Password Hashing (`src/lib/password.ts`)
```typescript
import bcrypt from 'bcrypt';
const SALT_ROUNDS = 12;
export async function hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, SALT_ROUNDS);
}
export async function verifyPassword(
password: string,
hash: string
): Promise<boolean> {
return bcrypt.compare(password, hash);
}
JWT Utility (src/lib/jwt.ts)#
import jwt from 'jsonwebtoken';
const JWT_SECRET = process.env.JWT_SECRET!;
const JWT_EXPIRES_IN = '24h';
export function signToken(userId: string): string {
return jwt.sign({ userId }, JWT_SECRET, {
expiresIn: JWT_EXPIRES_IN,
});
}
export function verifyToken(token: string): { userId: string } {
return jwt.verify(token, JWT_SECRET) as { userId: string };
}
Auth Middleware (src/middleware/auth.ts)#
import { NextRequest, NextResponse } from 'next/server';
import { verifyToken } from '@/lib/jwt';
export function withAuth(handler: Function) {
return async (req: NextRequest) => {
const token = req.headers.get('authorization')?.replace('Bearer ', '');
if (!token) {
return NextResponse.json(
{ error: 'Authentication required' },
{ status: 401 }
);
}
try {
const payload = verifyToken(token);
req.userId = payload.userId;
return handler(req);
} catch {
return NextResponse.json(
{ error: 'Invalid token' },
{ status: 401 }
);
}
};
}
Phase 6: Status#
Running /spec-status user-authentication shows:
# User Authentication - Status Report
Generated: 2026-02-02 14:30
## Progress: 100% (9/9 tasks complete)
### Completed Tasks ✅
- TASK 1: Database Setup (45 min)
- TASK 2: Password Hashing Utility (30 min)
- TASK 3: JWT Utility (30 min)
- TASK 4: Register Endpoint (1 hour)
- TASK 5: Login Endpoint (45 min)
- TASK 6: Forgot Password Endpoint (1 hour)
- TASK 7: Auth Middleware (45 min)
- TASK 8: Protected Route Example (30 min)
- TASK 9: Integration Tests (1 hour)
### Blockers: None
### Total Time: 6 hours 15 minutes
### Next Steps:
- Deploy to staging
- Perform security audit
- Update API documentation
Key Takeaways#
What Went Well#
- Clear requirements prevented scope creep
- Design phase caught security issues early
- Task breakdown made estimation accurate
- Tests caught integration issues
Lessons Learned#
- Rate limiting should be implemented early (moved to TASK 5)
- Email service needed mock for testing
- JWT secret rotation process needed documentation
Security Checklist#
- ✅ Passwords hashed with bcrypt
- ✅ JWT tokens expire in 24 hours
- ✅ Rate limiting on login
- ✅ Input validation on all endpoints
- ✅ Generic error messages for security
- ✅ HTTPS enforcement in production
Want to try this yourself? Run the commands above in your own project!