Only this pageAll pages
Powered by GitBook
1 of 36

v3.0

Loading...

Loading...

Loading...

Loading...

Webauthn In A Nutshell

Loading...

Loading...

Pre-requisites

Loading...

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...

Loading...

Loading...

Loading...

Loading...

Loading...

Migration

Loading...

Authenticators

What is an authenticator?

An Authenticator is a cryptographic entity used to 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.

Roaming Authenticators

The roaming 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.

Authenticators of this class are removable from, and can "roam" among, client devices.

Platform Authenticators

A platform authenticator is usually not removable from the client device. For example an Android smartphone or a Windows 10 computer with the associated security chips can act as an authenticator.

Introduction

Overview of the framework

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

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.

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

Class, Constant and Property Names

Naming things may be complicated. That’s why the following rule applies on the whole framework: the name of classes, constants and properties are identical to the ones you will find in the specification.

  • authenticatorData

  • signature

  • userHandle

Supported features

  • Attestation Types

    • Empty

    • Basic

    • Self

    • Private CA

    • Elliptic Curve Direct Anonymous Attestation (ECDAA)

  • Attestation Formats

    • FIDO U2F

    • Packed

    • TPM

    • Android Key

    • Android Safetynet

  • Token Binding support

  • Cose Algorithms

    • RS1, RS256, RS384, RS512

    • PS256, PS384, PS512

    • ES256, ES256K, ES384, ES512

    • ED25519

  • Extensions

    • Supported (not fully tested)

    • appid extension

Compatible Authenticators

The framework is already compatible with all authenticators except the one that use ECDAA Attestation format.

The ECDAA Attestation format is very rare at that time (January 2020) thus this framework can safely be used in production.

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

Support

I bring solutions to your problems and answer your questions.

Contributing

Requests for new features, bug fixed and all other ideas to make this framework useful are welcome.

The complete specification can be found on .

As an example, the shows an object named AuthenticatorAssertionResponse that extends AuthenticatorResponse with the following properties:

You will find in the PHP class provided by the library.

It is also tested using the official FIDO Alliance testing tools. The status of the compliance tests are . At the time of writing (end of January. 2020), the main features and algorithms are supported and 99% of the tests pass. Full compliance with the Webauthn specification is expected in early 2020.

If you really love that project, and the work I have done or if you want I prioritize your issues, then !

If you feel comfortable writing code, you could try to fix or .

Do not forget to follow .

If you think you have found a security issue, DO NOT open an issue. .

the W3C dedicated page
section 5.3.3 “Web Authentication Assertion”
EXACTLY the same structure
reported in this issue
you can help me out for a couple of🍻 or more
opened issues where help is wanted
those that are easy to fix
these best practices
You MUST submit your issue here

Credential Source Repository

Authenticator details and how to manage them

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

Filesystem Repository

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

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);
    }
}

Doctrine Repository

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

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()
            ;
    }
}

You must to convert plain PHP objects into your ORM. Please have a look at to find examples.

add custom Doctrine types
this folder

Web Browser Support

Adoption by web browsers

Webauthn is now supported by all main web browsers:

  • Mozilla Firefox 60+ and Firefox for Android 68+

  • Google Chrome 67+

  • Microsoft EDGE 18+ and Microsoft EDGE Chromium 79+

  • Opera 54+

  • Safari 13+ and iOS Safari 13.3+

  • Android Browser 76+

For more information and limitation on these browsers, please have a look at the following page:

https://caniuse.com/#feat=webauthn

The Relying Party

aka the application you are interacting with

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.

<?php

use Webauthn\PublicKeyCredentialRpEntity;

$rpEntity = new PublicKeyCredentialRpEntity(
    'ACME Webauthn Server' // The application name
);

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

Relying Party ID

In the example above, we created a simple relying party object with it’s name. The relying party may also have an ID that corresponds to the domain applicable for that rp. By default, the relying party ID is null i.e. the current domain will be used.

It may be useful to specify the rp ID, especially if your application has several sub-domains. The rp ID can be set during the creation of the object as 2nd constructor parameter.

<?php

use Webauthn\PublicKeyCredentialRpEntity;

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

Even if it is optional, we highly recommend setting the application ID

The rp ID shall be the domain of the application without the scheme, userinfo, port, path, user…. IP addresses are not allowed either.

Allowed: www.sub.domain.com, sub.domain.com, domain.com

Not allowed:

  • www.sub.domain.com:1337, https://domain.com:443, sub.domain.com/index, https://user:password@www.domain.com.

  • 12.65.76.43 or [2001:db8:85a3:8d3:1319:8a2e:370:7348]

The domain localhost can be used if the browser considers the context is safe (especially the IP address corresponds to a local address)

How to determine the Relying Party ID?

The Relying Party ID should be determined depending on the common URLs for your web application.

If the domain is shared between sub-projects, the rp ID should be limited to that sub-projects.

For example, a web site is located at https://(www.)site1.host.com and another at https://(www.)site2.host.com, then the Relying Party IDs should be site1.host.com and site2.host.com respectively. If you set host.com, there is a risk that users from site1.host.com can log in at site2.host.com.

Relying Party Icon

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.

<?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'
);

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.

If you have a web application that can be reached at (for mobiles) and or (for other devices), your Relying Party ID should be my-app.com.

https://m.my-app.com
https://my-app.com
https://www.my-app.com
USB device with fingerprint reader
Webauthn compatible devices
Android screenshot
Fingerprint reader on a laptop

Ceremonies

Registration and Authentication process overview

In the Webauthn context, there 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 perform:

  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 computes a response that has 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

How to install the library or the Symfony bundle?

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 preferred 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?

Framework and Dependency Sizes

The total size of the core package is approximately 760ko. Hereafter the detail for each component:

  • web-auth/cose-lib: 85ko

  • web-auth/metadata-service: 81ko

  • web-auth/webauthn-lib: 207ko

  • web-auth/webauthn-symfony-bundle: 385ko

  • web-auth/conformance-toolset: N/A

The total size of the core package + the direct dependencies is approximately 1.7Mo.

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.

RFC8152
Fido Alliance Metadata Service

Contributing

You have just found a bug?

First of all, thank you for contributing.

Bugs or feature requests can be posted online on the GitHub issues section of the project.

Few rules to ease code reviews and merges:

  • You MUST run the test suite (see below).

  • You MUST write (or update) unit tests when bugs are fixed or features are added.

  • You SHOULD write documentation.

We use the following branching workflow:

  • Each minor version has a dedicated branch (e.g. v1.1, v1.2, v2.0, v2.1…)

  • The default branch is set to the last minor version (e.g. v2.1).

Your PR should NOT be submitted to the master branch but to the last minor version branch or to another minor version in case of bug fix.

Run test suite

  • install composer: curl -s http://getcomposer.org/installer | php

  • install dependencies: php composer.phar install

  • run tests: vendor/bin/phpunit

You MUST follow the and coding standards.

You MAY follow the and .

To contribute use , please, write commit messages that make sense, and rebase your branch before submitting your PR.

PSR-1
PSR-4
PSR-5
PSR-19
Pull Requests

Register Authenticators

First 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 gets 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 are 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.

<?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 this 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
);

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 sends 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:

{
    "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"
}

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

<?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!
}

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

the Javascript page

User Entity

It's all about users

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 username must be unique,

Hereafter a minimalist example of user entity:

<?php

use Webauthn\PublicKeyCredentialUserEntity;

$userEntity = new PublicKeyCredentialUserEntity(
    'john.doe',                             // Username
    'ea4e7b55-d8d0-4c7e-bbfa-78ca96ec574c', // ID
    'John Doe'                              // Display name
);

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

Developers should not add rules that prevent users from choosing the username they want.

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

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

<?php

use Webauthn\PublicKeyCredentialUserEntity;

$userEntity = new PublicKeyCredentialUserEntity(
    'john.doe',
    'ea4e7b55-d8d0-4c7e-bbfa-78ca96ec574c',
    'John Doe',
    'data:image/png;base64,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'
);

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.

User Entity 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 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.

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 optional and could be null
        return new PublicKeyCredentialUserEntity(
            $user->username,
            $user->id,
            $user->displayName,
            $user->avatarUrl
        );
    }
}

The Easy Way

How to run a basic Webauthn server?

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

<?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
);

That’s it!

You can now or .

register a new authenticator
authenticate your users

Easy or Hard Way?

Is it complicated?

In this documentation, different ways to setup a Webauthn server are presented:

  • The easy way: the examples are designed for developers who want to start quickly without knowing all specification details

  • The hard way: you will find detailed information about the classes and how to prepare a tailor-made server

The Symfony way is for Symfony developers. These pages contain configuration examples for Symfony-based applications.

Javascript

Examples for dynamic interactions

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

It is mandatory to use the HTTPS scheme to use Webauthn otherwise it will not work.

Installation

You can use npm or yarn to install the package:

npm i @web-auth/webauthn-helper
#or
yarn add @web-auth/webauthn-helper

Registration

// Import the registration hook
import {useRegistration} from '@web-auth/webauthn-helper';

// Create your register function.
// By default the urls are "/register" and "/register/options"
// but you can change those urls if needed.
const register = useRegistration({
    actionUrl: '/api/register',
    optionsUrl: '/api/register/options'
});


// We can call this register function whenever we need (e.g. form submission)
register({
    username: 'john.doe',
    displayName: 'JD'
})
    .then((response) => console.log('Registration success'))
    .catch((error) => console.log('Registration failure'))
;

Additional options can be set during the registration process. See the section “Deep into the framework” to know more. Hereafter another example:

register({
    username: 'john.doe',
    displayName: 'JD',
    attestation: 'none',
    authenticatorSelection: {
        authenticatorAttachment: 'platform',
        requireResidentKey: true,
        userVerification: 'required'
    }
})
    .then((response) => console.log('Registration success'))
    .catch((error) => console.log('Registration failure'))
;

Authentication

// Import the login hook
import {useLogin} from '@web-auth/webauthn-helper';

// Create your login function.
// By default the urls are "/login" and "/login/options"
// but you can change those urls if needed.
const login = useLogin({
    actionUrl: '/api/login',
    optionsUrl: '/api/login/options'
});


// We can call this login function whenever we need (e.g. form submission)
login({
    username: 'john.doe'
})
    .then((response) => console.log('Authentication success'))
    .catch((error) => console.log('Authentication failure'))
;

As done during the registration, additional options are available. See the section “Deep into the framework” to know more. Hereafter another example:

login({
    username: 'john.doe',
    userVerification: 'required'
})
    .then((response) => console.log('Authentication success'))
    .catch((error) => console.log('Authentication failure'))
;

A package is available at . It contains functions that will ease the interaction with the login or the registration endpoints.

https://github.com/web-auth/webauthn-helper
The attestation ceremony
The assertion ceremony

Authenticate Your Users

First user authentication

Credential Request Options

To authenticate you user, you need to send a Webauthn\PublicKeyCredentialRequestOptions object.

To generate that object, you just need to call the method generatePublicKeyCredentialRequestOptions of the $server object.

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.

<?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
);

Response Verification

When the authenticator sends 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:

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

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

<?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!
}

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

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

your user repository
the Javascript page

Authenticate Your Users

During this step, your application will send a challenge to the list of registered devices of the user. The security token will resolve this challenge by adding information and digitally signing the data.

Assertion Request

To perform a user authentication using a security device, you need to instantiate a Webauthn\PublicKeyCredentialRequestOptions object.

Let’s say you want to authenticate the user we used earlier. This options object will need:

  • A challenge (random binary string)

  • A timeout (optional)

  • The list with the allowed credentials

The PublicKeyCredentialRequestOptions object is designed to be easily serialized into a JSON object. This will ease the integration into an HTML page or through an API endpoint.

For v4.0+, the timeout will be set to null. The values recommended by the specification are as follow:

  • If the user verification is discouraged, timeout should be between 30 and 180 seconds

  • If the user verification is preferred or required, the range is 300 to 600 seconds (5 to 10 minutes)

Allowed Credentials

The user trying to authenticate must have registered at least one device. For this user, you have to get all Webauthn\PublicKeyCredentialDescriptor associated to his account.

// We gather all registered authenticators for this user
$registeredAuthenticators = $publicKeyCredentialSourceRepository->findAllForUserEntity($userEntity);

// We don’t need the Credential Sources, just the associated Descriptors
$allowedCredentials = array_map(
    static function (PublicKeyCredentialSource $credential): PublicKeyCredentialDescriptor {
        return $credential->getPublicKeyCredentialDescriptor();
    },
    $registeredAuthenticators
);

User Verification

Extensions

Example

<?php

declare(strict_types=1);

use Webauthn\PublicKeyCredentialRequestOptions;

// List of registered PublicKeyCredentialDescriptor classes associated to the user
$registeredAuthenticators = $publicKeyCredentialSourceRepository->findAllForUserEntity($userEntity);
$allowedCredentials = array_map(
    static function (PublicKeyCredentialSource $credential): PublicKeyCredentialDescriptor {
        return $credential->getPublicKeyCredentialDescriptor();
    },
    $registeredAuthenticators
);

// Public Key Credential Request Options
$publicKeyCredentialRequestOptions = new PublicKeyCredentialRequestOptions(
    random_bytes(32),                                                           // Challenge
    60000,                                                                      // Timeout
    'foo.example.com',                                                          // Relying Party ID
    $allowedCredentials                                                         // Extensions
);

Response Handling

The way you receive this response is out of scope of this library. In the previous example, the data is part of the query string, but it can be done through a POST request body or a request header.

What you receive must be a JSON object that looks like as follows:

{
    "id":"KVb8CnwDjpgAo[…]op61BTLaa0tczXvz4JrQ23usxVHA8QJZi3L9GZLsAtkcVvWObA",
    "type":"public-key",
    "rawId":"KVb8CnwDjpgAo[…]rQ23usxVHA8QJZi3L9GZLsAtkcVvWObA==",
    "response":{
        "clientDataJSON":"eyJjaGFsbGVuZ2UiOiJQbk1hVjBVTS[…]1iUkdHLUc4Y3BDSdGUifQ==",
        "authenticatorData":"Y0EWbxTqi9hWTO[…]4aust69iUIzlwBfwABDw==",
        "signature":"MEQCIHpmdruQLs[…]5uwbtlPNOFM2oTusx2eg==",
        "userHandle":""
    }
}

There are two steps to perform with this object:

  • Load the data

  • Verify the loaded data against the assertion options set above

Data Loading

<?php

declare(strict_types=1);

$data = '
{
    "id":"KVb8CnwDjpgAo[…]op61BTLaa0tczXvz4JrQ23usxVHA8QJZi3L9GZLsAtkcVvWObA",
    "type":"public-key",
    "rawId":"KVb8CnwDjpgAo[…]rQ23usxVHA8QJZi3L9GZLsAtkcVvWObA==",
    "response":{
        "clientDataJSON":"eyJjaGFsbGVuZ2UiOiJQbk1hVjBVTS[…]1iUkdHLUc4Y3BDSdGUifQ==",
        "attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSj[…]YcGhf"
    }
}';

$publicKeyCredential = $publicKeyCredentialLoader->load($data);

Response Verification

Now we have a fully loaded Public Key Credential object, but we need now to make sure that:

  1. The authenticator response is of type AuthenticatorAssertionResponse

  2. This response is valid.

The first is easy to perform:

<?php

declare(strict_types=1);

use Webauthn\AuthenticatorAssertionResponse;

$authenticatorAssertionResponse = $publicKeyCredential->getResponse();
if (!$authenticatorAssertionResponse instanceof AuthenticatorAssertionResponse) {
    //e.g. process here with a redirection to the public key login/MFA page. 
}

The second step is the verification against the Public Key Assertion Options we created earlier.

The Authenticator Assertion Response Validator service (variable $authenticatorAssertionResponseValidator) will check everything for you.

<?php

declare(strict_types=1);

use Symfony\Component\HttpFoundation\Request;

$request = Request::createFromGlobals();

$publicKeyCredentialSource = $authenticatorAssertionResponseValidator->check(
    $publicKeyCredential->getRawId(),
    $authenticatorAssertionResponse,
    $publicKeyCredentialRequestOptions,
    $request,
    $userHandle
);

If no exception is thrown, the response is valid and you can continue the authentication of the user.

The Public Key Credential Source returned allows you to know which device was used by the user.

i.e. your application domain (optional)

(optional)

(optional)

Eligible authenticators are filtered and only capable of satisfying this requirement will interact with the user. Please refer to the for all possible values.

Please refer to the to know how to manage authentication extensions.

This step is exactly the same as the one described in process.

The Relying Party ID
The user verification requirement
Extensions
User Verification page
Extension page
Public Key Credential Creation

Register Authenticators

During this step, your application will send a challenge to the device. The device will resolve this challenge by adding information and digitally signing the data.

The application will check the response from the device and get its credential ID. This ID will be used for further authentication requests.

Creation Request

To associate a device to a user, you need to instantiate a Webauthn\PublicKeyCredentialCreationOptions object.

It will need:

  • A challenge (random binary string)

  • A list of supported public key parameters i.e. an algorithm list (at least one)

  • A timeout (optional)

  • A list of public key credential to exclude from the registration process (optional)

Let’s see an example of the PublicKeyCredentialCreationOptions object. The following example is a possible Public Key Creation page for a dummy user "@cypher-Angel-3000".

<?php

declare(strict_types=1);

use Cose\Algorithms;
use Webauthn\AuthenticatorSelectionCriteria;
use Webauthn\PublicKeyCredentialDescriptor;
use Webauthn\PublicKeyCredentialCreationOptions;
use Webauthn\PublicKeyCredentialParameters;
use Webauthn\PublicKeyCredentialRpEntity;
use Webauthn\PublicKeyCredentialUserEntity;

// RP Entity
$rpEntity = new PublicKeyCredentialRpEntity(
    'My Super Secured Application', //Name
    'foo.example.com',              //ID
    null                            //Icon
);

// User Entity
$userEntity = new PublicKeyCredentialUserEntity(
    '@cypher-Angel-3000',                   //Name
    '123e4567-e89b-12d3-a456-426655440000', //ID
    'Mighty Mike',                          //Display name
    null                                    //Icon
);

// Challenge
$challenge = random_bytes(16);

// Timeout
$timeout = 60000; // 60 seconds

// Public Key Credential Parameters
$publicKeyCredentialParametersList = [
    new PublicKeyCredentialParameters('public-key', Algorithms::COSE_ALGORITHM_ES256),
    new PublicKeyCredentialParameters('public-key', Algorithms::COSE_ALGORITHM_RS256),
];

// Devices to exclude
$excludedPublicKeyDescriptors = [
    new PublicKeyCredentialDescriptor(PublicKeyCredentialDescriptor::CREDENTIAL_TYPE_PUBLIC_KEY, 'ABCDEFGH…'),
];

$publicKeyCredentialCreationOptions = new PublicKeyCredentialCreationOptions(
    $rpEntity,
    $userEntity,
    $challenge,
    $publicKeyCredentialParametersList,
    $timeout,
    $excludedPublicKeyDescriptors,
    new AuthenticatorSelectionCriteria(),
    PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE,
    null // Extensions
);

It is important to store the user entity and the options object (e.g. in the session) for the next step; they will be needed to check the response from the device.

Creation Response

What you receive must be a JSON object that looks like as follow:

{
    "id":"KVb8CnwDjpgAo[…]op61BTLaa0tczXvz4JrQ23usxVHA8QJZi3L9GZLsAtkcVvWObA",
    "type":"public-key",
    "rawId":"KVb8CnwDjpgAo[…]rQ23usxVHA8QJZi3L9GZLsAtkcVvWObA==",
    "response":{
        "clientDataJSON":"eyJjaGFsbGVuZ2UiOiJQbk1hVjBVTS[…]1iUkdHLUc4Y3BDSdGUifQ==",
        "attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSj[…]YcGhf"
    }
}

There are two steps to perform with this object:

  • Load the data

  • Verify it with the creation options set above

Data Loading

Now that all components are set, we can load the data we receive using the Public Key Credential Loader service (variable $publicKeyCredential).

<?php

declare(strict_types=1);

$data = '
{
    "id":"KVb8CnwDjpgAo[…]op61BTLaa0tczXvz4JrQ23usxVHA8QJZi3L9GZLsAtkcVvWObA",
    "type":"public-key",
    "rawId":"KVb8CnwDjpgAo[…]rQ23usxVHA8QJZi3L9GZLsAtkcVvWObA==",
    "response":{
        "clientDataJSON":"eyJjaGFsbGVuZ2UiOiJQbk1hVjBVTS[…]1iUkdHLUc4Y3BDSdGUifQ==",
        "attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSj[…]YcGhf"
    }
}';

$publicKeyCredential = $publicKeyCredentialLoader->load($data);

If no exception is thrown, you can go to the next step: the verification.

Response Verification

Now we have a fully loaded Public Key Credential object, but we need now to make sure that:

  1. The authenticator response is of type AuthenticatorAttestationResponse

  2. This response is valid.

The first is easy to perform:

<?php

declare(strict_types=1);

use Webauthn\AuthenticatorAttestationResponse;

$authenticatorAttestationResponse = $publicKeyCredential->getResponse();
if (!$authenticatorAttestationResponse instanceof AuthenticatorAttestationResponse) {
    //e.g. process here with a redirection to the public key creation page. 
}

The second step is the verification against

  • The Public Key Creation Options we created earlier,

  • The HTTP request

The Authenticator Attestation Response Validator service (variable $authenticatorAttestationResponseValidator) will check everything for you: challenge, origin, attestation statement and much more.

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

<?php

declare(strict_types=1);

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();

$publicKeyCredentialSource = $authenticatorAttestationResponseValidator->check(
    $authenticatorAttestationResponse,
    $publicKeyCredentialCreationOptions,
    $serverRequest
);

If no exception is thrown, the response is valid. You can store the Public Key Credential Source ($publicKeyCredentialSource) and associate it to the user entity.

The way you store and associate these objects to the user is out of scope of this library. However, please note that these objects implement \JsonSerializable and have a static method createFromJson(string $json). This will allow you to serialize the objects into JSON and easily go back to an object.

If you have just registered a new user, don’t forget to store it in your database as well.

The Symfony Way

Lucky Symfony applications!

An official bundle is provided in the package web-auth/webauthn-symfony-bundle.

Starting at v3.2.4, the bundle can be installed on Symfony 4.4 or 5.0+.

Before installing it, please make sure you installed and configured:

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

And add the Webauthn Route Loader:

Repositories

Configuration

With Flex, you have a minimal configuration file installed through a Flex Recipe. You must set the repositories you have just created. You also have to modify the environment variables Relying_PARTY_ID and Relying_PARTY_NAME.

You may also need to adjust other parameters.

If you don’t use Flex, hereafter an example of configuration file:

Repositories

The credential_repository and user_repository parameters correspond to the services we created above.

Token Binding Handler

Creation Profiles

If you don't create the creation_profiles section, a default profile is set.

Relying Party (rp)

The parameter id is optional but highly recommended.

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 follows:

For v4.0+, the timeout will be set to null. The values recommended by the specification are as follow:

  • If the user verification is discouraged, timeout should be between 30 and 180 seconds

  • If the user verification is preferred or required, the range is 300 to 600 seconds (5 to 10 minutes)

Authenticator Selection Criteria

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 changing the default list unless you exactly know what you are doing.

Attestation Conveyance

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

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

Extensions

Request Profiles

If you don't create the creation_profiles section, a default profile is set.

The parameters for the request profiles (i.e. the authentication) are very similar to the creation profiles. The only difference is that you don’t need all the detail of the Relying Party, but only its ID (i.e. its domain).

Please note that all parameters are optional. The following configuration is perfectly valid. However, and as mentioned above, the parameter id is highly recommended.

Firewall

(e.g. user presence requirement)

(optional)

(optional)

The options object can be converted into JSON and sent to the authenticator .

If you use Laravel, you may be interested in

The package ,

The package or any package,

The and enabled the PSR-7 support.

The first step is to create and .

Only . Other storage systems like filesystem or Doctrine ODM may be added in the future but, at the moment, you have to create these from scratch.

Please refer to . You should let the default value as it is.

The realying Party corresponds to your application. Please refer for more information.

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

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

You can set as many extensions as you want in the profile. Please also for more information.

The example below is totally fictive. Some extensions are but the support depends on the authenticators, on the browsers and on the relying parties (your applications).

Now you have a fully configured bundle, you can protect your routes and manage the user registration and authenticatin through the .

The Relying Party
The User data
The Authenticator Selection Criteria
Attestation conveyance preference
Extensions
using a JS script
composer require symfony/psr-http-message-bridge nyholm/psr7 annotations
config/packages/sensio:framework:extra.yaml
sensio_framework_extra:
    psr_message:
        enabled: true
src/AppKernel.php
<?php

public function registerBundles()
{
    $bundles = [
        // ...
        new Webauthn\Bundle\WebauthnBundle(),
    ];
}
config/routes/webauthn:routes.php
<?php

declare(strict_types=1);

use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;

return function (RoutingConfigurator $routes) {
    $routes->import('.', 'webauthn');
};
config/packages/webauthn.yaml
webauthn:
#    logger: null # PSR-3 compatible logging service
    credential_repository: 'Webauthn\Bundle\Repository\DummyPublicKeyCredentialSourceRepository' # CREATE YOUR REPOSITORY AND CHANGE THIS!
    user_repository: 'Webauthn\Bundle\Repository\DummyPublicKeyCredentialUserEntityRepository' # CREATE YOUR REPOSITORY AND CHANGE THIS!
    token_binding_support_handler: 'Webauthn\TokenBinding\IgnoreTokenBindingHandler' # We ignore the token binding instructions by default
    creation_profiles: # Authenticator registration profiles
        default: # Unique name of the profile
            rp: # Relying Party information
                name: '%env(Relying_PARTY_NAME)%' # CHANGE THIS! or create the corresponding env variable
                id: '%env(Relying_PARTY_ID)%' # Please adapt the env file with the correct relying party ID or set null
#                icon: null # Secured image (data:// scheme)
#            challenge_length: 32
#            timeout: 60000
#            authenticator_selection_criteria:
#                attachment_mode: !php/const Webauthn\AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_NO_PREFERENCE
#                require_resident_key: false
#                user_verification: !php/const Webauthn\AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_PREFERRED
#            extensions:
#                loc: true
#            public_key_credential_parameters: # You should not change this list
#                - !php/const Cose\Algorithms::COSE_ALGORITHM_EdDSA #Order is important. Preferred algorithms go first
#                - !php/const Cose\Algorithms::COSE_ALGORITHM_ES256
#                - !php/const Cose\Algorithms::COSE_ALGORITHM_ES256K
#                - !php/const Cose\Algorithms::COSE_ALGORITHM_ES384
#                - !php/const Cose\Algorithms::COSE_ALGORITHM_ES512
#                - !php/const Cose\Algorithms::COSE_ALGORITHM_RS256
#                - !php/const Cose\Algorithms::COSE_ALGORITHM_RS384
#                - !php/const Cose\Algorithms::COSE_ALGORITHM_RS512
#                - !php/const Cose\Algorithms::COSE_ALGORITHM_PS256
#                - !php/const Cose\Algorithms::COSE_ALGORITHM_PS384
#                - !php/const Cose\Algorithms::COSE_ALGORITHM_PS512
#            attestation_conveyance: !php/const Webauthn\PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE
    request_profiles: # Authentication profiles
        default: # Unique name of the profile
            rp_id: '%env(Relying_PARTY_ID)%' # Please adapt the env file with the correct relying party ID or set null
#            challenge_length: 32
#            timeout: 60000
#            user_verification: !php/const Webauthn\AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_PREFERRED
#            extensions:
#                loc: true
#    metadata_service:
#        enabled: false
#        repository: 'App\Repository\MetadataStatementRepository'
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'
app/config/webauthn.yaml
webauthn:
    request_profiles:
        acme:
            rp_id: 'example.com'
app/config/webauthn.yaml
webauthn:
    request_profiles:
        acme: ~
this project: https://github.com/asbiin/laravel-webauthn
symfony/psr-http-message-bridge
nyholm/psr7
other PSR-7
SensioFrameworkExtraBundle
your credential
user entity repositories
Doctrine ORM based repositories are provided
to this page
the advanced concepts
attestation of the authenticator
refer to this page
defined in the specification
Symfony Firewall

Entities with Doctrine

Credential Source

The Doctrine Entity

With Doctrine, you have to indicate how to store the Credential Source objects. Hereafter an example of 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 hasn’t such a requirement and is a binary string, thus we need to declare our own id field.

Do not forget to update your database schema!

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 Flex, 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 implement 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);

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;

    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();
        parent::__construct($publicKeyCredentialId, $type, $transports, $attestationType, $trustPath, $aaguid, $credentialPublicKey, $userHandle, $counter);
    }

    public function getId(): string
    {
        return $this->id;
    }
}
App/Repository/PublicKeyCredentialSourceRepository.php
<?php

declare(strict_types=1);

namespace App\Repository;

use App\Entity\PublicKeyCredentialSource;
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);
    }

    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\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Security\Core\User\UserInterface;
use Webauthn\PublicKeyCredentialUserEntity;

/**
 * @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="json")
     */
    protected $roles;

    public function __construct(string $id, string $name, string $displayName, ?string $icon = null, array $roles = [])
    {
        parent::__construct($name, $id, $displayName, $icon);
        $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);

namespace App\Repository;

use App\Entity\User;
use Doctrine\Common\Persistence\ManagerRegistry;
use Ramsey\Uuid\Uuid;
use Webauthn\Bundle\Repository\AbstractPublicKeyCredentialUserEntityRepository;
use Webauthn\PublicKeyCredentialUserEntity;

final class UserRepository extends AbstractPublicKeyCredentialUserEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, User::class);
    }

    public function createUserEntity(string $username, string $displayName, ?string $icon): PublicKeyCredentialUserEntity
    {
        $id = Uuid::uuid4()->toString();

        return new User($id, $username, $displayName, $icon, []);
    }

    public function saveUserEntity(PublicKeyCredentialUserEntity $userEntity): void
    {
        if (!$userEntity instanceof User) {
            $userEntity =  new User(
                $userEntity->getId(),
                $userEntity->getName(),
                $userEntity->getDisplayName(),
                $userEntity->getId()
            );
        }

        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: ~

Debugging

The Easy Way

<?php

use App\Service\MyPsr3Logger;
use Webauthn\Server;

$server = new Server(
    $rpEntity
    $publicKeyCredentialSourceRepository
);

// Set your logging service here
$server->setLogger(new MyPsr3Logger());

The Hard Way

The following classes have an optional constructor parameter $logger that can accept the logging service.

  • Webauthn\AttestationStatement\AttestationObjectLoader

  • Webauthn\AuthenticatorAssertionResponseValidator

  • Webauthn\AuthenticatorAttestationResponseValidator

  • Webauthn\PublicKeyCredentialLoader

  • Webauthn\Counter\ThrowExceptionIfInvalid

The Symfony Way

config/packages/webauthn.yaml
webauthn:
    logger: App\Service\MyPsr3Logger

If you have troubles during the development of your application or if you want to keep track of every critical/error messages in production, you can use a .

PSR-3 compatible logger

The Hard Way

If you want a fine grained Webauthn server

You will need the following components before loading or verifying the data:

  • An Attestation Statement Support Manager and at least one Attestation Statement Support object

  • An Attestation Object Loader

  • A Public Key Credential Loader

  • An Authenticator Attestation Response Validator

  • An Extension Output Checker Handler

That’s a lot off classes! But don’t worry, as their configuration is the same for all your application, you just have to set them once. Let’s see all of these in the next sections.

Public Key Credential Source Repository

The Public Key Credential Source Repository must implement Webauthn\PublicKeyCredentialSourceRepository. It will retrieve the credential source and update them when needed.

Token Binding Handler

The token binding handler is a service that will verify if the token binding set in the device response corresponds to the one set in the request.

Attestation Statement Support Manager

Every Creation Responses contain an Attestation Statement. This attestation contains data regarding the authenticator depending on several factors such as its manufacturer and model, what you asked in the options, the capabilities of the browser or what the user allowed.

With Firefox for example, the user may refuse to send information about the security token for privacy reasons.

Hereafter the types of attestations you can have:

  • none: no attestation is provided.

  • fido-u2f: for non-FIDO2 compatible devices (old U2F security tokens).

  • packed: generally used by authenticators with limited resources (e.g., secure elements). It uses a very compact but still extensible encoding method.

  • android key: commonly used by old or disconnected Android devices.

  • android safety net: for new Android devices like smartphones.

  • trusted platform module: for devices with built-in security chips.

<?php

declare(strict_types=1);

use Webauthn\AttestationStatement\AttestationStatementSupportManager;
use Webauthn\AttestationStatement\NoneAttestationStatementSupport;

// The manager will receive data to load and select the appropriate 
$attestationStatementSupportManager = new AttestationStatementSupportManager();

// The none type
$attestationStatementSupportManager->add(new NoneAttestationStatementSupport());

Attestation Object Loader

This object will load the Attestation statements received from the devices. It will need the Attestation Statement Support Manager created above.

<?php

declare(strict_types=1);

use Webauthn\AttestationStatement\AttestationObjectLoader;

$attestationObjectLoader = new AttestationObjectLoader($attestationStatementSupportManager);

Public Key Credential Loader

This object will load the Public Key using from the Attestation Object.

<?php

declare(strict_types=1);

use Webauthn\PublicKeyCredentialLoader;

$publicKeyCredentialLoader = new PublicKeyCredentialLoader($attestationObjectLoader);

Extension Output Checker Handler

If you use extensions, you may need to check the value returned by the security devices. This behaviour is handled by an Extension Output Checker Manager.

<?php

declare(strict_types=1);

use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler;

$extensionOutputCheckerHandler = new ExtensionOutputCheckerHandler();

You can add as many extension checkers as you want. Each extension checker must implement Webauthn\AuthenticationExtensions\ExtensionOutputChecker and throw a Webauthn\AuthenticationExtensions\ExtensionOutputError in case of an error.

Authenticator Attestation Response Validator

This object is what you will directly use when receiving Attestation Responses (authenticator registration).

<?php

declare(strict_types=1);

use Webauthn\AuthenticatorAttestationResponseValidator;

$authenticatorAttestationResponseValidator = new AuthenticatorAttestationResponseValidator(
    $attestationStatementSupportManager,
    $publicKeyCredentialSourceRepository,
    $tokenBindingHandler,
    $extensionOutputCheckerHandler
);

Authenticator Assertion Response Validator

This object is what you will directly use when receiving Assertion Responses (user authentication).

<?php

declare(strict_types=1);

use Webauthn\AuthenticatorAssertionResponseValidator;

$authenticatorAssertionResponseValidator = new AuthenticatorAssertionResponseValidator(
    $publicKeyCredentialSourceRepository,  // The Credential Repository service
    $tokenBindingHandler,                  // The token binding handler
    $extensionOutputCheckerHandler,        // The extension output checker handler
    $coseAlgorithmManager                  // The COSE Algorithm Manager  
);

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 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 Criteria

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 criterion 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 a resident credential.

This parameter will be deprecated in the next version of the Webauthn specification.

You can still set true or false as second argument, but also use the fourth one described hereafter.

Fourth argument possible values are:

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

  • AuthenticatorSelectionCriteria::RESIDENT_KEY_REQUIREMENT_DISCOURAGED: indicates the Relying Party prefers creating a server-side credential, but will accept a client-side discoverable credential,

  • AuthenticatorSelectionCriteria::RESIDENT_KEY_REQUIREMENT_PREFERRED: indicates the Relying Party prefers creating a client-side discoverable credential, but will accept a server-side credential.

  • AuthenticatorSelectionCriteria::RESIDENT_KEY_REQUIREMENT_REQUIRED:

    indicates the Relying Party requires a client-side discoverable credential, and is prepared to receive an error if a client-side discoverable credential cannot be created

User Verification

The Easy Way

<?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
    AuthenticatorSelectionCriteria::RESIDENT_KEY_REQUIREMENT_REQUIRED       // Same effect as the second argument
);

$publicKeyCredentialCreationOptions = $server->generatePublicKeyCredentialCreationOptions(
    $userEntity,
    PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE,
    $excludedPublicKeyDescriptors,
    $authenticatorSelectionCriteria
);

The Hard Way

$authenticatorSelectionCriteria = new AuthenticatorSelectionCriteria(
    null,
    false,
    AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_DISCOURAGED,
    AuthenticatorSelectionCriteria::RESIDENT_KEY_REQUIREMENT_DISCOURAGED
);

$publicKeyCredentialCreationOptions = new PublicKeyCredentialCreationOptions(
    $rpEntity,
    $userEntity,
    $challenge,
    $publicKeyCredentialParametersList,
    $timeout,
    $excludedPublicKeyDescriptors,
    $authenticatorSelectionCriteria
);

The Symfony Way

config/packages/webauthn.yaml
webauthn:
    …
    creation_profiles:
        default:
            rp:
                name: 'My Application'
                id: 'example.com'
            authenticator_selection_criteria:
                attachment_mode: !php/const Webauthn\AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_NO_PREFERENCE
                require_resident_key: false
                user_verification: !php/const Webauthn\AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_PREFERRED
                resident_key: !php/const Webauthn\AuthenticatorSelectionCriteria::RESIDENT_KEY_REQUIREMENT_DISCOURAGED

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): 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.

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.

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 their sources (local storage or distant service).

Your Metadata Statement Repository must implement the interface Webauthn\MetadataService\MetadataStatementRepository that has two methods:

  • findOneByAAGUID(string $aaguid): this method retrieves the MetadataStatement object with AAGUID. It shall return null in case of the absence of the MDS.

  • findStatusReportsByAAGUID(string $aaguid): retrieves all StatusReport objects. These objects show the MDS compliance history.

The library does not provide any Metadata Statement Repositroy. It is up to you to select the MDS suitable for your application and store them in your database.

The Easy Way

You just have to inject the Metadata Statement Repository to your Server class.

The Hard Way

There are few steps to acheive. First, you have to add support classes for all attestation statement types into your Attestation Metatdata Manager.

Next, you must inject the Metadata Statement Repository to your Attestation Object Loader.

The Symfony Way

With Symfony, you must enable this feature to enable all the metadata types.

You can set the Google API key for the Android SafetyNet Attestation Statement support with the following configuration:

If you have some troubles when validating Android SafetyNet Attestation Statement, this may be caused by the leeway of the server clocks or the age of the statement. You can modify the default values as follows:

The modification of these parameters is not recommended. You should try to sync your server clock first.

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 4 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.

  • ATTESTATION_CONVEYANCE_PREFERENCE_ENTERPRISE: the Relying Party wants to receive an attestation statement that may include uniquely identifying information. This is intended for controlled deployments within an enterprise where the organization wishes to tie registrations to specific authenticators.

ATTESTATION_CONVEYANCE_PREFERENCE_ENTERPRISE comes from the current draft version of the Webauthn specification. This value may not be supported by all browsers.

The Easy Way

The Hard Way

The Symfony Way

Firewall

How to register and authenticate my users?

Security Bundle

To authenticate or register your users with Symfony, the best and easiest way is to use the Security Bundle. First, install that bundle.

Firewall Configuration

Now you can tell Symfony to use your user provider and you enable the dedicated firewall

In some case, you may have several user providers that are used by other parts of your application. This Webauthn bundle allow you to override the default user provider.

Logout

User Authentication

As you have noticed, there is nothing to configure to have a fully functional firewall. The firewall routes are automatically created for you. They are namely:

  • /login/options: to create the request options (POST only)

  • /login: to submit the assertion response (POST only)

You should also ensure to allow anonymous users to contact those endpoints.

Credential Request Options

Prior to the authentication of the user, you must create 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 field with the name of the user being authenticated.

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

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

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

Assertion Response

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.

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

Your user can now be authenticated and retrieved as usual.

Request Profile

By default, the default profile is used. You may have created a request profile in the bundle configuration. You can use this profile instead of the default one.

User Registration

The user registration is also managed by the firewall. It is disabled by default. If you want that feature, please enable it:

The firewall routes are automatically created for you. They are namely:

  • /register/options: to create the creation options (POST only)

  • /register: to submit the attestation response (POST only)

You should also ensure to allow anonymous users to contact those endpoints.

Credential Creation Options

Prior to the registration of a user and its authenticator, you must create a PublicKey Credential Creation Options object. To do so, send a POST request to /register/options.

The body of this request is a JSON object that must contain username and displayName fields with the username of the user being registered and the name displayed in the application.

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

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

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

Attestation Response

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

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

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

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

In case of success, the user and the authenticator are correctly registered and automatically logged in.

Creation Profile

By default, the default profile is used. You may have created a creation profile in the bundle configuration. You can use this profile instead of the default one.

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 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. In the example below, the /admin path can be reached by users with the role ROLE_ADMIN and that have been verified during the ceremony.

Options Storage

Webauthn authentication and registration are 2 steps round trip processes:

  • Options issuance

  • Authenticator response verification

It is required to store the options and the user entity associated to it to verify the authenticator responses.

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 has to implement Webauthn\Bundle\Security\Storage\RequestOptionsStorage and be declared as a container service.

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

Response Handlers

You can customize the responses returned by the firewall by using a custom handler. This could be useful when you want to return additional information to your application.

There are 4 types of responses and handlers:

  • Request options,

  • Creation options,

  • Authentication Success,

  • Authentication Failure,

Request Options Handler

This handler is called when a client sends a valid POST request to the options_path during the authentication process. 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 has 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:

Creation Options Handler

This handler is very similar to the previous one, except that it is called during the registration of a new user and has to implement the interface Webauthn\Bundle\Security\Handler\CreationOptionsHandler.

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 has 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 has 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:

Authentication without username

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

  1. The user verification is required,

  2. The list of allowed authenticators must be empty

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

The Easy Way

Selection criteria for the registration of the authenticator:

The Request Options:

The Hard Way

Selection criteria for the registration of the authenticator:

The Request Options:

The Symfony Way

The bundle configuration should have a profile with the constraints listed above:

You can implement the required methods the way you want: Doctrine ORM, file storage… as mentioned on .

Please refer to .

All these attestation types are supported, but you should only use the none one unless you plan to use the .

The Android SafetyNet Attestation Statement is a JWT that can be verified by the library, but can also be checked online by hitting the Google API. This method drastically increases the security for the attestation type but requires a and .

This criterion is needed if you want to .

.

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

The Android SafetyNet Attestation Statement is a JWT that can be verified by the library, but can also be checked online by hitting the Google API. This method drastically increase the security for the attestation type but requires a and .

Next, you have to create a custom that will retrieve users on login requests. The following example uses the .

Users can logout as usual. You just have to add under your firewall and add the route.

No need to reinvent the wheel, you can use the package.

During the registration of the authenticator, a ,

The Public Key Credential Source Repository
A token binding handler
the dedicated page
the dedicated page
Attestation and Metadata Statement
PSR-18 compatible HTTP Client
an API key
authenticate users without username
Please refer to this page
<?php

use Webauthn\Server;
use Webauthn\PublicKeyCredentialRpEntity;

...

$server = new Server(
    $rpEntity
    $publicKeyCredentialSourceRepository,
    $myMetadataStatementRepository        // Inject your new service here
);
<?php

declare(strict_types=1);

use Cose\Algorithm\Manager;
use Cose\Algorithm\Signature\ECDSA;
use Cose\Algorithm\Signature\EdDSA;
use Cose\Algorithm\Signature\RSA;
use Webauthn\AttestationStatement\AndroidSafetyNetAttestationStatementSupport;
use Webauthn\AttestationStatement\AndroidKeyAttestationStatementSupport;
use Webauthn\AttestationStatement\AttestationStatementSupportManager;
use Webauthn\AttestationStatement\FidoU2FAttestationStatementSupport;
use Webauthn\AttestationStatement\NoneAttestationStatementSupport;
use Webauthn\AttestationStatement\PackedAttestationStatementSupport;
use Webauthn\AttestationStatement\TPMAttestationStatementSupport;

// You normally already do this
$attestationStatementSupportManager = new AttestationStatementSupportManager();
$attestationStatementSupportManager->add(new NoneAttestationStatementSupport());

// Additional classes to add
$attestationStatementSupportManager->add(new FidoU2FAttestationStatementSupport());
$attestationStatementSupportManager->add(new AndroidSafetyNetAttestationStatementSupport());
$attestationStatementSupportManager->add(new AndroidKeyAttestationStatementSupport(
    $psr18Client,         // Can be null if you don’t want to use the Google API
    $googleApiKey,        // Can be null if you don’t want to use the Google API
    $psr17RequestFactory  // Can be null if you don’t want to use the Google API
));
$attestationStatementSupportManager->add(new TPMAttestationStatementSupport());

// Cose Algorithm Manager
// The list of algorithm depends on the algorithm list you defined in your options
// You should use at least ES256 and RS256 algorithms that are widely used.
$coseAlgorithmManager = new Manager();
$coseAlgorithmManager->add(new ECDSA\ES256());
$coseAlgorithmManager->add(new RSA\RS256());

$attestationStatementSupportManager->add(new PackedAttestationStatementSupport($coseAlgorithmManager));
$attestationObjectLoader = new AttestationObjectLoader($attestationStatementSupportManager, $metadataStatementRepository);
config/packages/webauthn.yaml
webauthn:
    metadata_service:
        enabled: true
        repository: 'App\Repository\MyMetadataStatementRepository'
webauthn:
    android_safetynet:
        http_client: 'my.psr18.http.client'
        request_factory: 'my.psr17.request_factory'
webauthn:
    android_safetynet:
        max_age: 60000 # in milliseconds. Default set to 60000 = 1 min
        leeway: 2000 # in milliseconds. Default set to 0
<?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'
composer require symfony/security-bundle
src/Security/UserProvider.php
<?php

namespace App\Security;

use App\Entity\User;
use App\Repository\UserRepository;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;

class UserProvider implements UserProviderInterface
{
    /**
     * @var UserRepository
     */
    private $userRepository;

    public function __construct(UserRepository $userRepository)
    {
        $this->userRepository = $userRepository;
    }

    /**
     * @throws UsernameNotFoundException if the user is not found
     */
    public function loadUserByUsername($username): UserInterface
    {
        $user = $this->userRepository->find($username);
        if (null !== $user) {
            return $user;
        }

        throw new UsernameNotFoundException(sprintf('User with name "%s" does not exist', $username));
    }

    public function refreshUser(UserInterface $user): UserInterface
    {
        if (!$user instanceof User) {
            throw new UnsupportedUserException(sprintf('Invalid user class "%s".', get_class($user)));
        }

        return $this->loadUserByUsername($user->getUsername());
    }

    public function supportsClass($class): bool
    {
        return User::class === $class;
    }
}
config/packages/security.yaml
security:
    providers:
        main:
            id: 'App\Security\UserProvider'
    firewalls:
        main:
            webauthn: ~
config/packages/security.yaml
security:
    providers:
        backend_users:
            memory:
                users:
                    john_admin: { password: '$2y$13$jxGxc ... IuqDju', roles: ['ROLE_ADMIN'] }
                    jane_admin: { password: '$2y$13$PFi1I ... rGwXCZ', roles: ['ROLE_ADMIN', 'ROLE_SUPER_ADMIN'] }
    firewalls:
        backend:
            pattern: ^/backend/
            # ... Add your configuration here

        main:
            webauthn:
                user_provider: 'App\Security\UserProvider'
config/packages/security.yaml
security:
    # ...

    firewalls:
        main:
            # ...
            logout: ~
security:
    access_control:
        - { path: ^/login,  roles: IS_AUTHENTICATED_ANONYMOUSLY }
fetch('/login/options', {
    method  : 'POST',
    credentials : 'same-origin',
    headers : {
        'Content-Type' : 'application/json'
    },
    body: JSON.stringify({
        "username": "johndoe@example.com"
    })
}).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:
                authentication:
                    routes:
                        options_path: /security/authentication/options
    access_control:
        - { path: ^/security/authentication/options,  roles: IS_AUTHENTICATED_ANONYMOUSLY}
fetch('/login', {
    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:
                authentication:
                    routes:
                        result_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:
                authentication:
                    profile: 'acme'
security:
    firewalls:
        main:
            webauthn:
                registration:
                    enabled: true
security:
    access_control:
        - { path: ^/register,  roles: IS_AUTHENTICATED_ANONYMOUSLY }
fetch('/register/options', {
        method  : 'POST',
        credentials : 'same-origin',
        headers : {
            'Content-Type' : 'application/json'
        },
        body: JSON.stringify({
            "username": "johndoe@example.com",
            "displayName": "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:
                registration:
                    routes:
                        options_path: /security/registration/options
    access_control:
        - { path: ^/security/registration/options,  roles: IS_AUTHENTICATED_ANONYMOUSLY}
fetch('/register', {
    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:
                registration:
                    routes:
                        result_path: /security/registration/result
security:
    firewalls:
        main:
            webauthn:
                registration:
                    profile: 'acme'
security:
    access_control:
        - { path: ^/admin,  roles: [ROLE_ADMIN, IS_USER_VERIFIED]}
security:
    firewalls:
        main:
            webauthn:
                options_storage: 'App\Handler\MyCustomRequestOptionsStorage'
security:
    firewalls:
        main:
            webauthn:
                authentication:
                    options_handler: 'App\Handler\MyCustomRequestOptionsHandler'
ecurity:
    firewalls:
        main:
            webauthn:
                registration:
                    options_handler: 'App\Handler\MyCustomRequestOptionsHandler'
security:
    firewalls:
        main:
            webauthn:
                success_handler: 'App\Handler\MyCustomAuthenticationSuccessHandler'
security:
    firewalls:
        main:
            webauthn:
                failure_handler: 'App\Handler\MyCustomAuthenticationFailureHandler'
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
    AuthenticatorSelectionCriteria::RESIDENT_KEY_REQUIREMENT_REQUIRED
);
<?php

use Webauthn\PublicKeyCredentialRequestOptions;

$ublicKeyCredentialRequestOptions = $server->generatePublicKeyCredentialRequestOptions(
    PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_REQUIRED,
);
$authenticatorSelectionCriteria = new AuthenticatorSelectionCriteria(
    AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_NO_PREFERENCE,
    true,                                                                   // Resident key required
    AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_REQUIRED, // User verification required
    AuthenticatorSelectionCriteria::RESIDENT_KEY_REQUIREMENT_REQUIRED
);

$publicKeyCredentialCreationOptions = new PublicKeyCredentialCreationOptions(
    $rpEntity,
    $userEntity,
    $challenge,
    $publicKeyCredentialParametersList,
    $timeout,
    $excludedPublicKeyDescriptors,
    $authenticatorSelectionCriteria
);
// Public Key Credential Request Options
$publicKeyCredentialRequestOptions = new PublicKeyCredentialRequestOptions(
    random_bytes(32),
    60000, 
    'foo.example.com',
    [],
    PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_REQUIRED
);
webauthn:
    credential_repository: '…'
    user_repository: '…'
    creation_profiles:
        default:
            rp:
                name: 'My application'
                id: 'example.com'
            authenticator_selection_criteria:
                require_resident_key: true
                user_verification: !php/const Webauthn\AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_REQUIRED
                resident_key: !php/const Webauthn\AuthenticatorSelectionCriteria::RESIDENT_KEY_REQUIREMENT_REQUIRED
    request_profiles:
        default:
            rp_id: 'example.com'
            user_verification: !php/const Webauthn\AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_REQUIRED

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.

Eligible authenticators are filtered and only capable of satisfying this requirement will interact with the user.

Possible user verification values are:

  • required: this value indicates that the application requires user verification for the operation and will fail the operation if the response does not have the UV flag set.

  • preferred: this value indicates that the application prefers user verification for the operation if possible, but will not fail the operation if the response does not have the UV flag set.

  • discouraged: this value indicates that the application does not want user verification employed during the operation (e.g.,in the interest of minimizing disruption to the user interaction flow).

Public constants are provided by AuthenticatorSelectionCriteria.

  • AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_REQUIRED

  • AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_PREFERRED

  • AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_DISCOURAGED

The Easy Way

During the registration of a new authenticator, the user verification is given by the authenticator selection criteria object.

use Webauthn\AuthenticatorSelectionCriteria;

$authenticatorSelectionCriteria = new AuthenticatorSelectionCriteria(
    null,
    false,
    AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_DISCOURAGED
);

$publicKeyCredentialCreationOptions = $server->generatePublicKeyCredentialCreationOptions(
    $userEntity,
    PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE,
    $excludeCredentials
    $authenticatorSelectionCriteria
);

During the authentication of the user:

use Webauthn\AuthenticatorSelectionCriteria;

$publicKeyCredentialRequestOptions = $server->generatePublicKeyCredentialRequestOptions(
    AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_DISCOURAGED,
    $allowedAuthenticators
);

The Hard Way

Authenticator registration

$authenticatorSelectionCriteria = new AuthenticatorSelectionCriteria(
    null,
    false,
    AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_DISCOURAGED
);

$publicKeyCredentialCreationOptions = new PublicKeyCredentialCreationOptions(
    $rpEntity,
    $userEntity,
    $challenge,
    $publicKeyCredentialParametersList,
    $timeout,
    $excludedPublicKeyDescriptors,
    $authenticatorSelectionCriteria
);

User Authentication

// Public Key Credential Request Options
$publicKeyCredentialRequestOptions = new PublicKeyCredentialRequestOptions(
    random_bytes(32),
    60000, 
    'foo.example.com',
    $allowedCredentials,
    PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_DISCOURAGED
);

The Symfony Way

The easiest way to manage that is by using the creation and request profiles.

config/packages/webauthn.yaml
webauthn:
    …
    creation_profiles:
        default:
            rp:
                name: 'My Application'
                id: 'example.com'
            authenticator_selection_criteria:
                user_verification: !php/const Webauthn\AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_PREFERRED
    request_profiles:
        default:
            rp_id: 'example.com'
            user_verification: !php/const Webauthn\AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_PREFERRED
FIDO Alliance Metadata Service
PSR-18 compatible HTTP Client
an API key
the logout configuration
webauthn-helper
user provider
user repository showed on this page
Resident Key must have been asked

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.

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…).

The Easy Way

The Hard Way

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);
        }
    }
}

If the current user is registering authenticators for another user (admin), the userEntity passed to the methods generateOptions and validateResponse must correspond to the target user.

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'
);

As the user shall be authenticated to register a new authenticator, you should protect these routes in the security.yaml file.

config/packages/security.yaml
security:
    access_control:
        - { path: ^/add/device,  roles: IS_AUTHENTICATED_FULLY }

Dealing with “localhost”

aka non-https relying parties

Secured Context

If your are working on a development environment, https may not be available but the context could be considered as secured. You can bypass the scheme verification by passing the list of rpIds you consider secured.

Please be careful using this feature. It should NOT be used in production.

The Easy Way

The Hard Way

The Symfony Way

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

The procedure is the same as .

the one described in this page
the one described in this page
$server->setSecuredRelyingPartyId(['localhost']);
$publicKeyCredentialSource = $authenticatorAttestationResponseValidator->check(
    $authenticatorAttestationResponse,
    $publicKeyCredentialCreationOptions,
    $serverRequest,
    ['localhost']
);
$publicKeyCredentialSource = $authenticatorAssertionResponse->check(
    $publicKeyCredential->getRawId(),
    $authenticatorAssertionResponse,
    $publicKeyCredentialRequestOptions,
    $request,
    $userHandle,
    ['localhost']
);
security:
    firewalls:
        main:
            webauthn:
               secured_rp_ids:
                   - 'localhost'

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.

This library is ready to handle extension inputs and outputs, but no concrete implementations are provided.

It is up to you, depending on the extensions you want to support, to create the extension handlers.

Creation/Request Options

The following example is totally fictive. We will add an extension input loc=true to the request option object.

<?php

declare(strict_types=1);

use Webauthn\AuthenticationExtensions\AuthenticationExtension;
use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs;
use Webauthn\PublicKeyCredentialRequestOptions;

// Extensions
$extensions = new AuthenticationExtensionsClientInputs();
$extensions->add(new AuthenticationExtension('loc', true));

// List of registered PublicKeyCredentialDescriptor classes associated to the user
$registeredPublicKeyCredentialDescriptors = …;

// Public Key Credential Request Options
$publicKeyCredentialRequestOptions = new PublicKeyCredentialRequestOptions(
    random_bytes(32),                                                           // Challenge
    60000,                                                                      // Timeout
    'foo.example.com',                                                          // Relying Party ID
    $registeredPublicKeyCredentialDescriptors,                                  // Registered PublicKeyCredentialDescriptor classes
    PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_PREFERRED, // User verification requirement
    $extensions
);

Extension Output Checker

An Extension Output Checker will check the extension inputs and output.

It must implement the interface Webauthn\AuthenticationExtensions\ExtensionOutputChecker and throw an exception of type Webauthn\AuthenticationExtension\ExtensionOutputError in case of error.

Devices may ignore the extension inputs. The extension outputs are therefore not guaranteed.

In the previous example, we asked for the location of the device and we expect to receive geolocation data in the extension output.

<?php

declare(strict_types=1);

namespace Acme\Extension;

use Webauthn\AuthenticationExtensions\ExtensionOutputChecker;
use Webauthn\AuthenticationExtensions\ExtensionOutputError;

final class LocationExtensionOutputChecker
{
    public function check(AuthenticationExtensionsClientInputs $inputs, AuthenticationExtensionsClientOutputs $outputs): void
    {
        if (!$inputs->has('loc') || $inputs->get('loc') !== true) {
            return;
        }

        if (!$outputs->has('loc')) {
            //You may simply return but here we consider it is a mandatory extension output.
            throw new ExtensionOutputError(
                $inputs->get('loc'),
                'The location of the device is missing'
            );
        }

        $location = $outputs->get('loc');
        //... Proceed with the output e.g. by logging the location of the device
        // or verifying it is in a specific area.
    }
}

Extension Input

To enable an authenticator feature like the geolocation, you must ask it through the creation or the request option objects.

The Easy Way

Authenticator registration

<?php

declare(strict_types=1);

use Webauthn\AuthenticationExtensions\AuthenticationExtension;
use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs;

// Extensions
$extensions = new AuthenticationExtensionsClientInputs();
$extensions->add(new AuthenticationExtension('loc', true));

$publicKeyCredentialCreationOptions = $server->generatePublicKeyCredentialCreationOptions(
    $userEntity,
    $attestationMode,
    $excludedPublicKeyDescriptors, $authenticatorSelectionCriteria,
    $extensions
);

User Authentication

<?php

use Webauthn\AuthenticationExtensions\AuthenticationExtension;
use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs;
use Webauthn\PublicKeyCredentialCreationOptions;

// Extensions
$extensions = new AuthenticationExtensionsClientInputs();
$extensions->add(new AuthenticationExtension('loc', true));

$publicKeyCredentialRequestOptions = $server->generatePublicKeyCredentialRequestOptions(
    PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_PREFERRED,
    $allowedCredentials
    $extensions
);

The Hard Way

Authenticator registration

<?php

declare(strict_types=1);

use Webauthn\AuthenticationExtensions\AuthenticationExtension;
use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs;
use Webauthn\PublicKeyCredentialCreationOptions;

// Extensions
$extensions = new AuthenticationExtensionsClientInputs();
$extensions->add(new AuthenticationExtension('loc', true));

$publicKeyCredentialCreationOptions = new PublicKeyCredentialCreationOptions(
    $rpEntity,
    $userEntity,
    $challenge,
    $publicKeyCredentialParametersList,
    $timeout,
    $excludedPublicKeyDescriptors,
    $authenticatorSelectionCriteria,
    PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE,
    $extensions
);

User Authentication

<?php

declare(strict_types=1);

use Webauthn\AuthenticationExtensions\AuthenticationExtension;
use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs;
use Webauthn\PublicKeyCredentialRequestOptions;

// Extensions
$extensions = new AuthenticationExtensionsClientInputs();
$extensions->add(new AuthenticationExtension('loc', true));

// List of registered PublicKeyCredentialDescriptor classes associated to the user
$registeredAuthenticators = $publicKeyCredentialSourceRepository->findAllForUserEntity($userEntity);
$allowedCredentials = array_map(
    static function (PublicKeyCredentialSource $credential): PublicKeyCredentialDescriptor {
        return $credential->getPublicKeyCredentialDescriptor();
    },
    $registeredAuthenticators
);

// Public Key Credential Request Options
$publicKeyCredentialRequestOptions = new PublicKeyCredentialRequestOptions(
    random_bytes(32),                                                           // Challenge
    60000,                                                                      // Timeout
    'foo.example.com',                                                          // Relying Party ID
    $allowedCredentials,                                                        // Registered PublicKeyCredentialDescriptor classes
    PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_PREFERRED, // User verification requirement
    $extensions                                                                 // Extensions
);

The Symfony Way

The easiest way to manage that is by using the creation and request profiles.

config/packages/webauthn.yaml
webauthn:
    …
    creation_profiles:
        default:
            rp:
                name: 'My Application'
                id: 'example.com'
            extensions:
                loc: true
    request_profiles:
        default:
            rp_id: 'example.com'
            extensions:
                loc: true

Authenticator Counter

The authenticators may have an internal counter. This feature is very helpful to detect cloned devices.

The default behaviour is to reject the assertions. This behaviour might cause some troubles as it could reject the real device whilst the fake one can continue to be used.

It is therefore required to go deeper in the protection of your application by logging the error and locking the associated account.

To do so , you have to create a custom Counter Checker and inject it to your Authenticator Assertion Response Validator. The checker must implement the interface Webauthn\Counter\CounterChecker.

<?php

declare(strict_types=1);


namespace App\Service;

use App\SecuritySystem;
use Assert\Assertion;
use Throwable;
use Webauthn\PublicKeyCredentialSource;

final class CustomCounterChecker implements CounterChecker
{
    private $securitySystem;

    public function __construct(SecuritySystem $securitySystem)
    {
        $this->securitySystem = $securitySystem ;
    }

    public function check(PublicKeyCredentialSource $publicKeyCredentialSource, int $currentCounter): void
    {
        try {
            Assertion::greaterThan($currentCounter, $publicKeyCredentialSource->getCounter(), 'Invalid counter.');
        } catch (Throwable $throwable) {
            $this->securitySystem->fakeDeviceDetected($publicKeyCredentialSource);
            throw $throwable;
        }
    }
}

The Easy Way

<?php

use Webauthn\Server;

$server = new Server(
    $rpEntity
    $publicKeyCredentialSourceRepository
);

// Set your handler here
$server->setCounterChecker(new CustomCounterChecker());

The Hard Way

$authenticatorAssertionResponseValidator = new AuthenticatorAssertionResponseValidator(
    $publicKeyCredentialSourceRepository,
    $tokenBindingHandler,
    $extensionOutputCheckerHandler,
    $coseAlgorithmManager,
    new CustomCounterChecker()
);

The Symfony Way

config/packages/webauthn.yaml
webauthn:
    counter_checker: App\Service\CustomCounterChecker

Token Binding

When receiving a Webauthn Response, the property tokenBinding in the Webauthn\CollectedClientData object has one of the following values:

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

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

  • "present": the browser supports token binding, and it 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

The Hard Way

The Symfony Way

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

When you create your and , just inject the correct handler.

<?php

use Webauthn\Server;
use Webauthn\TokenBinding\TokenBindingNotSupportedHandler;

$server = new Server(
    $rpEntity
    $publicKeyCredentialSourceRepository
);

// Set your handler here
$server->setTokenBindingHandler(new TokenBindingNotSupportedHandler());
config/packages/webauthn.yaml
webauthn:
    token_binding_support_handler: Webauthn\TokenBinding\TokenBindingNotSupportedHandler
this page
RFC 8471
Authenticator Attestation
Authenticator Assertion Response Validators

From v2.x to v3.0

Step-by-step guide for migrating from v2.x to v3.0

Update the libraries

First of all, you have to make sure you are using the last v2.x release (v2.1.7 at the time of writing).

Spot deprecations

This section is not yet finished

General modifications

CBOR\Decoder Service Not Needed Anymore

You don't have to inject the CBOR\Decoder service anymore. This service is automatically created with the necessary options.

Example:

use Webauthn\AuthenticatorAssertionResponseValidator;

//Before:
$validator = new AuthenticatorAssertionResponseValidator(
    $publicKeyCredentialSourceRepository,
    $decoder,
    $tokenBindingHandler,
    $extensionOutputCheckerHandler,
    $algorithmManager
);

//After
$validator = new AuthenticatorAssertionResponseValidator(
    $publicKeyCredentialSourceRepository,
    $tokenBindingHandler,
    $extensionOutputCheckerHandler,
    $algorithmManager
);

Metadata Statement Repository Is Required

You must inject the Metadata Statement Repository to use Metadata Statement types other than none.

Removed methods, functions or properties

  • Webauthn\PublicKeyCredentialSource::createFromPublicKeyCredential()

  • Webauthn\ CertificateToolbox::checkAttestationMedata()

Internal Dependency Changes:

  • Added:

    • league/uri-components ^2.1

    • psr/log ^1.1

    • sensio/framework-extra-bundle ^5.2

  • Bumped:

    • league/uri from ^5.3 to ^6.0

    • spomky-labs/cbor-bundle from ^1.0 to ^2.0

    • symfony/* from ^4.3 to ^5.0

  • Removed:

    • symfony/http-client

Update your Configuration Files

To be written

Upgrade the libraries

It is now time to upgrade the libraries. In your composer.json, change all web-auth/* dependencies from v2.x to v3.0. When done, execute composer update.

This may also update other dependencies. You can list upgradable libraries by calling composer outdated. Please make sure these libraries do not impact your upgrade.

All Modifications In A Row

This project follows the and, contrary to upgrade a minor version (where the middle number changes) where no difficulty should be encountered, upgrade a major version (where the first number changes) is subject to significant modifications.

Next, you have to verify you don’t use any deprecated class, interface, method or property. If you have PHPUnit tests, .

If you want to see all modifications at once, please .

Semantic Versioning principles
you can easily get the list of deprecation used in your application
have a look at this page