Writing a Custom Adaptive Authentication Function to Detect FIDO2 Device Registrations

Thamindu Dilshan Jayawickrama
Identity Beyond Borders
6 min readApr 23, 2023
Photo by Dileep M on Unsplash

FIDO (Fast Identity Online) is a set of open standardized protocols that provides passwordless authentication. WSO2 Identity Server (WSO2 IS) offers a FIDO connector which can be integrated into your applications/ service providers to implement a passwordless authentication flow. WSO2 FIDO2 connector can be added to your applications as a first factor authentication method or a multi factor authentication method (MFA). What’s promising about WSO2 FIDO connector is it offers a complete usernameless flow where you’re required to enter neither the username nor password.

FIDO flow consists of two steps; namely registration ceremony and authentication ceremony. In simple terms this means, prior to authenticating with a FIDO key, the users should register their keys in the server. In WSO2 IS security key registration should be performed by login into My Account. During the authentication flow, users are prompted to authenticate with the registered FIDO keys.

However, sometimes based on the business use cases, during the authentication flow, it is required to determine whether the user has any FIDO key registrations. If the user hasn’t registered FIDO keys, different authentication methods can be prompted and maybe a FIDO key registration flow can be initiated. If the user has registered FIDO keys, the authentication flow can be continued with the FIDO connector.

This article explains how to write a custom adaptive authentication function to detect FIDO2 key registrations during the authentication flow. Further using the newly written function, an example authentication flow is described where the FIDO authentication is only prompted if the user has registered FIDO2 keys.

Adaptive Authentication

In WSO2 IS this can be achieved with an adaptive authentication script writing a custom adaptive function. Adaptive authentication is a programmatic way of customizing the default login flow with a JavaScript code engaged during the authentication flow. WSO2 IS offers a set of predefined javascript functions that can be directly used with a single button click. However if you need further customizations, it also offers writing and porting custom adaptive functions. Custom adaptive authentication functions can be written to invoke Identity Server core functions and any utility functions.

Prerequisites

1. Download and extract the latest version of the WSO2 Identity Server from the official website.

2. Install java 11 and maven in your development environment if not done so already.

Writing the Custom Adaptive Function

WSO2 IS FIDO connector exposes a WebAuthn service which implements a set of methods to perform FIDO2 related operations. It exposes a getFIDO2DeviceMetaData() method which can be used to determine the FIDO2 key registration. We can write a custom adaptive authentication function to invoke this method.

The sample code used for this article is available in the following git repository.
https://github.com/ThaminduDilshan/identity-conditional-auth-functions-fido

  1. Start by creating an Apache Maven module that has the packaging type as bundle and add the maven-bundle-plugin configuration.

2. Create a new Java class and define it as a Functional Interface (i.e. Interface with a single public method). The method name and parameters should be identical to the JavaScript function that will be used in adaptive script.

/**
* Class for conditional authentication function for FIDO2 key registrations.
*/
@FunctionalInterface
public interface Fido2RegistrationsFunction {

/**
* Check if the user has any FIDO2 key registrations.
*
* @param username Username of the user.
* @return True if the user has any FIDO2 key registrations.
*/
boolean hasFido2Registrations(String username);
}

The JavaScript function will be as follows.

var hasRegisteredFIDOKeys = hasFido2Registrations(username);

3. Create a class implementing the functional interface and add below logic.

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wso2.carbon.identity.application.authenticator.fido2.core.WebAuthnService;
import org.wso2.carbon.identity.application.authenticator.fido2.dto.FIDO2CredentialRegistration;
import org.wso2.carbon.identity.application.authenticator.fido2.exception.FIDO2AuthenticatorServerException;

import java.util.Collection;

/**
* Implementation of the Fido2RegistrationsFunction.
*/
public class Fido2RegistrationsFunctionImpl implements Fido2RegistrationsFunction {

private static final Log LOG = LogFactory.getLog(Fido2RegistrationsFunctionImpl.class);

@Override
public boolean hasFido2Registrations(String username) {

WebAuthnService webAuthnService = new WebAuthnService();
try {
Collection<FIDO2CredentialRegistration> fido2CredentialRegistration = webAuthnService
.getFIDO2DeviceMetaData(username);

if (CollectionUtils.isNotEmpty(fido2CredentialRegistration)) {
return true;
}
} catch (FIDO2AuthenticatorServerException e) {
LOG.error("Error while retrieving FIDO2 registrations for user: " + username, e);
}

return false;
}
}

Since we are using the WebAuthn service from the FIDO connector, it required to be added into the package dependencies. Additionally I have used Apache commons collections and logging packages in my code. Refer to the sample code for the pom.xml file definition.

4. Create a data holder class and implement the getter and setter methods for the JsFunctionRegistry object.

import org.wso2.carbon.identity.application.authentication.framework.JsFunctionRegistry;

/**
* Class to hold the OSGi service references.
*/
public class FidoFunctionsServiceDataHolder {

private static final FidoFunctionsServiceDataHolder instance = new FidoFunctionsServiceDataHolder();

private JsFunctionRegistry jsFunctionRegistry;

/**
* Private constructor to avoid instantiation.
*/
private FidoFunctionsServiceDataHolder() {
}

/**
* Get the instance of the FidoFunctionsServiceDataHolder.
*
* @return FidoFunctionsServiceDataHolder instance.
*/
public static FidoFunctionsServiceDataHolder getInstance() {

return instance;
}

/**
* Get the JsFunctionRegistry.
*
* @return JsFunctionRegistry.
*/
public JsFunctionRegistry getJsFunctionRegistry() {

return jsFunctionRegistry;
}

/**
* Set the JsFunctionRegistry.
*
* @param jsFunctionRegistry JsFunctionRegistry.
*/
public void setJsFunctionRegistry(JsFunctionRegistry jsFunctionRegistry) {

this.jsFunctionRegistry = jsFunctionRegistry;
}
}

5. Create a service component class and add JSFunctionRegistry service. In the bundle activator method, register the Fido2RegistrationsFunction class in the JSFunctionRegistry service.

import org.identity.conditional.auth.functions.fido.Fido2RegistrationsFunction;
import org.identity.conditional.auth.functions.fido.Fido2RegistrationsFunctionImpl;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ReferencePolicy;
import org.wso2.carbon.identity.application.authentication.framework.JsFunctionRegistry;

/**
* OSGi declarative services component which handles registration and un-registration of FIDO
* conditional authentication functions.
*/
@Component(
name = "identity.conditional.auth.functions.fido.component",
immediate = true
)
public class FidoFunctionsServiceComponent {

@Activate
protected void activate(ComponentContext context) {

Fido2RegistrationsFunction fido2RegistrationsFunction = new Fido2RegistrationsFunctionImpl();
FidoFunctionsServiceDataHolder.getInstance().getJsFunctionRegistry().register(
JsFunctionRegistry.Subsystem.SEQUENCE_HANDLER, "hasFido2Registrations", fido2RegistrationsFunction);
}

@Deactivate
protected void deactivate(ComponentContext context) {

JsFunctionRegistry jsFunctionRegistry = FidoFunctionsServiceDataHolder.getInstance().getJsFunctionRegistry();
if (jsFunctionRegistry != null) {
jsFunctionRegistry.deRegister(JsFunctionRegistry.Subsystem.SEQUENCE_HANDLER, "hasFido2Registrations");
}
}

@Reference(
service = JsFunctionRegistry.class,
cardinality = ReferenceCardinality.MANDATORY,
policy = ReferencePolicy.DYNAMIC,
unbind = "unsetJsFunctionRegistry"
)
public void setJsFunctionRegistry(JsFunctionRegistry jsFunctionRegistry) {

FidoFunctionsServiceDataHolder.getInstance().setJsFunctionRegistry(jsFunctionRegistry);
}

public void unsetJsFunctionRegistry(JsFunctionRegistry jsFunctionRegistry) {

FidoFunctionsServiceDataHolder.getInstance().setJsFunctionRegistry(null);
}
}

Deploy the Custom Adaptive Function in WSO2 Identity Server

1. Build the sample using maven clean install and copy the identity-conditional-auth-functions-fido-1.0.0.jar file from the target directory into the <IS_HOME>/repository/components/dropins directory.

2. Start WSO2 Identity Server by running the following command inside the <IS_HOME>/bin folder.

./wso2server.sh

Try it Out

1. Login to WSO2 IS Console (or management console) and register an application (if in management console, service provider). If you’re not familiar with the process refer to the documentation.

2. Go back to the applications section (if in the management console, service provider) and click on the registered application.

3. Go to the Sign in Method tab and start customizing the sign in flow (If in the management console, this will be Local and Outbound Authentication Configuration).

We need to add Identifier First connector to the first step to obtain the username. Based on the registered FIDO2 key availability, users will be prompted either with the FIDO authentication flow or basic username/ password flow.

4. Enable Conditional Authentication and add the following script (If in the management console, add it as the adaptive authentication script).

var onLoginRequest = function(context) {
executeStep(1, {
onSuccess: function(context) {
var user = context.steps[1].subject;
var username = user.username;
var hasRegisteredFIDOKeys = hasFido2Registrations(username);

if (hasRegisteredFIDOKeys === true) {
executeStep(2, {
authenticationOptions: [{authenticator:'FIDOAuthenticator'}]
}, {
onSuccess: function (context) {}
});
} else {
executeStep(2, {
authenticationOptions: [{authenticator:'BasicAuthenticator'}]
}, {
onSuccess: function (context) {}
});
}
}
});
};

5. Click on Update to save the changes.

6. Register two users in the system and register a FIDO key for one of them by login into the My Account and heading over to Security -> Additional Authentication -> Security Key/Biometrics.

7. Try login to the application with both of the users.

For the user, who has registered FIDO2 devices, the FIDO authentication flow will be prompted.

For the user who hasn’t registered FIDO keys, password will be prompted.

--

--

Thamindu Dilshan Jayawickrama
Identity Beyond Borders

Senior Software Engineer at WSO2 LLC. | B. Sc in Engineering (Hons), Computer Science and Engineering, University of Moratuwa