Let's face the reality, we can't live without passwords these days. In fact our entire online life is depending on them. But with passwords, comes heaps of problems not only for users, but also for us developers.
A bitter fact
Most people have a weak password (and probably the same) used for many websites they're member of. Social media, email, productivity tools, you name it, they all have different policies around passwords and majority of users find a way to bypass it using a weak password, or a strong one written on a post it π€·π½ββοΈ.
In fact 81% of all data breaches that happen due to hacking, leverage stolen or weak passwords.
There is a better way
But thanks to great efforts from W3C and FIDO, we're not bound to use passwords for everything. Web Authentication API, aka WebAuthn
is a fairly new specification which is introducing a new way of authentication without passwords, and also alongside them.
Google, Microsoft, Yubico and other big names in the industry are all part of the contributors to get this spec widespread. This API allows servers to register and authenticate users using public key cryptography instead of passwords.
Windows Hello, Apple's Touch ID, Yubikey devices, are all examples of a different type of authentication mechanism which doesn't require passwords. They use biometrics or hardware devices to be able to achieve this.
How it works
From 1000ft, instead of using a password, you will use a key pair (public and private) which is generated for a website.
The private key is stored securely on user's device, whereas the public key along with a randomly generated credential ID is sent to a web server to be stored and used later for authenticating that particular user.
Overall, WebAuthn relies on three major parties:
- Authenticator (the device which is doing the authentication and verification)
- Relying party (the web application owner)
- Browser's Web Authentication API
The flow is fairly simple, first step is registration, then it would be authentication.
Registration a credential
Same as password authentication flows where the web server usually provides a form for user to enter their password which is sent back to the server for verification.
With WebAuthn, it's fairly similar, but instead of asking for a password, the relying party asks for a public key.
Image from WebAuthn.io |
When prompting the user for a credential, the navigator.credentials.create()
is used:
const credential = await navigator.credentials.create(
{
publicKey: publicKeyCredentialCreationOptions,
}
)
In case you're wondering what does the publicKeyCredentialCreationOptions
looks like, it contains a bunch of mandatory and optional fields:
const publicKeyCredentialCreationOptions = {
challenge: Uint8Array.from(
randomStringFromServer,
c => c.charCodeAt(0)
),
rp: {
name: 'Google',
id: 'accounts.google.com',
},
user: {
id: Uint8Array.from('5T9AFCUZSL8', c =>
c.charCodeAt(0)
),
name: 'me@yashints.dev',
displayName: 'Yaser',
},
pubKeyCredParams: [
{ alg: -7, type: 'public-key' },
],
authenticatorSelection: {
authenticatorAttachment: 'cross-platform',
},
timeout: 60000,
attestation: 'direct',
}
challenge
: The most important part in the options is the challenge. It is a randomly generated bytes on the server which is used to prevent reply attacks.
rp
: Which stands for relying party, is the web server which user is trying to register to.
user
: This is the end user who is registering to a service. The id
is used to associate the public key to that user, but is suggested not be a personally identifying information.
pubKeyCredPrams
: Specifies which types of public key are acceptable by the server.
authenticatorSelection
: Is an optional field which helps relying parties to put further restrictions on authenticators allowed for registration. The possible values (platform
like Windows Hello, or cross-platform
like Yubikey) can be found on the spec.
timeout
: The time in millisecond that the user has to respond to the registration prompt.
attestation
: This is returned from the authenticator and contains information that can used to track the user. It indicates how important the attestation is to this registration event. For example, if none
is used, it means server doesn't care about it. indirect
means server will allow for anonymised attestation data. direct
means that server requires the data from authenticator. In general this is regarding privacy of users when registration is happening.
The object which is returned from the create()
call, contains a public key and other attributes which are used to validate the user:
console.log(credential);
PublicKeyCredential {
id: 'ADSUllKQmbqdGtpu4sjseh4cg2TxSvrbcHDTBsv4NSSX9...',
rawId: ArrayBuffer(59),
response: AuthenticatorAttestationResponse {
clientDataJSON: ArrayBuffer(121),
attestationObject: ArrayBuffer(306),
},
type: 'public-key'
}
Verifying the registration data by relying party
Once the public key is received along with other properties of registration data to the server, there is a procedure to validate this data.
Of course the implementation depends on what language you're using, but, there are some examples out there if you want to have an idea how to implement these steps. Duo Labs has provided full implementation in Go and Python.
Authenticating with WebAuthn
After completing the registration, the user is able to be to authenticate to the site. During registration user has obtained an assertion which indicates they have the private key. This contains a signature using the mentioned private key. Web server uses the public key during registration to verify this.
The authentication flow looks something like this:
Image from WebAuthn.io |
The navigator.credentials.get()
method is called to generate an assertion to prove the user own the private key. This will retrieve the credential generated during registration with a signature included.
const credential = await navigator.credentials.get(
{
publicKey: publicKeyCredentialRequestOptions,
}
)
The publicKeyCredentialRequestOptions
object contains a number of mandatory and optional properties which is specified by the server:
const publicKeyCredentialRequestOptions = {
challenge: Uint8Array.from(
randomStringFromServer,
c => c.charCodeAt(0)
),
allowCredentials: [
{
id: Uint8Array.from(credentialId, c =>
c.charCodeAt(0)
),
type: 'public-key',
transports: ['usb', 'ble', 'nfc'],
},
],
timeout: 60000,
}
The most interesting part in this object is the transport property. The server can optionally indicate what transports it prefers, like USB, NFC, and Bluetooth.
The returned object is a PublicKeyCredential
object, but slightly different from the previous one from registration.
console.log(assertion);
PublicKeyCredential {
id: 'SSX9lKQmbqdGtbcHDTBsvpu4sjseh4cg2TxSvr4ADSUlN...',
rawId: ArrayBuffer(59),
response: AuthenticatorAssertionResponse {
authenticatorData: ArrayBuffer(191),
clientDataJSON: ArrayBuffer(118),
signature: ArrayBuffer(70),
userHandle: ArrayBuffer(10),
},
type: 'public-key'
}
For more info around this object refer to spec.
Validating authentication data
After the assertion is acquired, it is sent to server for verification. After being validated, the signature is verified with the stored public key.
const storedCredential = await getCredentialFromDatabase(
userHandle,
credentialId
)
const signedData =
authenticatorDataBytes + hashedClientDataJSON
const signatureIsValid = storedCredential.publicKey.verify(
signature,
signedData
)
if (signatureIsValid) {
return 'Hooray! User is authenticated! π'
} else {
return 'Verification failed. π'
}
Further reading and resources
Following resources are a good starting point for any further reading:
- Webauthn.io: Due Labs has created this useful resource which contains all the source code and sample use cases with beautiful illustrations.
- Developments to WebAuthn and the FIDO2 Framework: An informative blog post which goes through some of the progress and shows the compatibility of each major browser and contributors to this area.
- Source Code: This GitHub repo contains all the source code Due Labs have created to demonstrate the capability.
- A Big Day for the Internet: W3C Standardizes WebAuthn
- Demo site by Yubico: Demonstrates the process in the end user perspective
Summary
We saw how simple and straight forward is to implement this amazing feature and I highly recommend if you're working on a green field project definitely consider having this implemented and give users ability to live passwordless π₯ππ». For the current products, this can be a turning point to gain more users into system by simplifying the most important part of their flow π.
Top comments (7)
Thanks for this, Iβve been waiting for cryptography-based authentication to develop and it looks like itβs on its way!
I suppose the user wouldnβt actually be aware of the private key, and since itβs stored on the device, do you know how logging into the same account from multiple devices would work with this spec?
Exactly what I was going to comment - I don't see how it does. I mean, the browsers might support it through something like Firefox/Chrome syncing.
Hopefully though password managers can be linked into working with this and they manage the keys for each site. Otherwise that will suck maintaining passwords in a password manager and the keys are just "magically" stored somewhere else.
This is something which is not in spec and should be handled by the implementation ππ½
github.com/w3c/webauthn/issues/151
Thanks for the link though also kinda worrying at the same time as it seems like a big thing to leave to individual implementations. For example, Android might support one method and iOS another - just another way to lock you to platforms. Or just Firefox and Chrome - now maybe you can't switch browsers as easily.
Or on the website side of things, some websites may have flows to support key-per-device (much like how GitHub allows multiple SSH keys) but some sites won't, making it more likely to confuse people when dealing with other implementations.
It just seems like such a big thing to leave off the table and has me concerned for sites that may only offer WebAuthn in the future.
EDIT: Just to highlight a little more why I think this is problematic while still being similar to the way existing things work - isn't half the point of removing passwords to help the average computer user not use bad passwords? The same average computer user likely would want it to be as easy as possible so if the solution isn't easily portable or has weird edge cases, I just see this as being DOA.
I understand your concern, but keep in mind big names are involved in delivering these features. Google, Microsoft, Yubico, Auth0, are just a few.
To give you an example, if you have a Yubikey, it works everywhere cross platform.
I'd be more thinking about how many public keys you'd have over time and what devices do to securely store those private keys π€·ββοΈ
Fido is working on a cross platform spec for BLE called caBLE to allow the credentials on your phone to be used with other devices like a Windows desktop. That is working now in beta on Android for Google accounts.
A external authenticator in combination with platform authenticators is probably the best option.
With passwordless the keypair is the only thing needed to get into the account making synchronising private keys across devices a very sensitive task.
Great post. Thanks.
Makes sense ππ½