1335 lines
31 KiB
Markdown

# 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