WebAuthn Tutorial: both server and client
By Mark Barsi-Siminszky
For a live demo, there is a working version on GitHub.
Please note that this is a tutorial, not a full documentation. To use this in a production environment, please read our FIDO docs also!
What is WebAuthn?
According to the FIDO Alliance, passwords are the root cause of 80% of data breaches! It is so common, yet so many fall for it… It’s simply time to fix this mess. So, the world is deploying a solution: WebAuthn.
WebAuthn allows you to login to websites using a gesture. For example, a fingerprint, an external authenticator, etc.
The server creates a challenge that the authenticator device signs using the credential. When the server verifies the signature, it can authenticate the user. Usually, the authenticator will require the user to verify their identity using a fingerprint, a pin code or other gesture.
Examples of authenticators include external hardware security keys and your device’s trusted platform module.
Here is the WebAuthn flowchart (to view it click on it). Don’t worry: you don’t need to understand this to use it in your website😀! Click on the flowchart to see it in big!
Here is the flowchart for registration:
Here is the flowchart to login:
Because of this mechanism, it is impossible to phish WebAuthn credentials, as they are never released from the authenticator. Only the signed challenge is released, which is not enough to obtain the credentials. Because a large portion of cyberattacks come from breached passwords, this can massively improve your security.
Setup your environment
Before proceeding, please install Python on your computer. Depending on your system, you may have Python already installed. If not, you can always get the latest version from the Python Homepage!
Next, use the Python package manager (pip) to install Krptn:
pip install krptn
If pip cannot find the wheels, you may need to build Krptn from source. The installation section of our documentation contains instructions.
Create the client
First, we need some external JS:
<script async src="https://cdn.jsdelivr.net/gh/herrjemand/Base64URL-ArrayBuffer@latest/lib/base64url-arraybuffer.js"></script>
Registration
We need a way to register the credential in the user’s browser, so that the challenge can be signed.
Secondly, we need to obtain the credential’s options from the server. In the next sections, we will discuss how we generate these:
const response = await fetch('/fidoReg', {cache: 'no-cache'});
const options = await response.json();
It is important that the response is decoded:
options.user.id = base64url.decode(options.user.id);
options.challenge = base64url.decode(options.challenge);
if (options.excludeCredentials) {
for (let cred of options.excludeCredentials) {
cred.id = base64url.decode(cred.id);
}
}
And the moment we were all waiting for! We can register the credential with the browser:
const cred = await navigator.credentials.create({
publicKey: options,
});
Unfortunately, our job is not done… We need to upload the browser’s response to the server. After this, the server will store the credential in the database!
We need to prepare the browser’s response first:
if (cred.response) {
const clientDataJSON =
base64url.encode(cred.response.clientDataJSON);
const attestationObject =
base64url.encode(cred.response.attestationObject);
credential.response = {
clientDataJSON,
attestationObject,
};
}
Finally, we are ready to upload the response and finalize the registration.
await fetch('/fidoFinishReg', {
body: JSON.stringify(credential),
cache: 'no-cache',
method: 'POST',
headers: {'Content-Type': 'application/json'}
});
Again, these mysterious server endpoints will the discussed!
Login
While according to the official WebAuthn website, the user does not need to enter the password, because of Krptn’s Zero Knowledge approach, we will require the user to provide the password anyway.
const email = "EMAIL";
const pwd = "PASSWORD";
First, we need to request the FIDO (WebAuthn) challenge from the server:
const query = {};
query.email = email;
const repsonse = await fetch('/getFidoLogin', // Mysterious endpoints will be discussed
{cache: 'no-cache',
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(query)}
);
const options = await repsonse.json();
Decode:
options.challenge = base64url.decode(options.challenge);
for (let cred of options.allowCredentials) {
cred.id = base64url.decode(cred.id);
}
Next, we can request the browser to sign the challenge, thereby proving the user’s identity:
const cred = await navigator.credentials.get({
publicKey: options
});
This response will be uploaded to the server. But first, we need some base64!
const credential = {};
credential.fido = 1;
credential.id = cred.id;
credential.type = cred.type;
credential.rawId = base64url.encode(cred.rawId);
if (cred.response) {
const clientDataJSON =
base64url.encode(cred.response.clientDataJSON);
const authenticatorData =
base64url.encode(cred.response.authenticatorData);
const signature =
base64url.encode(cred.response.signature);
const userHandle =
base64url.encode(cred.response.userHandle);
credential.response = {
clientDataJSON,
authenticatorData,
signature,
userHandle,
};
}
Finally, we can upload the response to the server and finish the authentication:
credential.pwd = pwd;
credential.email = email; // These are required by Krptn
authToken = await fetch('/fidoFinishLogin', {
body: JSON.stringify(credential),
cache: 'no-cache',
method: 'POST',
headers: {'Content-Type': 'application/json'}
});
Create the server
The following section assumes that the users have already been created in Krptn. You can quickly create them as discussed in the User Auth docs.
This also allows you to take advantage of other extensive security features in Krptn.
In order for WebAuthn to work, you need to set certain configuration options:
import krypton
krypton.configs.APP_NAME = "ExampleApp" # name of your app
## The below are only needed for FIDO
krypton.configs.HOST_NAME = "example.com" # hostname, as seen by the user's browser
krypton.configs.ORIGIN = "https://example.com/" # again, as seen by the user's browser
HOST_NAME
can be set to localhost
and ORIGIN
can be set to https://localhost
for development.
Registration
Inside the /fidoReg
endpoint:
from krypton.auth import users
model = users.standardUser(email)
key = model.login(...)
... # Standard Krptn login procedure
options = model.beginFIDOSetup()
This options
needs to be the response sent to the browser.
Inside /fidoFinishReg
:
model.completeFIDOSetup(request_json_string)
Of course, you will have to store the user model in the session.
This is best achieved by setting key
, as returned from model.login
in a cookie, so that on each request, you can restore the session:
model = users.standardUser(username_from_cookie)
model.restoreSession(key_from_cookie)
Login
Inside getFidoLogin
:
model = users.standardUser(email_slash_username)
options = model.getFIDOOptions()
options
needs to be provided in response to the request.
Inside fidoFinishLogin
:
model = users.standardUser(name_email)
key = user.login(password, fido=fidoChallangeFromBrowser)
As mentioned, key
can be set to keep the user authenticated in the session. Please see our User Auth docs for more information.
Pulling it all together
Depending on which web framework you are using, the client and server side needs to be glued together differently. We have an example where it is glued together with Flask on GitHub.
Last, but certainly not least, after creating a GUI where the user can enter the email and password, you are ready!
Copyright
Throughout the tutorial, we used some code from Google’s tutorial on FIDO. This code is in the client side where we decode/encode the credentials.
Here is the original, Google’s, copyright notice:
Copyright 2019 Google Inc. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License