WebAuthn Basic Web Client/Server

webauthn_logo.png

In this tutorial we’ll build a basic WebAuthn web client/server in go using Duo Labs’ awesome WebAuthn library. We’ll also briefly go over the WebAuthn API. All code for this tutorial can be found here. Note this tutorial is not meant to be used in production but more as a starting point to building a functioning WebAuthn server and client, and as an introduction to the WebAuthn API.

What Is WebAuthn

WebAuthn is a standard that enables passwordless authentication (aka login) in the browser (and beyond). It essentially defines a common API for web applications to handle passwordless technologies via FIDO2 which includes things like public key cryptography, security keys (i.e. Yubikey), and biometrics (i.e. Touch ID/Face ID, Windows Hello).

What You Need

  • a WebAuthn compatible browser (list via Firefox and list via Duo)
  • a security key such as a Yubikey OR a biometric enabled device such as a MacBook with Touch ID or a Microsoft Surface Book with Windows Hello

Resources

Incase you want to nerd out, here are some great WebAuthn resources

Code

Now for the fun part! Let’s code…

Simple Client

Let’s start with a super simple index.html that takes a username and performs Register and Login actions.

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <title>WebAuthn Demo</title>
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.0/jquery.min.js"></script>
</head>

<body>

  Username:
  <br>
  <input type="text" name="username" id="email" placeholder="i.e. foo@bar.com">
  <br>
  <br>
  <button onclick="registerUser()">Register</button>
  <button onclick="loginUser()">Login</button>

  <script>

    $(document).ready(function () {
        // TODO
    });

  </script>
</body>

</html>

index.html should look something like this (let’s call this style ‘minimalist’ 🙃)

index_html_example.png

Browser Support Check

Before we do anything let’s make sure the browser we’re using supports WebAuthn. Inside the $(document).ready function add

$(document).ready(function () {

    // check whether current browser supports WebAuthn
    if (!window.PublicKeyCredential) {
        alert("Error: this browser does not support WebAuthn");
        return;
    }
});

Register

At a high level, WebAuthn registration involves an authenticator generating a public/private key pair that is bound to information provided by the server (i.e. user info, organization info). An authenticator is an entity - could be a physical separate security key like a Yubikey, could be built into a device like Touch ID/Face ID/Windows Hello, could be an app on a phone - that performs cryptographic operations such as key generation and signing (technically authenticators do a little more than just crypto).

Begin Register

Part 1

webauthn_registration_mozilla_1.png

All registration code will live in the registerUser() function (inside the <script> tag). First we’ll get the username from the text input field. Then to kick off the registration process, we’ll make a GET call to /register/begin/{username}. This will return some information required to generate the public/private key pair.

function registerUser() {

	username = $("#email").val()
	if (username === "") {
	  alert("please enter a username");
	  return;
	}

	$.get(
	  '/register/begin/' + username,
	  null,
	  function (data) {
		return data
	  },
	  'json')
	  .then((credentialCreationOptions) => {
		  // TODO
	  });
}

Now let’s implement the server portion of Begin Register.

Create a new file called server.go (I put all the files in the root directory for simplicity/laziness) with the following:

package main

import (
	"log"
	"net/http"

	"github.com/duo-labs/webauthn.io/session"
	"github.com/duo-labs/webauthn/webauthn"
	"github.com/gorilla/mux"
)

var webAuthn *webauthn.WebAuthn
var sessionStore *session.Store
var userDB *userdb

func main() {

	var err error
	webAuthn, err = webauthn.New(&webauthn.Config{
		RPDisplayName: "Foobar Corp.",     // display name for your site
		RPID:          "localhost",        // generally the domain name for your site
	})

	if err != nil {
		log.Fatal("failed to create WebAuthn from config:", err)
	}

	userDB = DB()

	sessionStore, err = session.NewStore()
	if err != nil {
		log.Fatal("failed to create session store:", err)
	}

	r := mux.NewRouter()

	r.HandleFunc("/register/begin/{username}", BeginRegistration).Methods("GET")

	r.PathPrefix("/").Handler(http.FileServer(http.Dir("./")))

	serverAddress := ":8080"
	log.Println("starting server at", serverAddress)
	log.Fatal(http.ListenAndServe(serverAddress, r))
}

Let’s break down what we just added.

webAuthn (type webauthn.WebAuthn)

The primary interface that the github.com/duo-labs/webauthn package exposes. To initialize it, we provide a config with two fields (there are more optional fields), all related to the Relying Party (RP) which is the web application/entity that is registering and authenticating the user. The fields are RPDisplayName: the display name for the RP’s site (i.e. Foobar Corp.) and RPID: based on the RP’s domain name (i.e. foobar.com).

sessionStore (type *session.Store)

A nice wrapper around a cookie store via github.com/duo-labs/webauthn.io

userDB (type *userdb)

Provides a mock database that stores and retrieves users by their username. Add the following userdb.go file

package main

import (
	"fmt"
	"sync"
)

type userdb struct {
	users map[string]*User
	mu    sync.RWMutex
}

var db *userdb

// DB returns a userdb singleton
func DB() *userdb {

	if db == nil {
		db = &userdb{
			users: make(map[string]*User),
		}
	}

	return db
}

// GetUser returns a *User by the user's username
func (db *userdb) GetUser(name string) (*User, error) {

	db.mu.Lock()
	defer db.mu.Unlock()
	user, ok := db.users[name]
	if !ok {
		return &User{}, fmt.Errorf("error getting user '%s': does not exist", name)
	}

	return user, nil
}

// PutUser stores a new user by the user's username
func (db *userdb) PutUser(user *User) {

	db.mu.Lock()
	defer db.mu.Unlock()
	db.users[user.name] = user
}

You’ll notice reference to User, which is what github.com/duo-labs/webauthn uses to interact with WebAuthn users. To implement User, we need to implement duo-lab’s user interface. In a new user.go file, add

package main

import (
	"crypto/rand"
	"encoding/binary"

	"github.com/duo-labs/webauthn/protocol"
	"github.com/duo-labs/webauthn/webauthn"
)

// User represents the user model
type User struct {
	id          uint64
	name        string
	displayName string
	credentials []webauthn.Credential
}

// NewUser creates and returns a new User
func NewUser(name string, displayName string) *User {

	user := &User{}
	user.id = randomUint64()
	user.name = name
	user.displayName = displayName
	// user.credentials = []webauthn.Credential{}

	return user
}

func randomUint64() uint64 {
	buf := make([]byte, 8)
	rand.Read(buf)
	return binary.LittleEndian.Uint64(buf)
}

// WebAuthnID returns the user's ID
func (u User) WebAuthnID() []byte {
	buf := make([]byte, binary.MaxVarintLen64)
	binary.PutUvarint(buf, uint64(u.id))
	return buf
}

// WebAuthnName returns the user's username
func (u User) WebAuthnName() string {
	return u.name
}

// WebAuthnDisplayName returns the user's display name
func (u User) WebAuthnDisplayName() string {
	return u.displayName
}

// WebAuthnIcon is not (yet) implemented
func (u User) WebAuthnIcon() string {
	return ""
}

// AddCredential associates the credential to the user
func (u *User) AddCredential(cred webauthn.Credential) {
	u.credentials = append(u.credentials, cred)
}

// WebAuthnCredentials returns credentials owned by the user
func (u User) WebAuthnCredentials() []webauthn.Credential {
	return u.credentials
}

Now back to server.go. Let’s implement the BeginRegistration handler. We’ll start by pulling the username path variable from the request

func BeginRegistration(w http.ResponseWriter, r *http.Request) {

	// get username
	vars := mux.Vars(r)
	username, ok := vars["username"]
	if !ok {
		jsonResponse(w, fmt.Errorf("must supply a valid username i.e. foo@bar.com"), http.StatusBadRequest)
		return
    }
}

Also add the jsonResponse() helper method (from duo-labs/webauthn.io)

func jsonResponse(w http.ResponseWriter, d interface{}, c int) {
	dj, err := json.Marshal(d)
	if err != nil {
		http.Error(w, "Error creating JSON response", http.StatusInternalServerError)
	}
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(c)
	fmt.Fprintf(w, "%s", dj)
}

Now let’s attempt to get the user and create a new user if they don’t exist. It might seem weird to check for an existing user, but remember we’re registering an authenticator to a user who can exist already. For example, Bob can register a MacBook to his account, then register a Yubikey to that same account. Below the code we just added, add

// get user
user, err := userDB.GetUser(username)
// user doesn't exist, create new user
if err != nil {
    displayName := strings.Split(username, "@")[0]
    user = NewUser(username, displayName)
    userDB.PutUser(user)
}

Note how we’re using the email prefix as the display name - this is out of convenience (and laziness). duo-labs/webauthn has a high level function BeginRegistration() that handles - you guessed it - the beginning of the registration process.

// generate PublicKeyCredentialCreationOptions, session data
options, sessionData, err := webAuthn.BeginRegistration(user)

if err != nil {
    log.Println(err)
    jsonResponse(w, err.Error(), http.StatusInternalServerError)
    return
}

Let’s break this down

options (type protocol.CredentialCreation)

An implementation of PublicKeyCredentialCreationOptions which contains information necessary for the authenticator to generate the public/private key pair such as Relying Party info (i.e. what we set in webauthn.Config), user info (i.e. a unique user handle ID), and a randomly generated challenge (to be signed by the authenticator)*.

sessionData (type webauthn.SessionData)

Contains information required to verify the signed challenge, like the challenge itself as well as the user’s id.

Putting it all together, BeginRegistration() looks like this

func BeginRegistration(w http.ResponseWriter, r *http.Request) {

	// get username
	vars := mux.Vars(r)
	username, ok := vars["username"]
	if !ok {
		jsonResponse(w, fmt.Errorf("must supply a valid username i.e. foo@bar.com"), http.StatusBadRequest)
		return
	}

	// get user
	user, err := userDB.GetUser(username)
	// user doesn't exist, create new user
	if err != nil {
		displayName := strings.Split(username, "@")[0]
		user = NewUser(username, displayName)
		userDB.PutUser(user)
	}

	// generate PublicKeyCredentialCreationOptions, session data
	options, sessionData, err := webAuthn.BeginRegistration(
		user,
	)

	if err != nil {
		log.Println(err)
		jsonResponse(w, err.Error(), http.StatusInternalServerError)
		return
	}

	// store session data as marshaled JSON
	err = sessionStore.SaveWebauthnSession("registration", sessionData, r, w)
	if err != nil {
		log.Println(err)
		jsonResponse(w, err.Error(), http.StatusInternalServerError)
		return
	}

	jsonResponse(w, options, http.StatusOK)
}

Now back to the JS client, in registerUser() the credentialCreationOptions that gets returned looks like this

{
  "publicKey": {
    "challenge": "FsxBWwUb1jOFRA3ILdkCsPdCZkzohvd3JrCNeDqWpJQ=",
    "rp": {
      "name": "Foobar Corp.",
      "id": "localhost"
    },
    "user": {
      "name": "foo@bar.com",
      "displayName": "foo",
      "id": "xOywiL6f3q9EAA=="
    },
    "pubKeyCredParams": [
      {
        "type": "public-key",
        "alg": -7
      },
      {
        "type": "public-key",
        "alg": -35
      },
      ...
      {
        "type": "public-key",
        "alg": -8
      }
    ],
    "authenticatorSelection": {
      "userVerification": "preferred"
    },
    "timeout": 60000,
    "attestation": "direct"
  }
}

The publicKey (type PublicKeyCredentialCreationOptions) contains a lot of what we set, like the relying party (rp field), user (user field), and challenge (challenge field). github.com/duo-labs/webauthn autoset some properties we don’t really have to worry about.

Part 2

webauthn_registration_mozilla_2.png

Back with registerUser() in index.html, to finish the Begin Register process, we call navigator.credentials.create() (part of the Credential Management API) with credentialCreationOptions.publicKey

.then((credentialCreationOptions) => {

  credentialCreationOptions.publicKey.challenge = bufferDecode(credentialCreationOptions.publicKey.challenge);
  credentialCreationOptions.publicKey.user.id = bufferDecode(credentialCreationOptions.publicKey.user.id);

  return navigator.credentials.create({
    publicKey: credentialCreationOptions.publicKey
  })
})
.then((credential) => {
  // TODO
})

To help us convert base64 encoded data to an ArrayBuffer add

// Base64 to ArrayBuffer
function bufferDecode(value) {
    return Uint8Array.from(atob(value), c => c.charCodeAt(0));
}

Part 3

webauthn_registration_mozilla_3.png

Your browser and authenticator take over from here. Implementations vary, for example your browser might ask you to choose an authenticator, then your authenticator could prompt you to perform a gesture (i.e. press button, scan finger).

Finish Register

Part 4

webauthn_registration_mozilla_4.png

If all goes well the authenticator/browser will return an AuthenticatorAttestationResponse that looks like this

{
  "id": "ABJcniVJwrH45aueEJJsD0LFTGMUxot1sHCOttym_p7rNPF72Zc_NcGo05j3KzDkU5fWELqRz7h9",
  "rawId": "ABJcniVJwrH45aueEJJsD0LFTGMUxot1sHCOttym_p7rNPF72Zc_NcGo05j3KzDkU5fWELqRz7h9",
  "type": "public-key",
  "response": {
    "attestationObject": "o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZyZjc2lnWEcwRQIhAIMEvEj1LKkmGNQuL0zeiv7dAzQmZynWkIuzuoGKAus8AiBUQBxCjGwCenhRWbJut2PyeF_7A3FJ3Jx8PVDOhuqUFGhhdXRoRGF0YVi9SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFXMPTf63OAAI1vMYKZIsLJfHwVQMAOQASXJ4lScKx-OWrnhCSbA9CxUxjFMaLdbBwjrbcpv6e6zTxe9mXPzXBqNOY9ysw5FOX1hC6kc-4faUBAgMmIAEhWCC6NdvZp8idlfpaTuREhZ3YHOx2QNam7HQI5-uDx1JFRiJYIKe0q8Ax92EtplrfOtvfIxtOt7_XEvHYRQy16O-MpDjk",
    "clientDataJSON": "eyJjaGFsbGVuZ2UiOiJGc3hCV3dVYjFqT0ZSQTNJTGRrQ3NQZENaa3pvaHZkM0pyQ05lRHFXcEpRIiwib3JpZ2luIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwIiwidHlwZSI6IndlYmF1dGhuLmNyZWF0ZSJ9"
  }
}

The AuthenticatorAttestationResponse contains attestation information such as the newly generated public key, the attestation response, as well as information necessary for the server to verify the attestation/response (i.e. user id, client data).

Part 5

webauthn_registration_mozilla_5.png

As our last step in the registration process we return the AuthenticatorAttestationResponse to the server by performing a POST to /register/finish/{username}

.then((credential) => {

  let attestationObject = credential.response.attestationObject;
  let clientDataJSON = credential.response.clientDataJSON;
  let rawId = credential.rawId;

  $.post(
    '/register/finish/' + username,
    JSON.stringify({
      id: credential.id,
      rawId: bufferEncode(rawId),
      type: credential.type,
      response: {
        attestationObject: bufferEncode(attestationObject),
        clientDataJSON: bufferEncode(clientDataJSON),
      },
    }),
    function (data) {
      return data
    },
    'json')
})

We need to add a helper function bufferEncode() to convert ArrayBuffers to URLBase64

// ArrayBuffer to URLBase64
function bufferEncode(value) {
  return btoa(String.fromCharCode.apply(null, new Uint8Array(value)))
    .replace(/\+/g, "-")
    .replace(/\//g, "_")
    .replace(/=/g, "");;
}

Adding some success/fail handling and putting it all together, registerUser() looks like this

function registerUser() {

  username = $("#email").val()
  if (username === "") {
    alert("please enter a username");
    return;
  }

  $.get(
    '/register/begin/' + username,
    null,
    function (data) {
      return data
    },
    'json')
    .then((credentialCreationOptions) => {

      credentialCreationOptions.publicKey.challenge = bufferDecode(credentialCreationOptions.publicKey.challenge);
      credentialCreationOptions.publicKey.user.id = bufferDecode(credentialCreationOptions.publicKey.user.id);

      return navigator.credentials.create({
        publicKey: credentialCreationOptions.publicKey
      })
    })
    .then((credential) => {

      let attestationObject = credential.response.attestationObject;
      let clientDataJSON = credential.response.clientDataJSON;
      let rawId = credential.rawId;

      $.post(
        '/register/finish/' + username,
        JSON.stringify({
          id: credential.id,
          rawId: bufferEncode(rawId),
          type: credential.type,
          response: {
            attestationObject: bufferEncode(attestationObject),
            clientDataJSON: bufferEncode(clientDataJSON),
          },
        }),
        function (data) {
          return data
        },
        'json')
    })
    .then((success) => {
      alert("successfully registered " + username + "!")
      return
    })
    .catch((error) => {
      console.log(error)
      alert("failed to register " + username)
    })
}

Part 6

webauthn_registration_mozilla_6.png

The server side portion of registering a new credential involves 19 steps! Luckily github.com/duo-labs/webauthn exposes a simple FinishRegistration() function. Briefly, these steps include validating client data (i.e. domain name), authenticator data, and the attestation statement (i.e. the signature). FinishRegistration() takes the user, sessionData (what we stored in BeginRegister()), and the *http.Request, and returns a webauthn.Credential if everything goes well (and an error if it doesn’t). Finally we’ll store the credential with the user. To actually implement this server side, add a handler in main() (inside server.go)

	r := mux.NewRouter()

	r.HandleFunc("/register/begin/{username}", BeginRegistration).Methods("GET")
	r.HandleFunc("/register/finish/{username}", FinishRegistration).Methods("POST")

	r.PathPrefix("/").Handler(http.FileServer(http.Dir("./")))

and implement the function FinishRegistration()

func FinishRegistration(w http.ResponseWriter, r *http.Request) {

	// get username
	vars := mux.Vars(r)
	username := vars["username"]

	// get user
	user, err := userDB.GetUser(username)
	// user doesn't exist
	if err != nil {
		log.Println(err)
		jsonResponse(w, err.Error(), http.StatusBadRequest)
		return
	}

	// load the session data
	sessionData, err := sessionStore.GetWebauthnSession("registration", r)
	if err != nil {
		log.Println(err)
		jsonResponse(w, err.Error(), http.StatusBadRequest)
		return
	}

	credential, err := webAuthn.FinishRegistration(user, sessionData, r)
	if err != nil {
		log.Println(err)
		jsonResponse(w, err.Error(), http.StatusBadRequest)
		return
	}

	user.AddCredential(*credential)

	jsonResponse(w, "Registration Success", http.StatusOK)
	return
}

And just like that, we finished implementing WebAuthn registration! Here’s roughly what we just built looks like in Chrome

register_example.gif

Login

At a high level, WebAuthn login (aka authentication) involves an authenticator signing a challenge provided by the server using the previously generated private key.

Begin Login

Part 1

webauthn_login_mozilla_1.png

Let’s start with loginUser() in index.html by getting the username and making a GET call to /login/begin/{username}

function loginUser() {

  username = $("#email").val()
  if (username === "") {
    alert("please enter a username");
    return;
  }

  $.get(
    '/login/begin/' + username,
    null,
    function (data) {
      return data
    },
    'json')
    .then((credentialRequestOptions) => {
        // TODO
    })
}

In server.go, add a Begin Login handler

r.HandleFunc("/register/begin/{username}", BeginRegistration).Methods("GET")
r.HandleFunc("/register/finish/{username}", FinishRegistration).Methods("POST")
r.HandleFunc("/login/begin/{username}", BeginLogin).Methods("GET")

Begin Login is very similar to Begin Register. First we call duo-labs/webAuthns BeginLogin() with a user (and optional LoginOptions) and get returned options (implementation of PublicKeyCredentialRequestOptions discussed below), sessionData (used to verify the returned assertion), and an error if anything goes wrong. We store the session data and return the options to the user

func BeginLogin(w http.ResponseWriter, r *http.Request) {

	// get username
	vars := mux.Vars(r)
	username := vars["username"]

	// get user
	user, err := userDB.GetUser(username)

	// user doesn't exist
	if err != nil {
		log.Println(err)
		jsonResponse(w, err.Error(), http.StatusBadRequest)
		return
	}

    // generate PublicKeyCredentialRequestOptions, session data
	options, sessionData, err := webAuthn.BeginLogin(user)
	if err != nil {
		log.Println(err)
		jsonResponse(w, err.Error(), http.StatusInternalServerError)
		return
	}

	// store session data as marshaled JSON
	err = sessionStore.SaveWebauthnSession("authentication", sessionData, r, w)
	if err != nil {
		log.Println(err)
		jsonResponse(w, err.Error(), http.StatusInternalServerError)
		return
	}

	jsonResponse(w, options, http.StatusOK)
}

The credentialRequestOptions (type PublicKeyCredentialRequestOptions) only requires a challenge but has additional option fields, and looks like this

{
  "credentialRequestOptions": {
    "publicKey": {
      "challenge": "AZeYdiLc1hGDU0wo9NOZmZG9vu+aPnff2aPFgJEw1HI=",
      "timeout": 60000,
      "rpId": "localhost",
      "allowCredentials": [
        {
          "type": "public-key",
          "id": "ABJcniVJwrH45aueEJJsD0LFTGMUxot1sHCOttym/p7rNPF72Zc/NcGo05j3KzDkU5fWELqRz7h9"
        }
      ]
    }
  }
}

Important fields to take note of are challenge, random bits (different than the challenge in Register) that the authenticator will sign over using the private key created in the Register process, and allowCredentials which specifies which public key credentials are acceptable (i.e. previously registered ones).

Part 2

webauthn_login_mozilla_2.png

To finish the Begin Login process, we call navigator.credentials.get() (part of the Credential Management API) with credentialCreationOptions.publicKey (remember to decode encoded fields into ArrayBuffers), and the browser takes care of the rest. Back in index.html inside loginUser()

.then((credentialRequestOptions) => {

  credentialRequestOptions.publicKey.challenge = bufferDecode(credentialRequestOptions.publicKey.challenge);
  credentialRequestOptions.publicKey.allowCredentials.forEach(function (listItem) {
    listItem.id = bufferDecode(listItem.id)
  });

  return navigator.credentials.get({
    publicKey: credentialRequestOptions.publicKey
  })
})
.then((assertion) => {
    // TODO
})

Part 3

webauthn_login_mozilla_3.png

Your browser and authenticator take over from here. Implementations vary, for example your authenticator could prompt you to perform a gesture (i.e. press button, scan finger).

Finish Login

Part 4

webauthn_login_mozilla_4.png

If all goes well the browser will return an AuthenticatorAssertionResponse which looks like this

{
  "id": "ABJcniVJwrH45aueEJJsD0LFTGMUxot1sHCOttym_p7rNPF72Zc_NcGo05j3KzDkU5fWELqRz7h9",
  "rawId": "ABJcniVJwrH45aueEJJsD0LFTGMUxot1sHCOttym_p7rNPF72Zc_NcGo05j3KzDkU5fWELqRz7h9",
  "type": "public-key",
  "response": {
    "authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFXMPTha3OAAI1vMYKZIsLJfHwVQMAOQASXJ4lScKx-OWrnhCSbA9CxUxjFMaLdbBwjrbcpv6e6zTxe9mXPzXBqNOY9ysw5FOX1hC6kc-4faUBAgMmIAEhWCC6NdvZp8idlfpaTuREhZ3YHOx2QNam7HQI5-uDx1JFRiJYIKe0q8Ax92EtplrfOtvfIxtOt7_XEvHYRQy16O-MpDjk",
    "clientDataJSON": "eyJjaGFsbGVuZ2UiOiJBWmVZZGlMYzFoR0RVMHdvOU5PWm1aRzl2dS1hUG5mZjJhUEZnSkV3MUhJIiwib3JpZ2luIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwIiwidHlwZSI6IndlYmF1dGhuLmdldCJ9",
    "signature": "MEQCIFq-Q4v8DK89OUAyyOowjKMc6umCNLaQR1ry8obvFtRAAiBRf5c1ufdFR9w-nTsLm5NG_KtycZ0bWFLj5h-uRiYLCw",
    "userHandle": "xOywiL6f3q9EAA"
  }
}

Let’s break some of this down.

signature

Raw signature from the authenticator (using the registered private key) over the challenge.

userHandle

User id supplied by the Relying Party (same value as user.id from the credentialCreationOptions example in Begin Register - Part 1).

The rest should look familiar from the AuthenticatorAttestationResponse in Finish Register - Part 4.

Part 5

webauthn_login_mozilla_5.png

To finalize Finish Login, we need to base64 encode various ArrayBuffers, then send the AuthenticatorAssertionResponse back to the server via a POST to /login/finish/{username}

.then((assertion) => {

  let authData = assertion.response.authenticatorData;
  let clientDataJSON = assertion.response.clientDataJSON;
  let rawId = assertion.rawId;
  let sig = assertion.response.signature;
  let userHandle = assertion.response.userHandle;

  $.post(
    '/login/finish/' + username,
    JSON.stringify({
      id: assertion.id,
      rawId: bufferEncode(rawId),
      type: assertion.type,
      response: {
        authenticatorData: bufferEncode(authData),
        clientDataJSON: bufferEncode(clientDataJSON),
        signature: bufferEncode(sig),
        userHandle: bufferEncode(userHandle),
      },
    }),
    function (data) {
      return data
    },
    'json')
})

Adding some success/fail handling and putting it all together, loginUser() looks like this

function loginUser() {

  username = $("#email").val()
  if (username === "") {
    alert("please enter a username");
    return;
  }

  $.get(
    '/login/begin/' + username,
    null,
    function (data) {
      return data
    },
    'json')
    .then((credentialRequestOptions) => {

      credentialRequestOptions.publicKey.challenge = bufferDecode(credentialRequestOptions.publicKey.challenge);
      credentialRequestOptions.publicKey.allowCredentials.forEach(function (listItem) {
        listItem.id = bufferDecode(listItem.id)
      });

      return navigator.credentials.get({
        publicKey: credentialRequestOptions.publicKey
      })
    })
    .then((assertion) => {

      let authData = assertion.response.authenticatorData;
      let clientDataJSON = assertion.response.clientDataJSON;
      let rawId = assertion.rawId;
      let sig = assertion.response.signature;
      let userHandle = assertion.response.userHandle;

      $.post(
        '/login/finish/' + username,
        JSON.stringify({
          id: assertion.id,
          rawId: bufferEncode(rawId),
          type: assertion.type,
          response: {
            authenticatorData: bufferEncode(authData),
            clientDataJSON: bufferEncode(clientDataJSON),
            signature: bufferEncode(sig),
            userHandle: bufferEncode(userHandle),
          },
        }),
        function (data) {
          return data
        },
        'json')
    })
    .then((success) => {
      alert("successfully logged in " + username + "!")
      return
    })
    .catch((error) => {
      console.log(error)
      alert("failed to register " + username)
    })
}

Part 6

webauthn_login_mozilla_6.png

Finish Login - Part 6 is almost identical code wise to Finish Register - Part 6 except instead we call webAuthn.FinishLogin()

The server side portion of verifying a login assertion involves 18 steps! Again, luckily we can take advantage of duo-labs/webauthn’s simple FinishLogin() function. Briefly, these steps include looking up the user’s public key, validating client data (i.e. domain name, challenge), and validating the signature. Back in server.go

func FinishLogin(w http.ResponseWriter, r *http.Request) {

	// get username
	vars := mux.Vars(r)
	username := vars["username"]

	// get user
	user, err := userDB.GetUser(username)

	// user doesn't exist
	if err != nil {
		log.Println(err)
		jsonResponse(w, err.Error(), http.StatusBadRequest)
		return
	}

	// load the session data
	sessionData, err := sessionStore.GetWebauthnSession("authentication", r)
	if err != nil {
		log.Println(err)
		jsonResponse(w, err.Error(), http.StatusBadRequest)
		return
	}

    // in an actual implementation we should perform additional 
    // checks on the returned 'credential'
	_, err = webAuthn.FinishLogin(user, sessionData, r)
	if err != nil {
		log.Println(err)
		jsonResponse(w, err.Error(), http.StatusBadRequest)
		return
	}

	// handle successful login
	jsonResponse(w, "Login Success", http.StatusOK)
}

Also make sure to add the handlers in main()

r.HandleFunc("/register/begin/{username}", BeginRegistration).Methods("GET")
r.HandleFunc("/register/finish/{username}", FinishRegistration).Methods("POST")
r.HandleFunc("/login/begin/{username}", BeginLogin).Methods("GET")
r.HandleFunc("/login/finish/{username}", FinishLogin).Methods("POST")

We just successfully finished WebAuthn login! Here’s roughly what we just built looks like in Chrome

login_example.gif

More

If we want to dive a little deeper, there are a few things we can do to touch up our implementation.

Exclude Credentials

If you play around and hit register multiple times using the same username + authenticator pair (i.e. foo@bar.com and a MacBook), you’ll notice that multiple keys get registered! This is not ideal behavior since we only really need one key per user/authenticator for a relying party. In PublicKeyCredentialCreationOptions we can fill an optional list excludeCredentialDescriptorList with credentials (IDs and types). The browser can then use this list to block the creation of credentials that already exist on the authenticator for that user. To implement this, add the following function to user.go

// CredentialExcludeList returns a CredentialDescriptor array filled
// with all a user's credentials
func (u User) CredentialExcludeList() []protocol.CredentialDescriptor {

	credentialExcludeList := []protocol.CredentialDescriptor{}
	for _, cred := range u.credentials {
		descriptor := protocol.CredentialDescriptor{
			Type:         protocol.PublicKeyCredentialType,
			CredentialID: cred.ID,
		}
		credentialExcludeList = append(credentialExcludeList, descriptor)
	}

	return credentialExcludeList
}

Next, in server.go inside BeginRegistration(), add the following

registerOptions := func(credCreationOpts *protocol.PublicKeyCredentialCreationOptions) {
  credCreationOpts.CredentialExcludeList = user.CredentialExcludeList()
}

options, sessionData, err := webAuthn.BeginRegistration(
  user,
  registerOptions,
)

Finally, add this code inside registerUser() in index.html

.then((credentialCreationOptions) => {

  credentialCreationOptions.publicKey.challenge = bufferDecode(credentialCreationOptions.publicKey.challenge);
  credentialCreationOptions.publicKey.user.id = bufferDecode(credentialCreationOptions.publicKey.user.id);
  if (credentialCreationOptions.publicKey.excludeCredentials) {
    for (var i = 0; i < credentialCreationOptions.publicKey.excludeCredentials.length; i++) {
      credentialCreationOptions.publicKey.excludeCredentials[i].id = bufferDecode(credentialCreationOptions.publicKey.excludeCredentials[i].id);
    }
  }

  return navigator.credentials.create({
    publicKey: credentialCreationOptions.publicKey
  })
})

Now if you try to register twice, you’ll see an error like this

multiple_register_error.png

Signature Counter

In an actual implementation, we would verify the sign counter of the credential returned in the AuthenitcatorAttestationResponse (from Finish Login) is greater than the sign counter we have stored for that credential. This is to check for cloned authenticators. Here’s an example implementation, and below is where it’s mentioned in the spec

Properly Storing Users

For simplicity sake we stored/retrieved users via their username in userdb. To do this properly you should store/retrieve users via the PublicKeyCredential’s id or rawID as per the spec’s verifying assertion step 3. For reference implementation, view

Assessing Attestation Statements

As per the specs registering a new credential steps 15 and 16, inside FinishRegister() we should analyze, assess, and verify an attestation statement against some policy. For example, we can weigh trust by which attestation type (i.e. none, self, basic) was used, as well as which trust anchor was used. For more on trust anchors take a look at the FIDO Metadata Service.

Credential Exists Check

According to the specs registering a new credential step 17 inside FinishRegister() we should check that the credential’s credentialId isn’t registered yet to any other user. If it is we could decide to fail the registration, or we could do something like accept the registration and delete the old credential/user association.

Notes

* technically there’s an attestation option ‘none’ (see here) where the challenge isn’t actually signed