Merge pull request #10925 from Ujjwaljain16/fix-10740-mfa-recovery-code-validation

fix: resolve MFA recovery code validation in 1.8.0
This commit is contained in:
Steven Nguyen 2025-12-11 06:28:06 -08:00 committed by GitHub
commit 35fe622548
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 87 additions and 4 deletions

View file

@ -4421,7 +4421,9 @@ App::post('/v1/account/mfa/recovery-codes')
'recoveryCodes' => $mfaRecoveryCodes
]);
$response->dynamic($document, Response::MODEL_MFA_RECOVERY_CODES);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($document, Response::MODEL_MFA_RECOVERY_CODES);
});
App::patch('/v1/account/mfa/recovery-codes')
@ -4874,7 +4876,9 @@ App::post('/v1/account/mfa/challenge')
->setParam('userId', $user->getId())
->setParam('challengeId', $challenge->getId());
$response->dynamic($challenge, Response::MODEL_MFA_CHALLENGE);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($challenge, Response::MODEL_MFA_CHALLENGE);
});
App::put('/v1/account/mfa/challenge')
@ -4942,7 +4946,7 @@ App::put('/v1/account/mfa/challenge')
$recoveryCodeChallenge = function (Document $challenge, Document $user, string $otp) use ($dbForProject) {
if (
$challenge->isSet('type') &&
$challenge->getAttribute('type') === \strtolower(Type::RECOVERY_CODE)
$challenge->getAttribute('type') === Type::RECOVERY_CODE
) {
$mfaRecoveryCodes = $user->getAttribute('mfaRecoveryCodes', []);
if (in_array($otp, $mfaRecoveryCodes)) {
@ -4964,7 +4968,7 @@ App::put('/v1/account/mfa/challenge')
Type::TOTP => Challenge\TOTP::challenge($challenge, $user, $otp),
Type::PHONE => Challenge\Phone::challenge($challenge, $user, $otp),
Type::EMAIL => Challenge\Email::challenge($challenge, $user, $otp),
\strtolower(Type::RECOVERY_CODE) => $recoveryCodeChallenge($challenge, $user, $otp),
Type::RECOVERY_CODE => $recoveryCodeChallenge($challenge, $user, $otp),
default => false
});

View file

@ -3072,4 +3072,83 @@ class AccountCustomClientTest extends Scope
$this->assertEquals('test-identifier-updated', $response['body']['identifier']);
$this->assertEquals(false, $response['body']['expired']);
}
public function testMFARecoveryCodeChallenge(): void
{
// Generate recovery codes using existing authenticated session
$response = $this->client->call(Client::METHOD_POST, '/account/mfa/recovery-codes', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['recoveryCodes']);
$recoveryCodes = $response['body']['recoveryCodes'];
$this->assertGreaterThan(0, count($recoveryCodes));
// Create recovery code challenge
$challenge = $this->client->call(Client::METHOD_POST, '/account/mfa/challenge', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'factor' => 'recoveryCode'
]);
$this->assertEquals(201, $challenge['headers']['status-code']);
$this->assertNotEmpty($challenge['body']['$id']);
$challengeId = $challenge['body']['$id'];
// Test SUCCESS: Verify with valid recovery code (this tests the bug fix)
$verification = $this->client->call(Client::METHOD_PUT, '/account/mfa/challenge', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'challengeId' => $challengeId,
'otp' => $recoveryCodes[0]
]);
$this->assertEquals(200, $verification['headers']['status-code']);
$this->assertArrayHasKey('factors', $verification['body']);
$this->assertContains('recoveryCode', $verification['body']['factors']);
// Test that the code was consumed (can't use again)
$challenge2 = $this->client->call(Client::METHOD_POST, '/account/mfa/challenge', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'factor' => 'recoveryCode'
]);
$this->assertEquals(201, $challenge2['headers']['status-code']);
$verification2 = $this->client->call(Client::METHOD_PUT, '/account/mfa/challenge', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'challengeId' => $challenge2['body']['$id'],
'otp' => $recoveryCodes[0] // Same code should fail
]);
$this->assertEquals(401, $verification2['headers']['status-code']);
// Test FAILURE: Invalid recovery code
$challenge3 = $this->client->call(Client::METHOD_POST, '/account/mfa/challenge', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'factor' => 'recoveryCode'
]);
$this->assertEquals(201, $challenge3['headers']['status-code']);
$verification3 = $this->client->call(Client::METHOD_PUT, '/account/mfa/challenge', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'challengeId' => $challenge3['body']['$id'],
'otp' => 'invalid-code-123'
]);
$this->assertEquals(401, $verification3['headers']['status-code']);
}
}