Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
}
}