Introduction to WebAuthn API
…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 is FIDO2 and Webauthn, 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…
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. FIDO Alliance was created in 2013, and now it has over 250 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 origin based, challenge-response, phishing proof, digital signature authentication protocols. Here is a diagram that explain the process:
The relying party(the server) send 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. 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 RP information contains expected origin, and challenge, after which it checks the signature. If any of those steps are failed, we can detect that there was a phishing attack, thus preventing it.
If you want to know more about depths of FIDO protocols, you should check 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:
- FIDO —Fast IDentity Online, or FIDO Alliance. As I explained earlier it’s a consortium that develops secure, open, phishing proof, passwordless authentication standards. FIDO Protocol Family is a set of protocol that was developed by 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 project name for a new, modern, simple, secure, phishing proof, passwordless authentication protocol. Its core specifications are WebAuthn(the client API) and CTAP(the authenticator API). There are other support specifications that 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.
- CTAP1 — A formal name of U2F protocol.
- CTAP2 — A name for second version of the CTAP protocol. The main characteristic is use of CBOR for encoding structures, backwards compatibility with CTAP1(U2F), extensions and new attestation formats. Both CTAP1 and CTAP2 share same transport layer, so the version difference is mainly the structural.
- WebAuthn — 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 projects. 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 PasswordCredential object passed:
Upon the call you will see prompt to confirm you decision to store credential:
And that, if we want to retrieve it, we call navigator.credentials.get with “password” key set to true:
If there is only one credential, it will be returned by default, notifying user about it:
When there are more than one credential available, user will be prompted to select one of the available ones.
So basic methods are “store” and “get”. There is as well “create”, but we will talk about it in then next section.
I highly recommend to read Eiji Kitamura’s article “Sign-in on the Web — Credential Management API and Best Practices” if you’d like to get more depth.
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
Since credentials can not be stored, as they are created on the authenticator, we call “create”, to create credential
Let’s talk about this example:
- challenge — A random challenge that is generated by the server. It 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 authenticator to display. rp.id contains replying party identifier(the use of it we will discuss in Scenarios section)
- user — Information about user. id, name and displayName fields are mandatory. icon field is optional.
- user.id — Server generated user identified. Must NOT contain any user information. Should be randomly generated. Its main role is to relate credential to the user account in RP data base.
- 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 that server is supporting. Right now FIDO2 servers are mandated to support RS1, RS256 and ES256.
This is the basis for creating 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 —Force Resident Credential. The resident credential is a credential that can be accessed simply with RPID. So if generally when you call GetAssertion, you will have to provide “allowList” that will contain credential identifier. With the RK you don’t need it since the authentication will locate all RK(only RK credentials), and for each of them generate the assertion over the challenge and return all of them to the client. The client then will display all of the credentials to the user and user will pick one, thus returning selected credential to the relying party.
- 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 UV is available it will fail. “preferred” will try enforce it, 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. “indirect” if you want the client to anonymise your attestation, using for example Attestation CA. Default is “none”.
- extensions — Contains object with specified extensions.
After we have created “publicKey” structure, we need to wrap it in a dictionary, as a value of “publicKey” key. After that we can call navigator.credentials.create.
You can test this code your self here: https://herrjemand.github.io/FIDO2WebAuthnSeries/WebAuthnIntro/makeCredExample.html
When you call WebAuthn API you will see browser pops up with a choice of the authr(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(at least on chrome). After you picked up preferred option, tap your authenticator, or verify you’r self using your fingerprint, after which you should see “Success” pops up.
When you open your console, you will see returned attestation. The important fields to mention are:
attestationObject —Is the CBOR encoded attestation structure.
clientDataJSON — Is the browser generated structure that contains origin, challenge, type(of a call) and tokenBinding(if available). This is a very important structure as verification of clientDataJSON is a first step to ensure that there was not phishing attempt. The hash of clientDataJSON(or clientDataHash) is appended to authData and signed by the authenticator, so it is cryptographically protected.
Attestation is a way for the authenticator to prove it self to the server. In the situations where RP may need additional assurance, for legal and security reasons, such as requirement to only use FIPS/CC certified authenticators, or simply wanting to ensure that only FIDO certified devices are allowed to register, RP will need to fully validate attestation, against the metadata statement, and the metadata service.
It is certainly useless for most of the relying parties, as their goal is simply provide phishing proof authentication, and for that reason in current example “fmt” is set to “none”.
“none” is the default attestation mode in WebAuthn. When authenticator has returned the response, it is stripped of attestation information. So attStmt object is empty, and aaguid is set to 00000000–0000–0000–0000–000000000000. This is done as additional measure to preserve user privacy.
If you explicitly need the attestation, you will need to add “attestation” key with value “direct” into the call. This is will allow you to get full attestation, though it will require an explicit consent from the user.
Attestation response contains credential info, such as user public key, credential identifier, counter, as well as authenticator info, such as certificates, signature, aaguid and other information.
The attestation is sent to the server to be processed and verified. After all checks are done, server will extract credId and public key and save it in the database together with the other user information. To understand how server verifies attestations, you can read my article “WebAuthn/FIDO2: Verifying responses”.
When user has added the authenticator to his profile, he will be able to authenticate with FIDO2. To do that we need to get assertion of our challenge, and that is done by calling “get”.
challenge — Same thing as challenge in MakeCredential.
allowCredentials — A list of credIDs, for which you require one successful assertion. The list is sent to every authenticator available, and the authenticator will check if it knows any of them. If it does, it will return assertion for this credID, thus completing the call.
In the current example we have allowCredentials, but it is actually not mandatory. The only mandatory field is challenge. We will talk about the reason for in Scenarios section.
As soon as assertion is returned to browser, clientDataJSON and return entire structure back to user:
GetAssertion response 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 used when creating the credential. In our case it is empty, because U2F does not support userInfo struct.
This structure then returned to the server to be verified. How this done in details can be found in my article: “WebAuthn/FIDO2: Verifying responses”.
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 sample.
First we do username/password registration. We then get back status “startFIDOEnrollment” and so we call “getMakeCredentialChallenge” and obtain necessary challenge.
The fields are base64url encoded, because we can’t send buffers with the JSON, so we have to decode those field in base64url, and we do that by pre-processing request using preformatMakeCredReq.
We then call WebAuthnAPI and get assertion back, which we encode into JSON using publicKeyCredToJSON and send it to server, where server verifies the response.
The login mostly the same as registration. We login with previously registered username and password.
The client receives “startFIDOAuthentication” and so calls getGetAssertChallenge. When challenge received, it is decoded using preformatGetAssertReq 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 you self by following this link: https://herrjemand.github.io/FIDO2WebAuthnSeries/WebAuthnIntro/BasicExample.html
“I am a bank, and I need to attest users devices”
This scenario is exactly the same as previous, but this time we add “attestation” key with value “direct”. This will forces client to return full attestation:
You can play with it here: https://herrjemand.github.io/FIDO2WebAuthnSeries/WebAuthnIntro/BasicDirectExample.html
“I wan’t to kill passwords!”
Now. Some of you might not agree with “passwordless”, and the reason for this is because term “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 —you prove your self to the RP.
- User Verification —user proves it self to the device.
In “password authentication”, the way user proof it self to the device, is the same way as the device proves it self to the RP. So authentication and user verification are the same.
But in “passwordless authentication” those steps are different. 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 way it is a “passwordless authentication”, as no password is sent over the internet. Important fact is that the password in the secure storage on the device, and not hashed(or sometimes plaintext) in the server database that can be leaked. So the viability of bruteforce is minuscule, and users can use weaker passwords 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 still can be used during the verification process, as well as biometrics.
Back to implementing. To use passwordless authentication you simply need to enforce User Verification, or UV(I will be using this term onwards).
The UV is crucial to this scenario. If RP does not enforce UV, it will have no idea if the user claim is authentic, therefor the authentication is no longer multifactor, as only factor left is “proof of possession”. That means for the attacker to get access to to the account would require only username and security key.
So to avoid this attack scenario, when user is going to create credential, we will add “userVerification” set to “required” to the makeCredential call.
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 authenticator to perform user verification using one of the available methods(biometrics, clientPin, etc). If authenticator does not support user verification, the command will with an error.
When RP will receive the response, it will decode authenticatorData, and check that UV flag is set to “true”.
Here we can see that only username is sent.
And the server returned GetAssertion with “userVerification” set to “required”
GetAssertion response it self did not change, so we utilise the same endpoints.
Now, are you curious to see what would the process of passwordless authentication look like?
And this is how you kill passwords. Now, who’s next?
“Usernames must dies as well”
You would think that at least username is required to perform authentication process, but in reality it is not. As I mentioned before, Resident Credentials or RK(I will be using this term onwards), is a special type of the credential that can be acquired without prior knowledge of the credential identified. So basically: call with challenge, let user select credential and wait for the response.
How do we tie the response to the specific user? Well If you remember when we create credential we must pass userId. When we get the assertion back, it returned back as userHandle.
So since response already contain user identifier, the need for username drops, and we can locate user just by his user.id. And since there is no need for username, we can drop first operation operation entirely, which leaves us only with one request-response.
No significant change required for MakeCredential part. It is the same as for passwordless example.
Here we can see that to create credential we call makeCredentials with “requireResidentKey” set to true. This will force client to only use authenticators that support RP. If not authenticator found, the call will fail.
MakeCredential response is the same as in passwordless, so we send it to same endpoint.
Here we can notice lack of username. We simply get the getAssertion challenge. As you can see it does not contain anything except for the challenge field, since we don’t need “allowCredentials”.
And the response is still exactly the same, so we don’t need new endpoint. The RP will detect that no username was used during the session, and find user by useHandle.
Now, be aware that RK is only available to for FIDO2, so it won’t work with U2F. Additionally at the time of writing(January 2019) Chrome, Firefox and Safari do not support RK. I speculate that it won’t take too long before full support will be added, though if you are curious enough, have a Windows machine with an Edge you can play with this RK demo: https://herrjemand.github.io/FIDO2WebAuthnSeries/WebAuthnIntro/UsernamelessExample.html
And with username removed, we are down to One, Single, Login button:
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.
20/01/2019 — Updated passwordless section per advice from @Serianox_
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.