Compare commits

...
Author SHA1 Message Date
Matej Bačo faa375f27a Add magic URL sign-in exception for MFA edgecase 2025-10-23 14:41:29 +02:00
Matej Bačo 022e4bf869 revert bad changes 2025-10-23 14:28:49 +02:00
Matej Bačo 6bef350014 Fix MFA with email-verified factor 2025-10-09 11:34:23 +02:00
+35 -6
View File
@@ -1948,6 +1948,17 @@ App::post('/v1/account/tokens/magic-url')
$result = $dbForProject->findOne('users', [Query::equal('email', [$email])]);
if (!$result->isEmpty()) {
$user->setAttributes($result->getArrayCopy());
$mfaEnabled = $user->getAttribute('mfa', false);
if ($mfaEnabled) {
$isTotpEnabled = TOTP::getAuthenticatorFromUser($user)?->getAttribute('verified') ?? false;
$isPhoneEnabled = $user->getAttribute('phone', false) && $user->getAttribute('phoneVerification', false);
if (!$isTotpEnabled && !$isPhoneEnabled) {
throw new Exception(Exception::USER_MORE_FACTORS_REQUIRED, 'You cannot sign-in passwordless with email, because your account has multifactor enabled, but only has email factor enabled.');
}
}
} else {
$limit = $project->getAttribute('auths', [])['limit'] ?? 0;
@@ -4153,18 +4164,36 @@ App::get('/v1/account/mfa/factors')
])
->inject('response')
->inject('user')
->action(function (Response $response, Document $user) {
->inject('session')
->action(function (Response $response, Document $user, Document $session) {
$mfaRecoveryCodes = $user->getAttribute('mfaRecoveryCodes', []);
$recoveryCodeEnabled = \is_array($mfaRecoveryCodes) && \count($mfaRecoveryCodes) > 0;
$isRecoveryCodeEnabled = \is_array($mfaRecoveryCodes) && \count($mfaRecoveryCodes) > 0;
$totp = TOTP::getAuthenticatorFromUser($user);
$isTotpEnabled = $totp !== null && $totp->getAttribute('verified', false);
$isEmailEnabled = $user->getAttribute('email', false) && $user->getAttribute('emailVerification', false);
$isPhoneEnabled = $user->getAttribute('phone', false) && $user->getAttribute('phoneVerification', false);
// Disallow email or phone as 2nd factor, if it was used as 1st factor already
// This is just for informative purposes, actual protection lies in unique check when adding a factor
if (!is_null($session)) {
$existingFactors = $session->getAttribute('factors', []);
if (\in_array(Type::EMAIL, $existingFactors) && $isEmailEnabled) {
$isEmailEnabled = false;
}
if (\in_array(Type::PHONE, $existingFactors) && $isPhoneEnabled) {
$isPhoneEnabled = false;
}
}
$factors = new Document([
Type::TOTP => $totp !== null && $totp->getAttribute('verified', false),
Type::EMAIL => $user->getAttribute('email', false) && $user->getAttribute('emailVerification', false),
Type::PHONE => $user->getAttribute('phone', false) && $user->getAttribute('phoneVerification', false),
Type::RECOVERY_CODE => $recoveryCodeEnabled
Type::TOTP => $isTotpEnabled,
Type::EMAIL => $isEmailEnabled,
Type::PHONE => $isPhoneEnabled,
Type::RECOVERY_CODE => $isRecoveryCodeEnabled
]);
$response->dynamic($factors, Response::MODEL_MFA_FACTORS);