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.
You can attach several authenticators to a user account. It is recommended in case of lost devices or if the user gets access on your application using multiple platforms (smartphone, laptop…).
With a Symfony application, the fastest way for a user to register additional authenticators is to use the “controller” feature.
To add a new authenticator to a user, the bundle needs to know to whom it should be added. This can be:
The current user itself e.g. from its own account
An administrator acting for another user from a dashboard
For that purpose, a User Entity Guesser service should be created. This service shall implement the interface Webauthn\Bundle\Security\Guesser\UserEntityGuesser and its unique method findUserEntity.
You can directly use the Webauthn\Bundle\Security\Guesser\CurrentUserEntityGuesser as a Symfony service. It is designed to identify the user that is currently logged in.
In the example herafter where the current user is guessed using a controller parameter. This can be used when an administrator is adding an authenticator to another user account.
App\Guesser\FromQueryParameterGuesser.php
<?phpdeclare(strict_types=1);namespaceApp\Guesser;useAssert\Assertion;useSymfony\Component\HttpFoundation\Request;useWebauthn\Bundle\Repository\PublicKeyCredentialUserEntityRepository;useWebauthn\Bundle\Security\Guesser\UserEntityGuesser;useWebauthn\PublicKeyCredentialUserEntity;finalclassFromQueryParameterGuesserimplementsUserEntityGuesser{publicfunction__construct(privatePublicKeyCredentialUserEntityRepository $userEntityRepository ) { }publicfunctionfindUserEntity(Request $request):PublicKeyCredentialUserEntity { $userHandle = $request->query->get('user_id');Assertion::string($userHandle,'User entity not found. Invalid user ID'); $user =$this->userEntityRepository->findOneByUserHandle($userHandle);Assertion::isInstanceOf($user,PublicKeyCredentialUserEntity::class,'User entity not found.');return $user; }}
In the case the current user s supposed to be administrator, the user entity can be determined using the query parameters and a route like /admin/add-authenticator/for/{user_id}.
Now you just have to enable the feature and set the routes to your options and response controllers.
config/packages/webauthn.yaml
webauthn:controllers:enabled:true# We enable the featurecreation:from_user_account:# Endpoints accessible by the user itselfoptions_path:'/profile/security/devices/add/options'# Path to the creation options controllerresult_path:'/profile/security/devices/add'# Path to the response controlleruser_entity_guesser:Webauthn\Bundle\Security\Guesser\CurrentUserEntityGuesser# See abovehide_existing_credentials:falsefrom_admin_dashboard:# Endpoint accessible by an administratoroptions_path:'/admin/security/user/{user_id}/devices/add/options'# Path to the creation options controllerresult_path:'/admin/security/user/{user_id}/devices/add'# Path to the response controlleruser_entity_guesser:App\Guesser\FromQueryParameterGuesser# From the examplehide_existing_credentials:false
As the user shall be authenticated to register a new authenticator, you should protect these routes in the security.yaml file.
The option hide_existing_credentials should be false when adding authenticators from a secured area. Set true when this action is performed by anonymous users.
config/packages/security.yaml
security:access_control: - { path:^/profile,roles:IS_AUTHENTICATED_FULLY } # We protect all the /profile path - { path:^/admin,roles:ROLE_ADMIN }
Now you can send requests to these new endpoints. For example, if you are using the Javascript library, the calls will look like as follow:
// Import the registration hookimport {useRegistration} from'webauthn-helper';// Create your register function.// By default the urls are "/register" and "/register/options"// but you can change those urls if needed.constregister=useRegistration({ actionUrl:'/profile/security/devices/add', optionsUrl:'/profile/security/devices/add/options'});// We can call this register function whenever we need// No "username" or "displayName" parameters are needed// as the user entity is guessed by the dedicated serviceregister({}).then((response) =>console.log('Registration success')).catch((error) =>console.log('Registration failure'));
Creation Profile
The defaultcreation profile is used. You can change it using the dedicated option.
You can customize the responses returned by the controllers by using custom handlers. This could be useful when you want to return additional information to your application.
There are 3 types of responses and handlers:
Creation options,
Success,
Failure.
Creation Options Handler
This handler is called during the registration of a authenticator and has to implement the interface Webauthn\Bundle\Security\Handler\CreationOptionsHandler.
config/packages/webauthn.yaml
webauthn:controllers:enabled:truecreation:from_user_account:…options_handler:…# Your handler here
Success Handler
This handler is called when a client sends a valid assertion from the authenticator. This handler shall implement the interface Webauthn\Bundle\Security\Handler\SuccessHandler. The default handler is Webauthn\Bundle\Service\DefaultSuccessHandler.
config/packages/webauthn.yaml
webauthn:controllers:enabled:truecreation:from_user_account:…success_handler:…# Your handler here
Failure Handler
This handler is called when an error occurred during the process. This handler shall implement the interface Webauthn\Bundle\Security\Handler\SuccessHandler. The default handler is Webauthn\Bundle\Service\DefaultFailureHandler.
config/packages/webauthn.yaml
webauthn:controllers:enabled:truecreation:from_user_account:…failure_handler:…# Your handler here