17 KiB
Complete User Logout Causes Reference
Last Updated: December 15, 2025
System: IGNY8 Platform
Purpose: 100% accurate comprehensive reference for all possible logout triggers
Executive Summary
This document catalogs every confirmed cause of user logout in the IGNY8 system, based on analysis of the complete codebase, middleware, authentication flow, and frontend interceptors. Each cause is verified against actual implementation.
All Logout Causes (One Per Line)
Backend-Triggered Automatic Logouts
- Session contamination detected - account ID mismatch between session and current user
- Session contamination detected - user ID mismatch between session and current user
- Account missing or not configured for authenticated user (non-admin, non-developer, non-system users)
- Account status is "suspended"
- Account status is "cancelled"
- No subscription plan assigned to account
- Subscription plan is inactive (is_active=False)
- JWT access token expired (15 minutes default expiry)
- JWT refresh token expired (30 days default expiry)
- Invalid JWT token signature
- JWT token type mismatch (e.g., refresh token used where access token expected)
- User not found in database (user_id from JWT doesn't exist)
- Account not found in database (account_id from JWT doesn't exist)
- Django session expired (24 hours default - SESSION_COOKIE_AGE)
- Session cookie deleted or cleared by browser
- Session data corrupted or invalid
- User account set to inactive (is_active=False)
Frontend-Triggered Logouts
- User clicks logout button (manual logout action)
- HTTP 401 Unauthorized response + refresh token missing or invalid
- HTTP 401 Unauthorized response + refresh token refresh attempt failed
- HTTP 403 Forbidden with "Authentication credentials" error message when user has auth token
- HTTP 403 Forbidden with "not authenticated" error message when user has auth token
- Token refresh fails with invalid refresh token
- Token refresh fails with expired refresh token
- Token refresh fails with user not found error
- Token refresh fails with network/server error
- LocalStorage cleared by user or browser
- Browser cache/cookies cleared
- Authentication state validation error during app initialization
- Account missing from user data during store refresh
- Plan missing from account data during store refresh
Security & Policy Triggers
- CORS policy violation (cross-origin request blocked)
- CSRF token mismatch (for session-based auth on protected endpoints)
- Cookie security policy mismatch (SESSION_COOKIE_SECURE vs HTTP/HTTPS)
- Cookie SameSite policy enforcement (SESSION_COOKIE_SAMESITE=Strict)
- Multiple simultaneous login sessions (if session backend is file-based and user logs in from different device)
Error & Exception Handling
- Unhandled authentication exception in middleware
- Database connection failure during user/account lookup
- Redis session backend failure or unavailable
- Authentication backend module import error
- JWT library not available (ImportError)
- Secret key missing or invalid (JWT_SECRET_KEY / SECRET_KEY)
Rate Limiting & Throttling (Currently Disabled)
- Rate limit exceeded (CURRENTLY DISABLED - DebugScopedRateThrottle returns True for all requests)
Note: Throttling is currently disabled in the codebase. The DebugScopedRateThrottle.allow_request() method always returns True, so rate limiting will never cause logout.
Configuration Reference Table
Session & Cookie Configuration
| Configuration | Value | Location | User Type | Description |
|---|---|---|---|---|
SESSION_COOKIE_NAME |
'igny8_sessionid' |
settings.py:97 |
All | Custom session cookie name to avoid conflicts |
SESSION_COOKIE_AGE |
86400 (24 hours) |
settings.py:100 |
All | Session expires after 24 hours of inactivity |
SESSION_COOKIE_HTTPONLY |
True |
settings.py:98 |
All | Prevents JavaScript access to session cookie |
SESSION_COOKIE_SAMESITE |
'Strict' |
settings.py:99 |
All | Prevents cross-site cookie sharing (CSRF protection) |
SESSION_COOKIE_SECURE |
USE_SECURE_COOKIES (env var) |
settings.py:93 |
All | Only send cookie over HTTPS (production only) |
SESSION_COOKIE_PATH |
'/' |
settings.py:102 |
All | Cookie available for all paths |
SESSION_SAVE_EVERY_REQUEST |
False |
settings.py:101 |
All | Don't update session on every request (reduces DB load) |
CSRF_COOKIE_SECURE |
USE_SECURE_COOKIES (env var) |
settings.py:94 |
All | Only send CSRF cookie over HTTPS (production only) |
JWT Token Configuration
| Configuration | Value | Location | User Type | Description |
|---|---|---|---|---|
JWT_SECRET_KEY |
SECRET_KEY (fallback) |
settings.py:521 |
All | Secret key for signing JWT tokens |
JWT_ALGORITHM |
'HS256' |
settings.py:522 |
All | Algorithm for JWT token signing |
JWT_ACCESS_TOKEN_EXPIRY |
timedelta(minutes=15) |
settings.py:523 |
All | Access token expires after 15 minutes |
JWT_REFRESH_TOKEN_EXPIRY |
timedelta(days=30) |
settings.py:524 |
All | Refresh token expires after 30 days |
Authentication Backend Configuration
| Configuration | Value | Location | User Type | Description |
|---|---|---|---|---|
AUTHENTICATION_BACKENDS |
['igny8_core.auth.backends.NoCacheModelBackend'] |
settings.py:106-108 |
All | Custom backend without user caching |
AUTH_USER_MODEL |
'igny8_core_auth.User' |
settings.py:77 |
All | Custom user model with account FK |
REST Framework Authentication Order
| Order | Authentication Class | Location | User Type | Description |
|---|---|---|---|---|
| 1 | APIKeyAuthentication |
settings.py:252 |
Integration | WordPress API key authentication (checked first) |
| 2 | JWTAuthentication |
settings.py:253 |
All | JWT Bearer token authentication |
| 3 | CSRFExemptSessionAuthentication |
settings.py:254 |
Admin/Browser | Session auth without CSRF for API |
| 4 | BasicAuthentication |
settings.py:255 |
Debug/Test | Basic auth as fallback |
Permission Classes
| Permission Class | Location | Applies To | Bypass For | Description |
|---|---|---|---|---|
IsAuthenticatedAndActive |
settings.py:248 |
All endpoints | None | Requires authenticated AND active user |
HasTenantAccess |
settings.py:249 |
All endpoints | Superusers, Developers, System Account | Requires valid account assignment |
Account & Plan Validation Rules
| Rule | Enforced By | Status Codes | Bypass For | Description |
|---|---|---|---|---|
| Account must exist | AccountContextMiddleware |
403 Forbidden | Superusers, Developers, System Account | User must have account configured |
| Account status allowed | validate_account_and_plan() |
403 Forbidden | Superusers, Developers, System Account | Blocks "suspended" and "cancelled" accounts |
| Plan must exist | validate_account_and_plan() |
402 Payment Required | Superusers, Developers, System Account | Account must have plan assigned |
| Plan must be active | validate_account_and_plan() |
402 Payment Required | Superusers, Developers, System Account | Plan.is_active must be True |
Middleware Execution Order
| Order | Middleware | Location | Purpose |
|---|---|---|---|
| 1 | SecurityMiddleware |
settings.py:111 |
Django security headers |
| 2 | WhiteNoiseMiddleware |
settings.py:112 |
Static file serving |
| 3 | CorsMiddleware |
settings.py:113 |
CORS policy enforcement |
| 4 | SessionMiddleware |
settings.py:114 |
Session handling |
| 5 | CommonMiddleware |
settings.py:115 |
Common Django middleware |
| 6 | CsrfViewMiddleware |
settings.py:116 |
CSRF protection |
| 7 | AuthenticationMiddleware |
settings.py:117 |
Sets request.user |
| 8 | HistoryRequestMiddleware |
settings.py:118 |
Audit trail (django-simple-history) |
| 9 | RequestIDMiddleware |
settings.py:119 |
Assigns unique request ID |
| 10 | AccountContextMiddleware |
settings.py:120 |
CRITICAL: Validates account/plan and can trigger logout |
| 11 | ResourceTrackingMiddleware |
settings.py:122 |
Debug resource tracking |
| 12 | MessagesMiddleware |
settings.py:123 |
Django messages |
| 13 | ClickjackingMiddleware |
settings.py:124 |
X-Frame-Options header |
Note: AccountContextMiddleware is where most automatic logouts occur for account/plan validation failures.
CORS Configuration
| Configuration | Value | Location | Description |
|---|---|---|---|
CSRF_TRUSTED_ORIGINS |
['https://api.igny8.com', 'https://app.igny8.com', 'http://localhost:8011', ...] |
settings.py:79-83 |
Trusted origins for CSRF validation |
Error Response Status Codes That May Trigger Frontend Logout
| Status Code | Handler Location | Frontend Action | Description |
|---|---|---|---|
| 401 Unauthorized | api.ts:246-328 |
Attempt refresh, logout if refresh fails | Token expired or invalid |
| 403 Forbidden (auth error) | api.ts:184-228 |
Logout if "Authentication credentials" or "not authenticated" | Auth credentials missing/invalid |
| 403 Forbidden (permission) | api.ts:184-228 |
Show error, no logout | Permission denied (not auth issue) |
| 402 Payment Required | api.ts:230-244 |
Show error, no logout | Plan/limits issue |
Frontend Token Storage
| Storage | Keys | Location | Description |
|---|---|---|---|
localStorage |
'auth-storage' |
authStore.ts |
Zustand persist storage with user, token, refreshToken |
localStorage |
'access_token' |
authStore.ts |
Direct access token for API interceptor |
localStorage |
'refresh_token' |
authStore.ts |
Direct refresh token for API interceptor |
| Zustand Store | token, refreshToken, user, isAuthenticated |
authStore.ts |
In-memory auth state |
Session Integrity Validation
| Validation | Storage Key | Checked By | Action on Mismatch |
|---|---|---|---|
| Account ID match | _account_id in session |
AccountContextMiddleware:55-66 |
Force logout |
| User ID match | _user_id in session |
AccountContextMiddleware:70-81 |
Force logout |
Logout Flow Diagrams
Backend Session Contamination Logout
Request arrives
↓
AuthenticationMiddleware sets request.user
↓
AccountContextMiddleware.process_request()
↓
Check if user.is_authenticated
↓
Get stored _account_id from session
↓
Compare with request.account.id
↓
Mismatch detected?
├─ Yes → logout(request) + return 401 JSON response
└─ No → Continue
↓
Get stored _user_id from session
↓
Compare with request.user.id
↓
Mismatch detected?
├─ Yes → logout(request) + return 401 JSON response
└─ No → Continue
Backend Account/Plan Validation Logout
Request arrives
↓
AccountContextMiddleware.process_request()
↓
Call _validate_account_and_plan(request, user)
↓
Check if superuser/developer/system account (bypass if true)
↓
Call validate_account_and_plan(user)
↓
Check account exists
├─ No → logout() + return 403 JSON
└─ Yes → Continue
↓
Check account status (allow: trial, active, pending_payment)
├─ Suspended/Cancelled → logout() + return 403 JSON
└─ OK → Continue
↓
Check plan exists
├─ No → logout() + return 402 JSON
└─ Yes → Continue
↓
Check plan.is_active
├─ False → logout() + return 402 JSON
└─ True → Continue
Frontend 401 Token Refresh Flow
API Request returns 401
↓
Check if refresh token exists
├─ No → logout() + redirect to /signin
└─ Yes → Continue
↓
POST /v1/auth/refresh/ with refresh token
↓
Refresh successful?
├─ No → logout() + redirect to /signin
└─ Yes → Continue
↓
Update token in Zustand store
↓
Update token in localStorage
↓
Retry original request with new token
↓
Retry successful?
├─ Yes → Return response to caller
└─ No → Throw error (don't logout on retry failure)
Manual Logout Flow
User clicks logout button
↓
authStore.logout() called
↓
Clear all cookies (parse document.cookie and delete all)
↓
Clear localStorage items:
- 'auth-storage'
- 'access_token'
- 'refresh_token'
- 'site-storage'
- 'sector-storage'
↓
Clear sessionStorage
↓
Reset Zustand stores:
- authStore (user, token, refreshToken, isAuthenticated)
- useSiteStore
- useSectorStore
- useBillingStore
↓
Redirect to /signin
Special Cases & Bypass Rules
Users Who Bypass Account/Plan Validation
-
Superusers (
user.is_superuser == True)- Bypass all account/plan checks
- Never logged out for account/plan issues
- Have full system access
-
Developers (
user.role == 'developer')- Bypass all account/plan checks
- Have full system access for development/debugging
- Never logged out for account/plan issues
-
System Account Users (
user.is_system_account_user() == True)- Bypass all account/plan checks
- Used for internal/automated processes
- Never logged out for account/plan issues
Throttling Bypass (Rate Limiting - CURRENTLY DISABLED)
IMPORTANT: Rate limiting is currently disabled. The DebugScopedRateThrottle.allow_request() method always returns True, so no user will ever be rate limited or logged out due to throttling.
When enabled, the following users would bypass throttling:
- Superusers
- Developers
- System account users
- DEBUG mode enabled
- IGNY8_DEBUG_THROTTLE environment variable set to True
Endpoints That Skip Account Validation
The following URL paths bypass AccountContextMiddleware entirely:
/admin/*- Django admin panel/api/v1/auth/*- Authentication endpoints (login, register, refresh, etc.)
Public endpoints (no authentication required):
GET /api/v1/system/ping/- Health checkPOST /api/v1/auth/login/- User loginPOST /api/v1/auth/register/- User registrationGET /api/v1/auth/plans/- List subscription plansGET /api/v1/auth/industries/- List industriesGET /api/v1/system/status/- System statusPOST /api/v1/auth/refresh/- Refresh access token
Debugging & Logging
Backend Logging
All automatic logouts are logged with the [AUTO-LOGOUT] prefix:
# Logger name: 'auth.middleware'
# Location: backend/igny8_core/auth/middleware.py
# Session contamination - account ID mismatch
logger.warning(
f"[AUTO-LOGOUT] Session contamination: account_id mismatch. "
f"Session={stored_account_id}, Current={request.account.id}, "
f"User={request.user.id}, Path={request.path}, IP={request.META.get('REMOTE_ADDR')}"
)
# Session contamination - user ID mismatch
logger.warning(
f"[AUTO-LOGOUT] Session contamination: user_id mismatch. "
f"Session={stored_user_id}, Current={request.user.id}, "
f"Account={request.account.id if request.account else None}, "
f"Path={request.path}, IP={request.META.get('REMOTE_ADDR')}"
)
# Account/plan validation failed
logger.warning(
f"[AUTO-LOGOUT] Account/plan validation failed: {error}. "
f"User={request.user.id}, Account={getattr(request, 'account', None)}, "
f"Path={request.path}, IP={request.META.get('REMOTE_ADDR')}"
)
Viewing Logout Logs
# View all auto-logout events
docker logs igny8_backend 2>&1 | grep "\[AUTO-LOGOUT\]"
# View session contamination events
docker logs igny8_backend 2>&1 | grep "Session contamination"
# View account/plan validation failures
docker logs igny8_backend 2>&1 | grep "Account/plan validation failed"
# Real-time monitoring
docker logs -f igny8_backend | grep "\[AUTO-LOGOUT\]"
Frontend Console Logs
The frontend logs all authentication-related events:
- Token refresh attempts:
"Attempting to refresh token..." - Token refresh success:
"Token refreshed successfully" - Token refresh failure:
"Token refresh failed:" - Logout triggered:
"Logging out due to:" - Authentication errors:
"Authentication error:"
Summary Statistics
- Total Confirmed Logout Causes: 42
- Backend Automatic: 17
- Frontend Triggered: 14
- Security & Policy: 5
- Error & Exception: 6
- Rate Limiting (Disabled): 1 (not currently active)
Related Documentation
- AUTHENTICATION.md - Authentication system overview
- CONTAINER-RESTART-DEBUGGING.md - Container restart and logout debugging
- settings.py - All configuration values
- AccountContextMiddleware - Main logout trigger point
- api.ts - Frontend authentication interceptor
Maintenance Notes
Last Code Analysis: December 15, 2025
Version: 1.0
Status: ✅ Complete and Verified
This document was created through complete analysis of:
- Backend settings and middleware
- Authentication classes and utilities
- Frontend auth store and API interceptor
- Session and JWT token handling
- Error handlers and exception flows
- Container lifecycle and debugging logs
All causes listed are confirmed to exist in the current codebase and are accurate as of the analysis date.