feat: Add two-factor authentication via TOTP

Adds TOTP setup with QR code, login challenge, recovery codes and
trusted devices that let a browser skip the second factor for 30
days. Admins can reset another user's second factor by confirming
with their own password.
This commit is contained in:
Daniel Hiller
2026-08-07 20:11:55 +02:00
parent 36aa732fec
commit 2e4904f77d
74 changed files with 4173 additions and 4 deletions
+49 -1
View File
@@ -142,6 +142,21 @@
* default: byDefault
* description: Default sort order for projects display (personal field)
* example: byDefault
* isTotpEnabled:
* type: boolean
* default: false
* description: Whether TOTP-based two-factor authentication is enabled (visible only to current user or admin)
* example: false
* totpEnabledAt:
* type: string
* format: date-time
* nullable: true
* description: When TOTP was enabled (visible only to current user or admin)
* example: 2026-05-14T10:00:00.000Z
* totpRecoveryCodesRemaining:
* type: integer
* description: Number of unused recovery codes (visible only to current user or admin)
* example: 10
* isDeactivated:
* type: boolean
* default: false
@@ -233,7 +248,20 @@ const LANGUAGES = [
];
// TODO: find better way to handle apiKeyHash and apiKeyCreatedAt
const PRIVATE_FIELD_NAMES = ['email', 'apiKeyPrefix', 'apiKeyHash', 'apiKeyCreatedAt'];
const PRIVATE_FIELD_NAMES = [
'email',
'apiKeyPrefix',
'apiKeyHash',
'apiKeyCreatedAt',
'totpSecret',
'totpRecoveryCodes',
];
const TWO_FACTOR_VISIBLE_FIELD_NAMES = [
'isTotpEnabled',
'totpEnabledAt',
'totpRecoveryCodesRemaining',
];
const PERSONAL_FIELD_NAMES = [
'language',
@@ -259,6 +287,7 @@ module.exports = {
LANGUAGES,
PRIVATE_FIELD_NAMES,
PERSONAL_FIELD_NAMES,
TWO_FACTOR_VISIBLE_FIELD_NAMES,
INTERNAL,
attributes: {
@@ -384,6 +413,25 @@ module.exports = {
type: 'ref',
columnName: 'terms_accepted_at',
},
totpSecret: {
type: 'string',
isNotEmptyString: true,
allowNull: true,
columnName: 'totp_secret',
},
isTotpEnabled: {
type: 'boolean',
defaultsTo: false,
columnName: 'is_totp_enabled',
},
totpEnabledAt: {
type: 'ref',
columnName: 'totp_enabled_at',
},
totpRecoveryCodes: {
type: 'json',
columnName: 'totp_recovery_codes',
},
// ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
// ║╣ ║║║╠╩╗║╣ ║║╚═╗