From bb3ce82456ec6541b243f780544b6247111f6d16 Mon Sep 17 00:00:00 2001 From: Thijs Lacquet Date: Thu, 27 Feb 2025 13:15:52 +0100 Subject: [PATCH 1/6] Add the ability to reset multi factor authentication --- docs/user-management.rst | 13 +++++++++++++ src/Firebase/Auth.php | 5 +++++ src/Firebase/Contract/Auth.php | 8 ++++++++ src/Firebase/Request/EditUserTrait.php | 4 ++++ src/Firebase/Request/UpdateUser.php | 20 ++++++++++++++++++++ 5 files changed, 50 insertions(+) diff --git a/docs/user-management.rst b/docs/user-management.rst index b7579bb2..fe158e42 100644 --- a/docs/user-management.rst +++ b/docs/user-management.rst @@ -269,6 +269,7 @@ 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. ``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. ====================== ============ =========== @@ -322,6 +323,18 @@ Enable a user $updatedUser = $auth->enableUser($uid); +********************************* +Reset multi factor authentication +********************************* + +The Firebase Admin SDK allows removing all enrolled factors for multi factor authentication for a user: + +.. code-block:: php + + $uid = 'some-uid'; + + $updatedUser = $auth->resetMultiFactor($uid); + ****************** Custom user claims ****************** diff --git a/src/Firebase/Auth.php b/src/Firebase/Auth.php index 65a1e68b..e86f411a 100644 --- a/src/Firebase/Auth.php +++ b/src/Firebase/Auth.php @@ -235,6 +235,11 @@ public function disableUser(Stringable|string $uid): UserRecord return $this->updateUser($uid, UpdateUser::new()->markAsDisabled()); } + public function resetMultiFactor(Stringable|string $uid): UserRecord + { + return $this->updateUser($uid, UpdateUser::new()->resetMultiFactor()); + } + public function deleteUser(Stringable|string $uid): void { $uid = Uid::fromString($uid)->value; diff --git a/src/Firebase/Contract/Auth.php b/src/Firebase/Contract/Auth.php index dac2a3e5..2247b4f9 100644 --- a/src/Firebase/Contract/Auth.php +++ b/src/Firebase/Contract/Auth.php @@ -165,6 +165,14 @@ public function enableUser(Stringable|string $uid): UserRecord; */ public function disableUser(Stringable|string $uid): UserRecord; + /** + * @param Stringable|non-empty-string $uid + * + * @throws Exception\AuthException + * @throws Exception\FirebaseException + */ + public function resetMultiFactor(Stringable|string $uid): UserRecord; + /** * @param Stringable|non-empty-string $uid * 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..1ad1f7bd 100644 --- a/src/Firebase/Request/UpdateUser.php +++ b/src/Firebase/Request/UpdateUser.php @@ -152,6 +152,13 @@ public static function withProperties(array $properties): self $request, ); + break; + + case 'resetmultifactor': + if ($value === true) { + $request = $request->resetMultiFactor(); + } + break; } } @@ -205,6 +212,19 @@ public function withRemovedEmail(): self return $request; } + public function resetMultiFactor(): self + { + $request = clone $this; + + if (is_null($request->multiFactor)) { + $request->multiFactor = []; + } + + $request->multiFactor['enrolledFactors'] = []; + + return $request; + } + /** * @param array $customAttributes */ From 2bed3855e576e48fffddd92d5e569852f5cd9923 Mon Sep 17 00:00:00 2001 From: ThijsLacquet Date: Thu, 27 Feb 2025 13:46:20 +0100 Subject: [PATCH 2/6] Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 618147e8..d845ab3a 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 remove all enrolled factors for multi factor authentication for a user. + ## [7.17.0] - 2025-02-22 ### Added From 45e1dff2dd42d85b343c17328d6e1ec194982c3d Mon Sep 17 00:00:00 2001 From: Thijs Lacquet Date: Fri, 28 Feb 2025 15:17:23 +0100 Subject: [PATCH 3/6] Add withMultiFactors and tests --- docs/user-management.rst | 34 +++++++++------- src/Firebase/Auth.php | 5 --- src/Firebase/Contract/Auth.php | 8 ---- src/Firebase/Request/UpdateUser.php | 28 ++++++++++++- tests/Integration/Request/UpdateUserTest.php | 41 ++++++++++++++++++++ 5 files changed, 89 insertions(+), 27 deletions(-) diff --git a/docs/user-management.rst b/docs/user-management.rst index fe158e42..a016a8fc 100644 --- a/docs/user-management.rst +++ b/docs/user-management.rst @@ -269,7 +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. +``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. ====================== ============ =========== @@ -323,18 +324,6 @@ Enable a user $updatedUser = $auth->enableUser($uid); -********************************* -Reset multi factor authentication -********************************* - -The Firebase Admin SDK allows removing all enrolled factors for multi factor authentication for a user: - -.. code-block:: php - - $uid = 'some-uid'; - - $updatedUser = $auth->resetMultiFactor($uid); - ****************** Custom user claims ****************** @@ -409,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/Auth.php b/src/Firebase/Auth.php index e86f411a..65a1e68b 100644 --- a/src/Firebase/Auth.php +++ b/src/Firebase/Auth.php @@ -235,11 +235,6 @@ public function disableUser(Stringable|string $uid): UserRecord return $this->updateUser($uid, UpdateUser::new()->markAsDisabled()); } - public function resetMultiFactor(Stringable|string $uid): UserRecord - { - return $this->updateUser($uid, UpdateUser::new()->resetMultiFactor()); - } - public function deleteUser(Stringable|string $uid): void { $uid = Uid::fromString($uid)->value; diff --git a/src/Firebase/Contract/Auth.php b/src/Firebase/Contract/Auth.php index 2247b4f9..dac2a3e5 100644 --- a/src/Firebase/Contract/Auth.php +++ b/src/Firebase/Contract/Auth.php @@ -165,14 +165,6 @@ public function enableUser(Stringable|string $uid): UserRecord; */ public function disableUser(Stringable|string $uid): UserRecord; - /** - * @param Stringable|non-empty-string $uid - * - * @throws Exception\AuthException - * @throws Exception\FirebaseException - */ - public function resetMultiFactor(Stringable|string $uid): UserRecord; - /** * @param Stringable|non-empty-string $uid * diff --git a/src/Firebase/Request/UpdateUser.php b/src/Firebase/Request/UpdateUser.php index 1ad1f7bd..103f45dd 100644 --- a/src/Firebase/Request/UpdateUser.php +++ b/src/Firebase/Request/UpdateUser.php @@ -159,6 +159,11 @@ public static function withProperties(array $properties): self $request = $request->resetMultiFactor(); } + break; + + case 'multifactors': + $request = $request->withMultiFactors($value); + break; } } @@ -212,6 +217,27 @@ public function withRemovedEmail(): self return $request; } + /** + * @param array $enrollments + */ + public function withMultiFactors(array $enrollments): self + { + $request = clone $this; + + if (is_null($request->multiFactor)) { + $request->multiFactor = []; + } + + $request->multiFactor['enrollments'] = $enrollments; + + return $request; + } + public function resetMultiFactor(): self { $request = clone $this; @@ -220,7 +246,7 @@ public function resetMultiFactor(): self $request->multiFactor = []; } - $request->multiFactor['enrolledFactors'] = []; + $request->multiFactor['enrollments'] = []; return $request; } diff --git a/tests/Integration/Request/UpdateUserTest.php b/tests/Integration/Request/UpdateUserTest.php index df34a318..88d43f56 100644 --- a/tests/Integration/Request/UpdateUserTest.php +++ b/tests/Integration/Request/UpdateUserTest.php @@ -203,4 +203,45 @@ public function timeOfLastPasswordUpdateIsIncluded(): void $this->auth->deleteUser($user->uid); } } + + #[Test] + public function setMultiFactor(): void + { + $user = $this->auth->createUser(CreateUser::new()); + + $factor = [ + 'mfaEnrollmentId' => '85dc3f7b-7bef-45b9-b9e6-0a1c2c656fed', + 'phoneInfo' => '+31123456789', + 'displayName' => '', + 'enrolledAt' => '2025-02-28T15:30:00Z', + ]; + + $check = $this->auth->updateUser($user->uid, ['enrolledfactors' => [$factor]]); + + $this->assertEquals($factor['mfaEnrollmentId'], $check->mfaInfo?->mfaEnrollmentId); + $this->assertEquals($factor['phoneInfo'], $check->mfaInfo?->phoneInfo); + $this->assertEquals($factor['displayName'], $check->mfaInfo?->displayName); + $this->assertEquals($factor['enrolledAt'], $check->mfaInfo?->enrolledAt); + } + + #[Test] + public function resetMultiFactor(): void + { + $user = $this->auth->createUser(CreateUser::new()); + + $factor = [ + 'mfaEnrollmentId' => '85dc3f7b-7bef-45b9-b9e6-0a1c2c656fed', + 'phoneInfo' => '+31123456789', + 'displayName' => '', + 'enrolledAt' => '2025-02-28T15:30:00Z', + ]; + + $updatedUser = $this->auth->updateUser($user->uid, ['enrolledfactors' => [$factor]]); + + $this->assertNotNull($updatedUser->mfaInfo); + + $check = $this->auth->updateUser($user->uid, ['resetmultifactor' => true]); + + $this->assertNull($check->mfaInfo); + } } From 75d662024f485820c04af18d8ab6a129e545d56a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Gamez?= Date: Tue, 4 Mar 2025 01:21:02 +0100 Subject: [PATCH 4/6] Fix tests --- tests/Integration/Request/UpdateUserTest.php | 39 ++++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/tests/Integration/Request/UpdateUserTest.php b/tests/Integration/Request/UpdateUserTest.php index 88d43f56..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; @@ -207,7 +209,9 @@ public function timeOfLastPasswordUpdateIsIncluded(): void #[Test] public function setMultiFactor(): void { - $user = $this->auth->createUser(CreateUser::new()); + $user = $this->auth->createUser( + CreateUser::new()->withVerifiedEmail(self::randomEmail(__FUNCTION__)), + ); $factor = [ 'mfaEnrollmentId' => '85dc3f7b-7bef-45b9-b9e6-0a1c2c656fed', @@ -216,18 +220,27 @@ public function setMultiFactor(): void 'enrolledAt' => '2025-02-28T15:30:00Z', ]; - $check = $this->auth->updateUser($user->uid, ['enrolledfactors' => [$factor]]); + $enrolledAt = DT::toUTCDateTimeImmutable($factor['enrolledAt']); + + try { + $check = $this->auth->updateUser($user->uid, ['multifactors' => [$factor]]); - $this->assertEquals($factor['mfaEnrollmentId'], $check->mfaInfo?->mfaEnrollmentId); - $this->assertEquals($factor['phoneInfo'], $check->mfaInfo?->phoneInfo); - $this->assertEquals($factor['displayName'], $check->mfaInfo?->displayName); - $this->assertEquals($factor['enrolledAt'], $check->mfaInfo?->enrolledAt); + $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()); + $user = $this->auth->createUser( + CreateUser::new()->withVerifiedEmail(self::randomEmail(__FUNCTION__)), + ); $factor = [ 'mfaEnrollmentId' => '85dc3f7b-7bef-45b9-b9e6-0a1c2c656fed', @@ -236,12 +249,16 @@ public function resetMultiFactor(): void 'enrolledAt' => '2025-02-28T15:30:00Z', ]; - $updatedUser = $this->auth->updateUser($user->uid, ['enrolledfactors' => [$factor]]); + try { + $updatedUser = $this->auth->updateUser($user->uid, ['multifactors' => [$factor]]); - $this->assertNotNull($updatedUser->mfaInfo); + $this->assertInstanceOf(MfaInfo::class, $updatedUser->mfaInfo); - $check = $this->auth->updateUser($user->uid, ['resetmultifactor' => true]); + $check = $this->auth->updateUser($user->uid, ['resetmultifactor' => true]); - $this->assertNull($check->mfaInfo); + $this->assertNotInstanceOf(MfaInfo::class, $check->mfaInfo); + } finally { + $this->auth->deleteUser($user->uid); + } } } From e9da2aae60b47d5eced96a6cf08b6c7bc9767614 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Gamez?= Date: Tue, 4 Mar 2025 01:32:17 +0100 Subject: [PATCH 5/6] Use shorthand for initializing the multiFactor request property --- src/Firebase/Request/UpdateUser.php | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/Firebase/Request/UpdateUser.php b/src/Firebase/Request/UpdateUser.php index 103f45dd..aaf0becc 100644 --- a/src/Firebase/Request/UpdateUser.php +++ b/src/Firebase/Request/UpdateUser.php @@ -228,11 +228,7 @@ public function withRemovedEmail(): self public function withMultiFactors(array $enrollments): self { $request = clone $this; - - if (is_null($request->multiFactor)) { - $request->multiFactor = []; - } - + $request->multiFactor ??= []; $request->multiFactor['enrollments'] = $enrollments; return $request; @@ -241,11 +237,7 @@ public function withMultiFactors(array $enrollments): self public function resetMultiFactor(): self { $request = clone $this; - - if (is_null($request->multiFactor)) { - $request->multiFactor = []; - } - + $request->multiFactor ??= []; $request->multiFactor['enrollments'] = []; return $request; From 71e6bcf06ee60679005ae85f9b53d6a11f28cf3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Gamez?= Date: Wed, 5 Mar 2025 23:37:07 +0100 Subject: [PATCH 6/6] Update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d845ab3a..f9455451 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ Please read about the future of the Firebase Admin PHP SDK on the ### Added -* It is now possible to remove all enrolled factors for multi factor authentication for a user. +* It is now possible to configure multi factor authentication for a user. ## [7.17.0] - 2025-02-22