This is part one of a series. Find part two here.
For the past few months, I’ve been searching for a good authentication solution or my mashup Farcaster sports client Sportscaster. As I noticed myself increasingly drawn towards the convenience of sites and apps that use passkeys in place of a traditional username/password combo or something a little more exotic like magic email or text links, I decided to see if they would be a realistic solution for Sportscaster’s Clojure backend. The development process involved more than a little trial-and-error with sparse documentation, but the end result is pleasingly concise and functional, so over the course of the next few posts, I’m going to document how I put it together.
The Process
Passkeys are, essentially, a public-private key pair generated for a single website or application on behalf of a user, where the private key is managed by the user’s device’s passkey client implementation. This means that the server implementation consists of a database of known public keys for authenticating users, and API endpoints that handle the ceremony of generating new keypairs. The client application then just needs to acquire a challenge from the server and pass it along to its on-device client API, which handles key generation and storage.
The Implemenation
My server implementation of passkeys uses Yubico’s java-webauthn-server and runs just 303 lines of code. In this Part I, I’ll describe user storage and relying party generation. Part II will cover user registration, and Part III will cover user authentication.
Credential Repository
The java-webauthn-server
library relies on an implementation of its
CredentialRepository
interface
that comprises five methods it needs to query a user data store. With reify
in Clojure, they can look like this:
(ns example.passkeys
(:require [example.db :as db])
(:import [com.yubico.webauthn CredentialRepository RegisteredCredential]
[com.yubico.webauthn.data ByteArray]
[java.util HashSet Optional]))
(defn build-credential
[user-record]
(-> (RegisteredCredential/builder)
;; We are storing hex string representations of byte arrays like credential
;; ids in the database, and we use ByteArray/fromHex to restore them
(.credentialId (ByteArray/fromHex (:credential/id user-record)))
(.userHandle (ByteArray/fromHex (:user/handle user-record)))
(.publicKeyCose (ByteArray/fromHex (:credential/public-key-cose user-record)))
(.signatureCount (:credential/signature-count user-record))
(.build)))
(defn credential-repository
[db]
(reify CredentialRepository
(getCredentialIdsForUsername [_this username]
(->> (db/credential-ids-for-username db username)
(map :credential/id)
(map #(ByteArray/fromHex %))
(HashSet.)))
(getUserHandleForUsername [_this username]
(-> (db/user-handle-for-username db username)
first
:user/handle
(ByteArray/fromHex)
(Optional/of)))
(getUsernameForUserHandle [_this user-handle]
(-> (db/username-for-user-handle db user-handle)
first
:user/username
(Optional/of)))
(lookup [_this credential-id user-handle]
(-> (db/lookup db credential-id user-handle)
first
build-credential
(Optional/of)))
(lookupAll [_this credential-id]
(->> (db/lookup-all db credential-id)
(map :credential/id)
(map #(ByteArray/fromHex %))
(HashSet.)))))
I’ve extracted the backing database calls into an example.db
namespace; you
can use any data store you want, including a simple in-memory atom, as long as
it returns user records that look like {:credential/id ... :user/username ...
...}
. All of these methods are simple database reads that just return a user or
users based on a piece of a user record.
Relying Party
The relying party is the cryptographic representation of your server that is
passed to the client when initiating a registration or authentication.
java-webauthn-server
provides simple Builders we can use to create ours:
(defn rp-identity
[]
(-> (RelyingPartyIdentity/builder)
(.id "example.com")
(.name "Example")
(.build)))
(defn relying-party
[db]
(-> (RelyingParty/builder)
(.identity (rp-identity))
(.credentialRepository (credential-repository db))
(.build)))
Note above that in creating the relying-party
, you are passing along the
CredentialRepository
you built a function to create above;
java-webauthn-server
will use this when authenticating incoming requests.
Up Next
In four functions, we’ve built everything we need as a backend for our passkey implementation. In part two, we’ll see how to use this backend to handle incoming registration requests, and store successful ones.