Two-Factor Authentication (2FA)
Heimdall supports TOTP-based two-factor authentication for enhanced account security.
Overview
Two-factor authentication adds an extra layer of security by requiring users to provide a time-based one-time password (TOTP) from an authenticator app in addition to their regular credentials.
Key Features
- TOTP-based authentication using RFC 6238 standard (compatible with Google Authenticator, Authy, 1Password, etc.)
- User-level 2FA - protects the entire account regardless of login method
- Platform-level configuration - platforms can require or support 2FA
- Role-based enforcement - specific roles can require 2FA
- Backup codes - 10 single-use recovery codes for emergency access
- AES-256-GCM encryption for storing TOTP secrets
Required Permissions
2FA operations require specific RBAC permissions:
| Permission | Description |
|---|---|
two_factor:read | View own 2FA status and requirement |
two_factor:write | Setup, verify, disable, and regenerate backup codes for own 2FA |
two_factor:manage | Manage 2FA for any user (admin only) |
Permission Rules
- Own 2FA operations: Requires
two_factor:readfor status queries,two_factor:writefor modifications - Other users' 2FA: Requires
two_factor:managepermission or super admin role - System API keys: Have wildcard (
*:*) permission and can manage any user's 2FA whenuser_idis provided
Default Role Assignments
| Role | Permissions |
|---|---|
| Admin | *:* (wildcard - all permissions) |
| User | two_factor:read, two_factor:write |
| Moderator | two_factor:read, two_factor:write |
| Developer | two_factor:read, two_factor:write |
| API Read Only | two_factor:read |
| System | *:* (wildcard - all permissions) |
REST API Endpoints
Get 2FA Status
Get the current 2FA status for the authenticated user.
GET /v1/users/me/2fa/status
Authorization: Bearer YOUR_TOKEN
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
user_id | string | No* | Target user ID. Required for system API keys, optional for user-authenticated requests. |
*Required when using a system API key instead of a user token.
Response:
{
"enabled": true,
"enabled_at": "2025-01-15T10:30:00Z",
"setup_platform": "email",
"backup_codes_remaining": 8
}
Get 2FA Requirement Status
Check if 2FA is required for the authenticated user based on their roles.
GET /v1/users/me/2fa/requirement
Authorization: Bearer YOUR_TOKEN
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
user_id | string | No* | Target user ID. Required for system API keys, optional for user-authenticated requests. |
*Required when using a system API key instead of a user token.
Response:
{
"required": true,
"required_by_roles": ["admin", "moderator"],
"enabled": false,
"can_enable": true
}
Setup 2FA
Initiate 2FA setup. Returns a secret and QR code URL for the authenticator app.
POST /v1/users/me/2fa/setup
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json
{
"platform_id": "optional-platform-id",
"user_id": "optional-user-id"
}
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
platform_id | string | No | Platform ID to set up 2FA from (must support 2FA). Defaults to email platform. |
user_id | string | No* | Target user ID. Required for system API keys, optional for user-authenticated requests. |
*Required when using a system API key instead of a user token.
Response:
{
"secret": "JBSWY3DPEHPK3PXP",
"qr_code_url": "otpauth://totp/Heimdall:[email protected]?secret=JBSWY3DPEHPK3PXP&issuer=Heimdall",
"expires_at": "2025-01-15T10:40:00Z",
"platform_slug": "email"
}
The qr_code_url can be encoded into a QR code for easy scanning with authenticator apps. The secret can be entered manually if QR scanning is not available.
Verify and Enable 2FA
Verify the TOTP code and enable 2FA. Returns backup codes upon successful verification.
POST /v1/users/me/2fa/verify
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json
{
"code": "123456",
"user_id": "optional-user-id"
}
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
code | string | Yes | The 6-digit TOTP code from the authenticator app. |
user_id | string | No* | Target user ID. Required for system API keys, optional for user-authenticated requests. |
*Required when using a system API key instead of a user token.
Response:
{
"backup_codes": [
"ABC123DEF4",
"GHI567JKL8",
"MNO901PQR2",
"STU345VWX6",
"YZA789BCD0",
"EFG123HIJ4",
"KLM567NOP8",
"QRS901TUV2",
"WXY345ZAB6",
"CDE789FGH0"
],
"message": "Two-factor authentication has been enabled successfully.",
"platform_slug": "email"
}
Backup codes are only shown once. Make sure to save them in a secure location before completing the setup.
Disable 2FA
Disable 2FA for the authenticated user. Requires verification with TOTP code or backup code.
DELETE /v1/users/me/2fa
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json
{
"code": "123456",
"user_id": "optional-user-id"
}
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
code | string | Yes | TOTP code from authenticator app or a backup code. |
user_id | string | No* | Target user ID. Required for system API keys, optional for user-authenticated requests. |
*Required when using a system API key instead of a user token.
Response:
{
"success": true,
"message": "Two-factor authentication has been disabled."
}
Regenerate Backup Codes
Generate new backup codes. Requires verification with TOTP code (backup codes cannot be used).
POST /v1/users/me/2fa/backup-codes/regenerate
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json
{
"code": "123456",
"user_id": "optional-user-id"
}
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
code | string | Yes | TOTP code from authenticator app (backup codes cannot be used for this operation). |
user_id | string | No* | Target user ID. Required for system API keys, optional for user-authenticated requests. |
*Required when using a system API key instead of a user token.
Response:
{
"backup_codes": [
"NEW123DEF4",
"GHI567NEW8",
"..."
],
"message": "Backup codes have been regenerated. Your old codes are no longer valid.",
"platform_slug": "email"
}
GraphQL API
Queries
twoFactorStatus
Get the current 2FA status.
query TwoFactorStatus($userId: String!) {
twoFactorStatus(userId: $userId) {
enabled
enabledAt
setupPlatform
backupCodesRemaining
}
}
Variables:
| Variable | Type | Required | Description |
|---|---|---|---|
userId | String! | Yes | Target user ID. |
userRequiresTwoFactor
Check if 2FA is required for the user.
query UserRequiresTwoFactor($userId: String!) {
userRequiresTwoFactor(userId: $userId) {
required
requiredByRoles
enabled
canEnable
}
}
Variables:
| Variable | Type | Required | Description |
|---|---|---|---|
userId | String! | Yes | Target user ID. |
Mutations
setupTwoFactor
Initiate 2FA setup.
mutation SetupTwoFactor($input: SetupTwoFactorInput!) {
setupTwoFactor(input: $input) {
secret
qrCodeUrl
expiresAt
platformSlug
}
}
Input:
| Field | Type | Required | Description |
|---|---|---|---|
platformId | String | No | Platform ID to set up 2FA from. Defaults to email platform. |
userId | String | No* | Target user ID. Required for system API keys, optional for user-authenticated requests. |
*Required when using a system API key instead of a user token.
verifyTwoFactor
Verify TOTP code and enable 2FA.
mutation VerifyTwoFactor($input: VerifyTwoFactorInput!) {
verifyTwoFactor(input: $input) {
backupCodes
message
platformSlug
}
}
Input:
| Field | Type | Required | Description |
|---|---|---|---|
code | String! | Yes | The 6-digit TOTP code from the authenticator app. |
userId | String | No* | Target user ID. Required for system API keys, optional for user-authenticated requests. |
*Required when using a system API key instead of a user token.
disableTwoFactor
Disable 2FA.
mutation DisableTwoFactor($input: DisableTwoFactorInput!) {
disableTwoFactor(input: $input)
}
Input:
| Field | Type | Required | Description |
|---|---|---|---|
code | String! | Yes | TOTP code from authenticator app or a backup code. |
userId | String | No* | Target user ID. Required for system API keys, optional for user-authenticated requests. |
*Required when using a system API key instead of a user token.
regenerateBackupCodes
Generate new backup codes.
mutation RegenerateBackupCodes($input: RegenerateBackupCodesInput!) {
regenerateBackupCodes(input: $input) {
backupCodes
message
platformSlug
}
}
Input:
| Field | Type | Required | Description |
|---|---|---|---|
code | String! | Yes | TOTP code from authenticator app (backup codes cannot be used). |
userId | String | No* | Target user ID. Required for system API keys, optional for user-authenticated requests. |
*Required when using a system API key instead of a user token.
Platform Configuration
Platforms can be configured to support or require 2FA:
| Field | Description |
|---|---|
two_factor_supported | Whether 2FA can be set up from this platform |
two_factor_required | Whether 2FA is required to use this platform |
Query Platform 2FA Settings
query {
platforms {
id
name
slug
twoFactorSupported
twoFactorRequired
}
}
Role-Based Enforcement
Roles can be configured to require 2FA:
query {
roles {
id
name
requiresTwoFactor
}
}
When a user has a role with requiresTwoFactor: true, they will be prompted to enable 2FA.
Email Notifications
Heimdall sends email notifications for important 2FA events to keep users informed about security changes to their account:
| Event | Email Sent |
|---|---|
| 2FA Enabled | Confirmation email with platform name and timestamp |
| 2FA Disabled | Alert email notifying of security change |
| Backup Codes Regenerated | Notification that old codes are invalidated |
Email notifications are sent automatically and cannot be disabled. They serve as an important security measure to alert users of changes to their account security settings.
Security Considerations
- TOTP secrets are encrypted using AES-256-GCM before storage
- Backup codes are hashed using Argon2 (one-way, cannot be recovered)
- Setup sessions expire after 10 minutes
- Backup codes are single-use and cannot be reused
- Only 10 backup codes are generated at a time
Error Codes
| Error | Description |
|---|---|
400 Bad Request | Invalid code, 2FA already enabled/disabled, or setup expired |
401 Unauthorized | Authentication required |
403 Forbidden | Insufficient permissions |
404 Not Found | Platform not found |
Implementation Example
JavaScript/TypeScript
// 1. Start 2FA setup
const setupResponse = await fetch('/v1/users/me/2fa/setup', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_TOKEN',
'Content-Type': 'application/json'
},
body: JSON.stringify({})
});
const { secret, qr_code_url } = await setupResponse.json();
// 2. Display QR code to user (use a QR code library)
// User scans with authenticator app
// 3. Verify with code from authenticator
const verifyResponse = await fetch('/v1/users/me/2fa/verify', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_TOKEN',
'Content-Type': 'application/json'
},
body: JSON.stringify({ code: '123456' })
});
const { backup_codes } = await verifyResponse.json();
// 4. Store backup codes securely
console.log('Save these backup codes:', backup_codes);