Introduction to WebAuthn API and Passkey

Ackermann Yuriy
WebAuthn Works
Published in
19 min readJan 15, 2019

--

…or Level 1 Credential Management API extension for Public Key Credentials, and the untold stories of managing credentials in the browser…

What should I expect from this article?

Learn what FIDO2, Passkey, and WebAuthn are, and how to use them to kill passwords.

What is not going to be here?

Assertion and attestation verification. This is done by the server and so described in my series of articles: “WebAuthn/FIDO2: Verifying responses”.

Table of contents

A long time ago in a galaxy far, far away

Phishing!

Was killed by WebAuthn!… Or FIDO2… Hm… What do these terms even mean?

Well, if you are one of those confused readers, then welcome to my humble article. Sit down. Get a cuppa tea, and let’s dig into the world of phishing proof passwordless authentication.

What is FIDO?

FIDO, or FIDO Alliance is a consortium that develops secure, open, standard, phishing proof authentication protocols. The FIDO Alliance was created in 2013, and now it has over three hundred members across the globe. FIDO has three protocols: UAF, U2F, and FIDO2. They are the same family of the protocols, since they are all origin based, challenge-response, phishing proof, digital signature authentication protocols.

How does FIDO works?

As I mentioned before, FIDO protocols are origin based, challenge-response, phishing proof, digital signature authentication protocols. Here is a diagram that explain the process:

The relying party(the server) sends a challenge and credential identifier of the previously registered credential to the client(the browser). The client then attaches relying party information, such as origin of the call, and sends it to the authenticator.

Authenticator first either checks if user is present, by requesting user to press the button, or doing full user verification using on-device pin, biometrics, etc.

When verification is done, the device then signs the payload, using the private key, identified by the credential id, and returns the assertion to the client. The client then attaches information that it has given to the authenticator, as part of the signature payload, and forwards it to the relying party.

The relying party checks that information, and ensures that the RP information contains expected origin, and challenge, after which it checks the signature. If any of those steps are fail, we can detect that there was a phishing attack, thus preventing it.

If you want to know more about the depths of FIDO protocols, you should check out my 2016 KiwiPyCon talk slides “U2F authentication or why 2FA today is wubalubadubdub”

FIDO2 or WebAuthn?

So there is general confusion about the terms. FIDO, FIDO2, WebAuthn, CTAP1, CTAP2… What does this all even mean?

So let’s break it all down:

  • FIDOFast IDentity Online, or FIDO Alliance. As I explained earlier, it’s a consortium that develops secure, open, phishing proof, passwordless authentication standards. The FIDO protocols family is a set of protocol that was developed by the FIDO Alliance. UAF — Universal Authentication Framework. U2F — Universal Second Factor, and FIDO2. When I say use “FIDO” I generally mean “Use any of the three protocols”, as they are all conceptually the same protocol, with the difference being structural (UAF — TLV, U2F — RAW, FIDO2 — CBOR).
  • FIDO2 — A protocol name for a new, modern, simple, secure, phishing proof, passwordless authentication protocol. It contains core specifications WebAuthn(the client API) and CTAP(the authenticator API). There are other “support” specifications which we will ignore in this article.
  • CTAP — Client to Authenticator Protocols — A set of low level protocols to communicate with the authenticators over the BLE/NFC/USB. CTAP family includes CTAP1 and CTAP2 protocols.
  • U2F — A second factor only protocol. Formally referred as CTAP1.
  • CTAP2 — A name for the second version of the CTAP protocol. The main characteristic are use of CBOR for encoding structures, backwards compatibility with U2F(CTAP1), extensions and new attestation formats. Both CTAP1 and CTAP2 share the same transport layer, so the version difference is mainly structural.
  • Platform — Basically operating system and it’s underlying FIDO client APIs, be it system APIs or Browser. Basically Android, iOS, Windows, MacOS, Linux, etc.
  • Authenticator — A thing that authenticates user using FIDO protocol. FIDO2 has two types of the authenticators: security keys, and platform authenticators.
  • Security Key — A physical device that connects via USB, NFC, or BLE. Example: Yubikey, Trustkey, Feitian Biopass, etc.
  • Platform authenticator — A builtin into the platform(OS) authenticator. As of now available on almost all iOS, Android, MacOS and Windows devices.
  • WebAuthn API— A browser JS API that describes an interface for creating and managing public key credentials.

TL;DR: WebAuthn — is the JS API. FIDO2 is the name for the whole protocol. The right way to say about you authentication, is “Authentication with FIDO2" not “Authentication with WebAuthn”, but that’s just semantics.

Credentials Management API

WebAuthn is actually an extension to Credentials Management API, so before we go straight into passwordless authentication, we need to understand this API first.

In the nutshell CredManAPI is a JS “autofill”. What generally was done with UI prediction utilities, such as “save your password”, can now explicitly be done with the JS. For example, lets add new credential using navigator.credentials.store with the PasswordCredential object:

Note: Make sure that you have enabled “Offer to save passwords” in Chrome

Upon the call you will see prompt to confirm your decision to store new credential(username + password):

After that, if we want to retrieve it, we can call navigator.credentials.get with “password” key set to true:

If there is only one credential, it will be returned by default, notifying the user about it:

When there is more than one credential available, the user will be prompted to select one of the available ones.

So the basic methods are “store” and “get”. There is also “create”, but we will talk about it in the next section.

If you would like to learn more about Credential Management API I highly recommend reading Eiji Kitamura’s article “Sign-in on the Web — Credential Management API and Best Practices”

‏‏‎‏‏‎

Web Authentication API

WebAuthn is an API for managing public key credentials. In the nutshell, it is an interface to talk to FIDO authenticators. You give it a challenge. You get assertion back.

There are two operations you need to know about: MakeCredential and GetAssertion

Make Credential

Since credentials cannot be stored, as they are created on the authenticator, we call “navigator.credentials.create”, to create new credential… (so much tautology)

Let’s talk about this example:

  • challenge — A random challenge that is generated by the server. It is used to mitigate MITM attack. Type of BufferSource
  • rp —Information about relying party. rp.name is the only mandatory field. rp.name contains relying party friendly name. rp.icon contains a link to the RP icon that you want the authenticator to display. rp.id contains the relying party identifier(the use of it we will discuss in Scenarios section)
  • user — Information about user. id, name and displayName fields are mandatory.
  • user.id Server generated user identified. Must NOT contain any user information. Should be randomly generated. It is used by the relying party as
  • user.name — Username. Can contain email, username, phone number, what ever RP deems to be primary user identifier.
  • user.displayName — Actual user’s full name, like “John Bollocks” for example.
  • pubKeyCredParams —A list of signing algorithms the server supports. Right now, FIDO2 servers are mandated to support RS1, RS256, ES256 and ED25519.

This is the basis for creating a credential. There are other optional keys such as:

  • timeout — You guessed it right.
  • excludeCredentials — Contains a list of credentials that were already registered to the user. This list is then given to the authenticator, and if the authenticator recognises any of them, it cancels operation with error CREDENTIAL_EXISTS, thus preventing double registration of the same authenticator.
  • authenticatorSelection—Specifies authenticator selection preferences.
  • authenticatorSelection.authenticatorAttachment— Enforce type of the authenticator you require. “platform” if you want only built-in authenticators. “cross-platform” if you want only external, roaming, security keys.
  • authenticatorSelection.requireResidentKey / authenticatorSelection.residentKeyOptions of enforcing creation of discoverable credentials. See “Discoverable Credentials” scenario.
  • authenticatorSelection.userVerification — Specifies user verification requirement. If “required” the browser will try to enforce user verification with ClientPin, biometrics or anything else available. If no user verification option is available, it will fail. “preferred” will try to enforce user verification, but if none-available it will default to TUP(Test of User Presence. i.e. Touch of a button). If it is set to “discouraged” it will only enforce TUP.
  • attestation — Attestation response option . “direct” if you need full attestation. “none” if you don’t care about attestation. Default is “none”. There is as well “indirect” if you want the client to anonymise your attestation, using, for example, Attestation CA, but as far as I am aware this was not implemented by anyone yet.
  • extensions — Contains a map with specified extensions.

The “publicKey” structure need to be wrapped it in a dictionary, as a value for “publicKey” key. After that we can call navigator.credentials.create.

You can test this code your self here: https://webauthnworks.github.io/FIDO2WebAuthnSeries/WebAuthnIntro/makeCredExample.html

When you call WebAuthn API you will see browser pops up with a choice of the authenticator(at least on Chrome):

If you have a security key, then it will show it as one of the options, or otherwise it will use browser/platform built-in authenticator. After you select your option, tap your authenticator, or verify yourself using your fingerprint, after which you should see “Success” pops up.

When you open your console, PublicKeyCredential object is returned by the WebAuthn API.

id/rawId — The credential identifier. This is what relying party is using to identify the credential on the device by supplying it in the allowList, and exclude the authenticator from accidental re-registration using the disallowList. rawId is a byte array. Where id is a base64url encoded rawId.

type — A standard WebAuthn API type specified to signify to the RP that “public-key” credential is used. Currently it is always “public-key” type, but in future there might be other credential types.

type/id/rawId — Are all the same for both credential creation, and getting assertion.

response — Authenticator result. Different structures for credential creation and, and getting assertion.

response.attestationObject —Is the CBOR encoded attestation structure.

response.clientDataJSON —Browser session information. It contains base64url encoded challenge. Origin of the caller(e.g . “https://webauthnworks.github.io”). Type of the call, create or get, and information if it was a cross origin call.

Relying party checking that challenge is set to an expected value, and thus preventing MITM attack. The origin field shall be set to the expected origin, and a miss-match would mean a phishing attack. The ClientDataJSON is cryptographically protected, as the hash, clientDataHash is appended to the authData before signing.

This structure may change in future, which is why the relying party must properly decode the structure and validate the fields, and not use any form of template-based validation.

AttestationObject

The result attestation is a CBOR map containing three fields: fmt — attestation format, authData — freshly generated credential, and attStmt — the device attestation

It this example the attestation format is “none”, because we did not request attestation. In this situation the platform will set attStmt to empty map, fmt to none, and device aaguid in the authData will be 00000000–0000–0000–0000–000000000000. This is done to preserve user privacy as attestation has inherit privacy issues. See Demystifying attestation and MDS

AuthData is a byte array containing a concatenated hash of the caller host(SHA256 of webauthnworks.github.io), counter, flags that inform relying party if user verification was performed, device guid, new credential id(same as result.id/rawId), and newly generated public key. If you want to learn more about verifying result check out our article “WebAuthn/FIDO2: Verifying responses”

Short info into attestation

Attestation is a FIDO protocols mechanism that allows relying party to identify and prove device model. This is a useful mechanism for some of the relying parties, such as banks, and governments. Though majority of the relying parties do not need attestation and shall use none attestation, by either setting attestation field to none, or leaving it undefined.

If you wish to learn more in depth information on attestation, you can read my article: Demystifying attestation and MDS

If you explicitly need attestation, you can get it by setting attestation field in the call to direct. This will prompt user consent (Popup).

Try it here https://webauthnworks.github.io/FIDO2WebAuthnSeries/WebAuthnIntro/makeCredExample.attestationDirect

Get Assertion

After user adds the authenticator to his profile, they will be able to authenticate to their account with FIDO2. This is done using navigator.credentials.get.

challenge — Same as challenge in navigator.credentials.create.

allowCredentials — A list of credential identifiers registered with the user account. This list will be sent to all available devices. If the authenticator is able to identify a credential in the list, it will start the assertion generation process by prompting user action, such as a button press, or fingerprint. If authenticator is not able to identify a credential, it will return an error, thus notifying the platform that it is not the right device. The first successful device result will complete get assertion request.

The allowList is not mandatory for discoverable credentials, but we will talk about them little later.

As soon as the assertion is returned to browser, ClientDataJSON and return entire structure back to user:

Play with it here: https://webauthnworks.github.io/FIDO2WebAuthnSeries/WebAuthnIntro/getAssertionExample.html

GetAssertion result contains some fields that are different from MakeCredential:

authenticatorData —Equivalent of authData in attestationObject.

signature — Signature over the concatenation of authenticatorData and clientDataHash using the user private key and verified by user public key.

userHandle user.id that is set when creating the credential. For U2F credential it will be empty as U2F does not support user handle. This is useful for relying parties that wish tie credential to their own index registry.

This structure is then returned to the server to be verified. How this done in details can be found in my article: “WebAuthn/FIDO2: Verifying responses”.

Scenarios

In this section we will discuss various authentication scenarios that can be accomplished with WebAuthn.

“I am a blog about the cats. I just want secure 2FA”

This is the simplest scenario available. As soon as user logs in using his username and password, you start 2FA flow.

As you can see here, this is a bit more abstract than previous examples, showing more real life example.

First we perform user registration. Upon success we call “getMakeCredentialChallenge” to obtain make credential challenge.

The fields are base64url encoded, because we cannot send buffers using JSON, so we have to decode base64url encoded fields, by pre-processing request using preformatMakeCredReq.

We then can call WebAuthn API with the formatted request, and the result attestation is then encoded to JSON using publicKeyCredToJSON and sent to server, where server verifies it.

The login mostly the same as registration. User logs in with the previously registered username and password.

The client obtains a login challenge. The challenge is received, it is decoded using preformatGetAssertReq and passed to WebAuthn API.

The response then encoded and forwarded to the server. If server successfully verifies it, it will return status: ok.

ㅤ‏‏‎

‏‏ㅤ‎

You can test it yourself by following this link: https://webauthnworks.github.io/FIDO2WebAuthnSeries/WebAuthnIntro/BasicExample.html

‏‏‏‏ㅤ‎

‏‏ㅤ‎‎

“I am a bank, and I need to attest user authenticators”

This scenario is exactly the same as previous, but this time we add “attestation” key set to “direct”. This will forces client to return full attestation:

You can play with it here: https://webauthnworks.github.io/FIDO2WebAuthnSeries/WebAuthnIntro/BasicDirectExample.html

“I wan’t to kill passwords!”

Some of you might not agree with the term “passwordless”, and the reason for this is because “passwordless” is so overused by a lot of companies around, and genuine understanding that passwords have advantages over biometrics in some situations, causes normal reflexive “This is bs!”. So before we start talking about “passwordless authentication”, let’s talk about what does it mean?

Three terms you need to learn:

  • Passwordless — means no password is used.
  • Authentication—proving yourself to third party
  • User Verification —process of unlocking your authenticator

In “password authentication” there is no authenticator, so authentication and verification terms are synonyms since relying party is performing both, verify your password, and thus authenticating you.

But in “passwordless authentication” these are different terms. RP only sees the assertion that is generated by the authenticator, and the user verification is done by the authenticator through the biometrics, pin or any other user verification methods.

This is why FIDO2 can perform passwordless authentication, as no password is sent over the internet. Important fact that in cases where client-pin is used, it is stored on the device. So the viability of a bruteforce is minuscule, and users can use weak pin without worrying that credentials will be compromised. Lastly, the ability to utilise biometrics makes the whole process even more user friendly.

TL;DR — It is called “passwordless authentication” because no password is sent over the internet, and so there is no password to compromise. Passwords/Pin still can be used during the verification process, as well as biometrics.

Back to implementation. To use passwordless authentication you simply need to enforce user verification.

User verification is crucial to this scenario. If the relying party does not enforce user verification, it will have no idea if the user claim is authentic, therefor, the authentication is no longer multifactor, as the only factor left is users device proof. This means that for the attacker to get access to the account it would require only a username and security key.

So to avoid these attack scenarios, when a user is going to create a credential, we will add “userVerification” set to “required” to the makeCredential call.

The first thing you notice in the workflow is lacking password field, hence the passwordless authentication.

‏‏‎

‏‏‎

‏‏‎

‏‏‎

‏‏‏‏‏‏‏‏‎‏‏‎

‏‏‏‏‎Next difference you can see is “userVerification” set to “required”. This will force the authenticator to perform user verification using one of the available methods(biometrics, clientPin, etc). If the authenticator does not support user verification, the command will fail with an error.

When the relying party receives the response, it will decode authData, and check that UV flag is set to “true”.

‏‏‎

‏‏‎‏‏‎

‏‏‎‏‏‎

‏‏‎‏‏‎‏‏‎

Here we can see that only username is sent.

‏‏‎‏‏‎‏‏‎‏‏‎‏‏‎

‏‏‎‏‏‎‏‏‎‏‏‎‏‏‎

‏‏‎‏‏‎‏‏‎‏‏‎‏‏‎

And the server generated GetAssertion challenge with “userVerification” set to “required”.

‏‏‎‏‏‎‏‏‎‏‏‎‏‏‎‏‏‎‏‏‎‏‏‎‏‏‎

‏‏‎‏‏‎‏‏‎‏‏‎‏‏‎‏‏‎‏‏‎‏‏‎‏‏‎‏‏‎‏‏‎‏‏‎‏‏‎‏‏‎‏‏‎

‏‏‎‏‏‎‏‏‎‏‏‎

‏‏‎‏‏‎‏‏‎‏‏‎

‏‏‎‏‏‎‏‏‎‏‏‎

‎‎‎‎

‎‎

‎‎‏‏‎

Now, are you curious to see what would the process of passwordless authentication look like?

Try it here: https://webauthnworks.github.io/FIDO2WebAuthnSeries/WebAuthnIntro/PasswordlessExample.html

And this is how you kill passwords. Now, who’s next?

“Why can I just use users phone/laptop?” — Using platform authenticator

Today, each and every Android, iOS, MacOS and Windows device containing OS builtin authenticator that we call platform authenticator. That means that any website can start using FIDO without requiring purchasing security keys.

Platform authenticators are system wide, which means that all browsers will have access to the same keystore, and thus registering credential with Chrome would mean that it is still accessible in Firefox.

Beware that as of right now, Jan 2021, MacOS is still in active development, and so Chromium based browsers will have a separate credentials, and UI/UX experience, from Safari. This will change is future.

Platform support

Platform authenticator are available for these platforms:

  • Windows 10+ (starting from 2019 updates)
  • MacOS 10.15+
  • Android 8+
  • iOS 14+

Detection

To detect if platform supports FIDO authentication, you can use PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable():

isWebAuthnSupported checks if WebAuthn API is supported by checking if PublicKeyCredential is defined.

isWebAuthnSupported checks if WebAuthn API is available, and then if it is supporting isUserVerifyingPlatformAuthenticatorAvailable() it will call it.

This is a little more than you usually need. Most of the browsers support WebAuthn API. But there are some outliers like “Samsung Internet” and many smaller browsers that do not support it yet.

Using platform authenticator

When calling platform API, users will, by default, be asked if they want to register with the platform authenticator or security key.

If you want user to only see platform option, you must set authenticatorSelection.authenticatorAttachment to platform. (note: cross-platform is for security keys only)

From relying party perspective, there is no change in the attestation response, but you may wish to add some flag, note, in your database entry to signify that this is a platform credential, for things like analytics and user experience.

During the GetAssertion, nothing really changes. The relying party may want to specify that credential is platform by specifying in the credential descriptor, transports field value “internal”:

Line 7: “transports”: [“internal”]

And just like that you are able to use platform authenticators to enable FIDO for any user.

“Usernames must die as well” — Introducing discoverable credentials

You would think that at least username is required to perform authentication process, but in reality it is not. As I mentioned before, Discoverable Credentials, previously called Resident Credentials, or RK, is a special type of the credential that can be acquired without prior knowledge of the credential identified. So basically: call with challenge, let the user select a credential and wait for the response.

To tie the result credential relying party has two important identifiers: id and userHandle. The relying party can identify the credential using either a credential id, or userHandle, which is what relying party sets user.id when it registers a credential.

The relying part is then able to find the user, by searching the one that has an authenticator with the corresponding id or userHandle.

No significant change required for MakeCredential part. It is the same as for passwordless example.

‏‏‎‎‏‏‎

‎‏‏‎‏‏‎

‏‏‎‎‏‏‎‏‏‎‏‏‎

‎‏‏‎‎‏‏‎

‏‏‎‎‏‏‎

‏‏‎‎‏‏‎

To create discoverable credential, the relying party sets “requireResidentKey” sets to true. This will force platform to only use authenticators that support discoverable credentials. If no authenticators found, the call will fail.

‏‏‎‎‏‏‎

The result is the same as in usernameless scenario.

‏‏‎‎‏‏‎

‎‏‏‎‏‏‎‏‏‎‏‏‎

‎‏‏‎‏‏‎‏‏‎‏‏‎

‎‏‏‎‏‏‏‏‎‏‏‎

‎‏‏‎‏‏‎

During the authentication, the relying party must only send a challenge. This is because we are forcing platform to switch to discoverable credentials mode.

‏‏‎‎‏‏‎

‏‏‎‎‏‏‎

The relying party will find the user by either using id or userHandle values.

‏‏‎

‎‎‏‏‎

And with username removed, we are down to One, Single, Login button:

https://webauthnworks.github.io/FIDO2WebAuthnSeries/WebAuthnIntro/UsernamelessExample.html

Like in Apple’s sweetest dream, we came up with the best authentication experience people ever wanted. A press of button.

Welcome to the world of tomorrow.

Note on residentKey and requireResidentKey

The residentKey field is the replacement for the requireResidentKey. In future, the relying party, would specify the preference for the discoverable credentials. For example it may be “preferred” that user credential is resident, but the user device may not support it. That way the platform will do it best to create discoverable credentials, but will still allow non-discoverable normal credentials. Or relying party may have it “required” that credential is discoverable.

As of right now, relying parties can start to add this feature in their API calls when creating discoverable credentials, but the support for the residentKey field may lack for a while.

Current state of discoverable/resident credentials, and future

Be aware that as of writing (January 2021) the support for discoverable credentials is problematic. Currently on Windows supports discoverable credentials properly, and all other platforms are either do not support at all, or early support. There are few reasons for that:

  1. Privacy issues in CTAP2.0 — For CTAP2.0 the platform may call any RK credential without any user verification. This means that theoretically, you may be using your security key for some “naughty” website. Then an attacker, like airport security, can just plug your security key in their fancy bruteforcer, which will check if there is a credential for that naughty website. This is fixed in CTAP2.1. We will discuss it shortly.
  2. Lack of credential management in CTAP2.0 — For CTAP2.0, user may not manage their credentials. Security keys have very limited storage, 25–50 credentials, and so the storage can be filled rather quickly. The only way to remove credentials is to reset, wipe, the entire device. This is again, fixed in CTAP2.1

Future of discoverable credentials

CTAP2.1 is a recent update to the FIDO2 authenticators. It includes four, very important features that make discoverable credentials finally safe a usable:

  1. Change in user verification logic — if previously a platform was able to obtain result silently, in CTAP2.1, the authenticator will fail if it has biometrics or pin configured.
  2. CredProtect extension — New Credential Protection extension can be used to specify protection level for the credential. Level 1 means that credential can be used in silent mode. Level 2 means that only credentials specified by the credential identifier can be called in silent mode. Discoverable credentials will be user verification protected. Level 3 makes both discoverable and non-discoverable credentials user verification protected.
  3. Credential Management API —A device level API for credentials management. Finally users will be able to remove and modify their discoverable credentials. This means that if users device runs low, the platform will be able to notify user about it, and user will be able to remove old unused credentials.
  4. Authenticator Config API — AuthrConfig API allows platform to enforce some rules and configurations, one of which is AlwaysUV, which allows user to make device enforce user verification on each and every action.

These changes are still six to twelve months away (as of Jan 2022) from support in platforms, and security key with the support of CTAP2.1 are just arriving on the market, so it will be some time before we will everything coming into place.

References

Updates

20/01/2019 — Updated passwordless section per advice from @Serianox_

01/01/2021 — Updated language. Fixed typoes. Added platform authentication section. Updated discoverable credentials(rk) usernameless section.

License

This article is licensed under Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International (CC BY-NC-ND 4.0). So you are free to read, share, etc. If you are interested in commercial use of this article, or wish to translate it to a different language, please contact ackermann(dot)yuriy(at)gmail(dot)com.

The code samples are licensed under MIT license.

--

--