Files
Trilium/docs/Security/Authentication-Access-Control-Guide.md
2025-08-21 15:55:44 +00:00

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

// 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

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:

  1. Clear browser cookies and cache
  2. Restart Trilium server
  3. Check database permissions
  4. 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:

  1. Synchronize system time with NTP
  2. Use recovery codes
  3. Regenerate TOTP secret
  4. 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:

  1. Increase session timeout
  2. Enable secure cookies for HTTPS
  3. Check browser cookie settings
  4. 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.