Date Implemented: December 12, 2025
Status: β
Complete
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β LOGIN REQUEST β
β (email, password, city, device) β
ββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββββββββ
β β
Credentials Verified β
β β
Device Info Captured β
β β
Failed Attempts Checked β
β β
Account Status Verified β
ββββββββββββββββββ¬ββββββββββββββββββββββββ
β
βββββββββββββββββ΄βββββββββββββββββ
β β
βΌ βΌ
ACCESS TOKEN REFRESH TOKEN
(Short-lived) (Long-lived)
ββββββββββββββββ ββββββββββββββββ
β 15m-2h β β 7-30 days β
β In memory β β DB + Cookie β
β Stateless β β Revocable β
β JWT β β Hashed β
ββββββββββββββββ ββββββββββββββββ
β β
β Response Body β HttpOnly Cookie
ββββββββββββββββ¬ββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββ
β CLIENT (Frontend) β
β - Store access in state β
β - Cookie auto-included β
ββββββββββββββββββββββββββββ
| Feature | Customers | Restaurant Owners | Delivery Partners |
|---|---|---|---|
| Access Token | 15 minutes | 30 minutes | 2 hours |
| Refresh Token | 7 days | 30 days | 30 days |
| Max Sessions | 5 devices | 3 devices | 2 devices |
| Typical Usage | Mobile app | Web dashboard | Mobile delivery app |
| Activity | Frequent | Regular | Active (long deliveries) |
Location: services/auth-service/src/config/tokens.ts
Contains:
TOKEN_CONFIG- Access & refresh token durationsMAX_SESSIONS- Max active sessions per user typeJWT_SECRETS- Secret keys for token signing- Email verification & password reset tokens
// Example:
TOKEN_CONFIG.ACCESS_TOKEN.customer = { expiresIn: '15m' }
TOKEN_CONFIG.REFRESH_TOKEN.customer = { expiresIn: '7d' }
TOKEN_CONFIG.MAX_SESSIONS.customer = 5Location: services/auth-service/src/services/tokenService.ts
Core service with methods:
generateAccessToken()- Create short-lived JWTgenerateRefreshToken()- Create long-lived DB tokenverifyAccessToken()- Validate access tokensverifyRefreshToken()- Validate refresh tokensrevokeRefreshToken()- Logout from one devicerevokeAllUserTokens()- Logout from all devices
Location: services/auth-service/src/routes/login.ts
Enhanced with:
- β Dual token generation
- β Failed login attempt tracking
- β Account locking (5 attempts = 15 min lock)
- β Login history logging
- β HttpOnly cookie for refresh token
- β Device info tracking
Location: services/auth-service/src/routes/refresh.ts
Implements:
- Access token refresh endpoint
- Automatic token rotation
- Refresh token validation
Location: services/auth-service/src/routes/logout.ts
Provides endpoints:
POST /auth/logout- Logout from current devicePOST /auth/logout-all- Logout from all devicesGET /auth/sessions- View active sessionsDELETE /auth/sessions/:id- Revoke specific session
Changes:
- Added
RefreshTokenmodel (stores hashed tokens in DB) - Added
LoginHistorymodel (audit trail) - Enhanced
Usermodel with:failedLoginAttemptsaccountLockedUntillastLogin,lastLoginIptwoFactorEnabled,twoFactorSecret
Changes:
- Registered
refreshRouter - Registered
logoutRouter - Updated health check endpoint
New variables:
JWT_ACCESS_SECRETJWT_REFRESH_SECRETEMAIL_SECRETPASSWORD_RESET_SECRET- Token duration configs
- Max sessions configs
curl -X POST http://localhost:3001/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "Password123!",
"city": "San Francisco",
"deviceName": "Chrome on MacBook"
}'// Check credentials, device, and account status
const user = await prisma.user.findUnique({ where: { email } });
if (!validPassword) {
// Increment failed attempts
// Lock account if >= 5 attempts
}
// Generate tokens
const accessToken = TokenService.generateAccessToken(payload); // 15m
const refreshToken = await TokenService.generateRefreshToken(payload, deviceInfo); // 7 days
// Store refresh token hash in DB with device info{
"success": true,
"message": "Login successful",
"data": {
"userId": "user123",
"email": "user@example.com",
"role": "customer",
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"expiresIn": "15m"
}
}Headers:
Set-Cookie: refreshToken=abc123def456...; HttpOnly; Secure; SameSite=Strict; Path=/auth; Max-Age=604800
// React: Store in state, NOT localStorage
const [accessToken, setAccessToken] = useState(null);
// Make API request
fetch('https://api.example.com/orders', {
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
credentials: 'include' // β
Include HttpOnly cookie
});
// Browser automatically includes refreshToken cookieClient tries to access protected resource
β
Gets 401 Unauthorized
β
Sends refresh request with refreshToken cookie
β
POST /auth/refresh
{
"city": "San Francisco"
}
β
Server validates refresh token in DB
β
Generates new access token (15m)
β
Returns new access token
// Auto-refresh before expiry (React)
useEffect(() => {
if (!accessToken) return;
// Refresh 1 minute before expiry (14 min for 15 min token)
const timeout = setTimeout(async () => {
const response = await fetch('http://localhost:3001/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include', // β
Send HttpOnly cookie
body: JSON.stringify({ city: userCity })
});
const data = await response.json();
if (data.success) {
setAccessToken(data.data.accessToken);
} else {
// Refresh token expired, logout user
logout();
}
}, 14 * 60 * 1000); // 14 minutes
return () => clearTimeout(timeout);
}, [accessToken]);curl -X POST http://localhost:3001/auth/logout \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{
"refreshToken": "abc123def456..."
}' \
-b "refreshToken=abc123def456..."Result:
- Current refresh token revoked (isRevoked = true)
- Other devices remain active
- Access token continues working until expiry
curl -X POST http://localhost:3001/auth/logout-all \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-b "refreshToken=abc123def456..."Result:
- All refresh tokens revoked
- All devices logged out immediately
- User must login again on all devices
curl http://localhost:3001/auth/sessions \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-b "refreshToken=abc123def456..."Response:
{
"success": true,
"data": {
"activeSessions": 2,
"sessions": [
{
"id": "session123",
"deviceInfo": "Chrome on MacBook",
"ipAddress": "192.168.1.1",
"lastUsedAt": "2025-12-12T10:30:00Z",
"createdAt": "2025-12-12T10:00:00Z"
},
{
"id": "session456",
"deviceInfo": "Safari on iPhone",
"ipAddress": "192.168.1.50",
"lastUsedAt": "2025-12-12T09:15:00Z",
"createdAt": "2025-12-11T14:30:00Z"
}
]
}
}curl -X DELETE http://localhost:3001/auth/sessions/session456 \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-b "refreshToken=abc123def456..."- β Short expiry = limited damage if compromised
- β Stored in memory only (not localStorage)
- β Stateless JWT (fast verification)
- β Unique tokenId per token
- β Stored in database (hashable & revocable)
- β HttpOnly cookie (XSS protection)
- β Secure flag (HTTPS only in production)
- β SameSite=Strict (CSRF protection)
- β Hash stored in DB (never plain text)
- β Failed attempt tracking
- β Account locking (5 attempts β 15 min lock)
- β Device information tracking
- β IP address logging
- β Login history audit trail
- β Max sessions per user (5 for customers, 3 for restaurant, 2 for delivery)
- β Automatic revocation of oldest session when limit exceeded
- β Single device logout (one refresh token)
- β All devices logout (all refresh tokens)
- β View and revoke specific sessions
- β Auto-refresh access token before expiry
- β Refresh token can be rotated
- β Old tokens can be invalidated
cd services/auth-service
npm install# Generate JWT secrets (minimum 32 characters)
openssl rand -base64 32
# Example output: kj3lh4kl5jh6kj7lh8kj9lh0kj1lh2kj3lh4kl5jh6kj7lh8=
# Generate another one for refresh token secret
openssl rand -base64 32
# Example output: qw9er8ty7ui6op5as4df3gh2jk1lz9xc8vb7nm6qw5er4ty=# Copy .env.example to .env
cp .env.example .env
# Edit .env and add the generated secrets:
JWT_ACCESS_SECRET=kj3lh4kl5jh6kj7lh8kj9lh0kj1lh2kj3lh4kl5jh6kj7lh8=
JWT_REFRESH_SECRET=qw9er8ty7ui6op5as4df3gh2jk1lz9xc8vb7nm6qw5er4ty=
EMAIL_SECRET=zx9cv8bn7mk6lq5ws4ed3rf2tg1yh0ju9ki8ol7pq6wr5es=
PASSWORD_RESET_SECRET=as0qw9ed8rf7tg6yh5uj4ik3ol2pm1lk0qw9ed8rf7tg6yh=
# Update database URLs
POSTGRES_SHARD_A_URL=postgresql://user:password@localhost:5432/shard_a
POSTGRES_SHARD_B_URL=postgresql://user:password@localhost:5433/shard_b
POSTGRES_SHARD_C_URL=postgresql://user:password@localhost:5434/shard_c# Create and migrate databases
npm run prisma:migrate:shardA
npm run prisma:migrate:shardB
npm run prisma:migrate:shardCnpm run dev
# Output: β
Auth Service is running on port 3001curl -X POST http://localhost:3001/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "customer@example.com",
"password": "SecurePassword123!",
"city": "San Francisco",
"deviceName": "Test Device"
}' \
-vExpected Response:
{
"success": true,
"message": "Login successful",
"data": {
"userId": "...",
"email": "customer@example.com",
"role": "customer",
"accessToken": "eyJ...",
"expiresIn": "15m"
}
}Expected Cookie Header:
Set-Cookie: refreshToken=...; HttpOnly; Secure; SameSite=Strict; Path=/auth
curl -X POST http://localhost:3001/auth/refresh \
-H "Content-Type: application/json" \
-d '{"city": "San Francisco"}' \
-b "refreshToken=..." \
-v# First 4 attempts (fail)
for i in {1..4}; do
curl -X POST http://localhost:3001/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "customer@example.com",
"password": "WrongPassword",
"city": "San Francisco"
}'
done
# 5th attempt (account locked)
# Expected: 423 "Account temporarily locked"# Get active sessions
curl http://localhost:3001/auth/sessions \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-b "refreshToken=$REFRESH_TOKEN"
# Logout from one device
curl -X POST http://localhost:3001/auth/logout \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{"refreshToken": "$REFRESH_TOKEN"}' \
-b "refreshToken=$REFRESH_TOKEN"
# Logout from all devices
curl -X POST http://localhost:3001/auth/logout-all \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-b "refreshToken=$REFRESH_TOKEN"CREATE TABLE "RefreshToken" (
"id" TEXT PRIMARY KEY,
"userId" TEXT NOT NULL,
"token" TEXT NOT NULL UNIQUE, -- Hashed token
"deviceInfo" TEXT,
"ipAddress" TEXT,
"userAgent" TEXT,
"expiresAt" TIMESTAMP NOT NULL,
"createdAt" TIMESTAMP DEFAULT now(),
"lastUsedAt" TIMESTAMP DEFAULT now(),
"isRevoked" BOOLEAN DEFAULT false,
"revokedAt" TIMESTAMP,
"revokedReason" TEXT,
FOREIGN KEY ("userId") REFERENCES "User"("id")
);
CREATE INDEX idx_refreshtoken_userid ON "RefreshToken"("userId");
CREATE INDEX idx_refreshtoken_token ON "RefreshToken"("token");CREATE TABLE "LoginHistory" (
"id" TEXT PRIMARY KEY,
"userId" TEXT,
"email" TEXT NOT NULL,
"success" BOOLEAN NOT NULL,
"failureReason" TEXT,
"ipAddress" TEXT NOT NULL,
"userAgent" TEXT,
"deviceInfo" TEXT,
"location" TEXT,
"timestamp" TIMESTAMP DEFAULT now(),
FOREIGN KEY ("userId") REFERENCES "User"("id")
);
CREATE INDEX idx_loginhistory_userid ON "LoginHistory"("userId");
CREATE INDEX idx_loginhistory_email ON "LoginHistory"("email");
CREATE INDEX idx_loginhistory_timestamp ON "LoginHistory"("timestamp");ALTER TABLE "User" ADD COLUMN "twoFactorEnabled" BOOLEAN DEFAULT false;
ALTER TABLE "User" ADD COLUMN "twoFactorSecret" TEXT;
ALTER TABLE "User" ADD COLUMN "lastLogin" TIMESTAMP;
ALTER TABLE "User" ADD COLUMN "lastLoginIp" TEXT;
ALTER TABLE "User" ADD COLUMN "failedLoginAttempts" INT DEFAULT 0;
ALTER TABLE "User" ADD COLUMN "accountLockedUntil" TIMESTAMP;- Generate strong JWT secrets (min 32 chars)
- Set NODE_ENV=production
- Enable HTTPS (secure flag = true)
- Configure SameSite cookie policy
- Setup database backups
- Enable login history archiving
- Monitor failed login attempts
- Setup email notifications for account lockouts
- Configure token secret rotation schedule
- Setup monitoring/alerting for token service
- Test token refresh in production
- Test logout from all devices
- Performance test with load (token generation)
- Setup session cleanup job (expired tokens)
Solution:
- Check if refresh token exists in database:
SELECT * FROM "RefreshToken" WHERE token = hash('...') - Verify
isRevoked = false - Check
expiresAt > now() - Ensure HttpOnly cookie is being sent with requests
Solution:
- Account locks for 15 minutes after 5 failed attempts
- Check
failedLoginAttemptsin User table - Clear attempts after successful login
Solution:
- Access tokens continue working until expiry (stateless JWT)
- Only refresh tokens are revoked
- This is intentional - logout revokes ability to get new access tokens
- To invalidate all access tokens immediately, implement token blacklist (optional)
Checklist:
- Is NODE_ENV=production? (secure flag requires HTTPS)
- Is path=/auth correct?
- Is credentials: 'include' in fetch?
- Is withCredentials: true in Axios?
- AUTH_DOCUMENTATION_INDEX.md - Complete documentation index
- THREE_TIER_AUTH_API.md - API endpoint reference
- THREE_TIER_IMPLEMENTATION_GUIDE.md - Complete setup guide
Implementation Complete β
Token storage and duration system fully implemented with dual token architecture, security features, and session management.