Register Additional Authenticators

In some circumstances, you may need to register a new authenticator for a user e.g. when adding a new authenticator or when an administrator acts as another user to replace a lost device.

It is possible to perform this ceremony programmatically.

The Easy Way

The procedure is the same as the one described in this page, except that you don’t have to save the user entity again.

The Hard Way

The procedure is the same as the one described in this page.

The Symfony Way

The following procedure is only available with the version 3.1.0 of the framework. For previous versions, please refer to the Hard Way above.

With a Symfony application, the fastest way for a user to register additional authenticators is to use the helper Webauthn\Bundle\Service\AuthenticatorRegistrationHelper provided by the bundle.

In the example below, we will create 2 routes: the first one to get the options, te second one to verify the authenticator response. These routes will return JSON responses, but you are free to use Twig templates or any of response type.

src/Controller/AddAuthenticatorController.php
<?php

declare(strict_types=1);

namespace App\Controller;

use Assert\Assertion;
use Symfony\Bridge\PsrHttpMessage\HttpMessageFactoryInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Throwable;
use Webauthn\AuthenticatorAttestationResponseValidator;
use Webauthn\Bundle\Security\Storage\SessionStorage;
use Webauthn\Bundle\Service\AuthenticatorRegistrationHelper;
use Webauthn\Bundle\Service\PublicKeyCredentialCreationOptionsFactory;
use Webauthn\PublicKeyCredentialLoader;
use Webauthn\PublicKeyCredentialSourceRepository;
use Webauthn\PublicKeyCredentialUserEntity;

class AddAuthenticatorController extends AbstractController
{
    /**
     * @var AuthenticatorRegistrationHelper
     */
    private $helper;

    public function __construct(HttpMessageFactoryInterface $httpMessageFactory, ValidatorInterface $validator, SerializerInterface $serializer, PublicKeyCredentialCreationOptionsFactory $publicKeyCredentialCreationOptionsFactory, PublicKeyCredentialSourceRepository $publicKeyCredentialSourceRepository, PublicKeyCredentialLoader $publicKeyCredentialLoader, AuthenticatorAttestationResponseValidator $authenticatorAttestationResponseValidator, SessionStorage $optionsStorage)
    {
        $this->helper = new AuthenticatorRegistrationHelper(
            $publicKeyCredentialCreationOptionsFactory,
            $serializer,
            $validator,
            $publicKeyCredentialSourceRepository,
            $publicKeyCredentialLoader,
            $authenticatorAttestationResponseValidator,
            $optionsStorage,
            $httpMessageFactory
        );
    }

    /**
     * @Route(name="add_authenticator_options", path="/add/device/options", schemes={"https"}, methods={"POST"})
     */
    public function getOptions(Request $request): Response
    {
        try {
            $userEntity = $this->getUser();
            Assertion::isInstanceOf($userEntity, PublicKeyCredentialUserEntity::class, 'Invalid user');
            $publicKeyCredentialCreationOptions = $this->helper->generateOptions($userEntity, $request);

            return $this->json($publicKeyCredentialCreationOptions);
        } catch (Throwable $e) {
            return $this->json([
                'status' => 'failed',
                'errorMessage' => 'Invalid request',
            ], 400);
        }

    }

    /**
     * @Route(name="add_authenticator", path="/add/device", schemes={"https"}, methods={"POST"})
     */
    public function addAuthenticator(Request $request): Response
    {
        try {
            $userEntity = $this->getUser();
            Assertion::isInstanceOf($userEntity, PublicKeyCredentialUserEntity::class, 'Invalid user');
            $this->helper ->validateResponse($userEntity, $request);
            return $this->json([
                'status' => 'ok',
                'errorMessage' => '',
            ]);
        } catch (Throwable $e) {
            return $this->json([
                'status' => 'failed',
                'errorMessage' => 'Invalid request',
            ], 400);
        }
    }
}

This controller may be directly integrated in the bundle.

By default the options profile is default. You can change it setting the profile name as third argument of the method generateOptions.

$publicKeyCredentialCreationOptions = $this->helper->generateOptions(
    $userEntity,
    $request,
    'profile1'
);
config/packages/security.yaml
security:
    access_control:
        - { path: ^/add/device,  roles: IS_AUTHENTICATED_FULLY }

Last updated

Was this helpful?