All pages
Powered by GitBook
1 of 10

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Authentication without username

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

  1. During the registration of the authenticator, a Resident Key must have been asked,

  2. The user verification is required,

  3. 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 bundle configuration should have a profile with the constraints listed above:

config/packages/webauthn.yaml
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
    request_profiles:
        default:
            rp_id: 'example.com'
            user_verification: !php/const Webauthn\AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_REQUIRED

User Verification

The easiest way to manage the user verification requirements in your Symfony application 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

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

With Symfony, you must enable this feature and define a Metadata Statement Repository in the configuration file.

config/packages/webauthn.yaml
webauthn:
    metadata_service:
        enabled: true
        repository: 'App\Repository\MyMetadataStatementRepository'

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 3 conveyance modes available using PHP constants provided by the class Webauthn\PublicKeyCredentialCreationOptions:

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

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

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

  • ATTESTATION_CONVEYANCE_PREFERENCE_ENTERPRISE: the Relying Party wants to receive uniquely identifying information from authenticators.

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'

Dealing with “localhost”

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.

config/packages/security.yaml
security:
    firewalls:
        main:
            webauthn:
               secured_rp_ids:
                   - 'localhost'

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

With a Symfony application, the fastest way for a user to register additional authenticators is to use the “controller” feature.

To add a new authenticator to a user, the bundle needs to know to whom it should be added. This can be:

  • The current user itself e.g. from its own account

  • An administrator acting for another user from a dashboard

For that purpose, a User Entity Guesser service should be created. This service shall implement the interface Webauthn\Bundle\Security\Guesser\UserEntityGuesser and its unique method findUserEntity.

You can directly use the Webauthn\Bundle\Security\Guesser\CurrentUserEntityGuesser as a Symfony service. It is designed to identify the user that is currently logged in.

In the example herafter where the current user is guessed using a controller parameter. This can be used when an administrator is adding an authenticator to another user account.

App\Guesser\FromQueryParameterGuesser.php
<?php

declare(strict_types=1);

namespace App\Guesser;

use Assert\Assertion;
use Symfony\Component\HttpFoundation\Request;
use Webauthn\Bundle\Repository\PublicKeyCredentialUserEntityRepository;
use Webauthn\Bundle\Security\Guesser\UserEntityGuesser;
use Webauthn\PublicKeyCredentialUserEntity;

final class FromQueryParameterGuesser implements UserEntityGuesser
{
    public function __construct(
        private PublicKeyCredentialUserEntityRepository $userEntityRepository
    ) {
    }

    public function findUserEntity(Request $request): PublicKeyCredentialUserEntity
    {
        $userHandle = $request->query->get('user_id');
        Assertion::string($userHandle, 'User entity not found. Invalid user ID');
        $user = $this->userEntityRepository->findOneByUserHandle($userHandle);
        Assertion::isInstanceOf($user, PublicKeyCredentialUserEntity::class, 'User entity not found.');

        return $user;
    }
}

In the case the current user s supposed to be administrator, the user entity can be determined using the query parameters and a route like /admin/add-authenticator/for/{user_id}.

Now you just have to enable the feature and set the routes to your options and response controllers.

config/packages/webauthn.yaml
webauthn:
    controllers:
        enabled: true # We enable the feature
        creation:
            from_user_account: # Endpoints accessible by the user itself
                options_path: '/profile/security/devices/add/options' # Path to the creation options controller
                result_path: '/profile/security/devices/add' # Path to the response controller
                user_entity_guesser: Webauthn\Bundle\Security\Guesser\CurrentUserEntityGuesser # See above
            from_admin_dashboard: # Endpoint accessible by an administrator
                options_path: '/admin/security/user/{user_id}/devices/add/options' # Path to the creation options controller
                result_path: '/admin/security/user/{user_id}/devices/add' # Path to the response controller
                user_entity_guesser: App\Guesser\FromQueryParameterGuesser # From the example

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: ^/profile,  roles: IS_AUTHENTICATED_FULLY } # We protect all the /profile path
        - { path: ^/admin,  roles: ROLE_ADMIN }

Now you can send requests to these new endpoints. For example, if you are using the Javascript library, the calls will look like as follow:

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

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


// We can call this register function whenever we need
//   No "username" or "displayName" parameters are needed
//   as the user entity is guessed by the dedicated service
register({})
    .then((response) => console.log('Registration success'))
    .catch((error) => console.log('Registration failure'))
;

Creation Profile

The default creation profile is used. You can change it using the dedicated option.

config/packages/webauthn.yaml
webauthn:
    controllers:
        enabled: true
        creation:
            from_user_account:
                …
                profile: custom_profile

Response Handlers

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

There are 3 types of responses and handlers:

  • Creation options,

  • Success,

  • Failure.

Creation Options Handler

This handler is called during the registration of a authenticator and has to implement the interface Webauthn\Bundle\Security\Handler\CreationOptionsHandler.

config/packages/webauthn.yaml
webauthn:
    controllers:
        enabled: true
        creation:
            from_user_account:
                …
                options_handler: … # Your handler here

Success Handler

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

config/packages/webauthn.yaml
webauthn:
    controllers:
        enabled: true
        creation:
            from_user_account:
                …
                success_handler: … # Your handler here

Failure Handler

This handler is called when an error occurred during the process. This handler shall implement the interface Webauthn\Bundle\Security\Handler\SuccessHandler. The default handler is Webauthn\Bundle\Service\DefaultFailureHandler.

config/packages/webauthn.yaml
webauthn:
    controllers:
        enabled: true
        creation:
            from_user_account:
                …
                failure_handler: … # Your handler here

Extensions

Extension Output Checker

An Extension Output Checker will check the extension 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('uvm') || $inputs->get('uvm') !== true) {
            return;
        }

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

        $uvm = $outputs->get('uvm');
        //... Proceed with the output
    }
}

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:
                uvm: true
    request_profiles:
        default:
            rp_id: 'example.com'
            extensions:
                uvm: true

Debugging

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 .

config/packages/webauthn.yaml
webauthn:
    logger: App\Service\MyPsr3Logger
PSR-3 compatible logger

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 criterion is needed if you want to authenticate users without username.

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

To be written

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 might cause some troubles as it could reject the real device whilst the fake one can continue to be used. You may also want to log the error, warn administrators or lock the associated user 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.

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

The following example is fictive and show how to lock a user, log the error and throw an exception.

<?php

declare(strict_types=1);

namespace Acme\Service;

use Assert\Assertion;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Throwable;
use Webauthn\PublicKeyCredentialSource;

final class CustomCounterChecker implements CounterChecker
{
    public function __construct(private UserRepository $userRepository)
    {
    }

    public function check(PublicKeyCredentialSource $publicKeyCredentialSource, int $currentCounter): void
    {
        if ($currentCounter > $publicKeyCredentialSource->getCounter()) {
            return;
        }
        
        $userId = $publicKeyCredentialSource->getUserHandle();
        $user = $this->userRepository->lockUserWithId($userId);
        $this->logger->error('The counter is invalid', [
            'current' => $currentCounter,
            'new' => $publicKeyCredentialSource->getCounter(),
        ]);
        throw new CustomSecurityException('Invalid counter. User is now locked.');
    }
}

Advanced Behaviors