Overview
Features
- TOTP (Time-Based OTP) - Works with Google Authenticator, Authy, 1Password, etc.
- QR Code Setup - Scan with your authenticator app
- Backup Codes - 10 single-use codes for account recovery
- Per-User - Each user manages their own 2FA settings
- Optional - 2FA is not required, users opt-in
Security Specifications
- Algorithm: SHA-1 (TOTP standard)
- Digits: 6
- Period: 30 seconds
- Window: ±1 period (90 seconds total)
- Secret Size: 20 bytes (160 bits)
- Backup Codes: 8 characters, alphanumeric (no confusable characters)
Implementation
Dockhand uses theotpauth npm package for TOTP generation and verification:
Setting Up 2FA
Via Web UI
- Navigate to Settings > Profile
- Click Enable Two-Factor Authentication
- Scan the QR code with your authenticator app
- Enter the 6-digit code to verify
- Save the 10 backup codes in a secure location
Via API
Step 1: Generate QR Code
- secret: Base32-encoded TOTP secret (for manual entry)
- qrDataUrl: QR code as data URL (for scanning)
Step 2: Verify and Enable
Scan the QR code with your authenticator app, then verify:Logging In with 2FA
Step 1: Submit Username and Password
Step 2: Submit TOTP Code
Using Backup Codes
If you lose access to your authenticator app, use a backup code:- Are accepted in place of TOTP codes
- Can only be used once
- Are removed from the database after use
- Work regardless of formatting (spaces and dashes are ignored)
Disabling 2FA
Via Web UI
- Navigate to Settings > Profile
- Click Disable Two-Factor Authentication
- Confirm the action
Via API
Users can disable their own 2FA:TOTP Details
QR Code Format
The QR code encodes anotpauth:// URI:
- Type:
totp(Time-Based OTP) - Issuer:
Dockhand (hostname)- includesDOCKHAND_HOSTNAMEenv var - Label: Username (
alice) - Secret: Base32-encoded secret
- Algorithm: SHA-1 (standard)
- Digits: 6
- Period: 30 seconds
Time Window
Dockhand accepts codes within a ±1 period window (90 seconds total):- Current period (30 seconds)
- Previous period (-30 seconds)
- Next period (+30 seconds)
Token Validation
Backup Codes
Generation
Backup codes are generated when 2FA is enabled:- Count: 10 codes
- Length: 8 characters
- Character Set:
ABCDEFGHJKLMNPQRSTUVWXYZ23456789(32 chars) - Entropy: ~40 bits per code (8 × log2(32))
- Excluded: Confusable characters (0/O, 1/I/l)
Storage
Backup codes are hashed with SHA-256 before storage:Database Format
MFA data is stored as JSON in theusers.mfa_secret column:
Verification
Compatible Authenticator Apps
Dockhand works with any TOTP-compatible authenticator:Mobile Apps
- Google Authenticator - iOS / Android
- Authy - iOS / Android
- Microsoft Authenticator - iOS / Android
- Duo Mobile - iOS / Android
Password Managers
- 1Password - Built-in TOTP support
- Bitwarden - Premium feature
- LastPass - Authenticator app
- KeePassXC - Desktop app with TOTP
Browser Extensions
- Authenticator (Chrome/Firefox/Edge)
- OTP Manager (Firefox)
Recovery Process
Lost Authenticator + Have Backup Codes
- Log in with username, password, and a backup code
- Navigate to Settings > Profile
- Disable 2FA
- Re-enable 2FA with a new QR code
- Save new backup codes
Lost Authenticator + No Backup Codes
Contact an admin to disable 2FA:Lost Password + 2FA Enabled
2FA doesn’t help if the password is lost. An admin must:- Reset the password:
- Optionally disable 2FA if the user lost their authenticator
Database Schema
MFA configuration is stored in theusers table:
Example Data
| username | mfa_enabled | mfa_secret |
|---|---|---|
| alice | true | {"secret":"JBSWY3DPEHPK3PXP","backupCodes":["e3b0c44...","d7a8fbb..."]} |
Security Considerations
Secret Storage
TOTP secrets are stored as plain text in the database. This is necessary because the server needs to compute OTP codes to verify user input. To protect secrets:- Encrypt the database at rest
- Restrict database access to the application only
- Use strong passwords for database authentication
- Enable audit logging (Enterprise) to track MFA changes
Backup Code Storage
Backup codes are hashed with SHA-256 before storage. This prevents attackers from recovering plain codes if the database is compromised. Why SHA-256 instead of Argon2?- Backup codes have high entropy (40 bits)
- Brute force is computationally infeasible
- Fast verification is acceptable
- SHA-256 is sufficient for high-entropy secrets
Time Synchronization
TOTP relies on synchronized clocks. Ensure:- Server: Uses NTP for accurate time
- Client: Device clock is roughly accurate (±60 seconds is fine)
Rate Limiting
2FA codes are subject to the same rate limiting as passwords:- Threshold: 5 failed attempts per IP + username
- Window: 15 minutes
- Lockout: 15 minutes
Best Practices
For Users
- Use a reputable authenticator app - Google Authenticator, Authy, 1Password
- Save backup codes immediately - Store in a password manager or offline
- Test a backup code - Verify one works before you need it
- Enable 2FA on critical accounts - Especially admin accounts
- Don’t screenshot QR codes - Screenshots can leak if your device is compromised
For Admins
- Require 2FA for admins - Policy, not technical enforcement
- Keep a local admin account without 2FA for emergency access
- Document recovery process - So users know what to do
- Audit 2FA status - Periodically check which users have 2FA enabled
- Monitor failed 2FA attempts - Check server logs for suspicious activity
Troubleshooting
Codes Always Invalid
Issue: All TOTP codes are rejected. Solutions:- Check server time:
date -u(should be accurate to ±60 seconds) - Enable NTP on the Docker host:
timedatectl set-ntp true - Verify authenticator app time sync (usually automatic)
- Ensure you’re entering the code within 30 seconds of generation
Backup Codes Not Working
Issue: Backup codes are rejected. Solutions:- Enter the code exactly as shown (uppercase, no spaces)
- Codes are case-insensitive, spaces/dashes ignored
- Verify you haven’t used this code before (single-use)
- Check you’re using codes from the most recent 2FA setup
Lost Backup Codes
Issue: User lost backup codes and authenticator app. Solutions:- Contact an admin
- Admin disables 2FA:
DELETE /api/users/{id}/mfa - User resets password if needed
- User re-enables 2FA with new codes
QR Code Won’t Scan
Issue: Authenticator app can’t read the QR code. Solutions:- Increase browser zoom (QR code gets larger)
- Use manual entry: Enter the secret from the UI
- Take a clear photo if scanning from another device
- Try a different authenticator app
Source Code Reference
src/lib/server/auth.ts:834-1036- TOTP implementationsrc/routes/api/users/[id]/mfa/+server.ts- MFA endpointssrc/routes/profile/MfaSetupModal.svelte- UI componentsrc/routes/login/+page.svelte:301- Login form with 2FA
Next Steps
Local Users
Manage user accounts and passwords
OIDC/SSO
Integrate with your Identity Provider
RBAC
Configure role-based access control (Enterprise)
Authentication
Back to authentication overview
