Simple PHP WebAuthn (Biometric, NFC, Passkey, ETC…)

Welcome to a tutorial and example on how to implement WebAuthn in PHP. So you have heard that it is possible for web apps to authenticate using biometrics, “face login”, NFC token, USB passkey, and whatever “passwordless” means. The magic behind is actually the Web Authentication API.

In the simplest manner, the Web Authentication API only consists of 2 Javascript functions:

  • Call navigator.credentials.create() to create the credentials. Upload it to the server, verify, and save it into the database.
  • Call navigator.credentials.get() whenever “passwordless authentication” is required. Upload the credentials to the server – Verify against the database and proceed to do whatever is required.

It seems harmless at first, but these “2 functions” got me stuck for days reading through countless documents. So here it is, a simplified version that I managed to work out – Read on!

 

 

TABLE OF CONTENTS

 

DOWNLOAD & NOTES

Here is the download link to the example code, so you don’t have to copy-paste everything.

 

EXAMPLE CODE DOWNLOAD

Source code on GitHub Gist

Just click on “download zip” or do a git clone. I have released it under the MIT license, so feel free to build on top of it or use it in your own project.

 

SORRY FOR THE ADS...

But someone has to pay the bills, and sponsors are paying for it. I insist on not turning Code Boxx into a "paid scripts" business, and I don't "block people with Adblock". Every little bit of support helps.

Buy Me A Coffee Code Boxx eBooks

 

 

PHP WEBAUTHN

All right, let us now get into a simple example of working with WebAuthn in PHP.

 

TUTORIAL VIDEO

 

SETUP & NOTES

  • The Web Authentication API requires HTTPS to work properly, http://localhost is an exception for testing.
  • The available authentication methods pretty much depend on the browser and OS.
  • We will be using this simple PHP WebAuth library – Just run composer require lbuchs/webauthn, do a git pull, or download as a zip…

 

PART 1) HTML DEMO PAGE

1A) THE HTML

1-demo.html
<button onclick="register.a()">Register</button>
<button onclick="validate.a()">Validate</button>

Let us start with something that does cause brain damage. As in the introduction, there are “only 2 phases” in Web Authentication – Registration and validation.

 

1B) JAVASCRIPT – HELPERS

2-demo.js
// (A) HELPER FUNCTIONS
var helper = {
  // (A1) ARRAY BUFFER TO BASE 64
  atb : b => {
    let u = new Uint8Array(b), s = "";
    for (let i=0; i<u.byteLength; i++) { s += String.fromCharCode(u[i]); }
    return btoa(s);
  },
 
  // (A2) BASE 64 TO ARRAY BUFFER
  bta : o => {
    let pre = "=?BINARY?B?", suf = "?=";
    for (let k in o) { if (typeof o[k] == "string") {
      let s = o[k];
      if (s.substring(0, pre.length)==pre && s.substring(s.length - suf.length)==suf) {
        let b = window.atob(s.substring(pre.length, s.length - suf.length)),
        u = new Uint8Array(b.length);
        for (let i=0; i<b.length; i++) { u[i] = b.charCodeAt(i); }
        o[k] = u.buffer;
      }
    } else { helper.bta(o[k]); }}
  },
 
  // (A3) AJAX FETCH
  ajax : (url, data, after) => {
    let form = new FormData();
    for (let [k,v] of Object.entries(data)) { form.append(k,v); }
    fetch(url, { method: "POST", body: form })
    .then(res => res.text())
    .then(res => after(res))
    .catch(err => { alert("ERROR!"); console.error(err); });
  }
};

For the Javascript, we will begin with 3 helper functions. Not going to explain line-by-line, just a quick summary:

  • helper.atb() Converts an array buffer to base 64.
  • helper.bta() Converts base 64 to array buffer.
  • helper.ajax() Does a fetch() call.

 

 

PART 2) PHP WEBAUTHN INIT

3-init.php
<?php
// (A) RELYING PARTY - CHANGE TO YOUR OWN!
$rp = [
  "name" => "Code Boxx",
  "id" => "localhost"
];
 
// (B) DUMMY USER
$user = [
  "id" => "12345678",
  "name" => "jon@doe.com",
  "display" => "Jon Doe"
];
$saveto = "user.txt"; 
 
// (C) START SESSION & LOAD WEBAUTHN LIBRARY
session_start();
require "vendor/autoload.php";
$WebAuthn = new lbuchs\WebAuthn\WebAuthn($rp["name"], $rp["id"]);

Before we get into the “registration and validation” processes, let us create a simple PHP snippet… So we don’t have to repeat this everywhere.

  1. Relying party – The fancy way of saying “domain and app name”. Change these to your own.
  2. There’s no database in this example to keep things simple. We will just assume the user to be “Jon Doe”, and save the credentials into user.txt.
  3. Load the PHP WebAuthn library.

 

PART 3) REGISTRATION

3A) THE JAVASCRIPT

2-demo.js
// (B) REGISTRATION
var register = {
  // (B1) CREATE CREDENTIALS
  a : () => helper.ajax("4-register.php", {
    phase : "a"
  }, async (res) => {
    try {
      res = JSON.parse(res);
      helper.bta(res);
      register.b(await navigator.credentials.create(res));
    } catch (e) { alert(res); console.error(e); }
  }),
 
  // (B2) SEND CREDENTIALS TO SERVER
  b : cred => helper.ajax("4-register.php", {
    phase : "b",
    transport : cred.response.getTransports ? cred.response.getTransports() : null,
    client : cred.response.clientDataJSON ? helper.atb(cred.response.clientDataJSON) : null,
    attest : cred.response.attestationObject ? helper.atb(cred.response.attestationObject) : null
  }, res => alert(res))
};

The first part of Web Authn is to “do registration”. Keep calm and study closely, there are two parts to it.

  • (B1) Make a fetch call to the server, this will return the “arguments” to feed into navigator.credentials.create().
  • (B2) After navigator.credentials.create() generates the credentials – Upload to the server for validation, and save it into the database.

 

 

3B) THE PHP

4-register.php
<?php
// (A) INIT & CHECK
require "3-init.php";
if (file_exists($saveto)) { exit("User already registered"); }
 
switch ($_POST["phase"]) {
  // (B) REGISTRATION PART 1 - GET ARGUMENTS
  case "a":
    $args = $WebAuthn->getCreateArgs(
      \hex2bin($user["id"]), $user["name"], $user["display"],
      30, false, true
    );
    $_SESSION["challenge"] = ($WebAuthn->getChallenge())->getBinaryString();
    echo json_encode($args);
    break;
 
  // (C) REGISTRATION PART 2 - SAVE USER CREDENTIAL
  // should be saved in database, but we save into a file instead
  case "b":
    // (C1) VALIDATE & PROCESS
    try {
      $data = $WebAuthn->processCreate(
        base64_decode($_POST["client"]),
        base64_decode($_POST["attest"]),
        $_SESSION["challenge"],
        true, true, false
      );
    } catch (Exception $ex) { exit("ERROR - "); print_r($ex); }
 
    // (C2) SAVE
    file_put_contents($saveto, serialize($data));
    echo "OK";
    break;
}

Here is the corresponding PHP to handle the registration.

  • (B) Generate the arguments to feed into navigator.credentials.create(). Take note of $_SESSION["challenge"] here, this is a random hash for validation – Something like CSRF.
  • (C) Verify the uploaded credentials before saving. Yes, there’s no database in this example. We just save the credentials into a flat text file.

 

PART 4) VALIDATION

4A) THE JAVASCRIPT

2-demo.js
// (C) VALIDATION
var validate = {
  // (C1) GET CREDENTIALS
  a : () => helper.ajax("5-validate.php", {
    phase : "a"
  }, async (res) => {
    try {
      res = JSON.parse(res);
      helper.bta(res);
      validate.b(await navigator.credentials.get(res));
    } catch (e) { alert(res); console.error(e); }
  }),
 
  // (C2) SEND TO SERVER & VALIDATE
  b : cred => helper.ajax("5-validate.php", {
    phase : "b",
    id : cred.rawId ? helper.atb(cred.rawId) : null,
    client : cred.response.clientDataJSON ? helper.atb(cred.response.clientDataJSON) : null,
    auth : cred.response.authenticatorData ? helper.atb(cred.response.authenticatorData) : null,
    sig : cred.response.signature ? helper.atb(cred.response.signature) : null,
    user : cred.response.userHandle ? helper.atb(cred.response.userHandle) : null
  }, res => alert(res))
};

The validation process is pretty similar to the registration.

  • (C1) Get the “arguments” from the server, feed into navigator.credentials.get().
  • (C2) navigator.credentials.get() will generate the credentials, upload them to the server for validation.

 

 

4B) THE PHP

5-validate.php
<?php
// (A) INIT & CHECK
require "3-init.php";
if (!file_exists($saveto)) { exit("User is not registered"); }
$saved = unserialize(file_get_contents("user.txt"));
 
switch ($_POST["phase"]) {
  // (B) VALIDATION PART 1 - GET ARGUMENTS
  case "a":
    $args = $WebAuthn->getGetArgs([$saved->credentialId], 30);
    $_SESSION["challenge"] = ($WebAuthn->getChallenge())->getBinaryString();
    echo json_encode($args);
    break;
 
  // (C) VALIDATION PART 2 - CHECKS & PROCESS
  case "b":
    $id = base64_decode($_POST["id"]);
    if ($saved->credentialId !== $id) { exit("Invalid credentials"); }
    try {
      $WebAuthn->processGet(
        base64_decode($_POST["client"]),
        base64_decode($_POST["auth"]),
        base64_decode($_POST["sig"]),
        $saved->credentialPublicKey,
        $_SESSION["challenge"]
      );
      echo "OK";
      // DO WHATEVER IS REQUIRED AFTER VALIDATION
    } catch (Exception $ex) { echo "ERROR - "; print_r($ex); }
    break;
}
  • (B) Generate the arguments to feed into navigator.credentials.get(). Take note of $_SESSION["challenge"] here again.
  • (C) Validate the uploaded credentials against the database (text file in this example). Proceed to do whatever is required upon validation.

 

EXTRAS

That’s all for the tutorial, and here is a small section on some extras and links that may be useful to you.

 

HOW ABOUT LOGIN? DATABASE?

  • Modify (C) of 4-register.php. Save the uploaded credentials into the database – Add a new column to your existing user table or create another “credentials” table.
  • Modify (C) of 5-validate.php. On validation – Sign the user in. That can be $_SESSION["user"] = XYZ, setting a JWT cookie, or whatever your system is doing.
  • Take note of $WebAuthn->getGetArgs(ARRAY OF CREDENTIAL ID) in 5-validate.php. The library is capable of dealing with multiple credentials; A user can have multiple different methods of “passwordless login”.
  • Lastly, login is not the only use for Web Authn. It can also be used as an OTP for secure transactions.

 

 

LINKS & REFERENCES

 

THE END

Thank you for reading, and we have come to the end. I hope that it has helped you to better understand, and if you want to share anything with this guide, please feel free to comment below. Good luck and happy coding!

Leave a Comment

Your email address will not be published. Required fields are marked *