Hello! In this article, we will learn how to add WebAuthn to web applications from the perspective of a frontend developer. WebAuthn represents a new authentication method that provides a higher level of security by replacing outdated passwords and SMS confirmations with public key-based authentication. This not only increases protection against unauthorized access, but also simplifies login for users.
Basics of Public Key Encryption
To understand WebAuthn, it is important to be familiar with the concept of public key encryption. This is a method where two keys are used: a public key and a private key. The public key can be freely distributed and is used to encrypt messages. The ciphertext thus generated can only be decrypted with the corresponding private key, which should remain secret. This technology underlies WebAuthn, where public keys provide reliable authentication without the transmission of confidential data.
How WebAuthn works
WebAuthn operates on several key stages:
Creating a Challenge: The server generates a unique challenge and sends it to the user's device.
User Confirmation: The user confirms their intention to authenticate using biometrics, a PIN code, or another method.
Signing the Challenge: The device signs the challenge with its private key, confirming the legitimacy of the request.
Sending the Assertion: The device sends the signed challenge back to the site.
Assertion Verification: The site verifies the signature using a previously stored public key.Successful verification confirms the user's authentication.
Example Implementation in a Web Application
In this part of the article, we will develop a simple web application using React.js. Our application will include a single field for entering a username and two functional buttons: 'Register' and 'Login'.
Registration
To register a new user, we need to implement a function that requests the creation of new credentials through navigator.credentials.create(). An important component here is publicKeyCredentialCreationOptions, which defines the parameters for creating a new key. For a more detailed understanding of these parameters, I recommend consulting the WebAuthn documentation.
// Function to create new credentials using the Web Authentication API.
const getCredential = async ({
challenge, // Hash (challenge) received from the server
}) => {
// Options for creating a public key.
const publicKeyCredentialCreationOptions = {
rp: {
name: 'my super app', // name of the Relying Party
id: 'webauthn.fancy-app.site', // identifier of the Relying Party (Domain)
},
user: {
id: Uint8Array.from(userId, (c) => c.charCodeAt(0)), // converting userId into Uint8Array
name: 'User', // username
displayName: 'Full username', // user's display name
},
challenge: Uint8Array.from(challenge, (c) => c.charCodeAt(0)), // converting challenge into Uint8Array
pubKeyCredParams: [
// preferred parameters for the cryptographic algorithm
{
type: 'public-key', // type of key
alg: -7, // algorithm ECDSA with curve P-256
},
{
type: 'public-key',
alg: -257, // algorithm RSA with SHA-256 limitation
},
],
timeout: 60000, // response timeout (in milliseconds)
excludeCredentials: [], // list of credentials to be excluded
authenticatorSelection: { // criteria for selecting an authenticator
residentKey: 'preferred',
requireResidentKey: false,
userVerification: 'required', // requirement for user verification
},
attestation: 'none', // type of attestation, not required here
extensions: { // extensions
credProps: true, // if true, credential properties are returned
},
};
// API call to create new credentials using the provided options.
return await navigator.credentials.create({
publicKey: publicKeyCredentialCreationOptions,
});
};
This function returns a PublicKeyCredential
object, which in JavaScript represents public credentials. They are created during registration through the WebAuthn API.
After these data are sent to the server, it conducts a verification of the public key and, upon successful verification, registers the user who sent it.
Authentication
For user authentication, we need to create a function that uses navigator.credentials.get()
. This function will use publicKeyCredentialRequestOptions
parameters, which should include challenge
(the challenge) and rpId
(the identifier of the relying party, usually the domain).
const getAssertion = async ({ challenge }) => {
// Creating an object with public key credential request options
const publicKeyCredentialRequestOptions = {
// Converting the challenge (provided by the server) to Uint8Array.
// This is necessary as WebAuthn requires the challenge to be in binary format.
challenge: Uint8Array.from(challenge, (c) => c.charCodeAt(0)),
// An empty array indicating that any registered credentials can be used.
// Specific credentials can be specified if there is a need to restrict access.
allowCredentials: [],
// The Relying Party ID (usually the domain) associated with the request.
rpId: 'webauthn.fancy-app.site',
// The timeout in milliseconds to complete the authentication process.
timeout: 60000,
// The level of user verification. In this case, confirmation ('required') is needed.
// Other options may include 'preferred' or 'discouraged'.
userVerification: 'required',
};
// Calling the get() method of navigator.credentials with the provided parameters.
// This call initiates the process of obtaining an authentication assertion from the user.
return await navigator.credentials.get({
publicKey: publicKeyCredentialRequestOptions,
});
};
The function will return a PublicKeyCredential
object, which then needs to be sent to the server. This is necessary for the authentication procedure and subsequent acquisition of an access token.
Demo
If you would like to try out this functionality on your own device, please use the link to the test stand. In the console, you will see the PublicKeyCredential
, which is used for both registration and authentication. Also, for a detailed study of the code, I suggest the link to the GitHub repository.
Thank you for your attention)
Top comments (1)
nice article!