Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable configuring multi factor authentication for a user. #993

Merged
merged 7 commits into from
Mar 5, 2025
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 @@
$request,
);

break;

case 'resetmultifactor':
if ($value === true) {
$request = $request->resetMultiFactor();

Check warning on line 159 in src/Firebase/Request/UpdateUser.php

View check run for this annotation

Codecov / codecov/patch

src/Firebase/Request/UpdateUser.php#L158-L159

Added lines #L158 - L159 were not covered by tests
}

break;

Check warning on line 162 in src/Firebase/Request/UpdateUser.php

View check run for this annotation

Codecov / codecov/patch

src/Firebase/Request/UpdateUser.php#L162

Added line #L162 was not covered by tests

case 'multifactors':
$request = $request->withMultiFactors($value);

Check warning on line 165 in src/Firebase/Request/UpdateUser.php

View check run for this annotation

Codecov / codecov/patch

src/Firebase/Request/UpdateUser.php#L165

Added line #L165 was not covered by tests

break;
}
}
Expand Down Expand Up @@ -205,6 +217,32 @@
return $request;
}

/**
* @param array<array-key, array{
* 'mfaEnrollmentId'?: string,
* 'displayName': string,
* 'phoneInfo': string,
* 'enrolledAt'?: string,
* }> $enrollments
*/
public function withMultiFactors(array $enrollments): self

Check warning on line 228 in src/Firebase/Request/UpdateUser.php

View check run for this annotation

Codecov / codecov/patch

src/Firebase/Request/UpdateUser.php#L228

Added line #L228 was not covered by tests
{
$request = clone $this;
$request->multiFactor ??= [];
$request->multiFactor['enrollments'] = $enrollments;

Check warning on line 232 in src/Firebase/Request/UpdateUser.php

View check run for this annotation

Codecov / codecov/patch

src/Firebase/Request/UpdateUser.php#L230-L232

Added lines #L230 - L232 were not covered by tests

return $request;

Check warning on line 234 in src/Firebase/Request/UpdateUser.php

View check run for this annotation

Codecov / codecov/patch

src/Firebase/Request/UpdateUser.php#L234

Added line #L234 was not covered by tests
}

public function resetMultiFactor(): self

Check warning on line 237 in src/Firebase/Request/UpdateUser.php

View check run for this annotation

Codecov / codecov/patch

src/Firebase/Request/UpdateUser.php#L237

Added line #L237 was not covered by tests
{
$request = clone $this;
$request->multiFactor ??= [];
$request->multiFactor['enrollments'] = [];

Check warning on line 241 in src/Firebase/Request/UpdateUser.php

View check run for this annotation

Codecov / codecov/patch

src/Firebase/Request/UpdateUser.php#L239-L241

Added lines #L239 - L241 were not covered by tests

return $request;

Check warning on line 243 in src/Firebase/Request/UpdateUser.php

View check run for this annotation

Codecov / codecov/patch

src/Firebase/Request/UpdateUser.php#L243

Added line #L243 was not covered by tests
}

/**
* @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);
}
}
}