31 KiB
Authentication and Authorization Issues
This guide covers authentication (who you are) and authorization (what you can do) problems in Changemaker Lite V2.
Overview
Authentication System
Changemaker Lite V2 uses JWT-based authentication:
- Access tokens - Short-lived (15 minutes), stored in memory
- Refresh tokens - Long-lived (7 days), stored in database
- bcrypt passwords - Hashed with salt (10 rounds)
- Token rotation - New refresh token on each refresh
Authorization System
Role-based access control (RBAC) with 5 roles:
| Role | Level | Permissions |
|---|---|---|
SUPER_ADMIN |
5 | Full access to everything |
INFLUENCE_ADMIN |
4 | Manage campaigns, responses, email queue |
MAP_ADMIN |
3 | Manage locations, cuts, shifts, canvass |
USER |
2 | View public content, canvass (if assigned shift) |
TEMP |
1 | Very limited - shift signup confirmation only |
Security Features
- Password policy - 12+ chars, uppercase, lowercase, digit
- User enumeration prevention - Generic error messages
- Rate limiting - 10 requests/min on auth endpoints
- Refresh token rotation - Atomic transaction prevents race conditions
- Redis authentication - Required password for Redis connection
Login Failures
Invalid Credentials
Severity: 🟡 Medium
Symptoms
{
"error": "Unauthorized",
"message": "Invalid credentials"
}
Same message for:
- User not found
- Wrong password
- User suspended
This is intentional (prevents user enumeration).
Common Causes
- Wrong password - Password incorrect
- User doesn't exist - Email not registered
- Typo in email - Email address wrong
- Account suspended - User marked as suspended
Solutions
Solution 1: Verify user exists
# Check database
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
-c "SELECT email, role FROM \"User\" WHERE email = 'user@example.com';"
# If no result, user doesn't exist
Solution 2: Reset password
# Generate bcrypt hash for new password
docker compose exec api node -e "
const bcrypt = require('bcryptjs');
const hash = bcrypt.hashSync('NewPassword123!', 10);
console.log(hash);
"
# Update password
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
-c "UPDATE \"User\" SET password = '\$2a\$10\$...' WHERE email = 'user@example.com';"
Solution 3: Create missing user
# Via API
curl -X POST http://localhost:4000/api/auth/register \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "SecurePass123!",
"name": "User Name"
}'
# Or via admin UI at /app/users
Solution 4: Check for suspended account
# Check user status
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
-c "SELECT email, role, \"createdAt\" FROM \"User\" WHERE email = 'user@example.com';"
# If suspended field exists and is true, unsuspend:
# (Note: V2 doesn't have suspended field yet, but may be added)
Solution 5: Check password requirements
Password must meet requirements:
- 12+ characters
- At least 1 uppercase letter
- At least 1 lowercase letter
- At least 1 digit
# Valid examples:
SecurePass123!
MyP@ssword99
Admin12345678
Prevention
- Password manager - Use password manager to avoid typos
- Password reset flow - Implement forgot password feature
- Clear requirements - Display password requirements on register
- User-friendly errors - Guide users without revealing if email exists
!!! warning "User Enumeration" The same error message for "user not found" and "wrong password" is intentional security behavior to prevent attackers from discovering valid email addresses.
Account Suspended
Severity: 🟠 High
Symptoms
{
"error": "Forbidden",
"message": "Account suspended"
}
User can't log in even with correct credentials.
Common Causes
- Manual suspension - Admin suspended account
- Security violation - Account flagged for suspicious activity
- Terms violation - Account suspended for policy violation
Solutions
Solution 1: Check suspension status
# Check if user has suspended flag (if implemented)
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
-c "SELECT email, role FROM \"User\" WHERE email = 'user@example.com';"
Solution 2: Unsuspend account
# If suspension field exists:
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
-c "UPDATE \"User\" SET suspended = false WHERE email = 'user@example.com';"
# Or delete user and recreate
Solution 3: Contact administrator
If you're a user:
- Contact system administrator
- Provide your email address
- Wait for account review
Prevention
- Suspension policy - Clear policy on suspension reasons
- Appeal process - Allow users to appeal suspensions
- Audit trail - Log suspension reasons and who suspended
!!! note "V2 Status" V2 doesn't currently have a suspended field. This section is for future implementation or if added via custom migration.
Email Not Verified
Severity: 🟢 Low
Symptoms
{
"error": "Forbidden",
"message": "Email not verified. Please check your email for verification link."
}
Common Causes
- Email not verified - User didn't click verification link
- Verification email not received - Email went to spam
- Verification link expired - Link older than 24 hours
Solutions
Solution 1: Check verification status
# Check if emailVerified field exists
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
-c "SELECT email, \"createdAt\" FROM \"User\" WHERE email = 'user@example.com';"
Solution 2: Manually verify email
# Mark email as verified
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
-c "UPDATE \"User\" SET \"emailVerified\" = true WHERE email = 'user@example.com';"
Solution 3: Resend verification email
# Via API (if endpoint exists)
curl -X POST http://localhost:4000/api/auth/resend-verification \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com"}'
Solution 4: Check spam folder
Verification emails may be marked as spam. Check:
- Spam/Junk folder
- Promotions tab (Gmail)
- Email filters
Prevention
- Clear instructions - Tell users to check spam
- From address - Use recognizable from address
- SPF/DKIM/DMARC - Configure email authentication
- Resend option - Easy way to resend verification
!!! note "V2 Status" V2 doesn't currently require email verification for login. This section is for future implementation.
Token Issues
Token Expired
Severity: 🟡 Medium
Symptoms
{
"error": "Unauthorized",
"message": "Token expired"
}
Or:
Error: jwt expired
Common Causes
- Access token expired - Normal after 15 minutes inactive
- Refresh token expired - After 7 days
- System clock skew - Server/client time difference
Solutions
Solution 1: Frontend auto-refresh
Frontend automatically refreshes tokens on 401. If this fails:
// Check refresh token in localStorage
const storage = JSON.parse(localStorage.getItem('auth-storage'));
console.log('Has refresh token:', !!storage?.state?.refreshToken);
// If missing, need to log in again
Solution 2: Manual refresh
# Refresh token via API
curl -X POST http://localhost:4000/api/auth/refresh \
-H "Content-Type: application/json" \
-d '{"refreshToken": "YOUR_REFRESH_TOKEN"}'
# Returns new access + refresh tokens
Solution 3: Check token expiration
// Decode JWT to see expiration
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...';
const payload = JSON.parse(atob(token.split('.')[1]));
console.log('Expires:', new Date(payload.exp * 1000));
console.log('Now:', new Date());
Solution 4: Check system time
# On server
docker compose exec api date
# Should match actual time
# If not, sync clock:
sudo ntpdate -s time.nist.gov
Solution 5: Log in again
If refresh token also expired:
- You'll be redirected to login automatically
- Log in with email/password
- New tokens issued
Prevention
- Sliding sessions - Auto-refresh extends session
- Long refresh window - 7-day refresh token validity
- Activity tracking - Keep track of last activity
- Clock sync - Keep server time accurate
Invalid Token
Severity: 🟠 High
Symptoms
{
"error": "Unauthorized",
"message": "Invalid token"
}
Or:
Error: jwt malformed
Error: invalid signature
Error: invalid algorithm
Common Causes
- Corrupted token - LocalStorage corruption
- Wrong secret - JWT_ACCESS_SECRET changed
- Tampered token - Someone modified the token
- Format error - Not a valid JWT
Solutions
Solution 1: Verify JWT format
Valid JWT has 3 parts separated by dots:
const token = 'header.payload.signature';
console.log(token.split('.').length); // Should be 3
// Example valid token:
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
// eyJpZCI6IjEyMyIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsInJvbGUiOiJVU0VSIiwiaWF0IjoxNzA4MDAwMDAwLCJleHAiOjE3MDgwMDA5MDB9.
// signature-here
Solution 2: Clear localStorage and re-login
// In browser console
localStorage.clear();
// Then log in again
Solution 3: Verify JWT_ACCESS_SECRET
# Check API .env
docker compose exec api sh -c 'echo $JWT_ACCESS_SECRET'
# Should be:
# - At least 32 characters
# - Never changed (changing invalidates all tokens)
# - Different from JWT_REFRESH_SECRET
Solution 4: Test token verification
# Test token via API
curl http://localhost:4000/api/auth/me \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
# If returns user, token is valid
# If 401, token is invalid
Solution 5: Check API logs
# View token validation errors
docker compose logs api | grep -i "jwt\|token"
# Common errors:
# - "jwt malformed" - not a valid JWT format
# - "invalid signature" - wrong secret or tampered
# - "invalid algorithm" - algorithm mismatch
Prevention
- Secure secrets - Use strong, random secrets
- Never change secrets - Changing logs out all users
- Don't expose secrets - Never commit to git
- Token validation - Validate before using
Token Not Found in Header
Severity: 🟡 Medium
Symptoms
{
"error": "Unauthorized",
"message": "No token provided"
}
Or:
{
"error": "Unauthorized",
"message": "Invalid authorization header format"
}
Common Causes
- Missing Authorization header - Header not sent
- Wrong header format - Not "Bearer token"
- Token not in localStorage - User not logged in
- API client misconfigured - axios interceptor not working
Solutions
Solution 1: Check if logged in
// In browser console
const storage = JSON.parse(localStorage.getItem('auth-storage'));
console.log('Access token:', storage?.state?.accessToken);
// If null, not logged in
Solution 2: Verify header format
# Correct format
curl http://localhost:4000/api/users \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
# Wrong formats:
# -H "Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." # Missing "Bearer"
# -H "Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." # Wrong header name
# -H "Authorization: Token eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." # Wrong prefix
Solution 3: Check axios interceptor
In admin/src/lib/api.ts:
// Request interceptor should add token
api.interceptors.request.use((config) => {
const storage = JSON.parse(localStorage.getItem('auth-storage') || '{}');
const token = storage?.state?.accessToken;
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
Solution 4: Test with curl
# Get token from localStorage (browser console)
const token = JSON.parse(localStorage.getItem('auth-storage')).state.accessToken;
console.log(token);
# Test with curl
curl http://localhost:4000/api/users \
-H "Authorization: Bearer [paste-token-here]"
Solution 5: Log in again
# If all else fails, log out and log in
localStorage.clear();
# Navigate to /login
Prevention
- axios interceptor - Automatically add token to requests
- Error handling - Redirect to login on 401
- Token persistence - Store token in localStorage
- Testing - Test auth flow regularly
Refresh Token Invalid
Severity: 🟠 High
Symptoms
{
"error": "Unauthorized",
"message": "Invalid refresh token"
}
Auto-refresh fails, user logged out.
Common Causes
- Refresh token expired - Older than 7 days
- Token revoked - User logged out explicitly
- Token not in database - Database was reset
- Token rotation - Token already used (consumed)
Solutions
Solution 1: Check refresh token in database
# Find refresh token
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
-c "SELECT id, \"userId\", \"expiresAt\", \"createdAt\" FROM \"RefreshToken\"
WHERE token = 'YOUR_REFRESH_TOKEN_HASH';"
# If no result, token doesn't exist (revoked or expired)
Solution 2: Check expiration
# Find all refresh tokens for user
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
-c "SELECT id, \"expiresAt\", \"createdAt\" FROM \"RefreshToken\"
WHERE \"userId\" = 'USER_UUID'
ORDER BY \"createdAt\" DESC;"
# Check if expiresAt < NOW()
Solution 3: Log in again
Refresh token can't be renewed. Must log in with email/password:
curl -X POST http://localhost:4000/api/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "SecurePass123!"
}'
Solution 4: Clear old refresh tokens
# Delete expired refresh tokens
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
-c "DELETE FROM \"RefreshToken\" WHERE \"expiresAt\" < NOW();"
# Or delete all refresh tokens for user (logs out all devices)
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
-c "DELETE FROM \"RefreshToken\" WHERE \"userId\" = 'USER_UUID';"
Prevention
- Long expiration - 7-day refresh token validity
- Token rotation - New refresh token on each refresh
- Cleanup job - Delete expired tokens periodically
- Multi-device support - Multiple refresh tokens per user
Permission Errors
Insufficient Permissions
Severity: 🟡 Medium
Symptoms
{
"error": "Forbidden",
"message": "Insufficient permissions"
}
Or role-specific:
{
"error": "Forbidden",
"message": "Requires one of: SUPER_ADMIN, MAP_ADMIN"
}
Common Causes
- Wrong role - User doesn't have required role
- TEMP user - Temporary user trying to access admin features
- Feature disabled - Feature flag not enabled
- Endpoint restricted - Endpoint requires specific role
Solutions
Solution 1: Check user role
# View user role
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
-c "SELECT email, role FROM \"User\" WHERE email = 'user@example.com';"
# Roles:
# SUPER_ADMIN - full access
# INFLUENCE_ADMIN - campaigns/responses
# MAP_ADMIN - locations/cuts/shifts
# USER - public content + canvass
# TEMP - very limited
Solution 2: Update user role
# Via Prisma Studio (recommended)
docker compose exec api npx prisma studio
# Navigate to User table, edit role field
# Or via SQL
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
-c "UPDATE \"User\" SET role = 'MAP_ADMIN' WHERE email = 'user@example.com';"
Solution 3: Check endpoint requirements
In API code (api/src/modules/*/routes.ts):
// Example from campaigns.routes.ts
router.post('/',
authenticate, // Must be logged in
requireRole('SUPER_ADMIN', 'INFLUENCE_ADMIN'), // Must be admin
validate(createCampaignSchema),
campaignsController.create
);
Solution 4: Verify feature flags
# Check .env for feature flags
cat .env | grep ENABLE
# Example:
ENABLE_MEDIA_FEATURES=true
LISTMONK_SYNC_ENABLED=true
Solution 5: Check TEMP user restrictions
TEMP users are created during shift signup and have very limited permissions:
// TEMP users blocked by requireNonTemp middleware
router.get('/my-data',
authenticate,
requireNonTemp, // Blocks TEMP users
controller.getData
);
To convert TEMP to USER:
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
-c "UPDATE \"User\" SET role = 'USER' WHERE email = 'temp@example.com';"
Prevention
- Clear role descriptions - Document what each role can do
- Role matrix - Table showing role → permission mapping
- Upgrade flow - Easy way for users to upgrade from TEMP to USER
- Helpful errors - Show which role is required
Role Restrictions
Severity: 🟢 Low
Symptoms
User logged in but can't access certain features.
Common Causes
- Not enough permissions - Role too low for feature
- Feature flag - Feature not enabled
- TEMP user - Temporary account with restrictions
Solutions
Solution 1: View role permissions
| Feature | SUPER_ADMIN | INFLUENCE_ADMIN | MAP_ADMIN | USER | TEMP |
|---|---|---|---|---|---|
| User management | ✅ | ❌ | ❌ | ❌ | ❌ |
| Settings | ✅ | ❌ | ❌ | ❌ | ❌ |
| Campaigns | ✅ | ✅ | ❌ | ❌ | ❌ |
| Responses | ✅ | ✅ | ❌ | ❌ | ❌ |
| Email queue | ✅ | ✅ | ❌ | ❌ | ❌ |
| Locations | ✅ | ❌ | ✅ | ❌ | ❌ |
| Cuts | ✅ | ❌ | ✅ | ❌ | ❌ |
| Shifts | ✅ | ❌ | ✅ | ❌ | ❌ |
| Canvass dashboard | ✅ | ❌ | ✅ | ❌ | ❌ |
| Public campaigns | ✅ | ✅ | ✅ | ✅ | ❌ |
| Public shifts | ✅ | ✅ | ✅ | ✅ | ✅ |
| Volunteer canvass | ✅ | ✅ | ✅ | ✅ | ❌ |
Solution 2: Request role upgrade
If you need higher permissions:
- Contact system administrator
- Explain why you need the role
- Wait for approval and role change
Solution 3: Create admin account
For first admin (if none exist):
# Connect to database
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2
# Create SUPER_ADMIN
# (Password must be pre-hashed with bcrypt)
INSERT INTO "User" (id, email, password, name, role)
VALUES (
gen_random_uuid(),
'admin@example.com',
'$2a$10$...', -- bcrypt hash of 'Admin123!'
'System Admin',
'SUPER_ADMIN'
);
Prevention
- Document roles - Clear description of each role
- Role request process - Easy way to request role upgrade
- Audit trail - Log role changes
- Principle of least privilege - Give minimum role needed
Session Issues
Session Timeout
Severity: 🟢 Low
Symptoms
User inactive for a while, then gets logged out.
Current Behavior
V2 uses JWT tokens (not sessions):
- Access token expires after 15 minutes
- Refresh token expires after 7 days
- Auto-refresh on API calls extends session
Solutions
Solution 1: Configure token expiration
In .env:
# Access token (default: 15m)
JWT_ACCESS_EXPIRATION=30m
# Refresh token (default: 7d)
JWT_REFRESH_EXPIRATION=14d
Restart API:
docker compose restart api
Solution 2: Implement activity tracking
// In frontend, track last activity
const updateActivity = () => {
localStorage.setItem('lastActivity', Date.now().toString());
};
// On any user action
document.addEventListener('click', updateActivity);
document.addEventListener('keypress', updateActivity);
// Check on load
useEffect(() => {
const lastActivity = parseInt(localStorage.getItem('lastActivity') || '0');
const now = Date.now();
const thirtyMinutes = 30 * 60 * 1000;
if (now - lastActivity > thirtyMinutes) {
// Log out due to inactivity
authStore.logout();
}
}, []);
Prevention
- Sliding sessions - Auto-refresh extends session
- Long refresh window - 7-day default
- Activity tracking - Reset timeout on activity
- Warning before logout - Show countdown before timeout
Multiple Device Conflicts
Severity: 🟢 Low
Symptoms
User logged in on multiple devices, behavior inconsistent.
Current Behavior
V2 supports multiple devices:
- Each login creates new refresh token
- All devices stay logged in independently
- No device limit by default
Solutions
Solution 1: View user's devices
# List all refresh tokens for user
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
-c "SELECT id, \"createdAt\", \"expiresAt\" FROM \"RefreshToken\"
WHERE \"userId\" = 'USER_UUID'
ORDER BY \"createdAt\" DESC;"
# Each row = one device/session
Solution 2: Log out all devices
# Delete all refresh tokens for user
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
-c "DELETE FROM \"RefreshToken\" WHERE \"userId\" = 'USER_UUID';"
# User must log in again on all devices
Solution 3: Log out specific device
# Delete specific refresh token
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
-c "DELETE FROM \"RefreshToken\" WHERE id = 'REFRESH_TOKEN_UUID';"
Solution 4: Implement device limit
In api/src/modules/auth/auth.service.ts:
// After creating refresh token, limit to 5 devices
const userTokens = await prisma.refreshToken.findMany({
where: { userId },
orderBy: { createdAt: 'desc' }
});
// Delete oldest tokens if > 5
if (userTokens.length > 5) {
await prisma.refreshToken.deleteMany({
where: {
id: { in: userTokens.slice(5).map(t => t.id) }
}
});
}
Prevention
- Device management UI - Show logged-in devices
- Device limit - Max 5-10 devices per user
- Device naming - Let users name their devices
- Remote logout - Let users log out other devices
Password Reset Issues
Reset Link Expired
Severity: 🟢 Low
Symptoms
{
"error": "Bad Request",
"message": "Password reset link expired or invalid"
}
Common Causes
- Link expired - Older than 24 hours
- Already used - Link can only be used once
- Wrong token - Token doesn't match database
Solutions
Solution 1: Request new reset link
# Via API (if endpoint exists)
curl -X POST http://localhost:4000/api/auth/forgot-password \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com"}'
Solution 2: Manually reset password
# Generate bcrypt hash
docker compose exec api node -e "
const bcrypt = require('bcryptjs');
console.log(bcrypt.hashSync('NewPassword123!', 10));
"
# Update password
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
-c "UPDATE \"User\" SET password = '\$2a\$10\$...' WHERE email = 'user@example.com';"
Prevention
- Longer expiration - 24-hour expiration is reasonable
- Clear messaging - Tell users link expires
- Easy re-request - Simple way to request new link
!!! note "V2 Status" V2 doesn't currently have password reset flow. This section is for future implementation.
Email Not Received
Severity: 🟡 Medium
Symptoms
User requests password reset but doesn't receive email.
Common Causes
- Email in spam - Filtered to spam folder
- SMTP issue - Email sending failed
- Wrong email - Typo in email address
- Email delay - Taking long to deliver
Solutions
Solution 1: Check spam folder
- Check Spam/Junk folder
- Check Promotions tab (Gmail)
- Check email filters
Solution 2: Check email logs
# API logs show email sending
docker compose logs api | grep -i "email\|smtp"
# Should show:
# Email sent to user@example.com: Password Reset
Solution 3: Check MailHog (dev mode)
If EMAIL_TEST_MODE=true:
# Open MailHog
http://localhost:8025
# All emails appear here instead of being sent
Solution 4: Test SMTP connection
# Test SMTP via API
curl -X POST http://localhost:4000/api/test-email \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"to": "test@example.com",
"subject": "Test",
"text": "Test email"
}'
Solution 5: Manually reset password
See "Manually reset password" in previous section.
Prevention
- Email testing - Test email delivery in production
- Clear from address - Use recognizable sender
- SPF/DKIM/DMARC - Configure email authentication
- Resend option - Easy way to resend email
Rate Limiting
Too Many Login Attempts
Severity: 🟡 Medium
Symptoms
{
"error": "Too Many Requests",
"message": "Too many login attempts. Please try again later."
}
Common Causes
- Too many failed logins - More than 10/minute
- Automated attack - Bot trying to brute-force
- Shared IP - Multiple users behind same NAT
Solutions
Solution 1: Wait and retry
Rate limit is per IP address:
- Limit: 10 requests per minute
- Window: 1 minute
- Action: Wait 1 minute, then try again
Solution 2: Check Redis rate limit
# Connect to Redis
docker compose exec redis redis-cli -a YOUR_REDIS_PASSWORD
# Find rate limit keys
KEYS rl:auth:*
# Check specific IP
GET rl:auth:192.168.1.100
# Shows number of requests in current window
Solution 3: Clear rate limit (admin)
# Delete rate limit key for IP
docker compose exec redis redis-cli -a YOUR_REDIS_PASSWORD DEL rl:auth:192.168.1.100
# Or clear all auth rate limits
docker compose exec redis redis-cli -a YOUR_REDIS_PASSWORD --scan --pattern "rl:auth:*" | xargs docker compose exec redis redis-cli -a YOUR_REDIS_PASSWORD DEL
Solution 4: Adjust rate limit
In api/src/middleware/rate-limit.ts:
export const authRateLimit = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 10, // 10 requests per minute
message: 'Too many login attempts. Please try again later.',
standardHeaders: true,
legacyHeaders: false,
});
// Increase to 20/minute:
max: 20,
Solution 5: Use different IP
If behind NAT with many users:
- Use VPN
- Use mobile network
- Contact administrator to whitelist IP
Prevention
- Reasonable limits - 10/min is reasonable
- Per-account limit - Also limit by email (not just IP)
- CAPTCHA - Add CAPTCHA after 3 failed attempts
- Account lockout - Lock account after 10 failed attempts
Account Temporarily Locked
Severity: 🟡 Medium
Symptoms
{
"error": "Forbidden",
"message": "Account temporarily locked due to too many failed login attempts. Please try again in 30 minutes."
}
Solutions
Solution 1: Wait for unlock
Accounts auto-unlock after lockout period (default: 30 minutes).
Solution 2: Manually unlock (admin)
# If lockout implemented in database:
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
-c "UPDATE \"User\" SET \"lockedUntil\" = NULL WHERE email = 'user@example.com';"
Solution 3: Contact administrator
If you're a user:
- Contact system administrator
- Verify your identity
- Request account unlock
Prevention
- Reasonable threshold - 10 failed attempts is reasonable
- Automatic unlock - Auto-unlock after time period
- Email notification - Notify user of lockout
- Appeal process - Way to appeal false positive
!!! note "V2 Status" V2 doesn't currently have account lockout. This section is for future implementation.
Debugging Auth
Checking JWT Payload
Severity: 🟢 Low (informational)
How to Decode JWT
// In browser console
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEyMyIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsInJvbGUiOiJVU0VSIiwiaWF0IjoxNzA4MDAwMDAwLCJleHAiOjE3MDgwMDA5MDB9.signature';
// Decode header
const header = JSON.parse(atob(token.split('.')[0]));
console.log('Header:', header);
// { alg: 'HS256', typ: 'JWT' }
// Decode payload
const payload = JSON.parse(atob(token.split('.')[1]));
console.log('Payload:', payload);
// {
// id: '123',
// email: 'test@example.com',
// role: 'USER',
// iat: 1708000000, // Issued at (Unix timestamp)
// exp: 1708000900 // Expires at (Unix timestamp)
// }
// Check expiration
console.log('Issued:', new Date(payload.iat * 1000));
console.log('Expires:', new Date(payload.exp * 1000));
console.log('Is expired:', Date.now() > payload.exp * 1000);
Verifying Refresh Tokens
Check Refresh Token in Database
# Find all refresh tokens for user
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
-c "SELECT rt.id, rt.\"createdAt\", rt.\"expiresAt\", u.email
FROM \"RefreshToken\" rt
JOIN \"User\" u ON rt.\"userId\" = u.id
WHERE u.email = 'user@example.com'
ORDER BY rt.\"createdAt\" DESC;"
# Output:
# id | createdAt | expiresAt | email
# uuid-here | 2026-02-10 10:00:00 | 2026-02-17 10:00:00 | user@example.com
# Check if expired:
# SELECT id FROM "RefreshToken" WHERE id = 'uuid' AND "expiresAt" > NOW();
Checking User Status
View User Details
# Full user details
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
-c "SELECT id, email, name, role, \"createdAt\", \"updatedAt\"
FROM \"User\"
WHERE email = 'user@example.com';"
# Check active sessions
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
-c "SELECT COUNT(*) as active_sessions
FROM \"RefreshToken\" rt
JOIN \"User\" u ON rt.\"userId\" = u.id
WHERE u.email = 'user@example.com'
AND rt.\"expiresAt\" > NOW();"
Related Documentation
Authentication Documentation
- Auth Issues - This guide
- API Reference - Auth endpoints
- Security Audit - Security improvements
Other Troubleshooting
- Common Errors - General errors
- Database Issues - Database problems
- Email Issues - Email and password reset
Security Resources
Last Updated: February 2026 Version: V2.0 Status: Complete