π Web users must be able to continue working even if Keycloak is unreachable
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β AUTHENTICATION LAYERS β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Layer 1: LOCAL AUTH (sys_user) βββββββΊ ALWAYS WORKS β
β β PRIMARY authentication β
β β Never depends on external services β
β β Passwords encrypted in database β
β β
β Layer 2: KEYCLOAK ENHANCEMENT ββββββββΊ BEST EFFORT β
β β SSO across applications β
β β Centralized user management β
β β LDAP integration for VMs β
β β OAuth/OIDC for third-party β
β β
β Layer 3: TOKEN CACHE ββββββββββββββββββΊ GRACE PERIOD β
β β Cached Keycloak tokens β
β β Valid for 24 hours β
β β Allows continued operation β
β β
β Layer 4: SYNC QUEUE βββββββββββββββββββΊ EVENTUAL SYNC β
β β Queue password changes β
β β Queue user updates β
β β Sync when Keycloak available β
β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
| State | Description | User Experience | Keycloak Status |
|---|---|---|---|
| NORMAL | Keycloak available | Full SSO, immediate sync | β Online |
| DEGRADED | Keycloak down, cache valid | SSO with cached tokens | β οΈ Offline (cached) |
| OFFLINE | Keycloak down, cache expired | Local auth only | β Offline (no cache) |
keycloak:
resilience:
enabled: true
failure_threshold: 3
cooldown_seconds: 60
token_grace_period_hours: 24
allow_offline_mode: true
def Login(username, password, package):
# STEP 1: Local auth (PRIMARY - always works)
if not authenticate_local(username, password):
return {'success': False, 'reason': 'invalid_credentials'}
# STEP 2: Keycloak enhancement (OPTIONAL - best effort)
keycloak_token = None
if keycloak.is_available():
try:
keycloak_token = keycloak.authenticate(username, password)
auth_mode = 'keycloak' if keycloak_token else 'local'
except KeycloakException:
auth_mode = 'local' # Fallback
else:
# Check cache
cached_token = keycloak.get_cached_token(username, package)
auth_mode = 'cached' if cached_token else 'local'
# STEP 3: Create session (always succeeds)
session_id = create_session(username, package, auth_mode)
return {
'success': True,
'session_id': session_id,
'auth_mode': auth_mode,
'keycloak_status': 'online' if keycloak.is_available() else 'offline'
}
def ChangePassword(username, old_password, new_password, package):
# STEP 1: Verify old password locally
if not verify_local_password(username, old_password):
return {'success': False}
# STEP 2: Update local password (ALWAYS)
update_local_password(username, new_password)
result = {'local_updated': True}
# STEP 3: Update Keycloak (BEST EFFORT)
if keycloak.is_available():
try:
keycloak.update_password(username, new_password)
result['keycloak_updated'] = True
result['mode'] = 'synchronized'
except KeycloakException:
# Queue for later sync
keycloak.queue_sync(username, package, 'password_change', new_password)
result['keycloak_updated'] = False
result['mode'] = 'queued'
else:
# Queue for later sync
keycloak.queue_sync(username, package, 'password_change', new_password)
result['mode'] = 'queued'
return {'success': True, **result}
{
'keycloak_status': {
'available': True/False,
'circuit_state': 'CLOSED'/'OPEN',
'last_check': datetime,
'mode': 'normal'/'degraded'/'offline'
},
'active_sessions': 142,
'cached_tokens': 38,
'pending_syncs': 5,
'failed_syncs': 0,
'auth_modes_last_hour': {
'keycloak': 95,
'local': 12,
'cached': 8
}
}
Keycloak: β
Online
Result: Full SSO, immediate sync
Auth Mode: keycloak
Keycloak: β Offline
Result: Local auth, queue sync
Auth Mode: local
User Experience: No impact, can login and work
Keycloak: β Offline (was online)
Result: Session continues with cached token
Auth Mode: cached
User Experience: No impact, session remains valid
Keycloak: β
Online (was offline)
Result: Sync queue processed, normal operation resumes
Auth Mode: keycloak
User Experience: Seamless, may not notice recovery
Keycloak: β Offline
User Action: Change password
Result: Password updated locally, queued for sync
User Experience: Password works immediately, will sync later
keycloak:
resilience:
failure_threshold: 1 # Fast fallback
cooldown_seconds: 30 # Quick retry
token_grace_period_hours: 48 # Long grace
keycloak:
resilience:
failure_threshold: 3
cooldown_seconds: 60
token_grace_period_hours: 24
keycloak:
resilience:
failure_threshold: 5 # Try harder
cooldown_seconds: 120 # Longer cooldown
token_grace_period_hours: 12 # Shorter grace
objuser_keycloak_resilience_design.md (28 KB)
objuser_keycloak_quick_implementation.md (22 KB)
KEYCLOAK_INTEGRATION_SUMMARY.md (This file)
-- Create token cache
CREATE TABLE sys_keycloak_token_cache (
User varchar(150) NOT NULL,
Package varchar(100) NOT NULL,
AccessToken text NOT NULL,
RefreshToken text NULL,
ExpiresAt datetime NOT NULL,
CachedAt datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (User, Package)
);
-- Create sync queue
CREATE TABLE sys_keycloak_sync_queue (
SyncId char(36) NOT NULL DEFAULT (UUID()),
User varchar(150) NOT NULL,
Package varchar(100) NOT NULL,
SyncType varchar(50) NOT NULL,
SyncData text NULL,
QueuedAt datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
Status varchar(20) NOT NULL DEFAULT 'pending',
PRIMARY KEY (SyncId)
);
from factory.web.ObjUser import User
user = User()
result = user.Login("test@example.com", "password", "homechoice")
print(f"Success: {result['success']}")
print(f"Auth mode: {result['auth_mode']}")
print(f"Keycloak status: {result['keycloak_status']}")
from ObjKeycloakResilient import ObjKeycloakResilient
keycloak = ObjKeycloakResilient()
status = keycloak.get_status()
print(f"Available: {status['available']}")
print(f"Circuit: {status['circuit_state']}")
print(f"Mode: {status['mode']}")
β
Users can login when Keycloak is down
β
Users can change passwords when Keycloak is down
β
Sessions remain valid during Keycloak outage
β
Changes sync automatically when Keycloak recovers
β
No manual intervention required
β
Operations team has visibility into system state
β
Alerts notify team when Keycloak unavailable
Check:
Check:
SELECT * FROM sys_keycloak_sync_queue WHERE Status = 'pending'Run cleanup:
DELETE FROM sys_keycloak_token_cache
WHERE CachedAt < DATE_SUB(NOW(), INTERVAL 48 HOUR);
Remember: Keycloak is an enhancement, not a dependency. The system always works with local authentication.