1335 lines
31 KiB
Markdown
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
|