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 single-factor authentication mode, the flow can be configured as either a usernameless (passkey) flow or passwordless flow: 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).
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.
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:
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.
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).
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.
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
:
#set ($webAuthnCtx= $authenticationContext.getSubcontext("net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext")) #set ($webAuthnUsername = $webAuthnCtx.getUsername())
You can use this information to automatically fill in the username field:
<input type="text" id="username" name="j_username" #if($webAuthnUsername)readonly#end value="#if($webAuthnUsername)$encoder.encodeForHTML($webAuthnUsername)#end" />
See UserInterface for a detailed look at the IdP’s view templates.
While this would not prevent a malicious party from altering it, which would require server-side measures, there is typically no need to worry about that 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 supportedPrincipals
compatible 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.