Only this pageAll pages
Powered by GitBook
1 of 26

v2.x

Loading...

Loading...

Webauthn In A Nutshell

Loading...

Loading...

Pre-requisites

Loading...

Loading...

Loading...

Loading...

The Webauthn Server

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Deep into the framework

Loading...

Loading...

Loading...

Loading...

Loading...

Ceremonies

In the Webauthn context, ther are two ceremonies:

  • The attestation ceremony: it corresponds to the registration of a new authenticator,

  • The assertion ceremony: it is used for the authentication of a user.

For both ceremonies, there are two steps to permform:

  1. The creation of options: these options are sent to the authenticator and indicate what to do and how.

  2. The response of the authenticator: after the user interacted with the authenticator, the authenticator compute a response that have to be verified.

Depending on the options and the capabilities of the authenticator, the user interaction may differ. It can be a simple touch on a button or a complete authentication using biometric means (PIN code, fingerprint, facial recognition…).

Installation

This framework contains several sub-packages that you don’t necessarily need. It is highly recommended to install what you need and not the whole framework.

The prefered way to install the library you need is to use composer:

composer require web-auth/webauthn-lib

Hereafter the dependency tree:

  • web-auth/webauthn-lib: this is the core library. This package can be used in any PHP project or within any popular framework (Laravel, CakePHP…)

    • web-auth/webauthn-symfony-bundle: this is a Symfony bundle that ease the integration of this authentication mechanism in your Symfony project.

      • web-auth/conformance-toolset: this component helps you to verify your application is compliant with the specification. It is meant to be used with the FIDO Alliance Tools. You usually don’t need it.

The core library also depends on web-auth/cose-lib and web-auth/metadata-service. What are these dependencies?

web-auth/cose-lib contains several cipher algorithms and COSE key support to verify the digital signatures sent by the authenticators during the creation and authentication ceremonies. These algorithms are compliant with the . This library can be used by any other PHP projects. At the moment only signature algorithms are available, but it is planned to add encryption algorithms

web-auth/metadata-service provides classes to support the . If you plan to use Attestation Statements during the creation ceremony, this service is mandatory. Please note that Attestation Statements decreases the user privacy as they may leak data that allow to identify a specific user. The use of Attestation Statements and this service are generally not recommended unless you REALLY need this information. This library can also be used by any other PHP projects.

The Relying Party

The Relying Party (or rp) corresponds to the application that will ask for the user to interact with the authenticator.

The library provides a simple class to handle the rp information: Webauthn\PublicKeyCredentialRpEntity.

This $rpEntity object will be useful for the next steps.

The ID can be null

Introduction

This outdated and not supported anymore. Please see to the documentation of the last release.

Overview of the framework

Webauthn defines an API enabling the creation and use of strong, attested, scoped, public key-based credentials by web applications, for the purpose of strongly authenticating users.

RFC8152
Fido Alliance Metadata Service

The Hard Way

To be written

, the domain or sub-domain of your application.

Even if it is optional, we highly recommend to set the application ID. If absent, the current domain will be used

The scheme, userinfo, port, path, user… are not allowed.

Example: www.sub.domain.com, sub.domain.com, domain.com but not com, www.sub.domain.com:1337, https://domain.com:443, sub.domain.com/index, https://user:[email protected].

Your application may also have a logo. You can indicate this logo as third argument. Please note that for safety reason this icon is a priori authenticated URL i.e. an image that uses the data scheme.

The Webauthn specification does not set any limit for the length of the third argument.

The icon may be ignored by browsers, especially if its length is greater than 128 bytes.

<?php

use Webauthn\PublicKeyCredentialRpEntity;

$rpEntity = new PublicKeyCredentialRpEntity(
    'ACME Webauthn Server', // The application name
    'acme.com'              // The application ID = the domain
);
<?php

use Webauthn\PublicKeyCredentialRpEntity;

$rpEntity = new PublicKeyCredentialRpEntity(
    'ACME Webauthn Server',
    'acme.com',
    'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAMAAAC6V+0/AAAAwFBMVEXm7NK41k3w8fDv7+q01Tyy0zqv0DeqyjOszDWnxjClxC6iwCu11z6y1DvA2WbY4rCAmSXO3JZDTxOiwC3q7tyryzTs7uSqyi6tzTCmxSukwi9aaxkWGga+3FLv8Ozh6MTT36MrMwywyVBziSC01TbT5ZW9z3Xi6Mq2y2Xu8Oioxy7f572qxzvI33Tb6KvR35ilwTmvykiwzzvV36/G2IPw8O++02+btyepyDKvzzifvSmw0TmtzTbw8PAAAADx8fEC59dUAAAA50lEQVQYV13RaXPCIBAG4FiVqlhyX5o23vfVqUq6mvD//1XZJY5T9xPzzLuwgKXKslQvZSG+6UXgCnFePtBE7e/ivXP/nRvUUl7UqNclvO3rpLqofPDAD8xiu2pOntjamqRy/RqZxs81oeVzwpCwfyA8A+8mLKFku9XfI0YnSKXnSYZ7ahSII+AwrqoMmEFKriAeVrqGM4O4Z+ADZIhjg3R6LtMpWuW0ERs5zunKVHdnnnMLNQqaUS0kyKkjE1aE98b8y9x9JYHH8aZXFMKO6JFMEvhucj3Wj0kY2D92HlHbE/9Vk77mD6srRZqmVEAZAAAAAElFTkSuQmCC'
);

This framework contains PHP libraries and Symfony bundle to allow developpers to integrate that authentication mechanism into their web applications.

Supported features

  • Attestation Types

    • Empty

    • Basic

    • Self

    • Private CA

    • Elliptic Curve Direct Anonymous Attestation (ECDAA)

  • Attestation Formats

    • FIDO U2F

    • Packed

    • TPM

  • Token Binding support

  • Cose Algorithms

    • RS1, RS256, RS384, RS512

    • PS256, PS384, PS512

    • ES256, ES256K, ES384, ES512

  • Extensions

    • Supported (not fully tested)

    • appid extension

Compatible Authenticators

The compliance of the framework is ensured by running unit and functional tests during its development.

It is also tested using the official FIDO Alliance testing tools. The status of the compliance tests are reported in this issue. At the time of writing (end of Oct. 2019), the main features and algorithms are supported. Full compliance with the Webatuhn specification is expected by the end of Nov. 2019.

In any case, the framework can already compatible with all authenticators except the one that use ECDAA Attestation format. As this format is very rare at that time, this framework can safely be used in production.

Authenticators

An Authenticator s a cryptographic entity used generate a public key credential and registered by a Relying Party (i.e. an application). This public key is used to authenticate by potentially verifying a user in the form of an authentication assertion and other data.

Authenticators may have additional features such as PIN code or biometric sensors (fingerprint, facial recognition…) that offer user verification.

USB device with fingerprint reader

Physical Authenticators

The authenticator may have different forms. The most common form is a USB device the user plugs into its computer. It can be a paired Bluetooth device or a card with NFC capabilities.

Software-Based Authenticators

The authenticator can also be software based and integrated in an Operating System ; for example the smartphone using Android or a laptop with Windows 10 can act as an authenticator.

Android Key

  • Android Safetynet

  • ED25519

    Webauthn compatible devices
    Android screenshot

    Token Binding

    Browsers may support the Token Binding protocol (see RFC 8471). This protocol defines a way to bind a token (the Responses in the Webauthn context) to the underlying TLS layer.

    When receiving a Webauthn Response, the property tokenBinding in the CollectedClientData object have one of the following value:

    • null: the token binding is not supported by the browser

    • "supported": the browser supports token binding, but no negociation was performed during the communication

    • "present": the browser supports token binding and is present in the response. The token binding ID is provided.

    This feature is not yet implemented in the library, but you can decide how the library will react in case of the presence of the token binding ID.

    The library provides two concrete classes for the moment:

    • Webauthn\TokenBinding\IgnoreTokenBindingHandler: the library will ignore the token binding,

    • Webauthn\TokenBinding\TokenBindingNotSupportedHandler: the library will throw an exception if the token binding is present.

    You can change this behavior by creating your own implementation. The handler must implement the interface Webauthn\TokenBinding\TokenBindingHandler.

    The Easy Way

    To Be Written

    The Hard Way

    To Be Written

    The Symfony Way

    To Be Written

    Extensions

    To be written

    The Easy Way

    The easiest way to create a Webauthn Server is to use the class Webauthn\Server.

    That’s it!

    You can now or authenticate your users.

    <?php
    
    use Webauthn\Server;
    use Webauthn\PublicKeyCredentialRpEntity;
    
    $rpEntity = new PublicKeyCredentialRpEntity(
        'Webauthn Server',
        'my.domain.com'
    );
    $publicKeyCredentialSourceRepository = …; //Your repository here. Must implement Webauthn\PublicKeyCredentialSourceRepository
    
    $server = new Server(
        $rpEntity
        $publicKeyCredentialSourceRepository
    );
    register a new authenticator

    User Authentication

    Credention Request Options

    To authenticate you user, you need to send a Webauthn\PublicKeyCredentialRequestOptions object. using your $server object, call the method generatePublicKeyCredentialRequestOptions

    In general, to authenticate your user you will ask them for their username first. With this username and your user repository, you will find the associated Webauthn\PublicKeyCredentialUserEntity.

    And with the user entity you will get all associated Public Key Credential Source objects. The credential list is used to build the Public Key Credential Request Options.

    Now send the options to the authenticator using your favorite Javascript framework, library or the example availbale in .

    Response Verification

    When the authenticator send you the computed response (i.e. the user touched the button, fingerprint reader, submitted the PIN…), you can load it and check it.

    The authenticator response looks similar to the following example:

    The library needs PSR-7 requests. In the example below, we use nyholm/psr7-server to get that request.

    Authenticate Your Users

    To authenticate your users, you need to create a PublicKeyCredentialRequestOptions object. You can create this object using the .... Similarly to the authentication registration process, there is another approach.

    The bundle provides a factory and manages profiles to ease the creation of the options. The factory is available as a public service: Webauthn\Bundle\Service\PublicKeyCredentialRequestOptionsFactory. To use it, you must first create a least one profile in your configuration file.

    No other option is needed to create a profile!

    With this profile, now we can create options with the following code lines:

    Authentication without username

    With Webauthn, it is possible to authenticate a user without username. This behavior implies several constraints:

    1. During the registration of the authenticator, a ,

    2. The user verification is required,

    3. The list of allowed authenticators must be empty

    The Symfony Way

    Symfony is a very popular framework and an official bundle is provided in the package web-auth/webauthn-symfony-bundle.

    If you use Laravel, you may be intersted in

    If you are using Symfony Flex then the bundle will automatically be installed. Otherwise you need to add it in your AppKernel.php file:

    {
        "id":"LFdoCFJTyB82ZzSJUHc-c72yraRc_1mPvGX8ToE8su39xX26Jcqd31LUkKOS36FIAWgWl6itMKqmDvruha6ywA",
        "rawId":"LFdoCFJTyB82ZzSJUHc-c72yraRc_1mPvGX8ToE8su39xX26Jcqd31LUkKOS36FIAWgWl6itMKqmDvruha6ywA",
        "response":{
            "authenticatorData":"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MBAAAAAA",
            "signature":"MEYCIQCv7EqsBRtf2E4o_BjzZfBwNpP8fLjd5y6TUOLWt5l9DQIhANiYig9newAJZYTzG1i5lwP-YQk9uXFnnDaHnr2yCKXL",
            "userHandle":"",
            "clientDataJSON":"eyJjaGFsbGVuZ2UiOiJ4ZGowQ0JmWDY5MnFzQVRweTBrTmM4NTMzSmR2ZExVcHFZUDh3RFRYX1pFIiwiY2xpZW50RXh0ZW5zaW9ucyI6e30sImhhc2hBbGdvcml0aG0iOiJTSEEtMjU2Iiwib3JpZ2luIjoiaHR0cDovL2xvY2FsaG9zdDozMDAwIiwidHlwZSI6IndlYmF1dGhuLmdldCJ9"
        },
        "type":"public-key"
    }
    the Javascript page

    Javascript

    You will interact with the authenticators through an HTML page and Javascript using the Webauthn API.

    No script is provided with the library because it could become hard to manage all types of scripts and application specificity. However, you will find on this page two JS scripts: the first one for the registration of an authenticator (Attestation Ceremony). The other one for the user authentication (Assertion Ceremony).

    Feel free to adapt these script for your application (React, Vue…).

    attestaion.js
    const publicKey = "{PLACE YOUR CREDENTIAL OPTIONS HERE}";
    
    function arrayToBase64String(a) {
        return btoa(String.fromCharCode(...a));
    }
    
    function base64url2base64(input) {
        input = input
            .replace(/=/g, "")
            .replace(/-/g, '+')
            .replace(/_/g, '/');
    
        const pad = input.length % 4;
        if(pad) {
            if(pad === 1) {
                throw new Error('InvalidLengthError: Input base64url string is the wrong length to determine padding');
            }
            input += new Array(5-pad).join('=');
        }
    
        return input;
    }
    
    publicKey.challenge = Uint8Array.from(window.atob(base64url2base64(publicKey.challenge)), function(c){return c.charCodeAt(0);});
    publicKey.user.id = Uint8Array.from(window.atob(publicKey.user.id), function(c){return c.charCodeAt(0);});
    if (publicKey.excludeCredentials) {
        publicKey.excludeCredentials = publicKey.excludeCredentials.map(function(data) {
            data.id = Uint8Array.from(window.atob(base64url2base64(data.id)), function(c){return c.charCodeAt(0);});
            return data;
        });
    }
    
    navigator.credentials.create({ 'publicKey': publicKey })
        .then(function(data){
            const publicKeyCredential = {
                id: data.id,
                type: data.type,
                rawId: arrayToBase64String(new Uint8Array(data.rawId)),
                response: {
                    clientDataJSON: arrayToBase64String(new Uint8Array(data.response.clientDataJSON)),
                    attestationObject: arrayToBase64String(new Uint8Array(data.response.attestationObject))
                }
            };
            
            //Send the response to your server
            // You can use JSON.stringify(publicKeyCredential); to have the JSON object as a string
        })
        .catch(function(error){
            alert('Open your browser console!');
            console.log('FAIL', error);
        });

    Relying Party ID

    As mentioned earlier, it is preferable to indicate the Relying Party ID. By default it is set to null i.e. the current domain is used.

    Challenge Length

    By default, the length of the challenge is 32 bytes. You may need to select a smaller or higher length. This length can be configured for each profile:

    Timeout

    The default timeout is set to 60 seconds (60 000 milliseconds). You can change this value as follow:

    User Verification

    By default, the authenticator will verify the user if it is possible. You can enforce or disable the user verification using this option.

    Extensions

    The mechanism for generating public key credentials, as well as requesting and generating Authentication assertions, can be extended to suit particular use cases. Each case is addressed by defining a registration extension.

    The example below is tatolly fictive. Some extensions are defined in the specification but the supports depends on the authenticators and on the relying parties.

    In case of failure, you should continue with the standard authentication process i.e. by asking the username of the user.

    Examples

    The Easy Way

    Selection criterias for the registration of the authenticator:

    The Request Options:

    Resident Key must have been asked
    use Webauthn\AuthenticatorSelectionCriteria;
    use Webauthn\PublicKeyCredentialCreationOptions;
    
    $authenticatorSelectionCriteria = new AuthenticatorSelectionCriteria(
        AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_NO_PREFERENCE,
        true,                                                                  // Resident key required
        AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_REQUIRED // User verification required
    );
    Entities

    At the moment, only Doctrine is supported, however there is no technical constraint to allow other data storage systems.

    • With Doctrine

    Configuration

    The minimal configuration requires the user repository and the pk credential source repository.

    Now you may want to:

    • Register your first authenticators,

    • Authenticate your users.

    this project: https://github.com/asbiin/laravel-webauthn
    <?php
    
    use Webauthn\PublicKeyCredentialRequestOptions;
    use Webauthn\PublicKeyCredentialUserEntity;
    
    // UseEntity found using the username.
    $userEntity = $userEntityRepository->findWebauthnUserByUsername('john.doe');
    
    // Get the list of authenticators associated to the user
    $credentialSources = $credentialSourceRepository->findAllForUserEntity($userEntity);
    
    // Convert the Credential Sources into Public Key Credential Descriptors
    $allowedCredentials = array_map(function (PublicKeyCredentialSource $credential) {
    return $credential->getPublicKeyCredentialDescriptor();
    }, $credentialSources);
    
    // We generate the set of options.
    $publicKeyCredentialRequestOptions = $server->generatePublicKeyCredentialRequestOptions(
        PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_PREFERRED, // Default value
        $allowedCredentials
    );
    <?php
    
    use Nyholm\Psr7\Factory\Psr17Factory;
    use Nyholm\Psr7Server\ServerRequestCreator;
    
    $psr17Factory = new Psr17Factory();
    $creator = new ServerRequestCreator(
        $psr17Factory, // ServerRequestFactory
        $psr17Factory, // UriFactory
        $psr17Factory, // UploadedFileFactory
        $psr17Factory  // StreamFactory
    );
    
    $serverRequest = $creator->fromGlobals();
    
    try {
        $publicKeyCredentialSource = $server->loadAndCheckAssertionResponse(
            '_The authenticator response you received…',
            $publicKeyCredentialRequestOptions, // The options you stored during the previous step
            $userEntity,                        // The user entity
            $serverRequest                      // The PSR-7 request
        );
        
        //If everything is fine, this means the user has correctly been authenticated using the
        // authenticator defined in $publicKeyCredentialSource
    } catch(\Throwable $exception) {
        // Something went wrong!
    }
    assertion.js
    const publicKey = "{PLACE YOUR CREDENTIAL OPTIONS HERE}";
    function arrayToBase64String(a) {
        return btoa(String.fromCharCode(...a));
    }
    function base64url2base64(input) {
        input = input
            .replace(/-/g, '+')
            .replace(/_/g, '/');
        const pad = input.length % 4;
        if(pad) {
            if(pad === 1) {
                throw new Error('InvalidLengthError: Input base64url string is the wrong length to determine padding');
            }
            input += new Array(5-pad).join('=');
        }
        return input;
    }
    publicKey.challenge = Uint8Array.from(window.atob(base64url2base64(publicKey.challenge)), function(c){return c.charCodeAt(0);});
    if (publicKey.allowCredentials) {
        publicKey.allowCredentials = publicKey.allowCredentials.map(function(data) {
            data.id = Uint8Array.from(window.atob(base64url2base64(data.id)), function(c){return c.charCodeAt(0);});
            return data;
        });
    }
    navigator.credentials.get({ 'publicKey': publicKey })
        .then(function(data){
            const publicKeyCredential = {
                id: data.id,
                type: data.type,
                rawId: arrayToBase64String(new Uint8Array(data.rawId)),
                response: {
                    authenticatorData: arrayToBase64String(new Uint8Array(data.response.authenticatorData)),
                    clientDataJSON: arrayToBase64String(new Uint8Array(data.response.clientDataJSON)),
                    signature: arrayToBase64String(new Uint8Array(data.response.signature)),
                    userHandle: data.response.userHandle ? arrayToBase64String(new Uint8Array(data.response.userHandle)) : null
                }
            };
    
            //Send the response to your server
            // You can use JSON.stringify(publicKeyCredential); to have the JSON object as a string
        })
        .catch(function(error){
            alert('Open your browser console!');
            console.log('FAIL', error);
        });
    app/config/webauthn.yaml
    webauthn:
        request_profiles:
            acme: ~
    use Webauthn\Bundle\Service\PublicKeyCredentialCreationOptionsFactory;
    use Webauthn\PublicKeyCredentialUserEntity;
    
    // UseEntity found using the username.
    $userEntity = $userEntityRepository->findWebauthnUserByUsername('john.doe');
    
    // Get the list of authenticators associated to the user
    $credentialSources = $credentialSourceRepository->findAllForUserEntity($userEntity);
    
    // Convert the Credential Sources into Public Key Credential Descriptors
    $allowedCredentials = array_map(function (PublicKeyCredentialSource $credential) {
    return $credential->getPublicKeyCredentialDescriptor();
    }, $credentialSources);
    
    $publicKeyCredentialCreationOptions = $container
        ->get(PublicKeyCredentialCreationOptionsFactory::class)
        ->create('acme', $allowedCredentials)
    ;
    app/config/webauthn.yaml
    webauthn:
        request_profiles:
            acme:
                rp_id: 'example.com'
    app/config/webauthn.yaml
    webauthn:
        request_profiles:
            acme:
                challenge_length: 16
    app/config/webauthn.yaml
    webauthn:
        creation_profiles:
            acme:
                timeout: 30000
    app/config/webauthn.yaml
    webauthn:
        creation_profiles:
            acme:
                user_verification: !php/const Webauthn\AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_REQUIRED
    app/config/webauthn.yaml
    webauthn:
        creation_profiles:
            acme:
                extensions:
                    loc: true
                    txAuthSimple: 'Please add your new authenticator'
    <?php
    
    use Webauthn\PublicKeyCredentialRequestOptions;
    
    $ublicKeyCredentialRequestOptions = $server->generatePublicKeyCredentialRequestOptions(
        PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_REQUIRED,
    );
    src/AppKernel.php
    <?php
    
    public function registerBundles()
    {
        $bundles = [
            // ...
            new Webauthn\Bundle\WebauthnBundle(),
        ];
    }
    app/config/webauthn.yaml
    webauthn:
        credential_repository: 'App\Repository\PublicKeyCredentialSourceRepository'
        user_repository: 'App\Repository\PublicKeyCredentialUserEntityRepository'

    Authenticator Selection Criteria

    By default, any type of authenticator can be used by your users and interact with you application. In certain circumstances, you may need to select specific authenticators e.g. when user verification is required.

    The Webauthn API and this library are allow you to define a set of options to disallow the registration of authenticators that do not fulfill with the conditions.

    The class Webauthn\AuthenticatorSelectionCriteria is designed for this purpose. It is used when generating the Webauthn\PublicKeyCredentialCreationOptions object.

    Available Criterias

    Authenticator Attachment Modality

    You can indicate if the authenticator must be attached to the client (platform authenticator i.e. it is usually not removable from the client device) or must be detached (roaming authenticator).

    Possible values are:

    • AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_NO_PREFERENCE: there is no requirement (default value),

    • AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_PLATFORM: the authenticator must be attached,

    • AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM: must be a roaming authenticator.

    A primary use case for platform authenticators is to register a particular client device as a "trusted device" for future authentication. This gives the user the convenience benefit of not needing a roaming authenticator, e.g., the user will not have to dig around in their pocket for their key fob or phone.

    Resident Key

    When this criteria is set to true, a Public Key Credential Source will be stored in the authenticator, client or client device. Such storage requires an authenticator capable to store such resident credential.

    This criteria is needed if you want to .

    User Verification

    User verification may be instigated through various authorization gesture modalities: a touch plus PIN code, password entry, or biometric recognition (presenting a fingerprint).The intent is to be able to distinguish individual users.

    Possible values are:

    • AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_NO_PREFERENCE: the authenticator should verify the user if possible (default value),

    • AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_REQUIRED: the authenticator must verify the user,

    • AuthenticatorSelectionCriteria::USER_VERIFICATION_DISCOURAGED: the authenticator must NOT try to verify the user. This option may be set in the interest of minimizing disruption to the user interaction flow.

    Examples

    The Easy Way

    The Hard Way

    To be written

    The Symfony Way

    To be written

    Credential Souce Repository

    After the registration of an authenticator, you will get a Public Key Credential Source object. It contains all the credential data needed to perform user authentication and much more.

    Each Credential Source is managed using the Public Key Credential Source Repository.

    The library does not provide any concrete implementation. It is up to you to create it depending on your application constraints. This only constraint is that your repository class must implement the interface Webauthn\PublicKeyCredentialSourceRepository.

    Examples

    Register Authenticators

    As described in the previous pages, you need to create a PublicKeyCredentialCreationOptions object to register new authenticators. You can create this object using the .... But there is another way to do that.

    The bundle provides a factory and manages profiles to ease the creation of the options. The factory is available as a public service: Webauthn\Bundle\Service\PublicKeyCredentialCreationOptionsFactory. To use it, you must first create a least one profile in your configuration file.

    authenticate users without username
    Filesystem Repository

    Please don’t use this example in production! It will store credential source objects in a temporary folder.

    Doctrine Repository

    _Y_ou must add custom Doctrine types to convert plain PHP objects into your ORM. Please have a look at this folder to find examples.

    If you use Symfony, this repository already exists and custom Doctrine types are automatically registered.

    The name is mandatory ; other options are null by default.

    The option id is highly recommended. See this page for acceptable values.

    With this profile, now we can create options with the following code lines:

    Challenge Length

    By default, the length of the challenge is 32 bytes. You may need to select a smaller or higher length. This length can be configured for each profile:

    Timeout

    The default timeout is set to 60 seconds (60 000 milliseconds). You can change this value as follow:

    Authenticator Selection Criteria

    This set of options allows you to select authenticators depending on their capabilities. The values are described in the advanced concepts of the protocol.

    Public Key Credential Parameters

    This option indicates the algorithms allowed for your application. By default, a large list of algorithms is defined, but you can add custom algorithms or reduce the list.

    The order is important. Preferred algorithms go first.

    It is not recommended to change the default list unless you exactly know what you are doing.

    Attestation Conveyance

    If you need the attestation of the authenticator, you can specify the preference regarding attestation conveyance during credential generation.

    Please note that the metadata service is mandatory to use this option.

    The use of Attestation Statements is generally not recommended unless you REALLY need this information.

    Extensions

    The mechanism for generating public key credentials, as well as requesting and generating Authentication assertions, can be extended to suit particular use cases. Each case is addressed by defining a registration extension.

    The example below is tatolly fictive. Some extensions are defined in the specification but the supports depends on the authenticators and on the relying parties.

    webauthn:
        creation_profiles:
            acme: #Unique name of the profile
                rp: # rp stands for Relying Party
                    name: 'ACME Webauthn Server'
                    id: 'acme.com'
                    icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAMAAAC6V+0/AAAAwFBMVEXm7NK41k3w8fDv7+q01Tyy0zqv0DeqyjOszDWnxjClxC6iwCu11z6y1DvA2WbY4rCAmSXO3JZDTxOiwC3q7tyryzTs7uSqyi6tzTCmxSukwi9aaxkWGga+3FLv8Ozh6MTT36MrMwywyVBziSC01TbT5ZW9z3Xi6Mq2y2Xu8Oioxy7f572qxzvI33Tb6KvR35ilwTmvykiwzzvV36/G2IPw8O++02+btyepyDKvzzifvSmw0TmtzTbw8PAAAADx8fEC59dUAAAA50lEQVQYV13RaXPCIBAG4FiVqlhyX5o23vfVqUq6mvD//1XZJY5T9xPzzLuwgKXKslQvZSG+6UXgCnFePtBE7e/ivXP/nRvUUl7UqNclvO3rpLqofPDAD8xiu2pOntjamqRy/RqZxs81oeVzwpCwfyA8A+8mLKFku9XfI0YnSKXnSYZ7ahSII+AwrqoMmEFKriAeVrqGM4O4Z+ADZIhjg3R6LtMpWuW0ERs5zunKVHdnnnMLNQqaUS0kyKkjE1aE98b8y9x9JYHH8aZXFMKO6JFMEvhucj3Wj0kY2D92HlHbE/9Vk77mD6srRZqmVEAZAAAAAElFTkSuQmCC'
    <?php
    
    use Webauthn\AuthenticatorSelectionCriteria;
    use Webauthn\PublicKeyCredentialCreationOptions;
    
    $authenticatorSelectionCriteria = new AuthenticatorSelectionCriteria(
        AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_PLATFORM,     // Platform authenticator
        true,                                                                  // Resident key required
        AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_REQUIRED // User verification required
    );
    
    $publicKeyCredentialCreationOptions = $server->generatePublicKeyCredentialCreationOptions(
        $userEntity,
        PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE,
        $excludedPublicKeyDescriptors,
        $authenticatorSelectionCriteria
    );
    Acme\Repository\PublicKeyCredentialSourceRepository.php
    <?php
    /**
     * EGroupware WebAuthn
     *
     * @link https://www.egroupware.org
     * @author Ralf Becker <rb-At-egroupware.org>
     * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
     */
    
    namespace Acme\Repository;
    
    use Webauthn\PublicKeyCredentialSourceRepository as PublicKeyCredentialSourceRepositoryInterface;
    use Webauthn\PublicKeyCredentialSource;
    use Webauthn\PublicKeyCredentialUserEntity;
    
    class PublicKeyCredentialSourceRepository implements PublicKeyCredentialSourceRepositoryInterface
    {
    	private $path = '/tmp/pubkey-repo.json';
    
        public function findOneByCredentialId(string $publicKeyCredentialId): ?PublicKeyCredentialSource
    	{
    		$data = $this->read();
            if (isset($data[base64_encode($publicKeyCredentialId)]))
            {
                return PublicKeyCredentialSource::createFromArray($data[base64_encode($publicKeyCredentialId)]);
    		}
    		return null;
    	}
    
        /**
         * @return PublicKeyCredentialSource[]
         */
        public function findAllForUserEntity(PublicKeyCredentialUserEntity $publicKeyCredentialUserEntity): array
    	{
    		$sources = [];
    		foreach($this->read() as $data)
    		{
    			$source = PublicKeyCredentialSource::createFromArray($data);
    			if ($source->getUserHandle() === $publicKeyCredentialUserEntity->getId())
    			{
    				$sources[] = $source;
    			}
    		}
    		return $sources;
    	}
    
        public function saveCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource): void
    	{
    		$data = $this->read();
    		$data[base64_encode($publicKeyCredentialSource->getPublicKeyCredentialId())] = $publicKeyCredentialSource;
    		$this->write($data);
    	}
    
    	private function read(): array
    	{
    		if (file_exists($this->path))
    		{
    			return json_decode(file_get_contents($this->path), true);
    		}
    		return [];
    	}
    
    	private function write(array $data): void
    	{
    		if (!file_exists($this->path))
    		{
                if (!mkdir($concurrentDirectory = dirname($this->path), 0700, true) && !is_dir($concurrentDirectory)) {
                    throw new \RuntimeException(sprintf('Directory "%s" was not created', $concurrentDirectory));
                }
    		}
    		file_put_contents($this->path, json_encode($data), LOCK_EX);
    	}
    }
    Acme\Repository\PublicKeyCredentialSourceRepository.php
    <?php
    
    declare(strict_types=1);
    
    /*
     * The MIT License (MIT)
     *
     * Copyright (c) 2014-2019 Spomky-Labs
     *
     * This software may be modified and distributed under the terms
     * of the MIT license.  See the LICENSE file for details.
     */
    
    namespace Webauthn\Repository;
    
    use Assert\Assertion;
    use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepositoryInterface;
    use Doctrine\Common\Persistence\ManagerRegistry;
    use Doctrine\ORM\EntityManagerInterface;
    use Webauthn\PublicKeyCredentialSource;
    use Webauthn\PublicKeyCredentialSourceRepository as PublicKeyCredentialSourceRepositoryInterface;
    use Webauthn\PublicKeyCredentialUserEntity;
    
    class PublicKeyCredentialSourceRepository implements PublicKeyCredentialSourceRepositoryInterface, ServiceEntityRepositoryInterface
    {
        /**
         * @var EntityManagerInterface
         */
        private $manager;
    
        /**
         * @var string
         */
        private $class;
    
        public function __construct(ManagerRegistry $registry, string $class)
        {
            Assertion::subclassOf($class, PublicKeyCredentialSource::class, sprintf(
                'Invalid class. Must be an instance of "Webauthn\PublicKeyCredentialSource", got "%s" instead.',
                $class
            ));
            $manager = $registry->getManagerForClass($class);
            Assertion::isInstanceOf($manager, EntityManagerInterface::class, sprintf(
                'Could not find the entity manager for class "%s". Check your Doctrine configuration to make sure it is configured to load this entity’s metadata.',
                $class
            ));
    
            $this->class = $class;
            $this->manager = $manager;
        }
    
        protected function getClass(): string
        {
            return $this->class;
        }
    
        protected function getEntityManager(): EntityManagerInterface
        {
            return $this->manager;
        }
    
        public function saveCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource, bool $flush = true): void
        {
            $this->manager->persist($publicKeyCredentialSource);
            if ($flush) {
                $this->manager->flush();
            }
        }
    
        /**
         * {@inheritdoc}
         */
        public function findAllForUserEntity(PublicKeyCredentialUserEntity $publicKeyCredentialUserEntity): array
        {
            $qb = $this->manager->createQueryBuilder();
    
            return $qb->select('c')
                ->from($this->getClass(), 'c')
                ->where('c.userHandle = :userHandle')
                ->setParameter(':userHandle', $publicKeyCredentialUserEntity->getId())
                ->getQuery()
                ->execute()
                ;
        }
    
        public function findOneByCredentialId(string $publicKeyCredentialId): ?PublicKeyCredentialSource
        {
            $qb = $this->manager->createQueryBuilder();
    
            return $qb->select('c')
                ->from($this->getClass(), 'c')
                ->where('c.publicKeyCredentialId = :publicKeyCredentialId')
                ->setParameter(':publicKeyCredentialId', base64_encode($publicKeyCredentialId))
                ->setMaxResults(1)
                ->getQuery()
                ->getOneOrNullResult()
                ;
        }
    }
    use Webauthn\Bundle\Service\PublicKeyCredentialCreationOptionsFactory;
    use Webauthn\PublicKeyCredentialUserEntity;
    
    $userEntity = new PublicKeyCredentialUserEntity(
        'john.doe',
        'ea4e7b55-d8d0-4c7e-bbfa-78ca96ec574c',
        'John Doe'
    );
    
    $publicKeyCredentialCreationOptions = $container
        ->get(PublicKeyCredentialCreationOptionsFactory::class)
        ->create('acme', $userEntity)
    ;
    app/config/webauthn.yaml
    webauthn:
        creation_profiles:
            acme:
                rp:
                    name: 'ACME Webauthn Server'
                challenge_length: 16
    app/config/webauthn.yaml
    webauthn:
        creation_profiles:
            acme:
                rp:
                    name: 'ACME Webauthn Server'
                timeout: 30000
    app/config/webauthn.yaml
    webauthn:
        creation_profiles:
            acme:
                rp:
                    name: 'ACME Webauthn Server'
                authenticator_selection_criteria:
                    attachment_mode: !php/const Webauthn\AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_PLATFORM
                    require_resident_key: true
                    user_verification: !php/const Webauthn\AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_REQUIRED
    app/config/webauthn.yaml
    webauthn:
        creation_profiles:
            acme:
                rp:
                    name: 'ACME Webauthn Server'
                public_key_credential_parameters:
                    - !php/const Cose\Algorithms::COSE_ALGORITHM_ES256
                    - !php/const Cose\Algorithms::COSE_ALGORITHM_RS256
    app/config/webauthn.yaml
    webauthn:
        creation_profiles:
            acme:
                rp:
                    name: 'ACME Webauthn Server'
                attestation_conveyance: !php/const Webauthn\PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_DIRECT
    app/config/webauthn.yaml
    webauthn:
        creation_profiles:
            acme:
                rp:
                    name: 'ACME Webauthn Server'
                extensions:
                    loc: true
                    txAuthSimple: 'Please add your new authenticator'

    Authenticator Registration

    Credential Creation Options

    Now we want to register a new authenticator and attach it to a user. This step can be done during the creation of a new user account or if the user already exists and you want to add another authenticator.

    You can attach several authenticators to a user account. It is recommended in case of lost devices or if the user get access on your application using multiple platforms (smartphone, laptop…).

    To register a new authenticator, you need to generate and send a set of options to it. These options defined in a Webauthn\PublicKeyCredentialCreationOptions object.

    To generate that object, you just need to call the methodgeneratePublicKeyCredentialCreationOptions of the $server object. This method requires a Webauthn\PublicKeyCredentialUserEntity object that represents the user entity to be associated with this new authenticator.

    Now send the options to the authenticator using your favorite Javascript framework, library or the example availbale in .

    The Public Key Credential Creation Options object (variable $publicKeyCredentialCreationOptions) can be serialized into JSON.

    The variable $publicKeyCredentialCreationOptions and $userEntity have to be stored somewhere. These are needed during the next step. Usually these values are set in the session or solutions like Redis.

    Response Verification

    When the authenticator send you the computed response (i.e. the user touched the button, fingerprint reader, submitted the PIN…), you can load it and check it.

    The authenticator response looks similar to the following example:

    The library needs PSR-7 requests. In the example below, we use nyholm/psr7-server to get that request.

    User Entity And Repository

    User Entity

    A User Entity object represents a user in the Webauthn context. It has the following constraints:

    • The user ID must be unique and must be a string,

    the Javascript page
    The username must be unique,

    Hereafter a minimalist example of user entity:

    The username can be composed of any displayable characters, including emojies. Username "😝🥰😔" is perfectly valid.

    For privacy reasons, it is not recommended to use the e-mail as username.

    As for the rp Entity, the User Entity may have an icon. This icon must also be secured.

    The Webauthn specification does not set any limit for the length of the icon.

    The icon may be ignored by browsers, especially if its length is greater than 128 bytes.

    Repository

    The User Entity Repository manages all Webauthn users of your application.

    There is no interface to implement or abstract class to extend so that it should be easy to integrate it in your application. You may already have a user repository.

    Whatever the database you use(MySQL, pgSQL…), it is not necessary to create relationships between your users and the Credential Sources.

    Hereafter an example of a User Entity repository. In this example we suppose you already have methods to find users using their username or ID.

    <?php
    
    use Webauthn\PublicKeyCredentialUserEntity;
    use Webauthn\PublicKeyCredentialCreationOptions;
    
    $userEntity = new PublicKeyCredentialUserEntity(
        'john.doe',
        'ea4e7b55-d8d0-4c7e-bbfa-78ca96ec574c',
        'John Doe'
    );
    
    /** This avoids multiple registration of the same authenticator with the user account **/
    /** You can remove these code if it is a new user **/
    // Get the list of authenticators associated to the user
    $credentialSources = $credentialSourceRepository->findAllForUserEntity($userEntity);
    
    // Convert the Credential Sources into Public Key Credential Descriptors
    $excludeCredentials = array_map(function (PublicKeyCredentialSource $credential) {
    return $credential->getPublicKeyCredentialDescriptor();
    }, $credentialSources);
    /** End of optional part**/
    
    $publicKeyCredentialCreationOptions = $server->generatePublicKeyCredentialCreationOptions(
        $userEntity,                                                                // The user entity
        PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE, // We will see this option later
        $excludeCredentials                                                         // Excluded authenticators
                                                                                    //   Set [] if new user
    );
    {
        "id": "LFdoCFJTyB82ZzSJUHc-c72yraRc_1mPvGX8ToE8su39xX26Jcqd31LUkKOS36FIAWgWl6itMKqmDvruha6ywA",
        "rawId": "LFdoCFJTyB82ZzSJUHc-c72yraRc_1mPvGX8ToE8su39xX26Jcqd31LUkKOS36FIAWgWl6itMKqmDvruha6ywA",
        "response": {
            "clientDataJSON": "eyJjaGFsbGVuZ2UiOiJOeHlab3B3VktiRmw3RW5uTWFlXzVGbmlyN1FKN1FXcDFVRlVLakZIbGZrIiwiY2xpZW50RXh0ZW5zaW9ucyI6e30sImhhc2hBbGdvcml0aG0iOiJTSEEtMjU2Iiwib3JpZ2luIjoiaHR0cDovL2xvY2FsaG9zdDozMDAwIiwidHlwZSI6IndlYmF1dGhuLmNyZWF0ZSJ9",
            "attestationObject": "o2NmbXRoZmlkby11MmZnYXR0U3RtdKJjc2lnWEcwRQIgVzzvX3Nyp_g9j9f2B-tPWy6puW01aZHI8RXjwqfDjtQCIQDLsdniGPO9iKr7tdgVV-FnBYhvzlZLG3u28rVt10YXfGN4NWOBWQJOMIICSjCCATKgAwIBAgIEVxb3wDANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowLDEqMCgGA1UEAwwhWXViaWNvIFUyRiBFRSBTZXJpYWwgMjUwNTY5MjI2MTc2MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEZNkcVNbZV43TsGB4TEY21UijmDqvNSfO6y3G4ytnnjP86ehjFK28-FdSGy9MSZ-Ur3BVZb4iGVsptk5NrQ3QYqM7MDkwIgYJKwYBBAGCxAoCBBUxLjMuNi4xLjQuMS40MTQ4Mi4xLjUwEwYLKwYBBAGC5RwCAQEEBAMCBSAwDQYJKoZIhvcNAQELBQADggEBAHibGMqbpNt2IOL4i4z96VEmbSoid9Xj--m2jJqg6RpqSOp1TO8L3lmEA22uf4uj_eZLUXYEw6EbLm11TUo3Ge-odpMPoODzBj9aTKC8oDFPfwWj6l1O3ZHTSma1XVyPqG4A579f3YAjfrPbgj404xJns0mqx5wkpxKlnoBKqo1rqSUmonencd4xanO_PHEfxU0iZif615Xk9E4bcANPCfz-OLfeKXiT-1msixwzz8XGvl2OTMJ_Sh9G9vhE-HjAcovcHfumcdoQh_WM445Za6Pyn9BZQV3FCqMviRR809sIATfU5lu86wu_5UGIGI7MFDEYeVGSqzpzh6mlcn8QSIZoYXV0aERhdGFYxEmWDeWIDoxodDQXD2R2YFuP5K65ooYyx5lc87qDHZdjQQAAAAAAAAAAAAAAAAAAAAAAAAAAAEAsV2gIUlPIHzZnNIlQdz5zvbKtpFz_WY-8ZfxOgTyy7f3Ffbolyp3fUtSQo5LfoUgBaBaXqK0wqqYO-u6FrrLApQECAyYgASFYIPr9-YH8DuBsOnaI3KJa0a39hyxh9LDtHErNvfQSyxQsIlgg4rAuQQ5uy4VXGFbkiAt0uwgJJodp-DymkoBcrGsLtkI"
        },
        "type": "public-key"
    }
    <?php
    
    use Nyholm\Psr7\Factory\Psr17Factory;
    use Nyholm\Psr7Server\ServerRequestCreator;
    
    $psr17Factory = new Psr17Factory();
    $creator = new ServerRequestCreator(
        $psr17Factory, // ServerRequestFactory
        $psr17Factory, // UriFactory
        $psr17Factory, // UploadedFileFactory
        $psr17Factory  // StreamFactory
    );
    
    $serverRequest = $creator->fromGlobals();
    
    try {
        $publicKeyCredentialSource = $server->loadAndCheckAttestationResponse(
            '_The authenticator response you received…',
            $publicKeyCredentialCreationOptions, // The options you stored during the previous step
            $serverRequest                       // The PSR-7 request
        );
        
        // The user entity and the public key credential source can now be stored using their repository
        // The Public Key Credential Source repository must implement Webauthn\PublicKeyCredentialSourceRepository
        $publicKeyCredentialSourceRepository->saveCredentialSource($publicKeyCredentialSource);
        
        // If you create a new user account, you should also save the user entity
        $userEntityRepository->save($userEntity);
    } catch(\Throwable $exception) {
        // Something went wrong!
    }
    <?php
    
    use Webauthn\PublicKeyCredentialUserEntity;
    
    $userEntity = new PublicKeyCredentialUserEntity(
        'john.doe',                             // Username
        'ea4e7b55-d8d0-4c7e-bbfa-78ca96ec574c', // ID
        'John Doe'                              // Display name
    );
    <?php
    
    use Webauthn\PublicKeyCredentialUserEntity;
    
    $userEntity = new PublicKeyCredentialUserEntity(
        'john.doe',
        'ea4e7b55-d8d0-4c7e-bbfa-78ca96ec574c',
        'John Doe',
        'data:image/png;base64,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'
    );
    Acme\Repository\PublicKeyCredentialUserEntityRepository.php
    <?php
    
    declare(strict_types=1);
    
    /*
     * The MIT License (MIT)
     *
     * Copyright (c) 2014-2019 Spomky-Labs
     *
     * This software may be modified and distributed under the terms
     * of the MIT license.  See the LICENSE file for details.
     */
    
    namespace Acme\Repository;
    
    use Webauthn\PublicKeyCredentialUserEntity;
    
    final class PublicKeyCredentialUserEntityRepository
    {
        public function findWebauthnUserByUsername(string $username): ?PublicKeyCredentialUserEntity
        {
            //We suppose you already have a method to find a user using its username
            $user = $this->findOneBy(['username' => $username]);
            if (null === $user) {
                return null;
            }
    
            return $this->createUserEntity($user);
        }
    
        public function findWebauthnUserByUserHandle(string $userHandle): ?PublicKeyCredentialUserEntity
        {
            //We suppose you already have a method to find a user using its ID
            $user = $this->findOneBy(['id' => $userHandle]);
            if (null === $user) {
                return null;
            }
            
            return $this->createUserEntity($user);
        }
    
        private function createUserEntity(User $user): PublicKeyCredentialUserEntity
        {
            //We create a PublicKeyCredentialUserEntity object
            // This object requires the username, the ID and the name to display (e.g. "John Doe")
            // The avatar URL is optionnal and could be null
            return new PublicKeyCredentialUserEntity(
                $user->username,
                $user->id,
                $user->displayName,
                $user->avatarUrl
            );
        }
    }

    Attestation and Metadata Statement

    Disclaimer: you should not ask for the Attestation Statement unless you are working on an application that requires a high level of trust (e.g. Banking/Financial Company, Government Agency...).

    Attestation Statement

    During the Attestation Ceremony (i.e. the registration of the authenticator), you can ask for the Attestation Statement of the authenticator. The Attestation Statements have one of the following types:

    • None (none): no Attestation Statement is provided

    • Basic Attestation (basic): Authenticator’s attestation key pair is specific to an authenticator model.

    • Surrogate Basic Attestation (or Self Attestation - self

    Metadata Statement

    The Metadata Statements are issued by the manufacturers of the authenticators. These statements contain details about the authenticators (supported algorithms, biometric capabilities...) and all the necessary information to verify the Attestation Statements generated during the attestation ceremony.

    There are several possible sources to get theses Metadata Statements. The main source is the that allows to fetch statements on-demand, but some of them may be provided by other means.

    The FIDO Alliance Metadata Service provides a limited number of Metadata Statements. It is mandatory to get the statement from the manufacturer of your authenticators otherwise the Attestation Statement won't be verified and the Attestation Ceremony will fail.

    Receiving Attestation Statement

    Attestation Metadata Repository

    First of all, you must prepare an Attestation Metadata Repository. This service will manage all Metadata Statements depending on there sources (local storage or distant service).

    Your Metadata Statement Repository must implement the interface Webauthn\MetadataService\MetadataStatementRepository that has a unique method findOneByAAGUID(string $aaguid).

    Basic Repository Implementation.

    The library web-auth/metadata-service provides a concrete class with basic support for local and distant statements with caching system: Webauthn\MetadataService\SimpleMetadataStatementRepository

    The example above is very limited and will only allow authenticators manufactured by Yubico to be registered. Make sure to add more sources of Metadata Statements to accept authenticators from other manufacturers.

    When the repository is ready, you must inject it to your server.

    The Easy Way

    The Hard Way

    To be written

    The Symfony Way

    With Symfony, every source of Metadata Statement is configured in the application configuration. It will be automatically injected to the services.

    Credential Creation Options

    By default, no Attestation Statement is asked to the Authenticators (type = none). To change this behavior, you just have to set the corresponding parameter in the Webauthn\PublicKeyCredentialCreationOptions object.

    There are 3 conveyance modes available using PHP constants provided by the class Webauthn\PublicKeyCredentialCreationOptions:

    • ATTESTATION_CONVEYANCE_PREFERENCE_NONE: the Relying Party is not interested in authenticator attestation (default)

    • ATTESTATION_CONVEYANCE_PREFERENCE_INDIRECT: the Relying Party prefers an attestation conveyance yielding verifiable attestation statements, but allows the client to decide how to obtain such attestation statements.

    • ATTESTATION_CONVEYANCE_PREFERENCE_DIRECT: the Relying Party wants to receive the attestation statement as generated by the authenticator.

    The Easy Way

    The Hard Way

    The Symfony Way

    )
    : Authenticators that have no specific attestation key use the credential private key to create the attestation signature
  • Attestation CA (AttCA): Authenticators are based on a Trusted Platform Module (TPM). They can generate multiple attestation identity key pairs (AIK) and requests an Attestation CA to issue an AIK certificate for each.

  • Elliptic Curve based Direct Anonymous Attestation (ECDAA): Authenticator receives direct anonymous attestation (DAA) credentials from a single DAA-Issuer. These DAA credentials are used along with blinding to sign the attested credential data.

  • FIDO Alliance Metadata Service
    use Webauthn\MetadataService\MetadataStatementRepository:
    use Symfony\Component\Cache\Adapter\FilesystemAdapter;
    use Webauthn\MetadataService\SingleMetadata;
    
    $myMetadataStatementRepository = new SimpleMetadataStatementRepository(
        new FilesystemAdapter('webauthn') // We use filesystem caching in this example
    );
    
    // We add a local matadata statement (adapted from the Yubico website)
    $myMetadataStatementRepository->addSingleStatement('yubico', new SingleMetadata(
        '{"description": "Yubico U2F Root CA Serial 457200631","aaguid": "f8a011f3-8c0a-4d15-8006-17111f9edc7d","protocolFamily": "fido2","attestationRootCertificates": ["MIIDHjCCAgagAwIBAgIEG0BT9zANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC/jwYuhBVlqaiYWEMsrWFisgJ+PtM91eSrpI4TK7U53mwCIawSDHy8vUmk5N2KAj9abvT9NP5SMS1hQi3usxoYGonXQgfO6ZXyUA9a+KAkqdFnBnlyugSeCOep8EdZFfsaRFtMjkwz5Gcz2Py4vIYvCdMHPtwaz0bVuzneueIEz6TnQjE63Rdt2zbwnebwTG5ZybeWSwbzy+BJ34ZHcUhPAY89yJQXuE0IzMZFcEBbPNRbWECRKgjq//qT9nmDOFVlSRCt2wiqPSzluwn+v+suQEBsUjTGMEd25tKXXTkNW21wIWbxeSyUoTXwLvGS6xlwQSgNpk2qXYwf8iXg7VWZAgMBAAGjQjBAMB0GA1UdDgQWBBQgIvz0bNGJhjgpToksyKpP9xv9oDAPBgNVHRMECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAjvjuOMDSa+JXFCLyBKsycXtBVZsJ4Ue3LbaEsPY4MYN/hIQ5ZM5p7EjfcnMG4CtYkNsfNHc0AhBLdq45rnT87q/6O3vUEtNMafbhU6kthX7Y+9XFN9NpmYxr+ekVY5xOxi8h9JDIgoMP4VB1uS0aunL1IGqrNooL9mmFnL2kLVVee6/VR6C5+KSTCMCWppMuJIZII2v9o4dkoZ8Y7QRjQlLfYzd3qGtKbw7xaF1UsG/5xUb/Btwb2X2g4InpiB/yt/3CpQXpiWX/K4mBvUKiGn05ZsqeY1gx4g0xLBqcU9psmyPzK+Vsgw2jeRQ5JlKDyqE0hebfC1tvFu0CCrJFcw=="]}',
        false // The statement is not base64 encoded
    ));
    <?php
    
    use Webauthn\Server;
    use Webauthn\PublicKeyCredentialRpEntity;
    
    ...
    
    $server = new Server(
        $rpEntity
        $publicKeyCredentialSourceRepository,
        $myMetadataStatementRepository        // Inject your new service here
    );
    config/packages/webauthn.yaml
    webauthn:
        metadata_service:
            http_client: ... # An HTTP client for distant sources
            request_factory: # PSR-17 request factory
            services: # Services compatible with the MDS Specification
                fido_alliance:
                    uri: 'https://mds2.fidoalliance.org'
                    additional_query_string_values:
                        token: '--ACCESS-TOKEN--' # We need to set the access token in the query string for this service
            distant_single_statements:
                solo: # A statement provided by Solo (https://solokeys.com/)
                    uri: 'https://raw.githubusercontent.com/solokeys/solo/2.1.0/metadata/Solo-FIDO2-CTAP2-Authenticator.json'
                    additional_headers: ~
            from_data: # Single statements from local data
                yubico:
                    data: '{"description": "Yubico U2F Root CA Serial 457200631","aaguid": "f8a011f3-8c0a-4d15-8006-17111f9edc7d","protocolFamily": "fido2","attestationRootCertificates": ["MIIDHjCCAgagAwIBAgIEG0BT9zANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC/jwYuhBVlqaiYWEMsrWFisgJ+PtM91eSrpI4TK7U53mwCIawSDHy8vUmk5N2KAj9abvT9NP5SMS1hQi3usxoYGonXQgfO6ZXyUA9a+KAkqdFnBnlyugSeCOep8EdZFfsaRFtMjkwz5Gcz2Py4vIYvCdMHPtwaz0bVuzneueIEz6TnQjE63Rdt2zbwnebwTG5ZybeWSwbzy+BJ34ZHcUhPAY89yJQXuE0IzMZFcEBbPNRbWECRKgjq//qT9nmDOFVlSRCt2wiqPSzluwn+v+suQEBsUjTGMEd25tKXXTkNW21wIWbxeSyUoTXwLvGS6xlwQSgNpk2qXYwf8iXg7VWZAgMBAAGjQjBAMB0GA1UdDgQWBBQgIvz0bNGJhjgpToksyKpP9xv9oDAPBgNVHRMECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAjvjuOMDSa+JXFCLyBKsycXtBVZsJ4Ue3LbaEsPY4MYN/hIQ5ZM5p7EjfcnMG4CtYkNsfNHc0AhBLdq45rnT87q/6O3vUEtNMafbhU6kthX7Y+9XFN9NpmYxr+ekVY5xOxi8h9JDIgoMP4VB1uS0aunL1IGqrNooL9mmFnL2kLVVee6/VR6C5+KSTCMCWppMuJIZII2v9o4dkoZ8Y7QRjQlLfYzd3qGtKbw7xaF1UsG/5xUb/Btwb2X2g4InpiB/yt/3CpQXpiWX/K4mBvUKiGn05ZsqeY1gx4g0xLBqcU9psmyPzK+Vsgw2jeRQ5JlKDyqE0hebfC1tvFu0CCrJFcw=="]}'
    <?php
    
    use Webauthn\PublicKeyCredentialCreationOptions;
    
    $publicKeyCredentialCreationOptions = $server->generatePublicKeyCredentialCreationOptions(
        $userEntity,
        PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_DIRECT,
    );
    <?php
    
    use Webauthn\PublicKeyCredentialCreationOptions;
    
    $publicKeyCredentialCreationOptions = new PublicKeyCredentialCreationOptions(
        $relyingParty
        $userEntity,
        $challenge,
        $pubKeyCredParams,
        $timeout, 
        $excludeCredentials,
        $authenticatorSelection,
        PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_DIRECT
    );
    config/packages/webauthn.yaml
    webauthn:
        credential_repository: ...
        user_repository: ...
        creation_profiles:
            acme:
                attestation_conveyance: !php/const Webauthn\PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_DIRECT
                rp:
                    name: 'My application'
                    id: 'example.com'

    Firewall

    To authenticate your users, you can follow the steps described on the previous page. But as Symfony offers a firewall, you may prefer to use it instead of writing a new authentication process and get all advantages of this firewall.

    First of all, you must install the dedicated bundle: symfony/security-bundle.

    Next, you must create a request profile as showed here.

    Then, you must have a PSR-7 message factory service. We recommend the use of nyholm/psr7, but feel free to use any other compatible library.

    The PSR-7 Message Factory shall be available as a service.

    That's it! You can now protect any route as usual.

    Credential Request Options

    Prior to the authentication of the user, you must get a PublicKey Credential Request Options object. To do so, send a POST request to /login/options.

    The body of this request is a JSON object that must contain a username member with the name of the user being authenticated.

    It is mandatory to set the Content-Type header to application/json.

    Example

    In case of success, you receive a valid PublicKeyCredentialRequestOptions object and your user will be asked to interact with its security devices.

    The default path is /login/options. You can change it if needed:

    User Assertion

    When the user touched the security device, you will receive a response from it. You just have to send a POST request to /login.

    The body of this request is the response of the security device.

    It is mandatory to set the Content-Type header to application/json.

    Example:

    The default path is /assertion/result. You can change that path is needed:

    Your user can now be authenticated and retrieved as usual.

    Handlers

    You can customize the responses returned by the firewall by using a custom handler. This could be useful when using an access token manager (e.g. ) or to modify the responses.

    There are 3 types of responses and handlers:

    • Request options,

    • Authentication Success,

    • Authentication Failure,

    Request Options Handler

    This handler is called when a client sends a valid POST request to the options_path. The default Request Options Handler is Webauthn\Bundle\Security\Handler\DefaultRequestOptionsHandler. It returns a JSON Response with the Public Key Credential Request Options objects in its body.

    Your custom handler have to implement the interface Webauthn\Bundle\Security\Handler\RequestOptionsHandler and be declared as a service.

    When done, you can set your new service in the firewall configuration:

    Authentication Success Handler

    This handler is called when a client sends a valid assertion from the authenticator. The default handler is Webauthn\Bundle\Security\Handler\DefaultSuccessHandler.

    Your custom handler have to implement the interface Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface and be declared as a container service.

    When done, you can set your new service in the firewall configuration:

    Authentication Failure Handler

    This handler is called when an error occurred during the authentication process. The default handler is Webauthn\Bundle\Security\Handler\DefaultFailureHandler.

    Your custom handler have to implement the interface Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface and be declared as a container service.

    When done, you can set your new service in the firewall configuration:

    Request Options Storage

    Webauthn authentication is a 2 steps round trip authentication:

    • Request options issuance

    • Authenticator assertion verification

    It is needed to store the request options and the user entity associated to it to verify the authenticator assertions.

    By default, the firewall uses Webauthn\Bundle\Security\Storage\SessionStorage. This storage system stores the data in a session.

    If this behaviour does not fit on your needs (e.g. you want to use a database, REDIS…), you can implement a custom data storage for that purpose. Your custom storage system have to implement Webauthn\Bundle\Security\Storage\RequestOptionsStorage and declared as a container service.

    When done, you can set your new service in the firewall configuration:

    Authentication Attributes

    The security token returned by the firewall sets some attributes depending on the assertion and the capabilities of the authenticator. The attributes are:

    • IS_USER_PRESENT: the user was present during the authentication ceremony. This attribute is usually set to true by Webauthn authenticators,

    • IS_USER_VERIFIED: the user was verified by the authenticator. Verification may be performed by several means including biometrics ones (fingerprint, iris, facial recognition…).

    You can then set constraints to the access controls.

    LexikJWTAuthenticationBundle
    security:
        firewalls:
            main:
                webauthn_json:
                    profile: 'acme'
                    http_message_factory: 'Nyholm\Psr7\Factory\Psr17Factory'
    security:
        access_control:
            - { path: ^/login,  roles: IS_AUTHENTICATED_ANONYMOUSLY }
            - { path: ^/admin,  roles: 'ROLE_ADMIN' }
            - { path: ^/page,   roles: 'ROLE_USER' }
            - { path: ^/,       roles: IS_AUTHENTICATED_ANONYMOUSLY }
    fetch('/login/options', {
        method  : 'POST',
        credentials : 'same-origin',
        headers : {
            'Content-Type' : 'application/json'
        },
        body: JSON.stringify({
            "username": "john.doe"
        })
    }).then(function (response) {
        return response.json();
    }).then(function (json) {
        console.log(json);
    }).catch(function (err) {
        console.log({ 'status': 'failed', 'error': err });
    })
    security:
        firewalls:
            main:
                webauthn_json:
                    options_path: /security/authentication/options
        access_control:
            - { path: ^/security,  roles: IS_AUTHENTICATED_ANONYMOUSLY}
    fetch('/assertion/result', {
        method  : 'POST',
        credentials : 'same-origin',
        headers : {
            'Content-Type' : 'application/json'
        },
        body: //put the security device response here
    }).then(function (response) {
        return response.json();
    }).then(function (json) {
        console.log(json);
    }).catch(function (err) {
        console.log({ 'status': 'failed', 'error': err });
    })
    security:
        firewalls:
            main:
                webauthn_json:
                    login_path: /security/authentication/login
    Acme\Controller\AdminController.php
    <?php
    
    declare(strict_types=1);
    
    namespace Acme\Controller;
    
    use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
    
    final class AdminController
    {
        /**
         * @var TokenStorageInterface
         */
        private $tokenStorage:
    
        public function __construct(TokenStorageInterface $tokenStorage)
        {
            $this->tokenStorage = $tokenStorage;
        }
    
        public function __invoke()
        {
            // $token is an object of type Webauthn\Bundle\Security\Authentication\Token\WebauthnToken
            $token = $this->tokenStorage->getToken();
            ...
        }
    }
    security:
        firewalls:
            main:
                webauthn_json:
                    request_options_handler: 'App\Handler\MyCustomRequestOptionsHandler'
    security:
        firewalls:
            main:
                webauthn_json:
                    success_handler: 'App\Handler\MyCustomAuthenticationSuccessHandler'
    security:
        firewalls:
            main:
                webauthn_json:
                    failure_handler: 'App\Handler\MyCustomAuthenticationFailureHandler'
    security:
        firewalls:
            main:
                webauthn_json:
                    request_options_storage: 'App\Handler\MyCustomRequestOptionsStorage'
    security:
        access_control:
            - { path: ^/admin,  roles: IS_USER_VERIFIED}

    Entities with Doctrine

    Credential Source

    The Doctrine Entity

    With Doctrine, you have to indicate how to store the Credential Source objects. Hereafter an example an entity. In this example we add an entity id and a custom field created_at. We also indicate the repository as we will have a custom one.

    As the ID must have a fixed length and because the credentialId field of Webauthn\PublicKeyCredentialSource haven’t such requirement and is a binary string, we need to declare our own id field.

    The Repository

    To ease the integration into your application, the bundle provides a concrete class that you can extend.

    In this following example, we extend that class and add a method to get all credentials for a specific user handle. Feel free to add your own methods.

    We must override the method saveCredentialSource because we may receive Webauthn\PublicKeyCredentialSource objects instead of App\Entity\PublicKeyCredentialSource.

    This repository should be declared as a Symfony service.

    With Symfony 4, this is usually done automatically

    User Entity

    Doctrine Entity

    In a Symfony application context, you usually have to manage several user entities. Thus, in the following example, the user entity class will extend the required calls and implements the interface provided by the Symfony Security component.

    Feel free to add the necessary setters as well as other fields you need (creation date, last update at…).

    Please note that the ID of the user IS NOT generated by Doctrine and must be a string. We highly recommend you to use UUIDs.

    The Repository

    The following example uses Doctrine to create, persist or perform queries using the User objects created above.

    This repository should be declared as a Symfony service.

    With Symfony 4, this is usually done automatically

    App/Entity/PublicKeyCredentialSource.php
    <?php
    
    declare(strict_types=1);
    
    /*
     * The MIT License (MIT)
     *
     * Copyright (c) 2014-2019 Spomky-Labs
     *
     * This software may be modified and distributed under the terms
     * of the MIT license.  See the LICENSE file for details.
     */
    
    namespace App\Entity;
    
    use Doctrine\ORM\Mapping as ORM;
    use Ramsey\Uuid\Uuid;
    use Ramsey\Uuid\UuidInterface;
    use Webauthn\PublicKeyCredentialSource as BasePublicKeyCredentialSource;
    use Webauthn\TrustPath\TrustPath;
    
    /**
     * @ORM\Table(name="public_key_credential_sources")
     * @ORM\Entity(repositoryClass="App\Repository\PublicKeyCredentialSourceRepository")
     */
    class PublicKeyCredentialSource extends BasePublicKeyCredentialSource
    {
        /**
         * @var string
         * @ORM\Id
         * @ORM\Column(type="string", length=100)
         * @ORM\GeneratedValue(strategy="NONE")
         */
        private $id;
    
        /**
         * @var \DateTimeImmutable
         * @ORM\Column(type="datetime_immutable")
         */
        private $createdAt;
    
        public function __construct(string $publicKeyCredentialId, string $type, array $transports, string $attestationType, TrustPath $trustPath, UuidInterface $aaguid, string $credentialPublicKey, string $userHandle, int $counter)
        {
            $this->id = Uuid::uuid4()->toString();
            $this->createdAt = new \DateTimeImmutable();
            parent::__construct($publicKeyCredentialId, $type, $transports, $attestationType, $trustPath, $aaguid, $credentialPublicKey, $userHandle, $counter);
        }
    
        public function getId(): string
        {
            return $this->id;
        }
    
        public function getCreatedAt(): \DateTimeImmutable
        {
            return $this->createdAt;
        }
    }
    App/Repository/PublicKeyCredentialSourceRepository.php
    <?php
    
    declare(strict_types=1);
    
    /*
     * The MIT License (MIT)
     *
     * Copyright (c) 2014-2019 Spomky-Labs
     *
     * This software may be modified and distributed under the terms
     * of the MIT license.  See the LICENSE file for details.
     */
    
    namespace App\Repository;
    
    use App\Entity\PublicKeyCredentialSource;
    use App\Entity\User;
    use Doctrine\Common\Persistence\ManagerRegistry;
    use Webauthn\Bundle\Repository\PublicKeyCredentialSourceRepository as BasePublicKeyCredentialSourceRepository;
    use Webauthn\PublicKeyCredentialSource as BasePublicKeyCredentialSource;
    
    final class PublicKeyCredentialSourceRepository extends BasePublicKeyCredentialSourceRepository
    {
        public function __construct(ManagerRegistry $registry)
        {
            parent::__construct($registry, PublicKeyCredentialSource::class);
        }
    
        /**
         * @return PublicKeyCredentialSource[]
         */
        public function allForUser(User $user): array
        {
            $qb = $this->getEntityManager()->createQueryBuilder();
    
            return $qb->select('c')
                ->from($this->getClass(), 'c')
                ->where('c.userHandle = :user_handle')
                ->setParameter(':user_handle', $user->getUserHandle())
                ->getQuery()
                ->execute()
            ;
        }
    
        public function saveCredentialSource(BasePublicKeyCredentialSource $publicKeyCredentialSource, bool $flush = true): void
        {
            if (!$publicKeyCredentialSource instanceof PublicKeyCredentialSource) {
                $publicKeyCredentialSource = new PublicKeyCredentialSource(
                    $publicKeyCredentialSource->getPublicKeyCredentialId(),
                    $publicKeyCredentialSource->getType(),
                    $publicKeyCredentialSource->getTransports(),
                    $publicKeyCredentialSource->getAttestationType(),
                    $publicKeyCredentialSource->getTrustPath(),
                    $publicKeyCredentialSource->getAaguid(),
                    $publicKeyCredentialSource->getCredentialPublicKey(),
                    $publicKeyCredentialSource->getUserHandle(),
                    $publicKeyCredentialSource->getCounter()
                );
            }
            parent::saveCredentialSource($publicKeyCredentialSource, $flush);
        }
    }
    config/services.yaml
    services:
        App\Repository\PublicKeyCredentialSourceRepository: ~
    app/Entity/User.php
    <?php
    
    declare(strict_types=1);
    
    namespace App\Entity;
    
    use Doctrine\Common\Collections\ArrayCollection;
    use Doctrine\ORM\Mapping as ORM;
    use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
    use Symfony\Component\Security\Core\User\UserInterface;
    use Symfony\Component\Validator\Constraints as Assert;
    
    /**
     * @ORM\Table(name="users")
     * @ORM\Entity(repositoryClass="App\Repository\UserRepository")
     * @UniqueEntity("name")
     */
    class User extends PublicKeyCredentialUserEntity implements UserInterface
    {
        /**
         * @ORM\Id
         * @ORM\Column(type="string", length=255)
         */
        protected $id;
    
        /**
         * @ORM\Column(type="string", length=255)
         * @Assert\Length(max = 100)
         */
        protected $name;
    
        /**
         * @ORM\Column(type="string", length=255)
         * @Assert\Length(max = 100)
         */
        protected $displayName;
    
        /**
         * @ORM\Column(type="array")
         */
        protected $roles;
    
        public function __construct(string $id, string $name, string $displayName, array $roles)
        {
            parent::__construct($name, $id, $displayName);
            $this->roles = $roles;
        }
    
        public function getRoles(): array
        {
            return array_unique($this->roles + ['ROLE_USER']);
        }
    
        public function getPassword(): void
        {
        }
    
        public function getSalt(): void
        {
        }
    
        public function getUsername(): ?string
        {
            return $this->name;
        }
    
        public function eraseCredentials(): void
        {
        }
    }
    app/Repository/UserRepository.php
    <?php
    
    declare(strict_types=1);
    
    /*
     * The MIT License (MIT)
     *
     * Copyright (c) 2014-2019 Spomky-Labs
     *
     * This software may be modified and distributed under the terms
     * of the MIT license.  See the LICENSE file for details.
     */
    
    namespace App\Repository;
    
    use App\Entity\User;
    use Doctrine\Common\Persistence\ManagerRegistry;
    use Webauthn\Bundle\Repository\AbstractPublicKeyCredentialUserEntityRepository;
    use Webauthn\PublicKeyCredentialUserEntity;
    
    final class PublicKeyCredentialUserEntityRepository extends AbstractPublicKeyCredentialUserEntityRepository
    {
        public function __construct(ManagerRegistry $registry)
        {
            parent::__construct($registry, User::class);
        }
    
        public function createUserEntity(string $username, string $displayName, ?string $icon): PublicKeyCredentialUserEntity
        {
            return new User($username, $displayName, [], $icon);
        }
    
        public function saveUserEntity(PublicKeyCredentialUserEntity $userEntity): void
        {
            if (!$userEntity instanceof User) {
                $userEntity =  $this->createUserEntity(
                    $userEntity->getName(),
                    $userEntity->getDisplayName(),
                    [],
                    $userEntity->getIcon()
                );
            }
    
            parent::saveUserEntity($userEntity);
        }
    
        public function find(string $username): ?User
        {
            $qb = $this->getEntityManager()->createQueryBuilder();
    
            return $qb->select('u')
                ->from(User::class, 'u')
                ->where('u.name = :name')
                ->setParameter(':name', $username)
                ->setMaxResults(1)
                ->getQuery()
                ->getOneOrNullResult()
            ;
        }
    }
    config/services.yaml
    services:
        App\Repository\UserRepository: ~