WebAuthnRegistration

WebAuthnRegistration

Overview

The plugin comes with an administration flow for registering and managing FIDO2 credentials. The inbuilt flow represents the minimum viable product for implementing such a feature. In the future other plugins may provide this functionality.

The registration flow can be accessed by navigating to:

http[s]://hostname/idp/profile/admin/webauthn-registration

Configuration Steps

Other than completing steps 1, 2, and 3, you do not need to change any default registration options to successfully register and use a FIDO2 credential with the IdP. If you require more control over the process, you can optionally review steps 4, 5, and 6.

  1. (Required) Decide how the user should authenticate to the registration flow.

  2. (Required) Decide if you should disable SSO to the registration page.

  3. (Required) Configure a suitable access control policy.

  4. (Optional) Configure how user account details are passed to the WebAuthn API and authenticator.

  5. (Optional) Decide what type of authenticator you want to support.

  6. (Optional) Decide if you want to require only ‘trusted’ (from FIDO Alliance Metadata) authenticators to be registered

Integration with the authentication flow

The registration flow uses the normal authentication process of the IdP to verify the user who intends to register a credential. It is recommended to implement authentication through the MFA flow, and the following instructions assume you have completed that step.

By default, the flow collects the username as a first step so it can look up any existing credentials before passing control to the authentication system. This information can be accessed in the MFA configuration using the isWebAuthnAvailable() method on the WebAuthnRegistrationContext, to decide which flow to use. The example MFA Flow With Password Fallback is a suitable initial setup that allows users to register their first credentials upon inputting their username and password; other combinations are possible.

Forcing Authentication for the Registration Page (1.1.0)

From version 1.1.0 onwards, it is possible to force authentication for the registration page, thereby preventing SSO during registration. This behaviour can be enabled by setting the idp.authn.webauthn.admin.registration.forceAuthn property to true in conf/webauthn-registration.properties. Enabling this option helps prevent unintended access to the registration page via complex MFA flows—forcing the user through the intended authentication policy, not whatever session they happen to already have.

AccessPolicy Configuration

The user and client accessing the registration flow are subject to an AccessControlConfiguration set by the property idp.authn.webauthn.admin.registration.accessPolicy. The default policy, AccessByCurrentUser, is not defined in the IdP's access control configuration and needs to be added for the flow to load (see the next section).

AccessByCurrentUser Access Policy

The default policy, AccessByCurrentUser must be added to the map in conf/access-control.xml like so:

... <entry key="AccessByCurrentUser"> <bean parent="shibboleth.PredicateAccessControl"> <constructor-arg> <bean id="AccessByCurrentUserPredicate" class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.AllowCurrentUserAccessPredicate"> </bean> </constructor-arg> </bean> </entry> ...

This policy is essential if you plan on creating an MFA flow (such as MFA Flow With Password Bypass) which provides another authentication method if the user has no registered credentials (e.g., either using isWebAuthnAvailable() or when signalling a custom event).

The AccessByCurrentUser policy checks the username found in the registration context—collected by the username view when the user first enters the registration flow—is the same as the principal name (identity) of the user that authenticated. This is important, if we imagine a scenario where a nefarious user wanted to ‘change' or possibly 'downgrade’ a user’s authenticate method from WebAuthn to, say, username and password (or whatever else was configured as a ‘backup’), all they would need to do is enter a non-existent username into the username collection step of the registration page and then have the MFA flow direct them to a different flow (because they do not have any registered credentials) where, for example, they can proceed to try different usernames and passwords for authentication—the registration flow uses the principal name of the authenticated user to register credentials against.

The AccessByCurrentUser policy safeguards this by ensuring that the username initially entered into the registration page—for which the credentials are retrieved, and any flow decision is based—matches the authenticating user's principal name. If they do not match, access to the registration page is denied. This measure prevents a user from switching usernames between the registration and authentication flows.

Custom Comparison Predicate

If the default comparison predicate used by the access policy is not sufficient for your needs. You can create a different comparison predicate and plug it into AccessByCurrentUser: see the example below.

<entry key="AccessByCurrentUser"> <bean parent="shibboleth.PredicateAccessControl"> <constructor-arg> <bean id="AccessByCurrentUserPredicate" class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.AllowCurrentUserAccessPredicate"> <property name="comparisonPredicate"> <bean id="StringMatchComparisonPredicate" class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.AllowCurrentUserAccessPredicate$DefaultCurrentUserComparisonPredicate"> <property name="comparisonPredicate"> <bean ... </bean> </property> </bean> </property> </bean> </constructor-arg> </bean> </entry>

Disabling Registration Username Collection

It is possible to turn off the username collection step in the registration flow to simplify this problem by setting the property idp.authn.webauthn.registration.collectUsername to false. You’d then need to think about how best to support users trying to register their first credential.

Strong Authentication Access Policy(1.1.0)

An access control policy is included in the distribution and can guard against the use of ‘weak’ authentication methods to access the registration page when the user already has FIDO2 credentials. For example, as extra protection against using only single-factor Password authentication to access the registration page when the user has already registered a FIDO2 credential.

This method is particularly effective for preventing misconfigurations. For instance, if you inadvertently alter and weaken your MFA settings and forget to implement the guard described here, the policy will deny access.

When enabled, access to the registration page is restricted as follows:

  • If the user already has one or more FIDO2 credentials, they must perform strong authentication (typically WebAuthn, though this may be configured to use any factor defined in your MFA policy).

  • If the user does not yet have any FIDO2 credentials, they may access the registration page after any successful authentication. For example, password-only authentication may be sufficient to register the first FIDO2 credential.

This policy effectively blocks SSO access to the registration page, even if permitted by the IdP.

The policy can be enabled by including it in, for example, the AccessByCurrentUser policy alongside the existing AccessByCurrentUserPredicate: <entry key="AccessByCurrentUser"> <bean parent="shibboleth.PredicateAccessControl"> <constructor-arg> <bean parent="shibboleth.authn.WebAuthn.MultiPredicateAccessPredicate"> <constructor-arg> <list> <bean id="AccessByCurrentUserPredicate" class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.AllowCurrentUserAccessPredicate"> <property name="comparisonPredicate"> <bean id="StringMatchComparisonPredicate" class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.AllowCurrentUserAccessPredicate$DefaultCurrentUserComparisonPredicate"> </bean> </property> </bean> <bean id="StrongAndFreshAuthenticationGuardPredicate" class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.RequireStrongFreshAuthnAccessPredicate" p:credentialRepository="#{getObject('shibboleth.authn.WebAuthn.CredentialRepository') ?: getObject('shibboleth.authn.WebAuthn.DefaultCredentialRepository')}"> </bean> </list> </constructor-arg> </bean> </constructor-arg> </bean> </entry> You then need to add a signal to the WebAuthnGuardContext whenever you think ‘strong' authentication has been performed. For example, in the MFA flow: <bean id="checkPasswordOrWebAuthnForRegistration" parent="shibboleth.ContextFunctions.Scripted" factory-method="inlineScript"> <constructor-arg> <value> <![CDATA[ nextFlow = "authn/WebAuthn" authCtx = input.getSubcontext("net.shibboleth.idp.authn.context.AuthenticationContext"); // 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"); // Get the guard context to signal strong authentication webAuthnGuardCtx = authCtx.ensureSubcontext("net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnGuardContext"); if (webauthnRegCtx != null){ if (!webauthnRegCtx.isWebAuthnAvailable()){ nextFlow = "authn/Password" // Set to false, this will allow access here because WebAuthn is not available (the user has no credentials) webAuthnGuardCtx.setStronglyAuthenticated(false); } else{ // Assume next flow is authn/WebAuthn and you take it to be strong auth, add to the context a flag to say we did more than just a single low assurance factor webAuthnGuardCtx.setStronglyAuthenticated(true); } } else{ // This is not a registration flow, and we will only allow authn/WebAuthn // Assume next flow is authn/WebAuthn and you take it to be strong auth, add to the context a flag to say we did more than just a single low assurance factor webAuthnGuardCtx.setStronglyAuthenticated(true); } nextFlow; // pass control to authn/Password or authn/WebAuthnFlow ]]> </value> </constructor-arg> </bean> The guard context can be used anywhere you want to signal strong authentication.

If you have another second-factor authentication method available, you can already mandate the use of ‘strong’ authentication by setting the defaultAuthenticationMethods property. In addition, starting with version 1.1.0, authentication can be enforced on the registration page. As a result, if a suitable MFA configuration is in place, this policy is unlikely to provide additional benefit.

Access Policies and Subject Canonicalization

There are some caveats to consider when enabling access policies, especially AccessByCurrentUser. The principal name that results from authentication is determined by the Authentication flow, the SubjectCanonicalization flow, and possibly any transformations that have been applied to the username. It is, therefore, entirely possible that the user who entered the registration flow is the same as the user who authenticated, but their username and principal name do not match.

To address this, the registration process collects the username from the username input view and applies two transformations. The first transformation utilizes configurable string manipulations, while the second executes the SubjectCanonicalization subflow. Together, these two steps create a final output, which is then matched against the principal name obtained during the authentication process.

Registration Username String Transformations

The following properties control the string transformations applied to the registration username:

  1. idp.authn.webauthn.registration.username.uppercase : upper case the username?

  2. idp.authn.webauthn.registration.username.lowercase : lowercase the username?

  3. idp.authn.webauthn.registration.username.trim : trim the username?

You can supply a custom transformation by defining the bean, shibboleth.authn.webauthn.registration.UsernameTransformations.

Note, if you do this and you are using the passwordless authentication mode, you will need to supply the same transformation settings to the authentication flow, otherwise, it will not match with the username used to register the credential.

  1. idp.authn.webauthn.passwordless.username.uppercase : upper case the username?

  2. idp.authn.webauthn.passwordless.username.lowercase : lower case the username?

  3. idp.authn.webauthn.passwordless.username.trim : trim the username?

You can supply a custom transformation by defining the bean, shibboleth.authn.webauthn.passwordless.UsernameTransformations.

Subject Canonicalization

The SubjectCanonicalization subflow is run immediatly after username collection. By default, the IdP wide shibboleth.PostLoginSubjectCanonicalizationFlows are run, but a different list of flows can be configured by setting the property idp.authn.webauthn.registration.c14n.postUsernameFlows.

User Identity for Credential Generation

The IdP passes user account details to the WebAuthn API during registration. Some of this information is used to enhance the user experience during credential creation, while other information is used by the authenticator to associate credentials with user accounts at the IdP. The generation of this information is described in the following sections.

User ID (UserHandle) Population

The user ID (user.id) should be the primary key of the user account within the IdP. It must not exceed 64 bytes in length and should not include personally identifiable information. The ID should not change for a given user and is used by the authenticator to bind a credential to a user during registration with the IdP. In an authentication response, it is returned as the user handle.

In the federated context, the credential is registered against the IdP and so the user ID of the credential will be the same no matter who the requesting, upstream, service provider is—an authenticator will only store one credential for the IdP per user ID.

By default, the user ID is generated at runtime using a random byte sequence of 64 bytes. To ensure that each user has only one ID, an ID will only be generated if the authenticated user does not already have one linked to an existing credential.

You may prefer to pull this value from the attribute resolver. This can be achieved by modifying the following properties. in conf/authn/webauthn-registration.properties:

  1. change the property idp.authn.webauthn.registration.userid.strategy to reference the bean shibboleth.authn.webauthn.registration.AttributeContextUserIdLookupStrategy.

  2. ensure the attribute resolver is enabled after authentication, idp.authn.webauthn.admin.registration.resolveAttributes=true.

  3. decide which attribute from the resolver context to use using idp.authn.webauthn.registration.userid.attributeId.

    1. Note, the AttributeContextUserIdLookupStrategy requires the attribute to be a single StringAttributeValue converted to a byte array assuming a UTF-8 character set.