Deprecation from U2F API to WebAuthn
Google Chrome will no longer support U2F API after February, so we go into the weeds on how to migrate from U2F API to WebAuthn.
Jan 14th, 2022 9:13am by
Photo by Charlotte May from Pexels
Sentry sponsored this post.
For anyone using U2F API in their web apps, like us at Sentry, their users who had 2FA enabled with U2F devices would not be able to sign in. To remedy this, there’s a shiny new specification written by the World Wide Web Consortium (W3C) and the Fast IDentity Online Alliance (FIDO) that will solve all our problems. In this blog post, we will go into the weeds on migrating from U2F API to WebAuthn.
WebAuthn
WebAuthn is an API that allows web services to seamlessly integrate strong authentication into applications.
Richard Ma
Richard is a software engineer at Sentry and a collector of cool clothing.
Migrating to WebAuthn from U2F API
Now, here’s the part you’re all here for, the migration to WebAuthn. Let’s break this down into two main parts:- Part 1: Authenticating existing U2F and new WebAuthn devices with WebAuthn
- Step 1: Generating the challenge and state
- Step 2: Creating PublicKeyCredential data
- Step 3: Verifying the device
- Part 2: Registering new devices with WebAuthn
- Step 1: Generating the challenge and state
- Step 2: Creating PublicKeyCredential data
- Step 3: Registering the device
Part 1: Authentication
Let’s start with authentication so existing users can continue to log in. This is also important because newly registered WebAuthn devices can’t log in without a working WebAuthn login. To understand the authentication flow, we can look at the following diagrams: We’ll be using Python for the backend APIs. The U2F API sequence (left) is very similar to the WebAuthn sequence (on the right). We simply have to replace three API calls:u2f.start_authentication() and u2f.finish_authentication() in the backend, and u2f.sign() in the frontend.
Let’s start with u2f.start_authentication(), which takes in the browser’s application ID and the currently registered devices.
The U2F API authentication process starts with the backend generating a challenge, an example of which is shown below:
{
"appId": "https://your-webauthn-app.io/2fa/u2fappid.json",
"challenge": "VwmGI-4…",
"registeredKeys": [
{
"appId": "https://your-webauthn-app.io/2fa/u2fappid.json",
"keyHandle": "cxSl4oQ…",
"publicKey": "BP4Q8MR…",
"transports": [
"usb"
],
"version": "U2F_V2"
}
]
}
u2f.sign() takes the challenge as an input and returns a promise that is the result of:
- verifying the application identity of the caller
- creating a client data object and using the client data
- the application ID
- the key handle
{
"keyHandle": "cxSl4oQ…",
"clientData": "eyJ0eXA…",
"signatureData": "AQAAAQ4…"
}
U2f.complete_authentication() with the following two parameters: the original challenge data and the newly generated client data object passed in. This method will verify the device with the parameters and return the device info if it succeeded. From there, the server can allow the user to pass through the 2FA process.
Step 1: Generating the Challenge and State
To start the migration process, let’s first replaceu2f.start_authentication() with its counterpart. The data types that the WebAuthn API takes are not quite the same ones used in U2F API. In fact, one of the main pain points was converting the necessary fields into the correct data type.
We want to authenticate users on legacy U2F API and WebAuthn, so we will create an authentication server first. The following will create an authentication server using WebAuthn that is backward compatible with U2F API:
webauthn_authentication_server = U2FFido2Server(
app_id=u2f_app_id,
rp={
"id": “sentry-webauthn.io”,
"name": "Sentry with WebAuthn"}
)
app_id will be the same value as before. The rp, or Relying Party, is an object that contains an ID, which is the hostname of the URL, and the name of your Relying Party.
Next, we need to generate a list of credentials, which is the same as the list of devices for U2F API. Keep in mind that the list of credentials will contain both WebAuthn and U2F API registered devices and that list needs to be manipulated.
credentials = []
for device in self.get_u2f_devices():
if type(device) == AuthenticatorData:
credentials.append(device.credential_data)
else:
credentials.append(create_credential_object(device))
def create_credential_object(registeredKey):
return base.AttestedCredentialData.from_ctap1(
websafe_decode(registeredKey["keyHandle"]),
websafe_decode(registeredKey["publicKey"]),
)
register_begin() on the WebAuthn server that we created earlier, with credentials as its parameter. This will return a challenge and state.
The challenge is needed for the browser to perform authentication, but we will only use the PublicKey object within the challenge. In addition, you should store the state in your sessions, as it will be needed later.
challenge, state = self.webauthn_authentication_server.authenticate_begin(
credentials=credentials
)
request.session["webauthn_authentication_state"] = state
return ActivationChallengeResult(
challenge=cbor.encode(challenge["publicKey"])
)
Step 2: Generating PublicKeyCredential for Authentication
To replaceu2f.sign(), we can call its WebAuthn equivalent navigator.credentials.get() with the challenge data. This library is now native to modern browsers, so don’t worry about importing any libraries.
const challengeArray = base64urlToBuffer(
challengeData.webAuthnAuthenticationData
);
const challenge = cbor.decodeFirst(challengeArray);
challenge.then(data => {
webAuthnSignIn(data);
}).catch(err => {
const failure = 'DEVICE_ERROR';
Sentry.captureException(err);
this.setState({
deviceFailure: failure,
hasBeenTapped: false,
});
});
function webAuthnSignIn(publicKeyCredentialRequestOptions) {
return navigator.credentials.get({
publicKey: publicKeyCredentialRequestOptions,
}).then(data => {
// Send to backend
})
}
navigator.credentials.get(), we need to send the appropriate data to the backend to finish authentication. To convert the PublicKeyCredential that was obtained from navigator.credentials.get(), we can run it through the following function:
getU2FResponse(data) {
if (data.response) {
const authenticatorData = {
keyHandle: data.id,
clientData: bufferToBase64url(data.response.clientDataJSON),
signatureData: bufferToBase64url(data.response.signature),
authenticatorData: bufferToBase64url(data.response.authenticatorData),
};
return JSON.stringify(authenticatorData);
}
return JSON.stringify(data);
}
Step 3: Verifying the Device
For the final step, we can pass the original challenge and this new response to the backend. We need to create a list of credentials to validate the device, then callauthenticate_complete on the authentication server that was made earlier with the following parameters:
-
- state: the value which we stored in session from start_authentication
- credentials: list which we just generated
- A websafe_decode for the following:
-
-
- credential_id: a “keyHandle” of the response object
- client_data: a “clientData” of the response object passed through
fido2.client.ClientData - auth_data: an “authenticatorData” of the response object passed through
fido2.ctap2.authenticatorData - signature: a “signatureData” of the response object
-
self.webauthn_authentication_server.authenticate_complete(
state=request.session["webauthn_authentication_state"],
credentials=credentials,
credential_id=websafe_decode(response["keyHandle"]),
client_data=ClientData(websafe_decode(response["clientData"])),
auth_data=AuthenticatorData(websafe_decode(response["authenticatorData"])),
signature=websafe_decode(response["signatureData"]),
)
Part 2: Registration
Similar to authentication, first, let’s take a look at the flow: The flow of registration is almost identical to that of the authentication process. The three components we need to deprecate in order to migrate to WebAuthn areu2f.begin_registration() and u2f.complete_registration() in the backend, and u2f.register() in the frontend. Once again, we will start with u2f.begin_registration(). This API call takes in the u2f application ID and list of registered devices. This results in the following data being sent to the browser to begin the registration process:
{
"appId": "https://your-webauthn-app/2fa/u2fappid.jso",
"registerRequests": [
{
"challenge": "uexgFSl…",
"version": "U2F_V2"
}
],
"registeredKeys": []
}
u2f.sign(), u2f.register() will take the previously generated results and return a promise that will look like the following if the device is registrable:
{
"registrationData": "BQQ1xlC…",
"version": "U2F_V2",
"challenge": "Jkh_Tfo…",
"appId": "https://your-webauthn-app.io/2fa/u2fappid.json",
"clientData": "eyJ0eXA…"
}
{
"appId": "https://your-webauthn-app.io/2fa/u2fappid.json",
"keyHandle": "SnllNGC…",
"publicKey": "BIs-gsW…",
"transports": [
"usb"
],
"version": "U2F_V2"
}
u2f.begin_registration()with its counterpart. We need to create a FIDO2Server and import it from fido2.server. We don’t need to make it backward compatible, as all new devices will be registered with WebAuthn.
Step 1: Generating the PublicKeyCredentialRpEntity and State
We start with importing the fido2.webauthn library to create a PublicKeyCredentialRpEntity. To create the entity, we need to pass in the Relying Party’s ID and name. With the entity, we pass it into Fido2Server to set things up.
from fido2.server import Fido2Server
from fido2.webauthn import PublicKeyCredentialRpEntity
rp = PublicKeyCredentialRpEntity(rp_id, "Sentry")
webauthn_registration_server = Fido2Server(rp)
register_begin() with:
user: dictionary with the user’s id, name, and display name
credentials: the list we just generated
user_verification: normally defaulted to discouraged
You should get a result similar to this:
{
"publicKey": {
"authenticatorSelection": {
"userVerification": <UserVerificationRequirement.DISCOURAGED: "discouraged">},
"challenge": b"\xe9)#\x86\xfa.\xa9\x82r\x86\xf7\x15e\xb5m\xdc"
b"\x1dR\xc4\x1b\xdb\xab\x94\x88\xb8\x94\xf43"
b"b\x03\xab\n",
"excludeCredentials": [],
"pubKeyCredParams": [
{"alg": -7,
"type": <PublicKeyCredentialType.PUBLIC_KEY: "public-key">},
{"alg": -8,
"type": <PublicKeyCredentialType.PUBLIC_KEY: "public-key">},
{"alg": -37,
"type": <PublicKeyCredentialType.PUBLIC_KEY: "public-key">},
{"alg": -257,
"type": <PublicKeyCredentialType.PUBLIC_KEY: "public-key">}],
"rp": {"id": "<$YOUR_APP>",
"name": "Sentry"},
"user": {"displayName": "<$YOUR_NAME>",
"id": b"\x00",
"name": "<$YOUR_APP>"
}
}
}
cbor.encode() method and base64 encode that to a string.
publicKeyCredentialCreate = cbor.encode(registration_data)
return b64encode(publicKeyCredentialCreate)
Step 2: Generating PublicKeyCredential for Registration
Once the registration data is received by the browser, we convert the string into a buffer and decode it with this library. This gives us the data that will be used as the input parameter ofnavigator.credentials.create():
challenge, state = self.webauthn_authentication_server.authenticate_begin(
credentials=credentials
)
request.session["webauthn_authentication_state"] = state
return ActivationChallengeResult(challenge=cbor.encode(challenge["publicKey"]))
webAuthnRegister(publicKey) {
const promise = navigator.credentials.create({publicKey});
this.submitU2fResponse(promise);
}
navigator.credentials.create() is resolved, we need to run it through getU2FResponse() before returning it to the server again.
Step 3: Registering the Device
We have reached the final step where we need to extract some data from the response fromnavigator.credentials.create(). The following are needed for register_complete():
- state: from user sessions, set earlier after begin_registration()
- client_data: from decoding the data’s cliendDataJSON and creating a ClientData Object with it.
- AttestationObject: from decoding the data’s attestationObject and creating an AttestationObject Object with it.
data = json.loads(response_data)
client_data = ClientData(
websafe_decode(data["response"]["clientDataJSON"])
)
att_obj = base.AttestationObject(
websafe_decode(data["response"]["attestationObject"])
)
binding = webauthn_registration_server.register_complete(
state, client_data, att_obj
)
{
"type": "webauthn.create",
"challenge": "_Uas89Y…",
"origin": "https://<$YOUR_APP>",
"crossOrigin": false
}
AttestationObject(
fmt: 'none',
auth_data: AuthenticatorData(
rp_id_hash: h'74cb1ce…5',
flags: 0x41,
counter: 281,
credential_data: AttestedCredentialData(
aaguid: h'0000000…',
credential_id: h'63af2c9…',
public_key: {...}
),
att_statement: {},
ep_attr: None,
large_blob_key: None
)
)
AuthenticatorData(
rp_id_hash: h'74cb1ce…',
flags: 0x41,
counter: 281,
credential_data: AttestedCredentialData(
aaguid: h'0000000…',
credential_id: h'63af2c9…',
public_key: {...}
)
)
That’s a Wrap
With that, WebAuthn should be set up and you can purge U2F API from your codebase. If you have made it this far, we hope that this guide was useful to you. With some planning, you will make it in time before Chrome locks out users from your application. All the best! Everything we do at Sentry is built in the open. Find us on GitHub.
YOUTUBE.COM/THENEWSTACK
Tech moves fast, don't miss an episode. Subscribe to our YouTube
channel to stream all our podcasts, interviews, demos, and more.
