diff --git a/CHANGELOG.md b/CHANGELOG.md index 618147e8..f9455451 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ Please read about the future of the Firebase Admin PHP SDK on the ## [Unreleased] +### Added + +* It is now possible to configure multi factor authentication for a user. + ## [7.17.0] - 2025-02-22 ### Added diff --git a/docs/user-management.rst b/docs/user-management.rst index b7579bb2..a016a8fc 100644 --- a/docs/user-management.rst +++ b/docs/user-management.rst @@ -269,6 +269,8 @@ Property Type Description ``deletePhotoUrl`` boolean Whether or not to delete the user's photo. ``deleteDisplayName`` boolean Whether or not to delete the user's display name. ``deletePhoneNumber`` boolean Whether or not to delete the user's phone number. +``resetMultiFactor`` boolean Whether or not to reset all of the user's enrolled factors. Including phone and TOTP factors. +``multiFactors`` array An array of multi-factor factors. ``deleteProvider`` string|array One or more identity providers to delete. ``customAttributes`` array A list of custom attributes which will be available in a User's ID token. ====================== ============ =========== @@ -396,6 +398,25 @@ This method always returns an instance of ``Kreait\Firebase\Auth\DeleteUsersResu Cloud Functions for Firebase. This is because batch deletes do not trigger a user deletion event on each user. Delete users one at a time if you want user deletion events to fire for each deleted user. +********************************* +Set multi factor authentication +********************************* + +The Firebase Admin SDK allows setting multi-factor authentication for a user, consisting of phone factors. Setting the +multi-factor authentication overwrites all existing factors. Setting the `mfaEnrollmentId` and `enrolledAt` properties is +optional. For example: + +.. code-block:: php + + $uid = 'some-uid'; + + $updatedUser = $auth->updateUser($uid, ['multifactors' => [[ + 'mfaEnrollmentId' => '85dc3f7b-7bef-45b9-b9e6-0a1c2c656fed', + 'phoneInfo' => '+31123456789', + 'displayName' => 'foo', + 'enrolledAt' => '2025-02-28T15:30:00Z', + ]]); + ************************************** Duplicate/Unregistered email addresses ************************************** diff --git a/src/Firebase/Request/EditUserTrait.php b/src/Firebase/Request/EditUserTrait.php index 6de79268..8c8c92c0 100644 --- a/src/Firebase/Request/EditUserTrait.php +++ b/src/Firebase/Request/EditUserTrait.php @@ -38,6 +38,9 @@ trait EditUserTrait protected ?string $clearTextPassword = null; + /** @var array|null */ + protected ?array $multiFactor = null; + /** * @param Stringable|mixed $uid */ @@ -168,6 +171,7 @@ public function prepareJsonSerialize(): array 'phoneNumber' => $this->phoneNumber, 'photoUrl' => $this->photoUrl, 'password' => $this->clearTextPassword, + 'mfa' => $this->multiFactor, ], static fn($value): bool => $value !== null); } diff --git a/src/Firebase/Request/UpdateUser.php b/src/Firebase/Request/UpdateUser.php index ba0116c4..aaf0becc 100644 --- a/src/Firebase/Request/UpdateUser.php +++ b/src/Firebase/Request/UpdateUser.php @@ -152,6 +152,18 @@ public static function withProperties(array $properties): self $request, ); + break; + + case 'resetmultifactor': + if ($value === true) { + $request = $request->resetMultiFactor(); + } + + break; + + case 'multifactors': + $request = $request->withMultiFactors($value); + break; } } @@ -205,6 +217,32 @@ public function withRemovedEmail(): self return $request; } + /** + * @param array $enrollments + */ + public function withMultiFactors(array $enrollments): self + { + $request = clone $this; + $request->multiFactor ??= []; + $request->multiFactor['enrollments'] = $enrollments; + + return $request; + } + + public function resetMultiFactor(): self + { + $request = clone $this; + $request->multiFactor ??= []; + $request->multiFactor['enrollments'] = []; + + return $request; + } + /** * @param array $customAttributes */ diff --git a/tests/Integration/Request/UpdateUserTest.php b/tests/Integration/Request/UpdateUserTest.php index df34a318..edb49019 100644 --- a/tests/Integration/Request/UpdateUserTest.php +++ b/tests/Integration/Request/UpdateUserTest.php @@ -5,10 +5,12 @@ namespace Kreait\Firebase\Tests\Integration\Request; use DateTimeImmutable; +use Kreait\Firebase\Auth\MfaInfo; use Kreait\Firebase\Contract\Auth; use Kreait\Firebase\Request\CreateUser; use Kreait\Firebase\Request\UpdateUser; use Kreait\Firebase\Tests\IntegrationTestCase; +use Kreait\Firebase\Util\DT; use PHPUnit\Framework\Attributes\Test; use function bin2hex; @@ -203,4 +205,60 @@ public function timeOfLastPasswordUpdateIsIncluded(): void $this->auth->deleteUser($user->uid); } } + + #[Test] + public function setMultiFactor(): void + { + $user = $this->auth->createUser( + CreateUser::new()->withVerifiedEmail(self::randomEmail(__FUNCTION__)), + ); + + $factor = [ + 'mfaEnrollmentId' => '85dc3f7b-7bef-45b9-b9e6-0a1c2c656fed', + 'phoneInfo' => '+31123456789', + 'displayName' => '', + 'enrolledAt' => '2025-02-28T15:30:00Z', + ]; + + $enrolledAt = DT::toUTCDateTimeImmutable($factor['enrolledAt']); + + try { + $check = $this->auth->updateUser($user->uid, ['multifactors' => [$factor]]); + + $this->assertInstanceOf(MfaInfo::class, $check->mfaInfo); + $this->assertSame($factor['mfaEnrollmentId'], $check->mfaInfo->mfaEnrollmentId); + $this->assertSame($factor['phoneInfo'], $check->mfaInfo->phoneInfo); + $this->assertSame($factor['displayName'], $check->mfaInfo->displayName); + $this->assertEquals($enrolledAt, $check->mfaInfo->enrolledAt); + } finally { + $this->auth->deleteUser($user->uid); + } + } + + #[Test] + public function resetMultiFactor(): void + { + $user = $this->auth->createUser( + CreateUser::new()->withVerifiedEmail(self::randomEmail(__FUNCTION__)), + ); + + $factor = [ + 'mfaEnrollmentId' => '85dc3f7b-7bef-45b9-b9e6-0a1c2c656fed', + 'phoneInfo' => '+31123456789', + 'displayName' => '', + 'enrolledAt' => '2025-02-28T15:30:00Z', + ]; + + try { + $updatedUser = $this->auth->updateUser($user->uid, ['multifactors' => [$factor]]); + + $this->assertInstanceOf(MfaInfo::class, $updatedUser->mfaInfo); + + $check = $this->auth->updateUser($user->uid, ['resetmultifactor' => true]); + + $this->assertNotInstanceOf(MfaInfo::class, $check->mfaInfo); + } finally { + $this->auth->deleteUser($user->uid); + } + } }