# 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 ```json { "error": "Unauthorized", "message": "Invalid credentials" } ``` Same message for: - User not found - Wrong password - User suspended This is intentional (prevents user enumeration). #### Common Causes 1. **Wrong password** - Password incorrect 2. **User doesn't exist** - Email not registered 3. **Typo in email** - Email address wrong 4. **Account suspended** - User marked as suspended #### Solutions **Solution 1: Verify user exists** ```bash # 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** ```bash # 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** ```bash # 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** ```bash # 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 ```bash # 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 ```json { "error": "Forbidden", "message": "Account suspended" } ``` User can't log in even with correct credentials. #### Common Causes 1. **Manual suspension** - Admin suspended account 2. **Security violation** - Account flagged for suspicious activity 3. **Terms violation** - Account suspended for policy violation #### Solutions **Solution 1: Check suspension status** ```bash # 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** ```bash # 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: 1. Contact system administrator 2. Provide your email address 3. 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 ```json { "error": "Forbidden", "message": "Email not verified. Please check your email for verification link." } ``` #### Common Causes 1. **Email not verified** - User didn't click verification link 2. **Verification email not received** - Email went to spam 3. **Verification link expired** - Link older than 24 hours #### Solutions **Solution 1: Check verification status** ```bash # 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** ```bash # 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** ```bash # 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: 1. Spam/Junk folder 2. Promotions tab (Gmail) 3. 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 ```json { "error": "Unauthorized", "message": "Token expired" } ``` Or: ``` Error: jwt expired ``` #### Common Causes 1. **Access token expired** - Normal after 15 minutes inactive 2. **Refresh token expired** - After 7 days 3. **System clock skew** - Server/client time difference #### Solutions **Solution 1: Frontend auto-refresh** Frontend automatically refreshes tokens on 401. If this fails: ```javascript // 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** ```bash # 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** ```javascript // 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** ```bash # 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: 1. You'll be redirected to login automatically 2. Log in with email/password 3. 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 ```json { "error": "Unauthorized", "message": "Invalid token" } ``` Or: ``` Error: jwt malformed Error: invalid signature Error: invalid algorithm ``` #### Common Causes 1. **Corrupted token** - LocalStorage corruption 2. **Wrong secret** - JWT_ACCESS_SECRET changed 3. **Tampered token** - Someone modified the token 4. **Format error** - Not a valid JWT #### Solutions **Solution 1: Verify JWT format** Valid JWT has 3 parts separated by dots: ```javascript 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** ```javascript // In browser console localStorage.clear(); // Then log in again ``` **Solution 3: Verify JWT_ACCESS_SECRET** ```bash # 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** ```bash # 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** ```bash # 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 ```json { "error": "Unauthorized", "message": "No token provided" } ``` Or: ```json { "error": "Unauthorized", "message": "Invalid authorization header format" } ``` #### Common Causes 1. **Missing Authorization header** - Header not sent 2. **Wrong header format** - Not "Bearer token" 3. **Token not in localStorage** - User not logged in 4. **API client misconfigured** - axios interceptor not working #### Solutions **Solution 1: Check if logged in** ```javascript // 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** ```bash # 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`: ```typescript // 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** ```bash # 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** ```bash # 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 ```json { "error": "Unauthorized", "message": "Invalid refresh token" } ``` Auto-refresh fails, user logged out. #### Common Causes 1. **Refresh token expired** - Older than 7 days 2. **Token revoked** - User logged out explicitly 3. **Token not in database** - Database was reset 4. **Token rotation** - Token already used (consumed) #### Solutions **Solution 1: Check refresh token in database** ```bash # 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** ```bash # 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: ```bash 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** ```bash # 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 ```json { "error": "Forbidden", "message": "Insufficient permissions" } ``` Or role-specific: ```json { "error": "Forbidden", "message": "Requires one of: SUPER_ADMIN, MAP_ADMIN" } ``` #### Common Causes 1. **Wrong role** - User doesn't have required role 2. **TEMP user** - Temporary user trying to access admin features 3. **Feature disabled** - Feature flag not enabled 4. **Endpoint restricted** - Endpoint requires specific role #### Solutions **Solution 1: Check user role** ```bash # 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** ```bash # 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`): ```typescript // 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** ```bash # 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: ```typescript // TEMP users blocked by requireNonTemp middleware router.get('/my-data', authenticate, requireNonTemp, // Blocks TEMP users controller.getData ); ``` To convert TEMP to USER: ```bash 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 1. **Not enough permissions** - Role too low for feature 2. **Feature flag** - Feature not enabled 3. **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: 1. Contact system administrator 2. Explain why you need the role 3. Wait for approval and role change **Solution 3: Create admin account** For first admin (if none exist): ```bash # 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`: ```bash # Access token (default: 15m) JWT_ACCESS_EXPIRATION=30m # Refresh token (default: 7d) JWT_REFRESH_EXPIRATION=14d ``` Restart API: ```bash docker compose restart api ``` **Solution 2: Implement activity tracking** ```typescript // 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** ```bash # 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** ```bash # 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** ```bash # 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`: ```typescript // 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 ```json { "error": "Bad Request", "message": "Password reset link expired or invalid" } ``` #### Common Causes 1. **Link expired** - Older than 24 hours 2. **Already used** - Link can only be used once 3. **Wrong token** - Token doesn't match database #### Solutions **Solution 1: Request new reset link** ```bash # 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** ```bash # 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 1. **Email in spam** - Filtered to spam folder 2. **SMTP issue** - Email sending failed 3. **Wrong email** - Typo in email address 4. **Email delay** - Taking long to deliver #### Solutions **Solution 1: Check spam folder** 1. Check Spam/Junk folder 2. Check Promotions tab (Gmail) 3. Check email filters **Solution 2: Check email logs** ```bash # 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`: ```bash # Open MailHog http://localhost:8025 # All emails appear here instead of being sent ``` **Solution 4: Test SMTP connection** ```bash # 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 ```json { "error": "Too Many Requests", "message": "Too many login attempts. Please try again later." } ``` #### Common Causes 1. **Too many failed logins** - More than 10/minute 2. **Automated attack** - Bot trying to brute-force 3. **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** ```bash # 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)** ```bash # 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`: ```typescript 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: 1. Use VPN 2. Use mobile network 3. 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 ```json { "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)** ```bash # 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: 1. Contact system administrator 2. Verify your identity 3. 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 ```javascript // 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 ```bash # 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 ```bash # 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](auth-issues.md) - This guide - [API Reference](../technical/api-reference.md#authentication) - Auth endpoints - [Security Audit](../../../SECURITY_AUDIT_2025-02-11.md) - Security improvements ### Other Troubleshooting - [Common Errors](common-errors.md) - General errors - [Database Issues](database-issues.md) - Database problems - [Email Issues](email-issues.md) - Email and password reset ### Security Resources - [OWASP Authentication Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html) - [JWT Best Practices](https://tools.ietf.org/html/rfc8725) - [bcrypt](https://github.com/kelektiv/node.bcrypt.js) --- **Last Updated:** February 2026 **Version:** V2.0 **Status:** Complete