mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 18:36:30 +01:00 
			
		
		
		
	
		
			
				
	
	
	
		
			31 KiB
		
	
	
	
	
	
		
			Vendored
		
	
	
	
			
		
		
	
	
			31 KiB
		
	
	
	
	
	
		
			Vendored
		
	
	
	
Authentication and Access Control Guide
This comprehensive guide covers Trilium's authentication mechanisms, session management, API security, and access control systems. Understanding these systems is crucial for maintaining a secure Trilium installation.
Authentication Overview
Trilium supports multiple authentication methods designed to provide flexible yet secure access control:
- Password Authentication: Primary method using scrypt-hashed passwords
- Multi-Factor Authentication (MFA): TOTP-based secondary authentication
- Single Sign-On (SSO): OpenID Connect integration for enterprise environments
- API Token Authentication: ETAPI tokens for programmatic access
Password Authentication
Password Security Architecture
Hashing Algorithm
- Primary: Scrypt with configurable parameters
- Parameters: N=16384, r=8, p=1 (adjustable)
- Salt: 32-byte cryptographically secure random salt
- Storage: Separate verification hash from encryption keys
Password Storage Schema
-- Password-related options in database
CREATE TABLE options (
    name TEXT PRIMARY KEY,
    value TEXT
);
-- Key storage entries
INSERT INTO options VALUES 
('passwordVerificationSalt', '<32-byte-base64-salt>'),
('passwordDerivedKeySalt', '<32-byte-base64-salt>'),
('passwordVerificationHash', '<scrypt-hash-base64>'),
('encryptedDataKey', '<aes-encrypted-key>');
Password Verification Process
// Verification flow
1. Extract stored salt and hash from options
2. Derive key from provided password + salt using scrypt
3. Compare derived hash with stored verification hash
4. Return boolean result (constant-time comparison)
Password Management
Setting Initial Password
// During setup or first run
function setPassword(password: string): ChangePasswordResponse {
    // Generate new salts
    const verificationSalt = randomSecureToken(32);
    const dataKeySalt = randomSecureToken(32);
    
    // Create verification hash
    const verificationHash = scrypt(password, verificationSalt);
    
    // Generate and encrypt data key
    const dataKey = randomSecureToken(16);
    const passwordKey = scrypt(password, dataKeySalt);
    const encryptedDataKey = aes128Encrypt(passwordKey, dataKey);
    
    // Store in options table
    setOption('passwordVerificationSalt', verificationSalt);
    setOption('passwordDerivedKeySalt', dataKeySalt);
    setOption('passwordVerificationHash', verificationHash);
    setOption('encryptedDataKey', encryptedDataKey);
}
Changing Password
// Password change process
function changePassword(currentPassword: string, newPassword: string) {
    // Verify current password
    if (!verifyPassword(currentPassword)) {
        throw new Error('Current password incorrect');
    }
    
    // Decrypt data key with current password
    const dataKey = getDataKey(currentPassword);
    
    // Generate new salts
    const newVerificationSalt = randomSecureToken(32);
    const newDataKeySalt = randomSecureToken(32);
    
    // Re-encrypt data key with new password
    const newPasswordKey = scrypt(newPassword, newDataKeySalt);
    const newEncryptedDataKey = aes128Encrypt(newPasswordKey, dataKey);
    
    // Update stored values
    updateOptions({
        passwordVerificationSalt: newVerificationSalt,
        passwordDerivedKeySalt: newDataKeySalt,
        passwordVerificationHash: scrypt(newPassword, newVerificationSalt),
        encryptedDataKey: newEncryptedDataKey
    });
}
Password Reset
// Complete password reset (loses all protected content)
function resetPassword() {
    updateOptions({
        passwordVerificationSalt: '',
        passwordDerivedKeySalt: '',
        passwordVerificationHash: '',
        encryptedDataKey: ''
    });
    
    // All protected content becomes inaccessible
    // Only unprotected content remains
}
Password Policy Configuration
Minimum Requirements
// Default password policy
const passwordPolicy = {
    minLength: 8,           // Minimum character count
    requireUppercase: false, // At least one uppercase letter
    requireLowercase: false, // At least one lowercase letter
    requireNumbers: false,   // At least one number
    requireSymbols: false,   // At least one special character
    maxAge: 0,              // Days before expiration (0 = never)
    preventReuse: 0         // Number of previous passwords to check
};
Configuration Options
-- Password policy options
INSERT INTO options VALUES 
('passwordMinLength', '8'),
('passwordRequireUppercase', 'false'),
('passwordRequireLowercase', 'false'),
('passwordRequireNumbers', 'false'),
('passwordRequireSymbols', 'false'),
('passwordMaxAge', '0'),
('passwordPreventReuse', '0');
Multi-Factor Authentication (MFA)
TOTP Implementation
Algorithm Details
- Standard: RFC 6238 (Time-Based One-Time Password)
- Hash Function: SHA-1 (standard for TOTP compatibility)
- Time Step: 30 seconds
- Code Length: 6 digits
- Clock Tolerance: ±1 time step (±30 seconds)
Secret Management
// TOTP secret lifecycle
class TotpSecretManager {
    // Generate new TOTP secret
    generateSecret(): string {
        const secret = generateSecretBase32();  // 32-character base32
        return secret;
    }
    
    // Store encrypted secret
    setTotpSecret(secret: string): void {
        const encryptedSecret = encryptWithDataKey(secret);
        setOption('totpEncryptedSecret', encryptedSecret);
        
        // Store verification hash
        const verificationHash = sha256(secret);
        setOption('totpVerificationHash', verificationHash);
    }
    
    // Retrieve and decrypt secret
    getTotpSecret(): string | null {
        const encrypted = getOption('totpEncryptedSecret');
        if (!encrypted) return null;
        
        return decryptWithDataKey(encrypted);
    }
    
    // Verify secret integrity
    verifyTotpSecret(secret: string): boolean {
        const storedHash = getOption('totpVerificationHash');
        const computedHash = sha256(secret);
        return constantTimeEquals(storedHash, computedHash);
    }
}
TOTP Validation
// TOTP code verification
function validateTOTP(submittedCode: string): boolean {
    const secret = getTotpSecret();
    if (!secret) return false;
    
    const currentTime = Math.floor(Date.now() / 1000);
    const timeStep = 30;
    
    // Check current time window and adjacent windows
    for (let i = -1; i <= 1; i++) {
        const timeWindow = Math.floor(currentTime / timeStep) + i;
        const expectedCode = generateTOTP(secret, timeWindow);
        
        if (constantTimeEquals(submittedCode, expectedCode)) {
            return true;
        }
    }
    
    return false;
}
Recovery Codes
Generation and Storage
// Recovery code management
class RecoveryCodeManager {
    generateRecoveryCodes(): string[] {
        const codes = [];
        for (let i = 0; i < 10; i++) {
            // Generate 24-character base64 codes ending in "=="
            const code = generateRecoveryCode();
            codes.push(code);
        }
        return codes;
    }
    
    storeRecoveryCodes(codes: string[]): void {
        // Encrypt codes with AES-256-CBC
        const encryptedCodes = codes.map(code => 
            aes256Encrypt(getDataKey(), code)
        );
        
        setOption('recoveryCodesEncrypted', JSON.stringify(encryptedCodes));
    }
    
    verifyRecoveryCode(submittedCode: string): boolean {
        const encryptedCodes = JSON.parse(getOption('recoveryCodesEncrypted') || '[]');
        
        for (let i = 0; i < encryptedCodes.length; i++) {
            const decryptedCode = aes256Decrypt(getDataKey(), encryptedCodes[i]);
            
            if (constantTimeEquals(submittedCode, decryptedCode)) {
                // Mark code as used (replace with timestamp)
                encryptedCodes[i] = aes256Encrypt(getDataKey(), `used:${Date.now()}`);
                setOption('recoveryCodesEncrypted', JSON.stringify(encryptedCodes));
                return true;
            }
        }
        
        return false;
    }
}
MFA Setup Process
User Enrollment
// MFA enrollment workflow
async function enrollMFA() {
    // 1. Generate TOTP secret
    const secret = totp.generateSecret();
    
    // 2. Display QR code for authenticator app
    const qrCodeUrl = generateQRCode({
        secret: secret,
        issuer: 'Trilium Notes',
        account: 'user@trilium.local'
    });
    
    // 3. User scans QR code or enters secret manually
    // 4. User submits test TOTP code
    const testCode = await promptForTOTP();
    
    // 5. Verify test code
    if (validateTOTPWithSecret(testCode, secret)) {
        // 6. Store secret and enable MFA
        setTotpSecret(secret);
        setOption('mfaEnabled', 'true');
        setOption('mfaMethod', 'totp');
        
        // 7. Generate and display recovery codes
        const recoveryCodes = generateRecoveryCodes();
        storeRecoveryCodes(recoveryCodes);
        displayRecoveryCodes(recoveryCodes);
    } else {
        throw new Error('TOTP verification failed');
    }
}
Authentication Flow
// MFA-enabled login process
async function authenticateWithMFA(username: string, password: string) {
    // 1. Verify primary credentials
    if (!verifyPassword(password)) {
        throw new Error('Invalid credentials');
    }
    
    // 2. Check if MFA is enabled
    if (isMFAEnabled()) {
        // 3. Prompt for TOTP code
        const totpCode = await promptForTOTP();
        
        // 4. Validate TOTP
        if (!validateTOTP(totpCode)) {
            // 5. Allow recovery code as fallback
            const recoveryCode = await promptForRecoveryCode();
            
            if (!verifyRecoveryCode(recoveryCode)) {
                throw new Error('MFA verification failed');
            }
        }
    }
    
    // 6. Create authenticated session
    return createSession(username);
}
Single Sign-On (SSO)
OpenID Connect Integration
Supported Providers
// Common OIDC provider configurations
const oidcProviders = {
    google: {
        issuer: 'https://accounts.google.com',
        scope: 'openid email profile'
    },
    microsoft: {
        issuer: 'https://login.microsoftonline.com/common/v2.0',
        scope: 'openid email profile'
    },
    github: {
        issuer: 'https://token.actions.githubusercontent.com',
        scope: 'openid email'
    },
    custom: {
        issuer: process.env.OIDC_ISSUER,
        scope: 'openid email profile'
    }
};
Configuration Setup
# config.ini - OpenID Connect settings
[OpenID]
enabled=true
issuer=https://your-provider.com
client_id=your-client-id
client_secret=your-client-secret
redirect_uri=https://your-trilium.com/auth/callback
scope=openid email profile
response_type=code
response_mode=query
Authentication Flow
// OIDC authentication process
class OIDCAuthenticator {
    async authenticate(req: Request, res: Response) {
        // 1. Redirect to OIDC provider
        const authUrl = buildAuthorizationUrl({
            issuer: config.oidc.issuer,
            clientId: config.oidc.clientId,
            redirectUri: config.oidc.redirectUri,
            scope: config.oidc.scope,
            state: generateSecureState(),
            nonce: generateSecureNonce()
        });
        
        res.redirect(authUrl);
    }
    
    async handleCallback(req: Request, res: Response) {
        // 2. Receive authorization code
        const { code, state } = req.query;
        
        // 3. Verify state parameter
        if (!verifyState(state)) {
            throw new Error('Invalid state parameter');
        }
        
        // 4. Exchange code for tokens
        const tokens = await exchangeCodeForTokens(code);
        
        // 5. Verify ID token
        const userInfo = await verifyIdToken(tokens.id_token);
        
        // 6. Create local session
        req.session.loggedIn = true;
        req.session.userInfo = userInfo;
        req.session.oidcTokens = tokens;
        
        res.redirect('/app');
    }
}
SSO Security Features
Token Validation
// ID token verification
async function verifyIdToken(idToken: string): Promise<UserInfo> {
    // 1. Parse JWT header and payload
    const [header, payload, signature] = idToken.split('.');
    const parsedHeader = JSON.parse(base64Decode(header));
    const parsedPayload = JSON.parse(base64Decode(payload));
    
    // 2. Fetch provider's public keys
    const jwks = await fetchJWKS(config.oidc.issuer);
    const publicKey = findMatchingKey(jwks, parsedHeader.kid);
    
    // 3. Verify signature
    const isValid = verifyJWTSignature(idToken, publicKey);
    if (!isValid) {
        throw new Error('Invalid token signature');
    }
    
    // 4. Verify claims
    validateClaims(parsedPayload, {
        issuer: config.oidc.issuer,
        audience: config.oidc.clientId,
        nonce: getExpectedNonce()
    });
    
    return {
        sub: parsedPayload.sub,
        email: parsedPayload.email,
        name: parsedPayload.name
    };
}
Session Management
// SSO session handling
class SSOSessionManager {
    async refreshTokens(req: Request): Promise<void> {
        const refreshToken = req.session.oidcTokens?.refresh_token;
        if (!refreshToken) {
            throw new Error('No refresh token available');
        }
        
        try {
            const newTokens = await refreshAccessToken(refreshToken);
            req.session.oidcTokens = newTokens;
        } catch (error) {
            // Refresh failed, require re-authentication
            req.session.destroy();
            throw new Error('Token refresh failed');
        }
    }
    
    async logout(req: Request, res: Response): Promise<void> {
        // 1. Destroy local session
        const oidcTokens = req.session.oidcTokens;
        req.session.destroy();
        
        // 2. Construct logout URL
        const logoutUrl = buildLogoutUrl({
            issuer: config.oidc.issuer,
            clientId: config.oidc.clientId,
            postLogoutRedirectUri: config.oidc.postLogoutRedirectUri,
            idTokenHint: oidcTokens?.id_token
        });
        
        // 3. Redirect to provider logout
        res.redirect(logoutUrl);
    }
}
Session Management
Session Architecture
Storage Backend
// Session store configuration
const sessionStore = new SqliteStore({
    database: './data/document.db',
    table: 'sessions',
    createTable: true,
    cleanupInterval: 3600000  // 1 hour
});
Session Schema
-- Session storage table
CREATE TABLE sessions (
    id TEXT PRIMARY KEY,        -- Session identifier
    expires INTEGER NOT NULL,   -- Expiration timestamp
    data TEXT NOT NULL         -- JSON session data
);
CREATE INDEX idx_sessions_expires ON sessions(expires);
Session Data Structure
// Session data interface
interface SessionData {
    loggedIn: boolean;
    userId?: string;
    userInfo?: {
        email: string;
        name: string;
        sub: string;
    };
    lastAuthState: {
        totpEnabled: boolean;
        ssoEnabled: boolean;
    };
    protectedSession?: {
        active: boolean;
        lastActivity: number;
    };
    csrf?: string;
    oidcTokens?: {
        access_token: string;
        refresh_token: string;
        id_token: string;
    };
}
Session Security Configuration
Cookie Security
// Secure session cookie configuration
const sessionConfig = {
    name: 'trilium.sid',
    secret: generateSessionSecret(),
    resave: false,
    saveUninitialized: false,
    rolling: true,
    cookie: {
        httpOnly: true,           // Prevent XSS access
        secure: isProduction,     // HTTPS only in production
        sameSite: 'strict',       // CSRF protection
        maxAge: 24 * 60 * 60 * 1000,  // 24 hours
        domain: undefined         // Current domain only
    }
};
Session Validation
// Session validation middleware
function validateSession(req: Request, res: Response, next: NextFunction) {
    // 1. Check session existence
    if (!req.session) {
        return res.status(401).json({ error: 'No session found' });
    }
    
    // 2. Verify session integrity
    if (!req.session.loggedIn) {
        return res.status(401).json({ error: 'Not authenticated' });
    }
    
    // 3. Check session expiration
    if (isSessionExpired(req.session)) {
        req.session.destroy();
        return res.status(401).json({ error: 'Session expired' });
    }
    
    // 4. Validate authentication state consistency
    const currentAuthState = getCurrentAuthState();
    const lastAuthState = req.session.lastAuthState;
    
    if (!authStateMatches(currentAuthState, lastAuthState)) {
        req.session.destroy();
        return res.status(401).json({ error: 'Authentication state changed' });
    }
    
    // 5. Update last activity
    req.session.lastActivity = Date.now();
    
    next();
}
Session Lifecycle Management
Session Creation
// Create new authenticated session
async function createSession(req: Request, userInfo: UserInfo): Promise<void> {
    // 1. Generate session data
    const sessionData: SessionData = {
        loggedIn: true,
        userId: userInfo.sub,
        userInfo: userInfo,
        lastAuthState: {
            totpEnabled: isTotpEnabled(),
            ssoEnabled: isSSoEnabled()
        },
        csrf: generateCSRFToken()
    };
    
    // 2. Store session
    req.session = sessionData;
    
    // 3. Set secure cookie
    req.session.save((err) => {
        if (err) {
            throw new Error('Failed to save session');
        }
    });
    
    // 4. Log security event
    logSecurityEvent('SESSION_CREATE', {
        userId: userInfo.sub,
        ip: req.ip,
        userAgent: req.headers['user-agent']
    });
}
Session Cleanup
// Automatic session cleanup
class SessionCleaner {
    constructor(private sessionStore: SqliteStore) {
        // Run cleanup every hour
        setInterval(() => this.cleanup(), 3600000);
    }
    
    async cleanup(): Promise<void> {
        const now = Date.now();
        
        // Remove expired sessions
        const expiredCount = await this.sessionStore.clear({
            where: 'expires < ?',
            params: [now]
        });
        
        // Log cleanup results
        if (expiredCount > 0) {
            logSecurityEvent('SESSION_CLEANUP', {
                expiredSessions: expiredCount,
                timestamp: now
            });
        }
        
        // Optimize database
        await this.sessionStore.optimize();
    }
}
CSRF Protection
Double Submit Cookie Implementation
Token Generation
// CSRF token management
class CSRFTokenManager {
    generateToken(): string {
        // Generate 32-byte random token
        const tokenBytes = crypto.randomBytes(32);
        return tokenBytes.toString('hex');
    }
    
    setToken(req: Request, res: Response): void {
        const token = this.generateToken();
        
        // Store in session
        req.session.csrf = token;
        
        // Send as cookie
        res.cookie('_csrf', token, {
            httpOnly: true,
            secure: isProduction,
            sameSite: 'strict',
            path: '/'
        });
    }
    
    validateToken(req: Request): boolean {
        const sessionToken = req.session.csrf;
        const cookieToken = req.cookies._csrf;
        const headerToken = req.headers['x-csrf-token'] || 
                           req.headers['x-xsrf-token'];
        
        // All tokens must be present and match
        return sessionToken && 
               cookieToken && 
               headerToken &&
               constantTimeEquals(sessionToken, cookieToken) &&
               constantTimeEquals(sessionToken, headerToken);
    }
}
CSRF Middleware
// CSRF protection middleware
function csrfProtection(req: Request, res: Response, next: NextFunction) {
    // Skip CSRF for safe methods
    if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
        return next();
    }
    
    // Skip CSRF for API token authentication
    if (isApiTokenAuthenticated(req)) {
        return next();
    }
    
    // Validate CSRF token
    if (!csrfTokenManager.validateToken(req)) {
        logSecurityEvent('CSRF_VIOLATION', {
            ip: req.ip,
            path: req.path,
            method: req.method,
            userAgent: req.headers['user-agent']
        });
        
        return res.status(403).json({ 
            error: 'CSRF token validation failed' 
        });
    }
    
    next();
}
API Authentication
ETAPI Token System
Token Structure
// ETAPI token format
interface EtapiToken {
    id: string;              // Unique token identifier
    name: string;            // Human-readable name
    tokenHash: string;       // SHA-256 hash of token
    isDeleted: boolean;      // Soft delete flag
    dateCreated: string;     // Creation timestamp
    dateModified: string;    // Last modification
}
Token Generation
// Generate new ETAPI token
function generateEtapiToken(name: string): { token: string, tokenHash: string } {
    // Generate 64-character random token
    const token = crypto.randomBytes(32).toString('hex');
    
    // Create hash for storage
    const tokenHash = crypto
        .createHash('sha256')
        .update(token)
        .digest('hex');
    
    // Store in database
    sql.execute(`
        INSERT INTO etapi_tokens (id, name, tokenHash, isDeleted, dateCreated, dateModified)
        VALUES (?, ?, ?, 0, datetime('now'), datetime('now'))
    `, [generateId(), name, tokenHash]);
    
    return { token, tokenHash };
}
Token Validation
// Validate ETAPI token
function validateEtapiToken(authHeader: string): boolean {
    if (!authHeader || !authHeader.startsWith('Bearer ')) {
        return false;
    }
    
    const token = authHeader.substring(7); // Remove 'Bearer ' prefix
    
    // Hash provided token
    const providedHash = crypto
        .createHash('sha256')
        .update(token)
        .digest('hex');
    
    // Check against stored hashes
    const storedToken = sql.getValue(`
        SELECT id FROM etapi_tokens 
        WHERE tokenHash = ? AND isDeleted = 0
    `, [providedHash]);
    
    return !!storedToken;
}
Rate Limiting
Token-based Rate Limiting
// Rate limiter for API endpoints
class ApiRateLimiter {
    private limits = new Map<string, TokenLimit>();
    
    constructor(
        private maxRequests: number = 1000,
        private windowMs: number = 60000  // 1 minute
    ) {}
    
    check(tokenHash: string): boolean {
        const now = Date.now();
        const limit = this.limits.get(tokenHash);
        
        if (!limit || now - limit.resetTime > this.windowMs) {
            // Reset or create new limit
            this.limits.set(tokenHash, {
                count: 1,
                resetTime: now
            });
            return true;
        }
        
        if (limit.count >= this.maxRequests) {
            return false; // Rate limit exceeded
        }
        
        limit.count++;
        return true;
    }
}
Access Control
Permission Matrix
// Access control matrix
enum Permission {
    READ_PUBLIC = 'read_public',
    READ_PROTECTED = 'read_protected',
    WRITE_PUBLIC = 'write_public', 
    WRITE_PROTECTED = 'write_protected',
    ADMIN = 'admin',
    API_ACCESS = 'api_access'
}
enum AuthState {
    ANONYMOUS = 'anonymous',
    AUTHENTICATED = 'authenticated',
    PROTECTED_SESSION = 'protected_session',
    API_TOKEN = 'api_token'
}
const accessMatrix: Record<AuthState, Permission[]> = {
    [AuthState.ANONYMOUS]: [
        Permission.READ_PUBLIC  // Only if sharing enabled
    ],
    [AuthState.AUTHENTICATED]: [
        Permission.READ_PUBLIC,
        Permission.WRITE_PUBLIC,
        Permission.ADMIN
    ],
    [AuthState.PROTECTED_SESSION]: [
        Permission.READ_PUBLIC,
        Permission.READ_PROTECTED,
        Permission.WRITE_PUBLIC,
        Permission.WRITE_PROTECTED,
        Permission.ADMIN
    ],
    [AuthState.API_TOKEN]: [
        Permission.READ_PUBLIC,
        Permission.WRITE_PUBLIC,
        Permission.API_ACCESS
    ]
};
Authorization Middleware
// Authorization enforcement
function requirePermission(permission: Permission) {
    return (req: Request, res: Response, next: NextFunction) => {
        const authState = getAuthState(req);
        const allowedPermissions = accessMatrix[authState];
        
        if (!allowedPermissions.includes(permission)) {
            logSecurityEvent('AUTHORIZATION_DENIED', {
                permission: permission,
                authState: authState,
                ip: req.ip,
                path: req.path
            });
            
            return res.status(403).json({
                error: 'Insufficient permissions'
            });
        }
        
        next();
    };
}
// Usage examples
app.get('/api/notes/:id', 
    requirePermission(Permission.READ_PUBLIC),
    getNoteHandler
);
app.put('/api/notes/:id',
    requirePermission(Permission.WRITE_PUBLIC),
    updateNoteHandler
);
app.get('/api/protected-notes/:id',
    requirePermission(Permission.READ_PROTECTED),
    getProtectedNoteHandler
);
Security Monitoring
Security Event Logging
// Security event types
enum SecurityEventType {
    LOGIN_SUCCESS = 'login_success',
    LOGIN_FAILURE = 'login_failure',
    MFA_SUCCESS = 'mfa_success',
    MFA_FAILURE = 'mfa_failure',
    SESSION_CREATE = 'session_create',
    SESSION_DESTROY = 'session_destroy',
    PROTECTED_SESSION_START = 'protected_session_start',
    PROTECTED_SESSION_END = 'protected_session_end',
    CSRF_VIOLATION = 'csrf_violation',
    RATE_LIMIT_EXCEEDED = 'rate_limit_exceeded',
    AUTHORIZATION_DENIED = 'authorization_denied',
    PASSWORD_CHANGE = 'password_change',
    API_TOKEN_CREATED = 'api_token_created',
    API_TOKEN_DELETED = 'api_token_deleted'
}
// Security event logger
class SecurityEventLogger {
    logEvent(type: SecurityEventType, data: any): void {
        const event = {
            timestamp: new Date().toISOString(),
            type: type,
            data: data,
            severity: this.getSeverity(type)
        };
        
        // Log to database
        sql.execute(`
            INSERT INTO security_events (timestamp, type, data, severity)
            VALUES (?, ?, ?, ?)
        `, [event.timestamp, event.type, JSON.stringify(event.data), event.severity]);
        
        // Log to file
        logger.info('Security Event', event);
        
        // Send alerts for high-severity events
        if (event.severity === 'HIGH') {
            this.sendAlert(event);
        }
    }
    
    private getSeverity(type: SecurityEventType): string {
        const highSeverityEvents = [
            SecurityEventType.LOGIN_FAILURE,
            SecurityEventType.MFA_FAILURE,
            SecurityEventType.CSRF_VIOLATION,
            SecurityEventType.AUTHORIZATION_DENIED
        ];
        
        return highSeverityEvents.includes(type) ? 'HIGH' : 'LOW';
    }
}
Intrusion Detection
// Basic intrusion detection
class IntrusionDetector {
    private failedAttempts = new Map<string, FailureRecord>();
    
    constructor(
        private maxFailures = 5,
        private windowMs = 300000  // 5 minutes
    ) {}
    
    recordFailure(ip: string): void {
        const now = Date.now();
        const record = this.failedAttempts.get(ip);
        
        if (!record || now - record.firstAttempt > this.windowMs) {
            this.failedAttempts.set(ip, {
                count: 1,
                firstAttempt: now,
                lastAttempt: now
            });
        } else {
            record.count++;
            record.lastAttempt = now;
            
            if (record.count >= this.maxFailures) {
                this.triggerLockout(ip);
            }
        }
    }
    
    private triggerLockout(ip: string): void {
        // Log security event
        logSecurityEvent(SecurityEventType.RATE_LIMIT_EXCEEDED, {
            ip: ip,
            failureCount: this.maxFailures,
            action: 'temporary_lockout'
        });
        
        // Implement temporary IP blocking
        this.blockIP(ip, 3600000); // 1 hour block
    }
}
Troubleshooting
Common Authentication Issues
Login Failures
Symptom: Cannot login with correct credentials
Diagnostic Steps:
# Check session storage
sqlite3 document.db "SELECT COUNT(*) FROM sessions;"
# Verify password hash
sqlite3 document.db "SELECT name, length(value) FROM options WHERE name LIKE 'password%';"
# Check for session errors
tail -f logs/trilium.log | grep -i session
Solutions:
- Clear browser cookies and cache
- Restart Trilium server
- Check database permissions
- Verify password case sensitivity
MFA Problems
Symptom: TOTP codes rejected
Diagnostic Steps:
# Check TOTP configuration
sqlite3 document.db "SELECT value FROM options WHERE name = 'mfaEnabled';"
# Verify time synchronization
timedatectl status
# Check TOTP secret
# (In protected session only)
Solutions:
- Synchronize system time with NTP
- Use recovery codes
- Regenerate TOTP secret
- Check authenticator app configuration
Session Issues
Symptom: Frequent logouts or session errors
Diagnostic Steps:
# Check session configuration
sqlite3 document.db "SELECT * FROM sessions LIMIT 5;"
# Monitor session cleanup
tail -f logs/trilium.log | grep -i "session cleanup"
# Check cookie settings
# Browser Developer Tools → Application → Cookies
Solutions:
- Increase session timeout
- Enable secure cookies for HTTPS
- Check browser cookie settings
- Verify database write permissions
Security Monitoring Queries
-- Recent login failures
SELECT timestamp, data FROM security_events 
WHERE type = 'login_failure' 
AND timestamp > datetime('now', '-1 hour')
ORDER BY timestamp DESC;
-- MFA bypass attempts
SELECT timestamp, data FROM security_events 
WHERE type = 'mfa_failure'
AND timestamp > datetime('now', '-1 day')
ORDER BY timestamp DESC;
-- CSRF violations
SELECT timestamp, data FROM security_events 
WHERE type = 'csrf_violation'
ORDER BY timestamp DESC LIMIT 10;
-- Active sessions
SELECT id, expires, length(data) as data_size 
FROM sessions 
WHERE expires > unixepoch('now') * 1000
ORDER BY expires ASC;
Remember: Proper authentication and access control are fundamental to Trilium's security. Regularly review your configuration and monitor for suspicious activity to maintain a secure environment.