We can all admit it—logging into online services with our fingerprints or Face ID, the same way we do on native apps, is something we’ve all imagined. And with the arrival of web biometrics, this scenario is much closer to reality. Picture abandoning those elaborate, hard-to-remember passwords altogether and gaining access to our preferred sites through just a glance or a touch. Pretty neat, right?
WebAuthn is the technology that brings biometric authentication to the web. In plain terms, it lets users verify their identity using the same fingerprint or face recognition they rely on their phones for, but right inside a browser. This removes the need to worry about passwords being compromised—just a straightforward scan, and access is granted.
Throughout this guide, we’ll take a practical approach to adding fingerprint and Face ID login to Angular applications. We’ll walk through fundamental concepts, such as the inner workings of the WebAuthn API and the backend requirements for maintaining security and reliability in the process. You’ll find it simpler than expected, and once we’re finished, our app will be ready for this modern authentication method. Let’s get started and simplify the login experience!
Understanding WebAuthn: The Basics for Fingerprints and Face-recognition in Angular Apps
Before we start writing code, it’s worth pausing to understand what WebAuthn really is. At its core, WebAuthn acts as a bridge, letting our apps tap into the biometric capabilities our phones already have—like fingerprints and Face ID—directly through the browser. It relies on public key cryptography rather than plain text passwords, so there’s no more worrying about hackers scooping up easily stolen credentials. Instead, we’re working with securely generated key pairs that make the login flow both secure and frictionless.
Key Objects and Their Roles
To get started, it helps to know the key pieces of the WebAuthn puzzle: PublicKeyCredentialCreationOptions and PublicKeyCredentialRequestOptions. Those names may sound intimidating, but in reality, they are just structured ways to tell the browser how we want to handle user registration and authentication.
1. PublicKeyCredentialCreationOptions
This object is what we rely on when we set up credentials for a new user. Its structure looks like this:
- challenge: A unique, random value produced by the server to guarantee the response is timely and immune to replay attacks.
- rp: Stands for Relying Party (our app) and holds essential details such as the app’s name and ID.
- user: Contains user-specific data, including a unique ID, username, and display name.
- pubKeyCredParams: Lists which public key algorithms we are willing to accept.
- authenticatorSelection: Guides the choice of authenticator type, covering criteria such as attachment type (platform or cross-platform) and how strictly user verification is enforced.
2. PublicKeyCredentialRequestOptions
When it’s time to verify a returning user, this object shifts into focus. Its key fields include:
- challenge: Same as before, it confirms that the authentication attempt is both current and one-time.
- allowCredentials: Determines which credentials the user is permitted to supply.
- userVerification: Sets the requirement level for verifying the user’s identity (e.g., requiring a fingerprint scan).
Once we have these objects in place, our Angular app can smoothly handle both registration of biometric data and quick, secure authentication. Now let’s move on to the code and see exactly how we bring it all together.
Setting Up the Angular App
Here, we’ll walk you through configuring an Angular application to support biometric authentication through WebAuthn. Our emphasis will be on fingerprints and Face ID, so let’s jump right in.
Step 1: Setting Up Our Angular Project
Kicking things off, we’ll create a fresh Angular project. Head over to your terminal and run the following commands:
ng new web-biometrics-demo
cd web-biometrics-demo
ng serve
This configures a basic Angular project, and executing ng serve will launch your application at http://localhost:4200/. The default Angular welcome screen will greet you. Now, it’s time to incorporate WebAuthn for biometric authentication.
Step 2: Creating the WebAuthn Service
An Angular service is required to handle all WebAuthn operations, covering both registration and biometric-based authentication. To build this service, run the following command:
ng generate service services/webauthn
Head over to webauthn.service.ts and insert the snippet below:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class WebAuthnService {
constructor() { }
// Generates a random buffer to use as a challenge, which is a unique value needed for security
private generateRandomBuffer(length: number): Uint8Array {
const randomBuffer = new Uint8Array(length);
window.crypto.getRandomValues(randomBuffer); // Fills the buffer with cryptographically secure random values
return randomBuffer;
}
// Registers a new credential (like a fingerprint or Face ID) for the user
async register() {
// Generate a unique challenge for the registration process
const challenge = this.generateRandomBuffer(32);
// PublicKeyCredentialCreationOptions is the core object needed for registration
const publicKey: PublicKeyCredentialCreationOptions = {
challenge: challenge, // A random value generated by the server to ensure the request is fresh and unique
rp: { // Relying Party (your app) information
name: "OurAwesomeApp" // Display name of your app
},
user: { // User information
id: this.generateRandomBuffer(16), // A unique identifier for the user
name: "user@example.com", // User's email or username
displayName: "User Example" // A friendly name for the user
},
pubKeyCredParams: [{ // Array of acceptable public key algorithms
type: "public-key",
alg: -7 // Represents the ES256 algorithm (Elliptic Curve Digital Signature Algorithm)
}],
authenticatorSelection: { // Criteria for selecting the appropriate authenticator
authenticatorAttachment: "platform", // Ensures we use the device's built-in biometric authenticator like Touch ID or Face ID
userVerification: "required" // Requires user verification (e.g., fingerprint or face scan)
},
timeout: 60000, // Timeout for the registration operation in milliseconds
attestation: "direct" // Attestation provides proof of the authenticator's properties and is sent back to the server
};
try {
// This will prompt the user to register their biometric credential
const credential = await navigator.credentials.create({ publicKey }) as PublicKeyCredential;
this.storeCredential(credential, challenge); // Store the credential details locally for demo purposes
console.log("Registration successful!", credential);
return credential; // Return the credential object containing the user's public key and other details
} catch (err) {
console.error("Registration failed:", err);
throw err; // Handle any errors that occur during registration
}
}
// Authenticates the user with stored credentials (like a fingerprint or Face ID)
async authenticate() {
const storedCredential = this.getStoredCredential(); // Retrieve stored credential information
if (!storedCredential) {
throw new Error("No stored credential found. Please register first."); // Error if no credentials are found
}
// PublicKeyCredentialRequestOptions is used to prompt the user to authenticate
const publicKey: PublicKeyCredentialRequestOptions = {
challenge: new Uint8Array(storedCredential.challenge), // A new challenge to ensure the request is fresh and unique
allowCredentials: [{ // Specifies which credentials can be used for authentication
id: new Uint8Array(storedCredential.rawId), // The ID of the credential to use
type: "public-key"
}],
userVerification: "required", // Requires user verification (e.g., fingerprint or face scan)
timeout: 60000 // Timeout for the authentication operation in milliseconds
};
try {
// This will prompt the user to authenticate using their registered biometric credential
const credential = await navigator.credentials.get({ publicKey }) as PublicKeyCredential;
console.log("Authentication successful!", credential);
return credential; // Return the credential object with authentication details
} catch (err) {
console.error("Authentication failed:", err);
throw err; // Handle any errors that occur during authentication
}
}
// Stores credential data in localStorage (for demo purposes only; this should be handled securely in production)
private storeCredential(credential: PublicKeyCredential, challenge: Uint8Array) {
const credentialData = {
rawId: Array.from(new Uint8Array(credential.rawId)), // Converts the raw ID to an array for storage
challenge: Array.from(challenge) // Converts the challenge to an array for storage
};
localStorage.setItem('webauthn_credential', JSON.stringify(credentialData)); // Store the data as a JSON string
}
// Retrieves stored credential data from localStorage
private getStoredCredential(): any {
const storedCredential = localStorage.getItem('webauthn_credential');
return storedCredential ? JSON.parse(storedCredential) : null; // Parse the stored JSON back into an object
}
}
What’s Happening in the Code?
generateRandomBuffer: This utility produces a random buffer, acting as a one-time challenge to guarantee that every registration or login attempt remains distinct.register: The registration flow is initialized through this method. Parameters such as thechallenge,relying party(your application), user details, and supported public key algorithms are configured viaPublicKeyCredentialCreationOptions. Upon execution ofnavigator.credentials.create(), the browser triggers a prompt for biometric enrollment.authenticate: Biometric user verification is managed by this method. Authentication specifics, including the challenge and permitted credentials, are outlined usingPublicKeyCredentialRequestOptions. The user is then prompted to confirm their identity with the already-registered biometrics.-
For demonstration purposes,
storeCredentialandgetStoredCredentialmanage saving and fetching credentials withinlocalStorage.Responsible production systems would instead keep this data securely on a server.
Step 3: Building the UI
Next, we’ll construct a straightforward interface with buttons that invoke the registration and login processes. This interface will indicate whether each operation succeeded or failed.
Navigate to app.component.ts, clear its contents, and introduce the following code:
import { Component } from '@angular/core';
import { WebAuthnService } from './services/webauthn.service';
@Component({
selector: 'app-root',
template: `
<div class="auth-container">
<h1>Web Biometrics in Angular</h1>
<button (click)="register()">Register with Fingerprint</button>
<button (click)="login()">Login with Face ID</button>
<p *ngIf="message" [ngClass]="{'success': isSuccess, 'error': !isSuccess}">{{ message }}</p>
</div>
`,
styles: [`
.auth-container {
text-align: center;
padding: 50px;
}
.success {
color: green;
}
.error {
color: red;
}
button {
margin: 10px;
padding: 10px 20px;
font-size: 16px;
}
p {
margin: 10px;
font-size: 16px;
}
`]
})
export class AppComponent {
message: string | null = null; // Message to display feedback to the user
isSuccess: boolean = false; // Indicates if the last action was successful
constructor(private webAuthnService: WebAuthnService) { }
// Trigger registration process and update the UI based on the outcome
async register() {
try {
await this.webAuthnService.register();
this.message = "Registration successful!"; // Success message if registration works
this.isSuccess = true;
} catch (err) {
this.message = "Registration failed. Please try again."; // Error message if something goes wrong
this.isSuccess = false;
}
}
// Trigger authentication process and update the UI based on the outcome
async login() {
try {
await this.webAuthnService.authenticate();
this.message = "Authentication successful!"; // Success message if authentication works
this.isSuccess = true;
} catch (err) {
this.message = "Authentication failed. Please try again."; // Error message if something goes wrong
this.isSuccess = false;
}
}
}
Inside the Component
The methods register and login delegate to the corresponding functions exposed by the WebAuthnService. A successful invocation triggers a positive message, while failures lead to an error notification.
For the UI, the template provides two buttons—one for registration and one for login—alongside a display area for feedback. These buttons are kept visually straightforward for the sake of clarity.
And with that, the setup is complete: a minimal Angular application that leverages WebAuthn for biometric verification, covering both fingerprints and Face ID. The implementation highlights the essential principles and serves as a starting point for future enhancements, including stronger security measures and additional functionalities suitable for production scenarios.
Server-Side Aspects
When incorporating biometric authentication methods such as fingerprints or Face ID via WebAuthn in web apps, the server is essential for managing data flow and ensuring security. Below is a conceptual overview of the backend’s role in handling both sign-up and sign-in processes.
Registration Process
1. How Users Sign Up
Collecting User Info: The user supplies standard details, like an email and password. Any biometric information is obtained via the WebAuthn response during this step.
Securing Passwords: Storing passwords in plain text is avoided. They are instead hashed, using a mechanism such as bcrypt, prior to being saved in the database.
-
Saving WebAuthn Credentials:
- Managing Challenges: A challenge—a random value designed to thwart replay attacks—is issued by the server at the start of registration.
- Verifying Responses: The client’s WebAuthn reply, containing clientDataJSON and attestationObject, must be decoded and checked.
- Persisting Credentials: Once confirmed, key bits of data, including the webauthnId (a special credential marker) and the publicKey (needed for later authentication), are logged with the user’s record.
2. Duties of Server Code
Libraries such as cbor are employed to interpret binary formats in the WebAuthn response, pulling out important parts like the public key and authenticator data.
The server confirms that the challenge from the original registration matches the one found in the WebAuthn response, ensuring the registration is legitimate.
Provided the WebAuthn response clears all validations, the credentials are stored in the database, associated with the user’s account.
Login Process
1. How Users Log In
Challenge Generation: Just as with registration, the server issues a
challengethat the client's authenticator must answer during the login flow.-
Validating the WebAuthn Response:
- A PublicKeyCredentialRequestOptions object carrying the challenge response is transmitted back from the client.
- The backend decodes and checks this response, confirming that both the challenge and the credentials align with the stored records.
-
Credential Verification:
- The public key saved during registration validates the signature present in the login response.
- Upon successful credential matching, the backend permits login and creates an authentication token (such as a JWT) to manage the session.
Error Handling:
Mismatch or Invalid Response: When the challenge response deviates from the anticipated values, or the WebAuthn credentials fail verification, the backend returns an error, blocking unauthorized entry.
Fallback to Password: Should WebAuthn become unavailable or fail, the system can switch back to conventional password authentication, guaranteeing users maintain access to their accounts.
Security Considerations
Data Integrity: Maintaining the integrity of WebAuthn credentials is paramount. Any tampering during storage or transmission would lead to verification failure, thus fortifying the authentication mechanism.
Challenge Nonces: Leveraging distinct, time-bound challenges prevents response reuse, offering protection against replay attacks.
Public Key Storage: By storing solely public keys—which cannot be used to impersonate a user—security is bolstered, with private keys staying secure on the client device.
Adhering to these principles enables the backend to manage biometric authentication effectively, delivering a safe and smooth experience for users leveraging features like fingerprint or Face ID within their Angular applications.
Summary
Throughout this guide, we've delved into integrating biometric authentication with Angular via WebAuthn. We've addressed the fundamentals, ranging from grasping core WebAuthn objects like PublicKeyCredentialCreationOptions and PublicKeyCredentialRequestOptions to configuring Angular services and UI elements for an efficient registration and login flow. Additionally, we explored the backend prerequisites for securely managing biometric authentication.
For those interested in observing WebAuthn in practice, I've included a live demo and a repository featuring a full implementation. The demo is accessible here, and the source code can be found on GitHub at this repository.
Adopting biometric authentication not only bolsters security but also streamlines the user experience, leading towards a future where logging in requires just a fingerprint scan or a brief facial recognition. As you build these capabilities into your Angular apps, you'll help foster a more secure and user-friendly web environment. Happy coding!


