Files
igny8/AUTHENTICATION-HOLISTIC-REVAMP.md
IGNY8 VPS (Salman) 5366cc1805 logo out issues fixes
2025-12-15 16:08:47 +00:00

313 lines
11 KiB
Markdown

# Authentication System Holistic Revamp - Complete
## Overview
This document summarizes the comprehensive authentication system overhaul completed to fix random logout issues and improve user experience.
## Problem Statement
Users were experiencing frequent, random logouts due to:
1. **Overly aggressive session validation** - Middleware checking session contamination on every request
2. **Short token expiry** - 15-minute JWT access tokens causing constant re-authentication
3. **False-positive logout triggers** - Session ID mismatches causing immediate logouts
4. **No user control** - No remember me option for extended sessions
## Changes Made
### 1. Backend Token Expiry Settings
**File:** `backend/igny8_core/settings.py`
```python
# BEFORE:
JWT_ACCESS_TOKEN_EXPIRY = timedelta(minutes=15) # Too short!
SESSION_COOKIE_AGE = 86400 # 24 hours
# AFTER:
JWT_ACCESS_TOKEN_EXPIRY = timedelta(hours=1) # Default: 1 hour
JWT_ACCESS_TOKEN_EXPIRY_REMEMBER_ME = timedelta(days=20) # Remember me: 20 days
SESSION_COOKIE_AGE = 3600 # 1 hour (aligned with JWT)
```
**Impact:** Users now have 1 hour before needing to re-authenticate (or 20 days if remember me is checked).
---
### 2. Remember Me Functionality
**Files:**
- `backend/igny8_core/auth/utils.py`
- `backend/igny8_core/auth/serializers.py`
- `backend/igny8_core/auth/urls.py`
**Backend Changes:**
```python
# utils.py - Added remember_me support
def get_access_token_expiry(remember_me=False):
if remember_me:
return timezone.now() + settings.JWT_ACCESS_TOKEN_EXPIRY_REMEMBER_ME
return timezone.now() + settings.JWT_ACCESS_TOKEN_EXPIRY
def generate_access_token(user, account=None, remember_me=False):
expiry = get_access_token_expiry(remember_me=remember_me)
payload = {
'user_id': user.id,
'account_id': account.id if account else None,
'email': user.email,
'exp': int(expiry.timestamp()),
'iat': int(now.timestamp()),
'type': 'access',
'remember_me': remember_me, # NEW: Track remember me in token
}
# ... rest of function
# serializers.py - Added field
class LoginSerializer(serializers.Serializer):
email = serializers.EmailField()
password = serializers.CharField(write_only=True)
remember_me = serializers.BooleanField(required=False, default=False) # NEW
# urls.py - LoginView updated
def post(self, request):
serializer = LoginSerializer(data=request.data)
if serializer.is_valid():
email = serializer.validated_data['email']
password = serializer.validated_data['password']
remember_me = serializer.validated_data.get('remember_me', False) # NEW
# ...
access_token = generate_access_token(user, account, remember_me=remember_me) # NEW
```
**Impact:** Backend now respects remember me checkbox and generates tokens with appropriate expiry.
---
### 3. Frontend Remember Me Integration
**Files:**
- `frontend/src/store/authStore.ts`
- `frontend/src/components/auth/SignInForm.tsx`
**Frontend Changes:**
```typescript
// authStore.ts - Updated login function
login: async (email, password, rememberMe = false) => { // NEW parameter
set({ loading: true });
try {
const API_BASE_URL = import.meta.env.VITE_BACKEND_URL || 'https://api.igny8.com/api';
const response = await fetch(`${API_BASE_URL}/v1/auth/login/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email,
password,
remember_me: rememberMe // NEW: Send to backend
}),
});
// ... rest of function
// SignInForm.tsx - Pass checkbox state to login
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// ...
try {
await login(email, password, isChecked); // NEW: Pass remember me state
// ... rest of function
```
**Impact:** Checkbox now functional - unchecked = 1 hour, checked = 20 days.
---
### 4. Removed Session Contamination Checks
**File:** `backend/igny8_core/auth/middleware.py`
**REMOVED ~50 lines of problematic code:**
```python
# DELETED CODE (lines ~67-116):
# Store account_id and user_id in session for contamination detection
if request.account:
stored_account_id = request.session.get('_account_id')
if stored_account_id and stored_account_id != request.account.id:
return self._deny_request(request, 'Session contamination...', 401)
request.session['_account_id'] = request.account.id
stored_user_id = request.session.get('_user_id')
if stored_user_id and stored_user_id != request.user.id:
return self._deny_request(request, 'Session contamination...', 401)
request.session['_user_id'] = request.user.id
```
**Replaced with simple comment:**
```python
# REMOVED: Session contamination checks on every request.
# These were causing random logouts - session integrity handled by Django
```
**Impact:** PRIMARY FIX - Eliminated false-positive logouts caused by timing issues and race conditions in session ID validation.
---
### 5. Logout Tracking System (Already Implemented)
**Files:**
- `backend/igny8_core/auth/middleware.py`
- `frontend/src/services/api.ts`
- `frontend/src/store/authStore.ts`
- `frontend/src/components/auth/SignInForm.tsx`
**Features:**
- Backend sends structured logout reasons with error codes
- Frontend captures and stores logout reasons before clearing auth state
- SignIn page displays yellow alert with logout reason and expandable technical details
- Prevents duplicate logging (React StrictMode handling)
- Automatic vs manual logout prioritization
**Impact:** When logouts do occur, users now see clear explanations.
---
## Authentication Flow Summary
### Login Flow (Now)
1. User enters credentials + checks "Keep me logged in" (optional)
2. Frontend sends `{ email, password, remember_me: true/false }` to `/api/v1/auth/login/`
3. Backend validates credentials
4. Backend generates access token:
- `remember_me=false`: Expires in 1 hour
- `remember_me=true`: Expires in 20 days
5. Frontend stores token in localStorage + Zustand store
6. User redirected to destination
### Request Authentication Flow (Now)
1. Frontend sends request with `Authorization: Bearer <token>` header
2. DRF authentication classes try in order:
- APIKeyAuthentication (for WordPress integration)
- JWTAuthentication (checks token validity + expiry)
- SessionAuthentication (fallback to session cookies)
- BasicAuthentication (last resort)
3. **Middleware NO LONGER runs session contamination checks**
4. Middleware validates account/plan status (for non-superusers)
5. Request.account set for multi-tenancy filtering
### Token Refresh Flow (Unchanged)
1. 401 received from backend
2. Frontend checks for `logout_reason` in response
3. If no logout_reason, attempts token refresh using refresh token
4. If refresh succeeds, retries original request
5. If refresh fails, logs out user
---
## Validation Bypasses
The following users are **exempt** from account/plan validation:
1. **Superusers** (`is_superuser=True`)
2. **Developers** (`role='developer'`)
3. **System Account Users** (`is_system_account_user()` returns True)
This prevents admin users from being logged out due to account/plan issues.
---
## Testing Checklist
### Manual Testing Required
1. ✅ Login without remember me → verify 1 hour expiry
2. ✅ Login with remember me → verify 20 day expiry
3. ✅ Verify no random logouts during normal usage
4. ✅ Check browser dev tools: token payload includes `remember_me` field
5. ✅ Test token refresh on 401 (without backend logout_reason)
6. ✅ Test account/plan validation logout (with logout_reason display)
7. ✅ Verify superuser can access without account/plan
8. ✅ Test cross-account switching (if applicable)
### Technical Verification
```bash
# 1. Check token expiry in Django shell (requires Docker or venv)
python manage.py shell -c "
from igny8_core.auth.utils import get_access_token_expiry
print('Default:', get_access_token_expiry(remember_me=False))
print('Remember me:', get_access_token_expiry(remember_me=True))
"
# 2. Check token payload in browser console after login
localStorage.getItem('auth-storage')
// Look for token field, decode JWT at jwt.io to see payload
# 3. Monitor middleware logs for logout events
docker-compose logs -f backend | grep "AUTO-LOGOUT"
```
---
## Files Modified
### Backend
1. `backend/igny8_core/settings.py` - Token expiry settings
2. `backend/igny8_core/auth/utils.py` - Remember me support in token generation
3. `backend/igny8_core/auth/serializers.py` - Added remember_me field
4. `backend/igny8_core/auth/urls.py` - LoginView updated to handle remember_me
5. `backend/igny8_core/auth/middleware.py` - Removed session contamination checks
### Frontend
1. `frontend/src/store/authStore.ts` - Login function accepts rememberMe parameter
2. `frontend/src/components/auth/SignInForm.tsx` - Passes checkbox state to login
3. `frontend/src/services/api.ts` - Already updated (logout tracking)
---
## Expected Results
### Before Revamp
- ❌ Users logged out every 15 minutes
- ❌ Random logouts due to session contamination false positives
- ❌ No user control over session length
- ❌ No visibility into logout causes
### After Revamp
- ✅ Users stay logged in for 1 hour (or 20 days with remember me)
- ✅ No more false-positive logouts from session validation
- ✅ User control via "Keep me logged in" checkbox
- ✅ Clear visibility when legitimate logouts occur (account issues, etc.)
---
## Migration Notes
### Existing Users
- Users logged in before this update will continue to use old 15-minute tokens until they expire
- After expiry, they'll get new 1-hour tokens on next login
- No database migrations required (settings-only change)
### Deployment
1. Deploy backend changes first (settings + utils + serializers + urls + middleware)
2. Deploy frontend changes (authStore + SignInForm)
3. Clear any cached tokens (optional - they'll expire naturally)
### Monitoring
- Watch for any increase in 401 errors (shouldn't happen - tokens more permissive now)
- Monitor `AUTO-LOGOUT` logs for legitimate account/plan issues
- Check user feedback for remaining logout issues (should be drastically reduced)
---
## Remaining Work (Optional Enhancements)
### Short-term
1. ❓ Check for cross-account AWS access contamination (user mentioned in feedback)
2. ❓ Audit all API endpoints for unnecessary authentication checks
3. ❓ Review ProtectedRoute component for excessive re-validation
### Long-term
1. Session cookie age could be dynamic based on remember_me (currently static 1 hour)
2. Consider "remember this device" feature with device fingerprinting
3. Add user preference for default remember me state
4. Implement "Sign out all devices" functionality
---
## Conclusion
The authentication system has been fundamentally improved by:
1. **Removing the root cause** of random logouts (session contamination checks)
2. **Extending token lifetimes** to reduce authentication friction (1 hour default, 20 days optional)
3. **Giving users control** via remember me checkbox
4. **Maintaining visibility** via logout tracking system
Users should now experience **drastically fewer random logouts** while retaining security through proper token expiration and validation.