feat: add TOTP multifactor authentication
This commit is contained in:
@@ -5,6 +5,7 @@ const APP_REQUIRED = [
|
||||
'APP_ORIGIN',
|
||||
'ORIGIN',
|
||||
'PASSKEY_RP_ID',
|
||||
'MFA_TOTP_ENCRYPTION_KEYS',
|
||||
'SCHEDULER_TOKEN',
|
||||
'TDR_DATABASE_PATH',
|
||||
'TDR_AVATAR_STORAGE_PATH',
|
||||
@@ -48,6 +49,7 @@ export function validateRuntimeEnvironment(mode, environment) {
|
||||
|
||||
requireValues(APP_REQUIRED);
|
||||
validateSchedulerToken(get('SCHEDULER_TOKEN'), errors);
|
||||
validateMfaTotpEncryptionKeys(get('MFA_TOTP_ENCRYPTION_KEYS'), errors);
|
||||
|
||||
const appOrigin = validateOrigin('APP_ORIGIN', get('APP_ORIGIN'), errors);
|
||||
const adapterOrigin = validateOrigin('ORIGIN', get('ORIGIN'), errors);
|
||||
@@ -230,3 +232,60 @@ function validateSchedulerToken(value, errors) {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the app-only TOTP encryption keyring without including configured
|
||||
* values in an error. The first entry is the active write key.
|
||||
*
|
||||
* @param {string} value
|
||||
* @param {string[]} errors
|
||||
*/
|
||||
function validateMfaTotpEncryptionKeys(value, errors) {
|
||||
if (!value) return;
|
||||
const entries = value.split(',').map((entry) => entry.trim());
|
||||
if (
|
||||
entries.length < 1 ||
|
||||
entries.length > 3 ||
|
||||
entries.some((entry) => !entry)
|
||||
) {
|
||||
errors.push(
|
||||
'MFA_TOTP_ENCRYPTION_KEYS must contain 1 through 3 key entries.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const ids = new Set();
|
||||
const keys = new Set();
|
||||
for (const entry of entries) {
|
||||
const separator = entry.indexOf(':');
|
||||
const keyId = separator > 0 ? entry.slice(0, separator) : '';
|
||||
const encodedKey = separator > 0 ? entry.slice(separator + 1) : '';
|
||||
if (
|
||||
!/^[A-Za-z0-9_-]{1,64}$/.test(keyId) ||
|
||||
!/^[A-Za-z0-9_-]+$/.test(encodedKey) ||
|
||||
encodedKey.length !== 43
|
||||
) {
|
||||
errors.push(
|
||||
'MFA_TOTP_ENCRYPTION_KEYS entries must use key-id and canonical 32-byte Base64URL values.',
|
||||
);
|
||||
continue;
|
||||
}
|
||||
let decoded;
|
||||
try {
|
||||
decoded = Buffer.from(encodedKey, 'base64url');
|
||||
} catch {
|
||||
decoded = Buffer.alloc(0);
|
||||
}
|
||||
if (decoded.length !== 32 || decoded.toString('base64url') !== encodedKey) {
|
||||
errors.push(
|
||||
'MFA_TOTP_ENCRYPTION_KEYS entries must use key-id and canonical 32-byte Base64URL values.',
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (ids.has(keyId) || keys.has(encodedKey)) {
|
||||
errors.push('MFA_TOTP_ENCRYPTION_KEYS must not repeat key IDs or keys.');
|
||||
}
|
||||
ids.add(keyId);
|
||||
keys.add(encodedKey);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user