Create your own
Lesson illustration

Biometric-Protected React Components and Routes

Welcome back. In our last session, we masterfully encapsulated all the complex biometric logic into a reusable useBiometricAuth hook. This was a crucial step in building a clean, maintainable application. Now, we'll leverage that work to implement a highly practical feature: securing parts of your application so they can only be accessed after a successful biometric scan.

This lesson will guide you through gating access to specific React components and routes. You'll learn how to create a "protected route" that uses our useBiometricAuth hook to either grant access to a page or block the user until they've authenticated. This is the final step in integrating our biometric plugin for access control within your app's UI.

1. The Protected Route Pattern in React

As a seasoned developer, you're likely familiar with the concept of protected routes in single-page applications. Typically, this involves a component that wraps a route's content and checks for an authentication token or session. If the user is authenticated, it renders the requested page; if not, it redirects them to a login screen.

We will adapt this same pattern for biometric authentication. Our goal is to create a component that, when used to guard a route, will:

  1. Check for biometric availability using our useBiometricAuth hook.
  2. If available, automatically trigger the authentication prompt.
  3. On success, render the protected content.
  4. On failure or cancellation, display a fallback UI with an option to retry or navigate away.

2. Implementation: A BiometricProtectedRoute Component

Let's build a component named BiometricProtectedRoute that implements this logic. It will serve as a gatekeeper for any route or component you wish to secure. This component will make direct use of the useBiometricAuth hook and react-router-dom for navigation.

First, ensure you have react-router-dom installed in your web package, as it's standard for routing in React SPAs. If not, you can add it:
bun add react-router-dom

Now, let's create the file packages/web/src/components/BiometricProtectedRoute.tsx and add the following implementation. This component manages the full authentication lifecycle for a route.

// packages/web/src/components/BiometricProtectedRoute.tsx

import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useBiometricAuth } from '../hooks/useBiometricAuth';

interface BiometricProtectedRouteProps {
  children: React.ReactNode;
}

const BiometricProtectedRoute: React.FC<BiometricProtectedRouteProps> = ({ children }) => {
  // Our hook provides all the necessary state and functions.
  const { isLoading, isAvailable, biometryType, error, authenticate } = useBiometricAuth();
  
  // Local state to track if the user has passed the biometric check for this session.
  const [isAuthenticated, setIsAuthenticated] = useState(false);
  
  // State to ensure we only trigger the automatic prompt once.
  const [authAttempted, setAuthAttempted] = useState(false);
  
  const navigate = useNavigate();

  useEffect(() => {
    // Wait until the hook has determined biometric availability.
    if (isLoading) {
      return;
    }

    // If biometrics are available and we haven't tried to authenticate yet, trigger the prompt.
    if (isAvailable && !authAttempted) {
      setAuthAttempted(true);
      handleAuthentication();
    }
  }, [isLoading, isAvailable, authAttempted]);

  const handleAuthentication = async () => {
    const success = await authenticate();
    if (success) {
      setIsAuthenticated(true);
    }
    // If auth fails (e.g., user cancellation), we'll stay on the fallback screen,
    // allowing them to retry or navigate away.
  };

  // While checking for hardware, show a loading state.
  if (isLoading) {
    return <div className="p-4 text-center">Checking for biometric capabilities...</div>;
  }

  // If biometrics are not set up on the device, render a clear error state with an exit.
  if (!isAvailable) {
    return (
      <div className="p-4 text-center">
        <h2 className="text-xl font-bold">Biometrics Not Available</h2>
        <p className="my-2">{error || 'Biometric authentication is not configured on this device.'}</p>
        <button 
          onClick={() => navigate(-1)} 
          className="px-4 py-2 bg-gray-200 rounded"
        >
          Go Back
        </button>
      </div>
    );
  }
  
  // If authentication was successful, render the protected content.
  if (isAuthenticated) {
    return <>{children}</>;
  }

  // This is the fallback UI: shown before auth is successful or after it fails.
  return (
    <div className="p-4 text-center">
      <h2 className="text-xl font-bold">Authentication Required</h2>
      <p className="my-2">Please authenticate to view this page.</p>
      <div className="flex justify-center gap-4">
        <button 
          onClick={handleAuthentication} 
          className="px-4 py-2 bg-blue-500 text-white rounded"
        >
          Authenticate with {biometryType || 'Biometrics'}
        </button>
        <button 
          onClick={() => navigate(-1)} 
          className="px-4 py-2 bg-gray-200 rounded"
        >
          Cancel
        </button>
      </div>
    </div>
  );
};

export default BiometricProtectedRoute;

How to Use It

You can now wrap any route's element with this component. For example, in your main router setup (e.g., App.tsx), you could have something like this:

// Example usage in your main router file (e.g., packages/web/src/App.tsx)
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
import BiometricProtectedRoute from './components/BiometricProtectedRoute';

const HomePage = () => (
  <div className="p-4">
    <h1 className="text-2xl">Home Page</h1>
    <Link to="/settings" className="text-blue-500">Go to Protected Settings</Link>
  </div>
);

const SettingsPage = () => (
  <div className="p-4">
    <h1 className="text-2xl">Secret Settings</h1>
    <p>You have been authenticated and can see this content!</p>
  </div>
);

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route 
          path="/settings" 
          element={
            <BiometricProtectedRoute>
              <SettingsPage />
            </BiometricProtectedRoute>
          } 
        />
      </Routes>
    </BrowserRouter>
  );
}

With this setup, navigating to /settings will automatically trigger the biometric prompt. Access is only granted upon successful authentication.

3. The Security Model: What's Happening Under the Hood

The authenticate() function, abstracted by our hook, does more than just show a UI prompt. It initiates a secure cryptographic challenge-response process managed by the device's operating system.

This sequence diagram illustrates a typical challenge-response flow for biometric login. Our Capacitor plugin and the underlying native OS handle this entire secure process for us.

Here’s how it maps to our use case:

  1. Client App → Device: Our call to authenticate() tells the plugin to request authentication.
  2. Device: The OS takes over. It prompts the user for their biometrics (e.g., Face ID, fingerprint).
  3. Secure Enclave: The biometric data is used by the device's secure hardware to unlock a stored private key. This key never leaves the secure hardware.
  4. Device → Client App: The OS simply informs our application whether the authentication succeeded or failed. The underlying cryptographic operations are completely abstracted from us.

This OS-level mediation is what makes the process secure. Our web app code never handles the raw biometric data or the private keys. We are simply asking the OS to verify the user's identity and report back with a yes or no.

4. Alternative: Layout-based Route Protection

For applications where you need to protect an entire group of routes, the "layout route" pattern using react-router-dom's <Outlet /> is more scalable. Instead of wrapping each route individually, you create a parent route that acts as the gatekeeper.

Here's how you could create a BiometricAuthLayout component:

// packages/web/src/components/BiometricAuthLayout.tsx

import React from 'react';
import { Outlet } from 'react-router-dom';
import BiometricProtectedRoute from './BiometricProtectedRoute'; // We can reuse our component!

const BiometricAuthLayout: React.FC = () => {
  // BiometricProtectedRoute already contains all our logic.
  // We just use it to wrap the <Outlet />, which will render the matched child route.
  return (
    <BiometricProtectedRoute>
      <Outlet />
    </BiometricProtectedRoute>
  );
};

export default BiometricAuthLayout;

And your router setup would be adjusted like this:

// Example usage in App.tsx for layout-based protection

// ... imports for HomePage, etc.
import BiometricAuthLayout from './components/BiometricAuthLayout';
import SecretDashboard from './pages/SecretDashboard';
import SecretProfile from './pages/SecretProfile';

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<HomePage />} />
        
        {/* All nested routes are now protected by the same biometric check */}
        <Route element={<BiometricAuthLayout />}>
          <Route path="/dashboard" element={<SecretDashboard />} />
          <Route path="/profile" element={<SecretProfile />} />
        </Route>
      </Routes>
    </BrowserRouter>
  );
}

This approach is cleaner and more maintainable if you have multiple protected routes, as the authentication logic is defined in a single place in your route tree.

Conclusion

In this lesson, you have successfully implemented route-level security using biometrics. By combining our custom useBiometricAuth hook with standard React routing patterns, you've created a robust and user-friendly way to protect sensitive areas of your application.

Here are the key takeaways:

  • Protected Route Patterns: You learned to implement biometric gating using both a component-wrapper approach for single routes and a more scalable layout-based approach for groups of routes.
  • State Management is Key: The BiometricProtectedRoute effectively managed various UI states (loading, unavailable, authenticated, fallback) by leveraging the state exposed by our useBiometricAuth hook.
  • Security is Abstracted: You now have a better understanding of the secure challenge-response model that the Capacitor plugin and native OS manage on your behalf, ensuring that sensitive data is never exposed to your web code.

This concludes our module on integrating core device features. You've now set up your project, run it on native devices, and integrated two powerful plugins for filesystems and biometrics.

In our next module, we will shift our focus from development to deployment and maintenance. We'll start with one of the most powerful features for hybrid apps: Over-the-Air (OTA) updates, which allow you to push updates to your web layer without going through the app store review process.

Can't find a good explanation? Sign up and we'll make it for you

Sign up