passkey - A developer guide
A quick technical introduction and exploration of passkeys, W3C webauthn and FIDO
In May 2022, three major companies — Apple, Google, and Microsoft — announced their support for a set of standards for passwordless authentication created by FIDO Alliance and W3C. This new technology, popularized by Apple as “passkey” on macOS 13 (Ventura) & iOS 16 during WWDC 2022. In October 2022, Google announced support for passkey on chrome (M108) & android (Android OS 9). Microsoft has supported passkeys since Windows 10, version 1809 (released in October 2018) through webauthn API. This brief historical overview should provide perspective on how recent the phenomenon of passkey is, and it’s worth noting that you are not late to the party. Additionally, please be aware that the core standards have been supported at varying levels by a majority of browsers for a while and have a proven track record.
If you are new to passkeys & WebAuthn in general, I would recommend checking out the various excellent resources available. My personal favorite for learning how this technology works is the Auth0 WebAuthn demo. It also features a WebAuthn debugger that you can use to experiment with different settings. If you want to delve deeper into the standard, visit https://webauthn.jhash.com, a website I created to simplify the understanding and implementation of the WebAuthn standard from a developer’s perspective.
Passkey is loosely defined by a combination of the following standards and features:
1. Webauthn standard defines how the web application can interact (through javascript) with browser to:
a) register and store a new credential, and
b) authenticate using a stored credential.
Most developers integrating with passkeys would be dealing with this standard, which is the focus of this article.
2. CTAP2 standard defines how the browser interacts with an authenticator (security key or device with camera) over NFC, BLE, and USB. This standard is typically intended for developers who are building browsers/OS or interfaces for security keys (such as Yubico, Titan), smart cards. We will not be covering this in the article.
3. Passkey storage and sync — Before the introduction of passkeys, any credentials created based on the standards mentioned above by the browser and authenticator (like a security key) would be stored on the authenticator itself (within the security key or secure enclave on a laptop/phone). These credentials are now referred to as device-bound passkeys. However, this implied that credentials would be lost if the corresponding authenticator (or device containing authenticator) was lost, resulting in a account recovery challenge. Passkey introduced the idea of syncing the credentials stored in the secure enclave on the device to the cloud and from there to other devices, addressing this shortcoming.
Code Please!
Let’s dig into some code.
Registering a credential
So what does it takes to register a new credential? You need a recent browser that supports FIDO authentication (check supported browser), developer tool, and a publicly accessible website over TLS/SSL (e.g. https://webauthn.jhash.com).
To register a new credential, copy and paste this code into the browser console, press enter, and then click on the page itself.
setTimeout(async() => {
var challengeVal = (new TextEncoder()).encode("ThisIsAVeryLongChallenge");
var userId = (new TextEncoder()).encode("ARandomUserIdThatDoesNotClashWithActualUserId");
var inputObj = {
publicKey: {
"rp": {
"name": "ACME"
},
"user": {
"id": userId,
"name": "aRandomUser",
"displayName": "A Random User"
},
"challenge": challengeVal,
"pubKeyCredParams": [
{
"type": "public-key",
"alg": -7
},
{
"type": "public-key",
"alg": -257
}
]
}
}
navigator.credentials.create(inputObj).then(function(newCredInfo){credValue=newCredInfo; credValue}).catch(function(err){console.log("PasskeyError: " + err)})
}, 3000)
This captures the flow on a chrome browser.
Let’s walk through the code
setTimeout(async() => { ...}, 3000)
This code initiates 3 seconds after pressing enter. This delay is necessary because Safari throws a PasskeyError: NotAllowedError: NotAllowedError: The document is not focused
if the console is in focus when the navigator.credentials.create
command is invoked. This marks your first encounter with the variations in implementation across different operating systems and browser combinations (we will cover similar variations in future articles).
var challengeVal = (new TextEncoder()).encode("ThisIsAVeryLongChallenge");
var userId = (new TextEncoder()).encode("ARandomUserIdThatDoesNotClashWithActualUserId");
Since certain values, such as challenge
and user.id
, need to be provided as array values, the input must be encoded.
var inputObj = {
publicKey: {
...
}
}
The provided code creates an object based on the standard. For a detailed analysis of what each setting means, please refer to https://webauthn.jhash.com.
navigator.credentials.create(inputObj).then(function(newCredInfo){
credValue=newCredInfo; credValue
}).catch(function(err){
console.log("PasskeyError: " + err)})
}
The navigator.credentials.create
method is passed the inputObj
, and it returns a promise that resolves with a new Credential
instance based on the provided options, or null
if no Credential
object can be created. In exceptional circumstances, the Promise
may reject.
Authenticate
Let’s attempt authentication using the credential we just created. As previously, copy and paste the code below, press enter, and click on page.
setTimeout(async() => {
var challengeVal = (new TextEncoder()).encode("ThisIsAVeryLongChallenge");
var inputObj = {
"publicKey": {
"challenge": challengeVal
}
}
navigator.credentials.get(inputObj).then(function(newCredInfo){credValue=newCredInfo}).catch(function(err){console.log("PasskeyError: " + err)})
}, 3000)
Most of the code remains the same, except that the input has only one value, challenge
, which is received from the server. The navigator.credentials.get
method is used to initiate authentication.
Bonus
Google has announced that the passkey will be the default authentication method, replacing passwords. So, go ahead and create one for your account. After creating the passkey and testing it, logout of google or open www.google.com in incognito mode (this step is optional).
Now, copy and paste the authentication JavaScript and run it just like before.
setTimeout(async() => {
var challengeVal = (new TextEncoder()).encode("ThisIsAVeryLongChallenge");
var inputObj = {
"publicKey": {
"challenge": challengeVal
}
}
navigator.credentials.get(inputObj).then(function(newCredInfo){credValue=newCredInfo}).catch(function(err){console.log("PasskeyError: " + err)})
}, 3000)
What do you see? Did any of the passkeys you created on the device pop up?
Now try this code.
setTimeout(async() => {
var challengeVal = (new TextEncoder()).encode("ThisIsAVeryLongChallenge");
var inputObj = {
"publicKey": {
"challenge": challengeVal,
"rpId": "google.com"
}
}
navigator.credentials.get(inputObj).then(function(newCredInfo){credValue=newCredInfo}).catch(function(err){console.log("PasskeyError: " + err)})
}, 3000)
Did you see your passkey pop up? So, how did "rpId": "google.com"
change the process?
Google creates new passkeys for the domain google.com
so that it can be used across all the Google websites. Since the passkey has been registered for the domain, during authentication, rpId
has to be specified with the same value to ensure that browser would use the passkey for authentication.
We will cover this concept and many other ideas in part II of the series for developers. We will explore how you can learn from some of the largest implementations of passkey out there.
Conclusion
Passkeys are a hot topic in the consumer identity space. As an authentication developer, it is important to understand their capabilities and limitations across standards and implementations to integrate existing products or implement your own solution.