The problem
As discussed previously, the credentials in webauthn are device bound. Each device has its secret key stored locally and hence each device must be registered individually. This leads to three possible actions:
- Register
- Login
- Add Device
However, using three such buttons in a UI would likely confuse most users. Instead, it is much nicer to present a single button like "Sign In/Up" and decide which action to take depending on the context.
- If the email is unknown => send a registration link
- If the email is known
- If the device is known => authenticate directly
- If the device is unknown => send link to add device
This represents the big picture. Let us dive into this in more details. Moreover, it is important to notice that this requires proper interaction between server and client side.
Only the server side can detect if the email is registered.
Only the client side can detect if the device is registered.
The logic in details
The first step is to know if this account exists.
GET /is-registered?email=...
The response of this could also be used to adapt the buttons displayed on the login page.
But is it OK privacy wise? To allow anyone to check if some user is registered there? It turns out that even if such a direct call is not available, anyone could find it out through the next step anyway, by checking whether a login attempt or a registration is triggered. This is basically unavoidable since the only input is the e-mail.
If the e-mail is not registered, the next step is straightforward. Request to send a registration link to your e-mail.
POST /send-registration-link?email=...
Otherwise, a challenge must be requested to attempt to perform a login. At this point, it is not known if the requesting device is registered.
GET /login-challenge?email=...
Once you obtain the challenge and data associated with it, the webauthn protocol can be invoked to request biometrics/PIN and sign the payload. If it succeeds, great, you can complete the login process!
POST /login {webauthn-created-payload}
If it fails, there might be multiple reasons that must be distinguished:
- this device is unknown, none of the registered key IDs matches
- the user failed to input the correct biometric/PIN
- the user cancelled
- there was an unexpected error
In case it is an unknown device, you can request a new registration link.
POST /send-registration-link?email=...
The confirmation page
During the login process with
GET /login-challenge?email=...
, a short lived challenge is perfect. For registration, something more long lived might be desirable. There is no clear cut rule about this, it is rather something to put into consideration.
The email could look like this:
Click here to register your device: https://.../confirm#{payload-including-challenge}
This page would then invoke the webauthn protocol with the provided challenge. Once succeeded, a simple registration call would complete the process.
POST /register-device {webauthn-created-payload}
This same endpoint could be used for both initial registration or adding separate devices.
Managing devices
The main difference with traditional authentication is that the webauthn protocol is device bound. As such, one of the important aspects would also be to manage the devices. This is especially important to block a device if it is stolen. The minimal additional endpoints could be as follows.
GET /devices
DELETE /devices/{id}
Of course, the devices are only added using the confirmation link.
Security
Here are a few last words about security. Both initial registration and later sign ins are two factor authentication. They both require your device and either biometrics or PIN. That's great.
Ironically, registering an additional device as described here is the weak spot. In this case, a hacked email account could enable the hacker to register its own device.
Security wise, registering a new device is the place were an additional verification (like a password, question/answer or SMS) would make most sense.