Merge pull request #10938 from appwrite/chore-sync-main

Sync main into 1.8.x
This commit is contained in:
Steven Nguyen 2025-12-11 20:06:52 -08:00 committed by GitHub
commit 278679ab10
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 87 additions and 4 deletions

View file

@ -333,6 +333,8 @@ class Create extends Action
->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);
}
}

View file

@ -110,7 +110,7 @@ class Update extends Action
$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)) {
@ -132,7 +132,7 @@ class Update extends Action
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

@ -101,6 +101,8 @@ class Create extends Action
'recoveryCodes' => $mfaRecoveryCodes
]);
$response->dynamic($document, Response::MODEL_MFA_RECOVERY_CODES);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($document, Response::MODEL_MFA_RECOVERY_CODES);
}
}

View file

@ -3095,4 +3095,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']);
}
}