< Back
calendar

May 24, 2020

It’s Time To Kill The Password

How to get ready for a passwordless future with Web Authentication

Header for posting It’s Time To Kill The Password
Photo by Icons8 Team on Unsplash

Imagine you had some very valuable jewellery stashed away in a safe somewhere.

The only way to open this safe is to give the code to another person who will then open it for you. You write the code on a piece of paper, put it in an envelope, close it and then give it to this person.

This person also has to walk all the way down the street to get to your safe. On the way there, the code could get lost or even be stolen.

Also, you don’t even know this the person and if he or she can even be trusted.

Would you agree to this kind of security? Doesn’t seem very secure, does it?

The problem here is that the code to open your safe is now a shared secret, and that’s also the problem with passwords.

When you need to login on a website to access your email or bank account, you send your password to the server to prove that you are the person you claim you are.

It’s the only thing that stands between you and your bank account, email or other sensitive data. It’s also the only think that stands between a hacker and your sensitive data.

So if a hacker manages to steal your password or guess it because it’s weak, they can fully access your data. About 80% of all hacking-related breaches involve stolen or weak passwords.

Passwords Are Doors To Your Data

You can put heavy locks on most doors to your business, but if just one is still wide open those locks won’t help you.

One weak password is enough to compromise all your security measures.

Password managers help to make your data more secure, but can still not prevent passwords being stolen through phishing attacks. This risk can be minimised by using two-factor authentication (2FA) and luckily, the adoption of 2FA has risen from only 28% in 2017 to 53% in 2019.

But even 2FA is not immune to sophisticated phishing attacks. It’s also inconvenient for people and convenience is the main reason people still use appallingly weak passwords.

How To Stop Sharing Your Secrets

If your password is a secret then the best way to protect it is obviously to stop sharing it. But since you need to share it with a web app to login, that’s not an option.

Web Authentication offers a solution for this.

It eliminates the need for a password by using asymmetric encryption, which involves a keypair consisting of a public and a private key. A public key is used to encrypt something that only the corresponding private key can decrypt.

Imagine you want to send me a secret message. I can give you a box with a key to put this secret message in. You can put the message in the box and then lock it, but you can’t unlock it.

The box for the message is the public key and the key that can unlock the box is the private key.

I can freely distribute copies of my public key (the box that can only be locked) to anyone that wants to send me secret messages, since it’s effectively useless without the corresponding private key. So my private key remains secret and should never be shared.

This is the foundation of the Web Authentication (WebAuthn).

How WebAuthn Works

WebAuthn enables you to login on a website using hardware devices called authenticators, like Apple’s TouchID, Windows Hello or a USB Security Key.

These authenticators typically generate and store a new keypair (called a credential) consisting of a public and a private key. This credential can then be used to register with a website.

After registration, this credential can be retrieved from the authenticator to login the user on this website.

Instead of the client sending a password to the server, the server sends a very large random string to the client, called a challenge. The client then signs this challenge with the private key, which means it produces a hash of this challenge.

The client then send this hash back to the server, along with the corresponding public key.

The server can then verify this hash with the public key, which proves the client is in possession of the corresponding private key and authentication is successful.

No secrets were shared, only a public key which is useless without the corresponding private key. This means that databases storing these public keys are not attractive to hackers anymore and phishing is useless as well.

The private key is safely stored in the hardware authenticator. On top of that, all credentials used in WebAuthn are scoped, which means that a credential created for a certain website (origin) can only be used for that origin.

Creating a credential for registration and then using it for authentication are fairly simple steps that require two calls on the client side:

navigator.credentials.create() to create a credential for registration, navigator.credentials.get() to retrieve a credential for authentication

Let’s have a detailed look at the steps involved in registering a new credential and then using it to authenticate.

Registering a credential

The first step we need to take is to create a new credential (keypair) and register it with the website we want to authenticate with.

To create a new credential:

const credential = await navigator.credentials.create({
publicKey: {
challenge: {
type: "Buffer",
data: [53, 69, 96, 194, ...]
},
rp: {
name: "What PWA Can Do Today",
id: "whatpwacando.today"
},
user: {
id: {
type: "Buffer",
data: [134, 196, 104, ...]
},
name: "webauthn@whatpwacando.today",
displayName: "WebAuthn"
},
pubKeyCredParams: [{alg: -7, type: "public-key"}],
authenticatorSelectionCriteria: {
attachment: "platform",
userVerification: "required"
},
timeout: 60000
}
});

The call to navigator.credentials.create() receives an object with the single key publicKey which contains the configuration to create a new credential.

Let’s break down the properties:

challenge is the random string that was received from the server and that must be signed with the public key to prove that the user is in possession of the corresponding private key.

rp stands for relying party, this is the entity responsible for registering and authenticating the user, i.e. the website we want to register with. id must be a subset of the domain of the website.

user contains information about the currently registering user. It must at least contain the property id which is used to associate the user with the credential. It’s generated on the server.

pubKeyCredParams is an array of objects describing the features of the credential that will be created. Currently, the only possible value for type is “public-key” and the value -7 for alg indicates the elliptic curve algorithm ECDSA with SHA-256.

authenticatorSelectionCriteria sets the criteria for authenticators that are allowed for creation of the credential.
attachment: "platform" means we want an authenticator that is bound to the client and not removable.
userVerification indicates whether user interaction is required to verify the user. Since we want to authenticate using a fingerprint the value should be “true”.

timeout contains the time in milliseconds the script will wait for the registration process to complete. Note that this is a hint that may be overridden by the browser.

When the registration process is complete, a PublicKeyCredential object is returned:

PublicKeyCredential {
id: "Aa-aGSY1jGxNZlF9...",
rawId: ArrayBuffer(103),
response: AuthenticatorAttestationResponse {
attestationObject: ArrayBuffer(265),
clientDataJSON: ArrayBuffer(266)
},
type: "public-key"
}

id is the ID for the newly created credential and is used to identify it when the user tries to authenticate.

rawId is id in binary form.

response is of type AuthenticatorAttestationResponse when we register a new credential.
When we authenticate it is of type AuthenticatorAssertionResponse.

clientDataJSON in response is the data that was passed from the browser to the authenticator and is used to associate the credential with the server and the browser.

attestationObject contains the newly generated public key, an optional attestation certificate and other metadata used for validation on the server.

type may currently only hold the value “public-key”.

We can now send the credential to the server for registration in the following form:

const data = {
rawId,
response: {
attestationObject,
clientDataJSON,
id: credential.id,
type: credential.type
}
};

Authenticating with a credential

Now that we have registered our credential, let’s try to authenticate!

Just like for registration, we only need one simple call to authenticate:

const credential = await navigator.credentials.get({
publicKey: {
challenge: {
type: "Buffer",
data: [236, 146, 80, ...]
},
allowCredentials: [
{
id: credential.rawId,
type: "public-key",
transports: ["internal"]
}
],
rpId: "whatpwacando.today",
userVerification: "required"
}
});

We now call navigator.credentials.get() to retrieve the credential that matches all criteria we pass it in the options object. This object again has a single key publicKey that holds the actual criteria.

challenge is again a random string from the server that must be signed with the public key to prove that the user is in possession of the corresponding private key.

allowCredentials is an array containing credential descriptors that restrict the allowed credentials for authentication.
id is the rawId property of the credential we created earlier.
Currently, the only possible value for type is “public-key”.
transports is an array specifying the possible transports between the client and the authenticator. internal means the authenticator is bound to the client device and not removable, like a fingerprint reader.

According to the spec allowCredentials is optional, but I noticed that when it’s omitted, Chrome falls back to registering a new credential instead of retrieving an existing one, resulting in an error.

Also note that this requires us to pass in the rawId of the credential which means we need to store it somewhere. In the demo I’ll provide later on this rawId is stored client-side in localStorage. In a real scenario, the user would provide a username which is then used to retrieve the rawId from a database on the server.

rpId is the identifier for the relying party. When not provided it defaults to the domain of the website.

userVerification indicates whether user interaction is required to verify the user. Since we want to authenticate using a fingerprint the value should be “true”.

When a credential is found that matches all criteria it will be returned as a PublicKeyCredential:

PublicKeyCredential {
id: "AdvZkPu73G7...",
rawId: ArrayBuffer(103),
response: AuthenticatorAssertionResponse {
authenticatorData: ArrayBuffer(37),
clientDataJSON: ArrayBuffer(372),
signature: ArrayBuffer(72),
userHandle: ArrayBuffer(32)
},
type: "public-key"
}

id is the ID for the retrieved credential .

rawId is id in binary form.

response is now of type AuthenticatorAssertionResponse.

authenticatorData in response is similar to attestationObject in the registration step but it doesn’t contain the public key. It’s used for verification during authentication.

clientDataJSON in response is the data that was passed from the browser to the authenticator and is used to associate the credential with the server and the browser.

signature is the (surprise) signature generated by the private key of the retrieved credential and this will be verified using the private key to check that it’s valid.

userHandle is the value of user.id that was supplied in the registration step and will be used to associate the user to the credential.

type may currently only hold the value “public-key”.

We now send the credential in the following for to the server for authentication:

const data = {
rawId: credential.rawId,
response: {
authenticatorData,
signature,
userHandle,
clientDataJSON,
id: credential.id,
type: credential.type
}
};

Server-side code

To validate the credential on the server-side, I’m using the fido2-lib library which makes this process fairly simple.

But since this library contains a bug that prevented me from using it in the demo, I forked the repo and fixed the bug. This fork is used in the demo and can be found here.

If you’re interested in the exact steps to parse and validate the authentication data, you can read the spec or read the excellent description on webauthn.guide.

The server-side code contains four routes needed to perform registration and authentication. The routes will be called subsequently in the order listed below.

/registration-options returns the configuration object which is passed to navigator.credentials.create(). This contains the challenge from the server, the user id, information about the relying party and other criteria for the credential to be created.

/register receives the credential that was created and validates it. If it’s validated the public key will be stored. Note that the demo stores this in the user session. In a real scenario a username would be stored in a database to retrieve the id of the credential and send that back in the next route.

/authentication-options returns the configuration object which is passed to navigator.credentials.get(). It again contains the challenge from the server. In the demo allowCredentials is added on the client-side and the needed rawId of the credential is retrieved from localStorage.
In a real scenario, the user would provide a username which is then used to retrieve the rawId of the credential from a database. In that case allowCredential can be filled on the server-side as well.

/authenticate receives the credential that was retrieved from the client and validates it. If it’s validated the authentication is successful.

You can check out the demo and source code on Github and a working demo is also part of What PWA Can Do Today.

Conclusion

Web Authentication enables web apps to provide a very reliable and safe authentication mechanism. It eliminates the use for a password which dramatically improves security since secrets are no longer shared and phishing attacks are rendered useless.

It relieves users from having to memorise passwords and web apps from having to store and manage passwords. Users can conveniently login using their fingerprint or USB key, making weak and reused passwords a thing of the past.


Join Modern Web Weekly, my weekly update on the modern web platform, web components, and Progressive Web Apps delivered straight to your inbox.