WebAuthnAuthentication

Overview

The WebAuthn flow is capable of running as either a second single-factor authentication (similar to U2F, but using the WebAuthn APIs) or as a first and only factor of authentication which could still satisfy multi-factor requirements. For example, device bound passkeys such as those found on a hardware security key, incorporate two factors; something the user has and something the user is. However, multi-device synched passkeys are not, technically, something you have—although they may still provide adequate multi-factor authentication assurances in certain circumstances.

When acting in a sole-factor authentication mode, the flow can be configured as either a usernameless (true passkey) flow or passwordless flow (the user must first identify themselves): this is toggled using the idp.authn.webauthn.usernameless.enabled property.

Authentication Flows

Usernameless (Passkey) flow

A usernameless flow does not require the user to enter their username during authentication. To support a usernameless flow, the authenticator must allow Discoverable Credentials (previously known as a Resident Key and now referred to as a passkey) where the private key and associated metadata is stored on the authenticator (FIDO2 compatible authenticators should work). This is important, as the IdP, without a known username, will not be able to preselect a user and credential to use; this must come instead from the user selecting the correct credential—suitable for the IdPs origin—from the authenticator itself.

During authentication, the authenticator is required to:

  • test the user is present by using some form of authorization gesture (for example, by touching the authenticator or clicking on a key to use), and

  • verify the user’s identity by some form of local authorization (for example, using a pin code or biometric recognition).

Passwordless flow

Collecting the username is the initial step in a passwordless flow, and therefore, it does not require storing credentials on the authenticator. Instead, for example, the credential can be encrypted and stored on the server (possibly in the credential ID sent to the server during registration and returned by the IdP during authentication).

During authentication, the authenticator is required to:

  • test the user is present by using some form of authorization gesture (for example, by touching the authenticator or clicking on a key to use), and

  • verify the user’s identity by some form of local authorization (for example, using a pin code or biometric recognition), and

  • test the credential requested by the IdP and used by the authenticator is allowed (has been registered with the IdP).

The passwordless flow will pass the credential IDs of the user to the browser in the allowCredentials request option. This could be used to perform username enumeration. If you are worried about this, either configure the usernameless or second factor flows.

Second factor authentication (2FA) flow

The WebAuthn flow will operate in second-factor mode automatically only if three conditions are met. First, the property idp.authn.webauthn.2fa.enabled must be set to true. Second, a previous authentication factor must have produced an Authentication Result from the MFA context. Finally, a principal name must be found through a lookup strategy by default from the C14N context or the Session Context using the CanonicalUsernameLookupStrategy. If these conditions are not met, authentication will revert to either a passwordless or usernameless flow (depending on which is enabled).

The acceptable previous factors can be controlled by listing (comma-separated) authentication flows in the property idp.authn.webauthn.2fa.allowedPreviousFactors. The default is authn/Password.

If enabled, and you want to completely bypass the existing logic such that the WebAuthn flow for 2FA is always used, you can set the property idp.authn.webauthn.2fa.forceSecondFactorFlow to true.

When acting as a 2nd factor of authentication, the username is gathered from the principal name as a result of the first factor by lookup strategy and any credentials registered to that username are retrieved. During authentication, the authenticator is then required to:

  • test the user is present by using some form of authorization gesture (for example, by touching the authenticator or clicking on a key to use), and

  • test the credential used is allowed (has been registered with the IdP).

This mode must be used within an appropriate MFA flow, where authn/WebAuthn is used as the second factor.

Configuration of the MFA flow

Before you start, make sure to enable the MFA login module and configure the flow in conf/authn/authn.properties. It should be possible to run the WebAuthn plugin as a standalone authentication method for the IdP. However, it is designed to work within a multi-factor authentication (MFA) flow. Even when used alone to provide single-factor authentication, this design allows for greater flexibility when configuring authentication routes. For example, if no credentials are available for WebAuthn authentication, it can fall back to using a password.

WebAuthn as the sole factor of authentication

In its simplest form, you can set up the WebAuthn plugin to be the only means of authentication in either usernameless or passwordless modes.

Note, that configuring only WebAuthn authentication will also require a FIDO2 credential to access the registration flow. Hence, unless you have another mechanism of seeding the registration of credentials within the credential repository, the user will not be able to access the registration page: please see the final example for a possible solution to this.

conf/authn/mfa-authn-config.xml

<util:map id="shibboleth.authn.MFA.TransitionMap"> <entry key=""> <bean parent="shibboleth.authn.MFA.Transition" p:nextFlow="authn/WebAuthn" /> </entry> </util:map>

WebAuthn as a second factor of authentication

As described in the second-factor authentication section, the WebAuthn flow can be used as a second factor of authentication where the user only needs to demonstrate possession of a registered credential (typically via a user gesture such as pressing a physical or virtual button). A simple MFA configuration which runs the Password flow before determining if the WebAuthn flow should run is shown below:

conf/authn/mfa-authn-config.xml

<util:map id="shibboleth.authn.MFA.TransitionMap"> <entry key=""> <bean parent="shibboleth.authn.MFA.Transition" p:nextFlow="authn/Password" /> </entry> <entry key="authn/Password"> <bean parent="shibboleth.authn.MFA.Transition" p:nextFlowStrategy-ref="checkSecondFactor" /> </entry> </util:map> <!-- If password flow is not enough, use WebAuthn flow as a 2nd factor. Although that will not work if no credentials --> <bean id="checkSecondFactor" parent="shibboleth.ContextFunctions.Scripted" factory-method="inlineScript"> <constructor-arg> <value> <![CDATA[ nextFlow = "authn/WebAuthn"; // Check if second factor is necessary for request to be satisfied. authCtx = input.getSubcontext("net.shibboleth.idp.authn.context.AuthenticationContext"); mfaCtx = authCtx.getSubcontext("net.shibboleth.idp.authn.context.MultiFactorAuthenticationContext"); if (mfaCtx.isAcceptable()) { nextFlow = null; } nextFlow; // pass control to second factor or end with the first ]]> </value> </constructor-arg> </bean>

WebAuthn as the sole factor but with password fallback for registration

The following MFA configuration establishes the WebAuthn plugin as a sole, single-factor, authentication method. However, it also permits the auth/Password flow to be used if the user is in a WebAuthn Registration flow and has not yet enrolled any FIDO2 credentials. This allows users to register their first FIDO2 credential by authenticating against their username and password (and possibly a different second-factor). After the first credential has been enrolled, there is no fallback option, and subsequent registration attempts will require a FIDO2 credential.

conf/authn/mfa-authn-config.xml

<util:map id="shibboleth.authn.MFA.TransitionMap"> <entry key=""> <bean parent="shibboleth.authn.MFA.Transition" p:nextFlowStrategy-ref="checkPasswordOrWebAuthn" /> </entry> </util:map> <!-- Use the webauthn flow unless a registration context exists and the user does not have any FIDO2 credentials registered, then use a password flow.--> <bean id="checkPasswordOrWebAuthn" parent="shibboleth.ContextFunctions.Scripted" factory-method="inlineScript"> <constructor-arg> <value> <![CDATA[ nextFlow = "authn/WebAuthn" // Check if we can use a WebAuthn flow, or if the user has no credentials available to them use a password flow webauthnRegCtx = input.getSubcontext("net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext"); if (webauthnRegCtx != null){ if (!webauthnRegCtx.isWebAuthnAvailable()){ nextFlow = "authn/Password" } } nextFlow; // pass control to WebAuthnFlow ]]> </value> </constructor-arg> </bean>

If you follow this approach you MUST ensure you use (and think about) a suitable access control policy to prevent users from ‘downgrading’ the authentication mechanism used, see the credential registration section for more information.

Signalling custom events when the user has no registered credentials

In case the user has not registered any FIDO2 credentials, there are two ways to signal a custom event to the IdP. The first signal can be emitted just after collecting the username for the passwordless or 2fa flows. The second signal can be emitted after the authenticator has sent the attestation (authentication) response back to the IdP—this is the only signal you can expect from the usernameless (passkey) flow since there is no username collection step.

For the passwordless and second factor flows, setting idp.authn.webauthn.passwordless.signalEventOnNoCredentials to true will signal a custom event ID (described by the property idp.authn.webauthn.passwordless.noCredentialsEventId) If the user (identified by the username entered in the username input step) has no registered FIDO2 credentials. This will signal a custom event immediately after username collection, ending the flow. To have return controlled to the MFA orchestration layer, the event must be described to the system in the conf/authn/authn-events-flow.xml in the usual way (see the example below).

For all flows, but specifically for a usernameless (passkey) flow, setting idp.authn.webauthn.signalEventOnNoCredentialsRegisteredForUserHandle to true will signal a custom eventID (described by the property idp.authn.webauthn.userHandleNoRegisteredCredentialsEventId) if the user, identified by the userHandle (user.id) in the authenticator's attestation response, does not belong to a known user with at least one registered FIDO2 credential. The event is signalled just before the attestation (authentication) response is validated. Again, the event must be described to the system in the conf/authn/authn-events-flow.xml in the usual way if it is to be used as a transition in the MFA flow (see the example below).

To capture the new event IDs as signals to transition to an end-state of the WebAuthn flow (and not trigger an IdP InvalidEvent):

conf/authn/authn-events-flow.xml

Example MFA flow configuration that uses the new events after first running the WebAuthn flow:

conf/authn/mfa-authn-config.xml

Using this feature needs careful consideration. You probably do not, for example, want to use signalEventOnNoCredentialsRegisteredForUserHandle as a signal to allow a user to drop back into a username and password flow because anybody could bypass WebAuthn authentication just by using a credential they know does not exist (although the ‘attacker’ would need to have an authenticator with a credential registered for that TLS protected origin, e.g., a previously registered and remove key for a different account).

WebAuthn as the sole factor using custom events to drive a fallback for registration and authentication

Some MFA configurations discussed so far allow a different authentication method to run as a fallback if the user has no registered FIDO2 credentials. This requires the registration step to provide its own username collection page so a decision can be made about which flow to follow when the user has not registered any credentials. This only affects registration, if a user performs authentication without first registering a FIDO2 credential, the authentication flow will ultimately fail. This is probably a good thing, however, for flexibility it is possible to disable username collection on the registration flow and delegate ‘NoCredentials’ signalling to the authentication flow using custom events.

To disable username collection on the registration flow, set the idp.authn.webauthn.registration.collectUsername property to false. Then, configure the MFA flow to trigger a different authentication flow if the user has no credentials—following the examples in the custom events topic. It is worth noting this will affect registration and standard authentication. That is, any user without credentials will end up signalling those custom events, and, if the MFA flow is set up with a fallback flow, they will authenticate using that fallback. For registration you can of course require that the fallback method (all methods really) was of a particular quality by setting the default authentication methods property idp.authn.webauthn.admin.registration.defaultAuthenticationMethods to require MFA (or whatever you want). Similarly, if the Service Provider has requested MFA in their request, the authentication fallback for a standard authentication also needs to support it.

WebAuthn as the sole factor using custom events to drive a different two-factor fallback for authentication and registration

Enabling event signalling when a user does not yet have a registered FIDO2 credential (see the previous section), allows us to configure a different type of two-factor authentication fallback. For example, the following will first run the WebAuthn flow which will fail if the user has no existing registered credentials, it then proceeds to the auth/Password flow and, if required (e.g, requested by the SP), to the authn/DuoOIDC flow. If either registration or admin flows are configured to require MFA (by setting their default authentication methods), they will also fall back to authn/Password and authn/DuoOIDC. Once users have registered their first key, they will proceed only to use the WebAuthn plugin for authentication.

conf/authn/mfa-authn-config.xml

 

 

Prepopulating the WebAuthn username into the authn/Password flow

When using the passwordless flow with a fallback to authn/Password, you could modify the login.vm view to pre-fill the username input with the username entered into the WebAuthn context. For example, at the top of login.vm:

Which you can then use to automatically fill in the username field:

See UserInterface for a detailed look at the IdP’s view templates. 

A malicious party could still alter the username, preventing which would require server-side measures (e.g. a post-flow step in the MFA Flow, or a post-login authentication flow). However, there is typically no need to add those since the service provider is responsible for determining the strength of the authentication required.

Authentication Context Classes (Supported Principals)

As with all authentication flows, the WebAuthn plugin exposes a collection of supportedPrincipalscompatible with the type of authentication mechanism used. By default, the idp.authn.webauthn.supportedPrincipals property contains placeholder examples for what these could be.

The set you specify for the flow is entirely up to you based on the type of authentication flow you have configured, and what authentication assurances you want. For example, when used as a second-factor in 2FA mode, it seems feasible to signal MFA from the WebAuthn plugin. On the other hand, using the plugin alone, in either passwordless or usernameless modes, may not be enough to signal MFA unless you are certain that the credential is tied to a specific device—which cannot currently be determined from the plugin. Even if the typical passkey process (which requires user verification) combines ‘something you are’ (biometrics) or ‘something you know’ (pin) with ‘something you have’ (an authenticator), if the credential is synchronized (as is common with passkey providers like iCloud Keychain and Google Password Manager), it could be argued that the authenticator does not really qualify as 'something you have'. Either way, it is up to you to decide how best to handle this.

 

Authentication Credential Policies

The authentication flow comes with a basic, extendable, policy engine for accepting and rejecting FIDO2 credentials at the point of use. To enable policy checks, set the property idp.authn.webauthn.credential.policy.enabled to true in conf/authn/webauthn.properties.

In addition to inspecting the authenticating FIDO2 credential, a policy can also make decisions regarding the authenticator that created the credential, but only if the Authenticator Attestation GUID (AAGUID) was stored with the credential during registration. For guidance on how to configure this, refer to the attestation conveyance registration section.

The default policy is defined by a list shibboleth.authn.WebAuthn.ChainedCredentialPolicyList of policies configured in conf/authn/webauthn-config.xml. Out of the box, the following policies are included:

Policy Name

Description

Value

Policy Name

Description

Value

SecondFactorOnlyCredentialPolicyRule

A list of authenticators based on their Authenticator Attestation GUID (AAGUID) that can only be used for second-factor authentication, and will be rejected if used as a sole factor of authentication.

Even if the authenticator indicates User Verification during authentication, the credential can still be excluded. This is potentially useful for omitting untrusted software authenticators.

The comma-separated list of authenticators can be directly specified in the XML configuration or, for convenience, set by the idp.authn.webauthn.authenticator.policy.secondFactorOnlyAuthenticators property.

Reference