Security Secure Login with FIDO2 Lead image: Lead Image © lassedesignen, 123RF.com
Lead Image © lassedesignen, 123RF.com
 

Secure authentication with FIDO2

Replacements

The FIDO and FIDO2 standard supports passwordless authentication. We discuss the requirements for the use of FIDO2 and show a sample implementation for a web service. By Matthias Wübbeling

Moving away from passwords to improve account security is a recurring theme for administrators and security researchers. On the Internet, the password as a knowledge-based factor of authentication still dominates the login methods of online services. Password managers are increasingly relieving the burden on users as a weak point in password selection. However, despite all technical support, many users still use passwords that are easy to remember and therefore easy to crack. Projects like Have I Been Pwned [1] or the researchers of the University of Bonn in their EIDI (effective information after a digital identity theft) project [2] follow a reactive approach to account security; web development needs to focus more strongly on alternative authentication methods.

Back in December 2014, the FIDO Alliance published the FIDO Universal Authentication Framework (FIDO UAF) standard, which was intended to enable passwordless authentication. Since the release of the FIDO2 standard with the Web Authentication (WebAuthn) and the Client to Authenticator Protocol (CTAP) components [3], all the major browsers have gradually introduced support for the Web Authentication JavaScript API and the use of security tokens over CTAP.

FIDO2 Functionality

Fortunately, FIDO2 is very straightforward and, although it mainly uses cryptographic keys, quite easy to understand. Before you can log in to a web service as a user, you first need to go through a registration process. During this process, you generate the cryptographic key material – a public and a private key – on a secure device known as the authenticator. The public key is transmitted later to the application server for authentication. The key is stored there and linked to your user account. The private key remains securely stored on the authenticator, which can be an external device or the trusted platform module (TPM) chip in your computer.

Logging in to a web service (Relying Party) works like this: The web application (Relying Party Application) is executed in your web browser. The server sends a challenge to your browser, which the browser signs with your private key and returns to the web service. The browser functions for FIDO authentication are accessed from within the application by the Java Script API.

Depending on the device you used to register key generation, your browser accesses your computer's TPM chip through the operating system or an external authenticator over the CTAP protocol.

The authenticator has your private key and needs to generate the signature for the challenge. The process often involves entering a PIN or presenting a biometric feature such as a fingerprint. On local devices, passwordless authentication with biometrics works without problem. The authenticator returns the signed challenge to the browser, which passes it on to the application server. The server in turn verifies the signature with the stored public key. If it is valid, the login is completed successfully.

The crucial difference with this approach is that you do not need a certificate authority (CA) to verify the user's identity and then issue a certificate. The identity of the user is not the main focus of FIDO, which is also true of password-based authentication. Instead, the aim is to recognize a user reliably, which means you can also use FIDO for secure authentication of what are basically anonymous user accounts. Also, the use of several, and even different, keys is supported. FIDO2 even lets you as a provider prescribe and verify the type of devices used as authenticators. In the course of certification, a model key pair is generated that can also be integrated into the signature process. In this way, you as a provider can ensure that your customers use certain device classes, such as devices that can only be unlocked with a PIN or fingerprint. The model key pair is then installed on all devices of a certain model (i.e., it is model-specific, but not unique to a device).

Trusted Authenticator

To sign the challenge from the application server, you need another device, known as the authenticator, which can be an internal device, the TPM in your computer, or an external device, such as a USB security token like a YubiKey. Android has had FIDO2 certification since February 2019. Therefore, on devices with suitable hardware and Android 7 or higher, you always have an internal authenticator in your pocket. You can unlock it with your fingerprint or the screen lock PIN. For communication with the security token, FIDO defines the CTAP1 and CTAP2 Client to Authenticator protocols; version 1 is also known as Universal-2nd-Factor (U2F) and version 2 as FIDO2 or WebAuthn. For test purposes in this article, I use a YubiKey NFC (near-field communication) stick, version 5, and a fairly new Android smartphone. The YubiKey is directly detected on Linux as Yubico YubiKey OTP+FIDO+CCID, which you can reveal with the command

sudo dmesg -w

by watching the kernel output when plugging in the key. You can then go to the WebAuthn demo page [4] for initial testing. If the authenticator works with your browser, the next step is to try FIDO2 on your own web page.

Creating the Server Application

To test the following examples, I will use the PHP WebAuthn library by Lukas Buchs [5]. If you already use a PHP-enabled server, you can provide the sample application directly in the WebAuthn _test folder and deliver the page. Without an appropriate environment, Docker Compose lets you set up NGINX with PHP quickly and easily (Listing 1).

Listing 1: docker-compose.yaml

web:
   image: nginx:latest
      ports:
         - "8080:80"
      volumes:
         - ./WebAuthn:/usr/share/nginx/html/WebAuthn
         - ./default.conf:/etc/nginx/conf.d/ default.conf
         left:
            - php
php:
         image: php:fpm
         volumes:
           - ./WebAuthn:/var/www/html/WebAuthn

The NGINX configuration requires the default.conf file (Listing 2), which configures forwarding to the PHP-FPM server for all files ending in .php. Because the client.html file, which is delivered as a static page, is located in the same folder as the server.php file, the same folder is included in both containers in docker-compose.yaml.

Listing 2: default.conf

server {
   index index.php index.html;
   server_name php-docker.local;
   error_log /var/log/nginx/error.log;
   access_log /var/log/nginx/access.log;
   root /code;
   location ~ \.php$ {
      try_files $uri =404;
      fastcgi_split_path_info ^(.+\.php)(/.+)$;
      fastcgi_pass php:9000;
      fastcgi_index index.php;
      include fastcgi_params;
      fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
      fastcgi_param PATH_INFO $fastcgi_path_info;
   }
}

You can now start the two Docker containers:

docker-compose up

If you then call http://localhost:8080/WebAuthn/_test/client.html in your browser, you will be redirected to a secure HTTPS page, although this connection will fail. The redirection is defined in the WebAuthn client.html file. For this test, it is not necessary, however. Just remove the JavaScript statement that starts with window.onload in client.html (lines 246-253). Afterward, you will probably have to clear the browser cache to avoid being redirected.

You can now access the test application website with the URL mentioned above. When you get there, you can access your browser's WebAuthn API by pressing New Registration and proceed to register the existing security token. If you want to test the application from your smartphone, you need to install a valid SSL certificate. On Android, you could otherwise receive a message that the browser is not compatible.

Passwordless Authentication with PHP

To help you upgrade your own web application with FIDO2, I will refer to the sample application as a guide and look for the important components in the examples for an abstract service of your own.

As a concrete use case, I will be looking into passwordless authentication with PHP based on the Lukas Buchs WebAuthn library mentioned above. This exercise assumes that your PHP-based web service is located on a publicly accessible server and that you have already installed a valid certificate for the server (e.g., from Let's Encrypt). As the database, you can use a database management system of your choice; you can also store multiple public keys for each of your users in the database.

As the JavaScript for your web application, use the client.html file in the _test directory of the WebAuthn library and save the functions in a separate file (e.g., fido.js), which you then include on the login page of your web service. You can dispense with the clearregistration() function if you are planning another approach to managing the stored public keys.

If you already have a working application with login capabilities, you need to adjust the paths in the four remaining calls to the window.fetch function. You don't really need the parameters for the HTTP requests coming from the getGetParams() method. The CA certificates are optional, and I do not plan to restrict the choice of security token vendor for the time being.

Queries with GET and POST

With PHP you can easily distinguish between the GET and POST methods. You only need two paths, which you can route to two functions in your web application according to the request method. For example, if the two paths are /fido/create and /fido/login, in /fido/create you use GET to request the parameters to create a new key pair, and you use POST to upload the signature and store the public key on the server. In /fido/login you use the GET method again to request the parameters for the signature and POST to send this signature to the server for authentication.

It is important that you always include the username, which needs to be known to assign the deposited public key to the correct account and log in. You can integrate the username into the path. For example, the path /fido/create/user/Hans would be used to create and upload the public key for user Hans. The next command lets you read the username dynamically from a corresponding input field of your login form and add the values to the previously defined paths:

user_url = 'user/' + (document.getElementById('user').value) + '/';

Depending on whether you have already authenticated the user at the time of reregistration of a public key and recognize them from a valid session, you will only need to specify the user in the URL for the login (i.e., the functions in checkregistration()). Once the paths have been adapted and assuming the username is reliably passed in, the client side is now set up.

Before making the adjustments on the server side, take a look in the _test folder from the sample WebAuthn application at the server.php file, which has four function areas that you can use for each of the paths mentioned above. The ASCII art rendering of the process in the header of the file again illustrates the process of registration and testing. I will be adopting the four relevant areas for the various endpoints of this example project.

In the upper part of the file, the supported formats are selected on the basis of the HTTP arguments passed in. Because you don't want to limit yourself in terms of the choice of security token at first, you have to pass in all supported devices as an array:

$WebAuthn = new \WebAuthn\WebAuthn('IT-Administrator', 'it-administrator.de', array('fido-u2f', 'packed', 'android-key', 'android-safetynet', 'none'));

To take most of the work off your hands, always create a WebAuthn object first. As a reference, you can pass in an arbitrary name for your application and the domain name as the ID of the relying party. This name is displayed to the user for verification during creation and input.

Creating a Key Pair

The endpoint created in /fido/create lets you create a new key pair. In the process, you will differentiate between the GET and POST methods. First, the JavaScript client uses the GET method. The server uses the following commands to send the required information to the client:

$WebAuthn = new \WebAuthn\WebAuthn('IT-Administrator', 'it-administrator.de', array('fido-u2f', 'packed', 'android-key', 'android-safetynet', 'none'));
$createArgs = $WebAuthn->getCreateArgs($user_id, $nick, $display name);

The values of the three arguments for getCreateArgs() can also be identical. They only need to be unique for each user because they are used to distinguish different keys on the security token, if supported by the token supports. To accept the new key, a challenge is sent along, signed on the token, and uploaded with the public key in the second step. The best idea would be to save this challenge in the current user session and then return the parameters created here in JSON format to the JavaScript client to complete the first step:

$_SESSION['fido_challenge'] = $WebAuthn->getChallenge();
print(json_encode($createArgs));
return;

Now the server is waiting for the public key and the first signature to be sent, which can then be verified with the public key. On an Android smartphone, you can now select which authentication method you want to use to unlock the private key locally on the smartphone (Figure 1).

Selecting an authenticator on the smartphone.
Figure 1: Selecting an authenticator on the smartphone.

Even if not provided for in the sample application, I recommend that the user additionally specify a name for the token or device in your application so that simple mapping is possible later on. This name is now also transferred to /fido/create in the POST request. In the called method, the generated signature must now be verified with the public key that was also uploaded. To do this, read it from the body of the request as follows and evaluate the JSON it contains accordingly:

$post = trim(file_get_contents('php://input'));
if ($post) {
$post = json_decode($post);
}

The token also sends a unique credential ID that is used to identify the key pair. This credential ID and the public key are stored in the database for the logged on user. The other values do not need to be stored permanently. To create the object, the challenge is first read from the session:

$challenge = $_SESSION['fido_challenge'];

You might see an error before reading the challenge from the session variable. In fact, this error occurs at session startup when PHP tries to create an object of the \WebAuthn\Binary\ByteBuffer type before the class is known to the script. This error can be remedied by simply including the WebAuthn library before session_start() and preloading the class with use:

require_once 'WebAuthn/WebAuthn.php';
use WebAuthn\Binary\ByteBuffer;

Next, the user information available in Base64 format and the information about the Authenticator need to be decoded, starting a generation process that, if successful, returns a corresponding object if the challenge has a valid signature:

$clientDataJSON = base64_decode($post->clientDataJSON);
$attestationObject = base64_decode($post->attestation-Object);
$data = $WebAuthn->processCreate($clientDataJSON, $attestationObject, $challenge);

The required credential ID and the user's public key can now be read from the object in the $data variable. How you store these values in your database depends on your current configuration. However, you will want to change the credential ID's encoding back to Base64 before saving, because many databases do not accept binary data. The public key is in privacy-enhanced mail (PEM) format and is therefore already Base64 encoded:

$credentialId = base64_encode($data->credentialId);
$credentialPublicKey = $data->credentialPublicKey;

Keep in mind that it has to be possible for each user to store multiple public keys. In this way, the user keeps access to their account even if they can no longer use one of the tokens. If you have stored these values appropriately for your database, you are winning. The JavaScript client accepts an object in JSON and evaluates the success and msg fields:

$return = new stdClass();
$return->success = true;
$return->msg = 'Registration Success;
print(json_encode($return));
return;

This successfully completes the process of generating the key, and you can verify that the two values are stored in the database.

First Login

Now that the public key and the credential ID assigned by the security token are stored in the database, the user can log on to your system. Again, the user first uses GET to request the challenge and other parameters from the application server. Because you do not have a valid session at this time, the JavaScript client in your application needs to query the username or, as described, read it from a login form. Along with the username, which you pass in with the URL for simplicity's sake, all the stored credential IDs of the user can now be read from the database. Remember that these are stored in Base64 encoding; therefore you transmit all stored IDs to the client so it can select a suitable one. You do not need the public key in this call yet. You can now create the challenge with the commands:

$ids = array();
foreach($dbdata AS $credentials){$ids[] = base64_decode($credentials['credentialId']);
}
$getArgs = $WebAuthn>getGetArgs($ids);
$_SESSION['challenge'] = $WebAuthn-> getChallenge();
print(json_encode($getArgs));
return;

You will have to adapt the variables and indexes in the foreach construct to match your database structure. Now the client can sign the challenge with a security token to match the IDs and POST the signature back to the web application in the body of the challenge. The challenge for selecting a security token in Firefox is shown in Figure 2.

A Firefox request to use a security token.
Figure 2: A Firefox request to use a security token.

Now read the HTTP body of the request again and decode the JSON it contains as shown above. The following commands take the data apart and decode the Base64 data it contains once again:

$clientDataJSON = base64_decode($post->clientDataJSON);
$authenticatorData = base64_decode($post->authenticatorData);
$signature = base64_decode($post->signature);
$credentialId = base64_decode($post->id);

Now find the public key to match the credential ID in your database and store it in the $credentialPublicKey variable. Again, remember that the data in the database is Base64 encoded. If there is no matching key, you need to return a corresponding error. To do so, use an object and the success and msg attributes as shown earlier. If the public key is found, you still need the challenge from the session, which you can then use to perform the check:

$challenge = $_SESSION['challenge'];
$WebAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $credentialPublicKey, $challenge);

This command throws a WebAuthnException if an error occurs during the check; you need to handle this accordingly. Without an error, the user is considered to be logged in. Now you can create all session data, just as after a normal login, and then report success back to the client. You can reload the page with JavaScript or configure a redirection to a subpage.

Other Possible Uses

Once you have completed the development of a FIDO2 login as shown, you will certainly start thinking about many potential adjustments to the process. For example, given the appropriate information, authentication can be implemented without the need to enter usernames. Or you can use FIDO2 as a second factor, just as some online services already make use of its functionality. With Windows 10 and Microsoft Hello as the authenticator, you can also use FIDO2 on the devices in your Windows domain.

Conclusions

In this article, I showed you how to enable FIDO2-based login for a web service. If you prefer to use languages other than PHP for your web development, you will find similar libraries for them. The principle remains the same, and you do not necessarily have to adapt the JavaScript page of the client. Just try out the different FIDO2 configuration and usage possibilities.