Skip to main content

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:

PermissionDescription
two_factor:readView own 2FA status and requirement
two_factor:writeSetup, verify, disable, and regenerate backup codes for own 2FA
two_factor:manageManage 2FA for any user (admin only)

Permission Rules

  • Own 2FA operations: Requires two_factor:read for status queries, two_factor:write for modifications
  • Other users' 2FA: Requires two_factor:manage permission or super admin role
  • System API keys: Have wildcard (*:*) permission and can manage any user's 2FA when user_id is provided

Default Role Assignments

RolePermissions
Admin*:* (wildcard - all permissions)
Usertwo_factor:read, two_factor:write
Moderatortwo_factor:read, two_factor:write
Developertwo_factor:read, two_factor:write
API Read Onlytwo_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:

ParameterTypeRequiredDescription
user_idstringNo*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:

ParameterTypeRequiredDescription
user_idstringNo*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:

FieldTypeRequiredDescription
platform_idstringNoPlatform ID to set up 2FA from (must support 2FA). Defaults to email platform.
user_idstringNo*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"
}
tip

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:

FieldTypeRequiredDescription
codestringYesThe 6-digit TOTP code from the authenticator app.
user_idstringNo*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"
}
warning

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:

FieldTypeRequiredDescription
codestringYesTOTP code from authenticator app or a backup code.
user_idstringNo*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:

FieldTypeRequiredDescription
codestringYesTOTP code from authenticator app (backup codes cannot be used for this operation).
user_idstringNo*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:

VariableTypeRequiredDescription
userIdString!YesTarget user ID.

userRequiresTwoFactor

Check if 2FA is required for the user.

query UserRequiresTwoFactor($userId: String!) {
userRequiresTwoFactor(userId: $userId) {
required
requiredByRoles
enabled
canEnable
}
}

Variables:

VariableTypeRequiredDescription
userIdString!YesTarget user ID.

Mutations

setupTwoFactor

Initiate 2FA setup.

mutation SetupTwoFactor($input: SetupTwoFactorInput!) {
setupTwoFactor(input: $input) {
secret
qrCodeUrl
expiresAt
platformSlug
}
}

Input:

FieldTypeRequiredDescription
platformIdStringNoPlatform ID to set up 2FA from. Defaults to email platform.
userIdStringNo*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:

FieldTypeRequiredDescription
codeString!YesThe 6-digit TOTP code from the authenticator app.
userIdStringNo*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:

FieldTypeRequiredDescription
codeString!YesTOTP code from authenticator app or a backup code.
userIdStringNo*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:

FieldTypeRequiredDescription
codeString!YesTOTP code from authenticator app (backup codes cannot be used).
userIdStringNo*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:

FieldDescription
two_factor_supportedWhether 2FA can be set up from this platform
two_factor_requiredWhether 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:

EventEmail Sent
2FA EnabledConfirmation email with platform name and timestamp
2FA DisabledAlert email notifying of security change
Backup Codes RegeneratedNotification that old codes are invalidated
info

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

Important Security Notes
  1. TOTP secrets are encrypted using AES-256-GCM before storage
  2. Backup codes are hashed using Argon2 (one-way, cannot be recovered)
  3. Setup sessions expire after 10 minutes
  4. Backup codes are single-use and cannot be reused
  5. Only 10 backup codes are generated at a time

Error Codes

ErrorDescription
400 Bad RequestInvalid code, 2FA already enabled/disabled, or setup expired
401 UnauthorizedAuthentication required
403 ForbiddenInsufficient permissions
404 Not FoundPlatform 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);

Next Steps