mirror of
https://github.com/zadam/trilium.git
synced 2025-11-01 10:55:55 +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.