User Impersonation System
The MyStoryFlow User Impersonation System allows administrators to securely access user accounts for customer support, debugging, and compliance purposes while maintaining strict security protocols and comprehensive audit trails.
🛡️ Security Framework
Core Security Principles
- Role-Based Access Control: Only users with ‘admin’ or ‘support’ roles can impersonate
- Time-Limited Sessions: Sessions automatically expire after 4 hours
- Comprehensive Auditing: Every impersonation start/end is logged via AuditService
- Session Tracking: All active sessions stored in database with full context
- Simple Token-Based Authentication: UUID-based session tokens for secure access
Security Architecture
The system implements a straightforward security model:
- Role Validation: Checks user has ‘admin’ or ‘support’ role before allowing impersonation
- Session Management: Maximum 4-hour session duration with automatic expiration
- Audit Logging: High-severity audit logs for session start/end events
- Database Tracking: All sessions stored in
impersonation_sessionstable - No JWT Complexity: Uses simple UUID tokens instead of JWT for session management
🏗️ System Architecture
Core Components
- UserImpersonationService (
apps/admin-app/src/lib/user-impersonation-service.ts) - Manages session lifecycle - AuditService (
apps/admin-app/src/lib/audit-service.ts) - Records all impersonation events - Database Table (
impersonation_sessions) - Stores active and historical sessions - Admin UI Pages - User search and impersonation initiation
- Web App Auth Flow - Handles impersonation token redirect (currently not implemented)
Actual Data Flow
The current implementation follows this flow:
- Admin navigates to
/dashboard/usersor/dashboard/users/impersonate - Admin searches for and selects a user to impersonate
- System calls
userManagementService.createImpersonationToken(userId, adminUserId) - A simple token is generated:
impersonate_${userId}_${Date.now()} - Admin is shown URL:
${MAIN_APP_URL}/auth-redirect?impersonate=${token} - New tab opens with the impersonation URL
- Note: Web app does NOT currently validate or handle the impersonation token
- Session ends automatically after 4 hours or can be manually ended
Current Implementation Limitations
- No Token Validation: Web app’s
/auth-redirectpage does not check for or validate impersonation tokens - No Active Session: Token is generated but not used to establish an impersonated session
- Manual Implementation: Currently relies on admin manually logging in as the user
- Audit Trail Only: System logs intent to impersonate but doesn’t track actual impersonated actions
🔐 Implementation Details
Database Schema
The impersonation_sessions table stores all impersonation sessions:
CREATE TABLE impersonation_sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
admin_user_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
target_user_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
target_user_email TEXT NOT NULL,
target_user_name TEXT NOT NULL,
reason TEXT NOT NULL,
session_token TEXT UNIQUE NOT NULL,
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'ended')),
started_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
ended_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Indexes for performance
CREATE INDEX idx_impersonation_sessions_admin_user ON impersonation_sessions(admin_user_id);
CREATE INDEX idx_impersonation_sessions_target_user ON impersonation_sessions(target_user_id);
CREATE INDEX idx_impersonation_sessions_token ON impersonation_sessions(session_token);
CREATE INDEX idx_impersonation_sessions_status ON impersonation_sessions(status, started_at DESC);
CREATE INDEX idx_impersonation_sessions_active ON impersonation_sessions(admin_user_id, target_user_id, status) WHERE status = 'active';TypeScript Interface
interface ImpersonationSession {
id: string
adminUserId: string
targetUserId: string
targetUserEmail: string
targetUserName: string
reason: string
startedAt: string
endedAt?: string
status: 'active' | 'ended'
sessionToken: string
}Actual Service Implementation
The actual implementation is simpler than originally documented:
class UserImpersonationService {
private supabase = supabaseAdmin
async startImpersonation(
adminUserId: string,
targetUserId: string,
reason: string
): Promise<{ success: boolean; sessionToken?: string; error?: string }> {
// 1. Verify admin has 'admin' or 'support' role
const { data: adminUser } = await this.supabase
.from('profiles')
.select('id, email, role')
.eq('id', adminUserId)
.single()
if (!adminUser || !['admin', 'support'].includes(adminUser.role)) {
throw new Error('Insufficient permissions')
}
// 2. Get target user details
const { data: targetUser } = await this.supabase
.from('profiles')
.select('id, email, full_name, subscription_tier, subscription_status')
.eq('id', targetUserId)
.single()
if (!targetUser) {
throw new Error('Target user not found')
}
// 3. Check for existing active session
const { data: existingSession } = await this.supabase
.from('impersonation_sessions')
.select('id')
.eq('admin_user_id', adminUserId)
.eq('target_user_id', targetUserId)
.eq('status', 'active')
.single()
if (existingSession) {
throw new Error('Active session already exists')
}
// 4. Generate simple UUID-based token
const sessionToken = crypto.randomUUID() + '-' + Date.now()
// 5. Create session record
const { data: session, error } = await this.supabase
.from('impersonation_sessions')
.insert({
admin_user_id: adminUserId,
target_user_id: targetUserId,
target_user_email: targetUser.email,
target_user_name: targetUser.full_name,
reason,
session_token: sessionToken,
status: 'active',
started_at: new Date().toISOString(),
})
.select()
.single()
// 6. Log audit event (high severity)
await AuditService.logAction({
userId: adminUserId,
action: 'user_impersonation_started',
resource: 'user',
resourceId: targetUserId,
details: {
targetUserEmail: targetUser.email,
targetUserName: targetUser.full_name,
reason,
sessionToken,
},
severity: 'high',
ipAddress: '',
userAgent: '',
})
return { success: true, sessionToken }
}
async endImpersonation(
adminUserId: string,
sessionToken: string
): Promise<{ success: boolean; error?: string }> {
// Update session to ended
const { data: session, error } = await this.supabase
.from('impersonation_sessions')
.update({
status: 'ended',
ended_at: new Date().toISOString(),
})
.eq('admin_user_id', adminUserId)
.eq('session_token', sessionToken)
.eq('status', 'active')
.select()
.single()
if (error) throw error
// Log audit event
await AuditService.logAction({
userId: adminUserId,
action: 'user_impersonation_ended',
resource: 'user',
resourceId: session.target_user_id,
details: {
sessionToken,
duration: new Date().getTime() - new Date(session.started_at).getTime(),
},
severity: 'medium',
ipAddress: '',
userAgent: '',
})
return { success: true }
}
async validateSession(
sessionToken: string
): Promise<{ valid: boolean; session?: ImpersonationSession }> {
const { data: session } = await this.supabase
.from('impersonation_sessions')
.select('*')
.eq('session_token', sessionToken)
.eq('status', 'active')
.single()
if (!session) return { valid: false }
// Check if session is older than 4 hours
const sessionAge = Date.now() - new Date(session.started_at).getTime()
const maxAge = 4 * 60 * 60 * 1000 // 4 hours
if (sessionAge > maxAge) {
await this.endImpersonation(session.admin_user_id, sessionToken)
return { valid: false }
}
return { valid: true, session }
}
}Admin Dashboard - Actual Implementation
The admin app provides two pages for user impersonation:
1. Users List Page (/dashboard/users)
// Simplified impersonation from users list
const handleImpersonate = async (userId: string) => {
try {
const token = await userManagementService.createImpersonationToken(
userId,
'admin-user'
)
if (token) {
const mainAppUrl = process.env.NEXT_PUBLIC_MAIN_APP_URL || 'http://localhost:3000'
const impersonateUrl = `${mainAppUrl}/auth-redirect?impersonate=${token}`
window.open(impersonateUrl, '_blank')
} else {
alert('Failed to create impersonation token')
}
} catch (error) {
alert('Error starting impersonation')
}
}2. Dedicated Impersonation Page (/dashboard/users/impersonate)
- Provides user search functionality
- Displays user details (name, email, subscription, status)
- Shows security warning about audit logging
- Creates session record in database (via UserImpersonationService)
- Opens new tab with impersonation URL
Current Token Generation (in userManagementService):
async createImpersonationToken(
userId: string,
adminUserId: string
): Promise<string | null> {
// Simple token - NOT stored or validated
const token = `impersonate_${userId}_${Date.now()}`
return token
}Important Note: This simplified token generation in userManagementService is different from the full UserImpersonationService implementation. The full service creates database records and audit logs, while the simple service just generates a token string.
Web App Integration - Current State
Status: NOT IMPLEMENTED
The web app’s /auth-redirect page currently:
- Handles session sharing between admin and web app (for cross-domain auth)
- Redirects authenticated users to dashboard
- Does NOT check for or validate impersonation tokens
What’s Missing:
// This code does NOT exist in the web app currently
export function ImpersonationBanner() {
// No impersonation detection
// No session validation
// No UI banner for impersonated sessions
}The /auth-redirect page checks for:
returnToparameter (for normal redirects)- Popup mode for session sharing
- User authentication status
But it does NOT check for:
impersonatequery parameter- Impersonation session tokens
- Active impersonation sessions
To Complete Implementation, Web App Needs:
- Check
searchParams.get('impersonate')in/auth-redirect - Validate token against
impersonation_sessionstable - Establish impersonated session (possibly using Supabase admin.getUserById)
- Display impersonation banner in layout
- Track impersonated actions in audit logs
- Provide “End Session” functionality
📊 Audit and Compliance
Actual Audit Implementation
The system uses AuditService to log impersonation events to the audit_logs table:
Audit Log Structure:
interface AuditLog {
id: string
userId: string // Admin user who initiated impersonation
userEmail?: string
action: string // 'user_impersonation_started' or 'user_impersonation_ended'
resource: string // 'user'
resourceId?: string // Target user ID
details: Record<string, any>
ipAddress?: string
userAgent?: string
timestamp: string
severity: 'low' | 'medium' | 'high' | 'critical'
category: 'auth' | 'data' | 'system' | 'security' | 'billing' | 'content'
}Events Logged:
- Impersonation Start (High Severity):
await AuditService.logAction({
userId: adminUserId,
action: 'user_impersonation_started',
resource: 'user',
resourceId: targetUserId,
details: {
targetUserEmail: targetUser.email,
targetUserName: targetUser.full_name,
reason: reason,
sessionToken: sessionToken
},
severity: 'high',
category: 'security'
})- Impersonation End (Medium Severity):
await AuditService.logAction({
userId: adminUserId,
action: 'user_impersonation_ended',
resource: 'user',
resourceId: targetUserId,
details: {
sessionToken: sessionToken,
duration: durationInMs
},
severity: 'medium',
category: 'security'
})Note: Individual actions performed during impersonation are NOT currently logged, only session start/end events.
Security Monitoring
Current Implementation: Basic session tracking only
The system provides:
- Session records in
impersonation_sessionstable - Audit logs for session start/end events
- Simple validation checking for:
- Active sessions (prevents duplicate sessions)
- Session age (auto-expires after 4 hours)
- Admin role verification
Not Implemented:
- Real-time suspicious activity detection
- Action-level monitoring during impersonation
- Rate limiting on impersonation attempts
- Automated alerts for security violations
- Pattern detection for abuse
Available Queries:
// Get active sessions for an admin
const sessions = await userImpersonationService.getActiveSessions(adminUserId)
// Get impersonation history
const history = await userImpersonationService.getImpersonationHistory(adminUserId, 50)
// Validate session is still active and not expired
const { valid, session } = await userImpersonationService.validateSession(token)🔒 Access Control and Permissions
Actual Role Requirements
Who Can Impersonate:
// Role check in UserImpersonationService.startImpersonation()
if (!adminUser || !['admin', 'support'].includes(adminUser.role)) {
throw new Error('Insufficient permissions for user impersonation')
}Allowed Roles:
admin- Full impersonation accesssupport- Full impersonation access (same as admin)
Session Duration:
- Fixed: 4 hours maximum
- No configurable duration options
- Automatic expiration on validation
Current Limitations:
- No distinction between admin and support permissions
- No target role restrictions (can impersonate any user)
- No action-level restrictions during impersonation
- No approval workflows
- No MFA requirement
📱 User Interface
Admin Dashboard Pages
1. Users List (/dashboard/users)
- Shows all users with filtering (tier, status)
- Impersonate button on each user row
- Quick access without requiring reason
2. Impersonation Page (/dashboard/users/impersonate)
- Dedicated search interface
- Security warning banner (red)
- User search by name/email/ID
- Displays: avatar, name, email, status, tier, campaigns/stories count
- Instructions on how impersonation works
Features:
- User avatar with initial
- Status badges (active, trial, expired, inactive)
- Subscription tier badges (starter, family, premium)
- Last login and join date display
- Loading states during token generation
Security UI Elements:
// Warning banner on impersonation page
<div className="bg-red-50 border border-red-200">
<h3>Security Warning</h3>
<p>
User impersonation is a powerful administrative tool. All
impersonation sessions are logged and monitored. Only use this
feature for legitimate support and troubleshooting purposes.
</p>
</div>Note: No active session monitoring dashboard currently exists.
🚨 Current Security Status
Implemented Security Features
Role-Based Access Control:
- ✅ Admin and support roles can impersonate
- ✅ Role verification before session creation
- ✅ Database constraints on admin/target user relationships
Session Management:
- ✅ Sessions stored in database
- ✅ 4-hour automatic expiration
- ✅ Prevents duplicate active sessions
- ✅ Session validation on access
Audit Logging:
- ✅ High-severity logs for session start
- ✅ Medium-severity logs for session end
- ✅ Session details (target, reason, duration)
- ✅ Logs stored in
audit_logstable
Security Gaps
Critical:
- ❌ Web app doesn’t validate or use impersonation tokens
- ❌ No actual impersonated session established
- ❌ No impersonation banner in web app UI
- ❌ No tracking of actions during impersonation
Important:
- ❌ IP address not captured in audit logs
- ❌ User agent not captured in audit logs
- ❌ No MFA requirement for impersonation
- ❌ No rate limiting on impersonation attempts
- ❌ No approval workflow for sensitive users
Nice to Have:
- ❌ No real-time monitoring dashboard
- ❌ No suspicious activity detection
- ❌ No automated alerts
- ❌ No compliance report generation
🔧 Troubleshooting
Known Issues
1. Impersonation Doesn’t Actually Work
- Token is generated but web app doesn’t use it
- User must manually log in as themselves
- This is a partial implementation - session tracking exists but session establishment doesn’t
2. Two Different Token Generation Methods
userManagementService.createImpersonationToken()- Simple string generation, no DB recorduserImpersonationService.startImpersonation()- Full session creation with audit logs- The UI pages use the simple method, not the full service
3. Missing IP/User Agent in Audit Logs
- Audit logs save empty strings for ipAddress and userAgent
- Would need to be captured from request context
4. Session Expiration
- Sessions expire after 4 hours
- Auto-expires on validation but no cleanup job runs
- Ended sessions remain in database indefinitely
Debugging Queries
// Check impersonation sessions in database
const { data } = await supabase
.from('impersonation_sessions')
.select('*')
.eq('status', 'active')
// Check audit logs
const { data: logs } = await supabase
.from('audit_logs')
.select('*')
.in('action', ['user_impersonation_started', 'user_impersonation_ended'])
.order('timestamp', { ascending: false })
// Verify admin role
const { data: profile } = await supabase
.from('profiles')
.select('role')
.eq('id', userId)
.single()📈 Required Completions
Critical Implementation Needs
1. Complete Web App Integration
- Add impersonation token validation in
/auth-redirect - Establish impersonated session using admin.getUserById
- Display impersonation banner when session is active
- Implement “End Session” functionality
- Store impersonation context in session/localStorage
2. Fix Token Generation Inconsistency
- Decide between simple tokens (userManagementService) or full sessions (userImpersonationService)
- Update UI to use the full UserImpersonationService
- Ensure database session records are created for all impersonations
3. Enhance Audit Logging
- Capture IP address from request headers
- Capture user agent from request headers
- Log individual actions during impersonation (optional)
- Add session end reason tracking
4. Add Session Management
- Create cleanup job for expired sessions
- Add ability to view active sessions dashboard
- Implement force-end session from admin panel
- Add session duration configuration
Nice-to-Have Improvements
Security:
- MFA requirement before impersonation
- Rate limiting on impersonation attempts
- Approval workflow for certain users/roles
- Real-time suspicious activity detection
User Experience:
- Reason requirement on all impersonation pages (currently optional)
- Session history view for admins
- Better error messages and validation
- Toast notifications instead of alerts
Monitoring:
- Active sessions monitoring dashboard
- Impersonation analytics and reports
- Alert system for unusual patterns
- Compliance export functionality
📝 Summary
What Works Today
The MyStoryFlow impersonation system is partially implemented:
Admin App (Functional):
- ✅ UI pages for user search and impersonation initiation
- ✅ Database schema for session tracking
- ✅ Audit logging for session start/end events
- ✅ Role-based access control (admin/support only)
- ✅ Session expiration (4 hours)
- ✅ UserImpersonationService with full functionality
Web App (Not Functional):
- ❌ Does not validate impersonation tokens
- ❌ Does not establish impersonated sessions
- ❌ No impersonation banner or UI
- ❌ No end session functionality
How It Currently Works
- Admin navigates to
/dashboard/usersor/dashboard/users/impersonate - Admin clicks “Impersonate” button
- System generates token:
impersonate_${userId}_${timestamp} - New tab opens to web app with token in URL
- Web app ignores the token and shows normal login page
- Admin must manually log in as themselves (impersonation doesn’t actually happen)
What Needs to Happen
To make impersonation functional, the web app needs to:
- Check for
impersonatequery parameter in/auth-redirect - Validate token against
impersonation_sessionstable - Use Supabase Admin SDK to authenticate as target user
- Display impersonation banner showing who is being impersonated
- Track session and allow ending it
File Locations
- Admin Service:
/apps/admin-app/src/lib/user-impersonation-service.ts - Admin Pages:
/apps/admin-app/src/app/dashboard/users/(page.tsx and impersonate/page.tsx) - Audit Service:
/apps/admin-app/src/lib/audit-service.ts - Database Schema:
/apps/admin-app/src/scripts/apply-db-optimizations.ts - Web App Auth:
/apps/web-app/app/auth-redirect/page.tsx(needs implementation)