Secure authentication with FIDO2

Replacements

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).

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.

Buy this article as PDF

Express-Checkout as PDF
Price $2.95
(incl. VAT)

Buy ADMIN Magazine

SINGLE ISSUES
 
SUBSCRIPTIONS
 
TABLET & SMARTPHONE APPS
Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

Related content

comments powered by Disqus