Skip to content

Commit

Permalink
Enable configuring multi factor authentication for a user. (#993)
Browse files Browse the repository at this point in the history
Co-authored-by: ThijsLacquet <[email protected]>
  • Loading branch information
jeromegamez and ThijsLacquet authored Mar 5, 2025
1 parent 753a6ce commit 7dd11e6
Show file tree
Hide file tree
Showing 5 changed files with 125 additions and 0 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions docs/user-management.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
====================== ============ ===========
Expand Down Expand Up @@ -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
**************************************
Expand Down
4 changes: 4 additions & 0 deletions src/Firebase/Request/EditUserTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ trait EditUserTrait

protected ?string $clearTextPassword = null;

/** @var array<string, mixed>|null */
protected ?array $multiFactor = null;

/**
* @param Stringable|mixed $uid
*/
Expand Down Expand Up @@ -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);
}

Expand Down
38 changes: 38 additions & 0 deletions src/Firebase/Request/UpdateUser.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -205,6 +217,32 @@ public function withRemovedEmail(): self
return $request;
}

/**
* @param array<array-key, array{
* 'mfaEnrollmentId'?: string,
* 'displayName': string,
* 'phoneInfo': string,
* 'enrolledAt'?: string,
* }> $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<string, mixed> $customAttributes
*/
Expand Down
58 changes: 58 additions & 0 deletions tests/Integration/Request/UpdateUserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}
}

0 comments on commit 7dd11e6

Please sign in to comment.