Additional Authenticators
Users can register multiple authenticators to their account for backup purposes or to use different devices. This page explains how to manage multiple authenticators per user.
Why Multiple Authenticators?
Allowing users to register multiple authenticators provides several benefits:
Backup authenticators: If a user loses their primary device, they can still access their account
Multiple devices: Use work laptop, personal phone, and security key
Device upgrades: Smoothly transition when replacing devices
Shared accounts: Family members can each use their own authenticator (if your security policy allows)
Listing User Authenticators
Display all authenticators registered to the current user:
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Repository\WebauthnCredentialRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[IsGranted('ROLE_USER')]
class SecurityController extends AbstractController
{
#[Route('/security/authenticators', name: 'app_list_authenticators')]
public function listAuthenticators(
WebauthnCredentialRepository $credentialRepository
): Response {
$user = $this->getUser();
$userHandle = $user->getUserIdentifier(); // Or your user ID method
$credentials = $credentialRepository->findAllForUserEntity($userHandle);
return $this->render('security/authenticators.html.twig', [
'credentials' => $credentials,
]);
}
}{% if credentials is empty %}
<p>No authenticators registered yet.</p>
{% else %}
<table>
<thead>
<tr>
<th>Authenticator ID</th>
<th>Transports</th>
<th>Counter</th>
<th>Registered</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for credential in credentials %}
<tr>
<td>{{ credential.publicKeyCredentialId|slice(0, 16) }}...</td>
<td>{{ credential.transports|join(', ') }}</td>
<td>{{ credential.counter }}</td>
<td>{{ credential.createdAt|date('Y-m-d H:i') }}</td>
<td>
<a href="{{ path('app_remove_authenticator', {id: credential.id}) }}">
Remove
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
<p>
<a href="{{ path('app_add_authenticator') }}" class="button">
Add New Authenticator
</a>
</p>Removing Authenticators
Allow users to remove authenticators they no longer use:
#[Route('/security/authenticators/{id}/remove', name: 'app_remove_authenticator')]
#[IsGranted('ROLE_USER')]
public function removeAuthenticator(
string $id,
WebauthnCredentialRepository $credentialRepository
): Response {
$user = $this->getUser();
$userHandle = $user->getUserIdentifier();
// Find the credential
$credential = $credentialRepository->find($id);
if (!$credential || $credential->userHandle !== $userHandle) {
throw $this->createNotFoundException('Authenticator not found');
}
// Check if this is the last authenticator
$userCredentials = $credentialRepository->findAllForUserEntity($userHandle);
if (count($userCredentials) === 1) {
$this->addFlash('error', 'Cannot remove your last authenticator');
return $this->redirectToRoute('app_list_authenticators');
}
// Remove the authenticator
$credentialRepository->remove($credential);
$this->addFlash('success', 'Authenticator removed successfully');
return $this->redirectToRoute('app_list_authenticators');
}Important Security Considerations:
Always verify the authenticator belongs to the current user before removal
Prevent users from removing their last authenticator (would lock them out)
Consider requiring re-authentication before removal for sensitive accounts
Log authenticator additions and removals for security auditing
Naming Authenticators
Allow users to give friendly names to their authenticators for easier management:
use Doctrine\ORM\Mapping as ORM;
use Webauthn\PublicKeyCredentialSource;
#[ORM\Entity]
class WebauthnCredential extends PublicKeyCredentialSource
{
#[ORM\Id]
#[ORM\Column(type: 'string')]
private string $id;
#[ORM\Column(type: 'string', nullable: true)]
private ?string $name = null;
#[ORM\Column(type: 'datetime_immutable')]
private \DateTimeImmutable $createdAt;
#[ORM\Column(type: 'datetime_immutable', nullable: true)]
private ?\DateTimeImmutable $lastUsedAt = null;
public function setName(?string $name): void
{
$this->name = $name;
}
public function getName(): ?string
{
return $this->name;
}
public function updateLastUsedAt(): void
{
$this->lastUsedAt = new \DateTimeImmutable();
}
// ... other methods
}Then allow users to set names during or after registration:
<form ...>
<label for="authenticator_name">Authenticator Name (optional)</label>
<input
type="text"
id="authenticator_name"
name="authenticator_name"
placeholder="e.g., My iPhone, Work Laptop, YubiKey"
>
<input type="hidden" id="attestation" name="attestation">
<button type="submit">Register New Authenticator</button>
</form>Best Practices
Encourage Backup Authenticators
Prompt users to register a backup authenticator after their first registration:
Authenticator Metadata
Display useful information about each authenticator:
Transport types: USB, NFC, Bluetooth, Internal
Last used date: Help users identify unused authenticators
Registration date: Track when each authenticator was added
AAGUID: Identify the authenticator model (if available)
Security Recommendations
Minimum authenticators: Consider requiring at least 2 authenticators for privileged accounts
Maximum authenticators: Limit to prevent abuse (e.g., 10 per user)
Inactive authenticators: Automatically remove authenticators not used for a long period
Notification: Email users when authenticators are added or removed
See Also
User Registration - Register the first authenticator
User Authentication - Authenticate with any registered authenticator
Advanced Behaviors - Advanced configuration options
Last updated
Was this helpful?