DEV Community

Cover image for How to build a secure voting web app with FACEIO
Ekemini Samuel
Ekemini Samuel

Posted on • Originally published at envitab.Medium

How to build a secure voting web app with FACEIO

In this tutorial, we are building an online voting web app with Next.js and FACEIO, where users can register, verify their age, authenticate their identity via facial recognition, and cast their votes on community projects.

This project will use FACEIO's SDK to ensure the authenticity and security of each vote.

At the end of this tutorial, this is what the voting page will look like, which can only be accessed after authentication with FACEIO 😀

vote app

Ready? Let's get started!

Prerequisites

What is FACEIO?

FACEIO is a facial authentication framework that can be implemented on any website with JavaScript to easily authenticate users via Face Recognition instead of using a login/password or OTP code.

Key Features of FACEIO to be implemented in the web app:

  • Age Verification: Ensure voters meet the age requirement of 18 years and above.
  • Facial Authentication: Authenticate voters using facial recognition.
  • Anti-Spoofing & Deepfake Prevention: Prevent fraudulent voting attempts with advanced security measures.

Learn more in the FACEIO documentation

FACEIO implements these in a single SDK so web developers can secure their web apps with FACEIO.

Let's begin!

Step 1: Developing the Client-Side: Setting Up Your App Router

Open your terminal, navigate to your workspace folder, and run the command below:

npx create-next-app@latest secure-voting
Enter fullscreen mode Exit fullscreen mode

This creates a new project secure-voting, which we use to build our application.

Select Yes for all the prompts as shown below (except the last one, as the default import alias is recommended) to install Typescript, TailwindCSS, and other dependencies needed for the Next.js project.

next

Navigate to the project directory like so:

cd secure-voting
Enter fullscreen mode Exit fullscreen mode

And run this command to open the project in VS Code editor:

code .
Enter fullscreen mode Exit fullscreen mode

To start building the client side of your secure voting web app, you'll first need to set up the application's routing. You'll create three essential pages:Home, Login, and Register. These pages will guide your users of the app through account creation, authentication, and participation in voting.

Creating the Homepage

First, create a root component to manage rendering the Homepage. This component will ensure that the web app checks whether the document environment is initialized before rendering the Homepage.

This is important to avoid errors related to undefined document objects, especially when using Next.js.

In the app\(Home) create a page.js file and enter the code below:

page

"use client";
import Home from "./_components";
import { useState, useEffect } from "react";

export default function Root() {
  const [docEnv, setDocEnv] = useState(false);

  useEffect(() => {
    if (typeof document !== "undefined") {
      setDocEnv(true);
    }
  }, []);

  return <div className="relative">{docEnv && <Home />}</div>;
}


Enter fullscreen mode Exit fullscreen mode

Let’s break down what this code does:

  • The useState and useEffect hooks from the React library tracks whether the document environment (docEnv) is initialized and updates the state once the component is loaded.

  • useEffect also ensures the document object is defined, and the Home component only renders when the app is running in a browser environment, preventing issues with server-side rendering in Next.js.

Step 2: Building the Home Page Components

In this section, you'll implement the logic for your Home page. The goal is to ensure that users who aren't logged in are redirected to the Login page, while authenticated users can stay on the Home page. Additionally, you'll implement functionality to check voting eligibility based on user information.

Checking User Authentication

To verify if a user is logged in and has valid credentials, you must check if userInfo is stored in the localStorage. If userInfo is not found, you'll redirect the user to the Login page.

In the app\(Home)\_components directory, create a page.js file and enter the code below:

  const router = useRouter();

  let userinfo;
  if (typeof window !== "undefined") {
    userinfo = JSON.parse(localStorage.getItem("userinfo"));
  }
  console.log(userinfo);
  useEffect(() => {
    if (!userinfo) {
      router.push("/login");
    }
  }, [userinfo, router]);

Enter fullscreen mode Exit fullscreen mode

As we are building the homepage and other components of the voting app, we have to look for the userInfo details located and stored in the localStorage with the Key userInfo

The main idea is that if this data stored with the key userInfo is null, the user should be directed to the login page, and still be on the Homepage. So, we use the router.push('/login) method from the useRouter hook to achieve this.

Also, we must check if the window object has been defined before setting the value from the localStorage to the userInfo variable. Without doing this, the whole initialization would be flagged as an error by Nextjs.

When the user has been authenticated on the login page, the next step is to create a function to check for the user's eligibility through the userInfo object.

Here is the function:

  const handleVotingEligibility = () => {
    if (userinfo?.details?.age < 18) {
      toast.error("You are not eligible to vote");
    } else {
      toast.error("You are eligible to vote");
    }
  };

Enter fullscreen mode Exit fullscreen mode

💡 Note that the handleVotingEligibility function will interact with the details from the FACEIO verification of the user when signing up.
If the user's age is under 18, it displays an error message; otherwise, it confirms eligibility.
The voting application does not directly ask for the user's age; the FACEIO SDK automates this process, and the result is sent to the function, which allows the user to get registered on the platform or not.

Step 3: Developing the Login page

In the Login screen, we have to check that the id of the document has been initialized. If the document has been initialized, we render the home components.

Create another page.js file, but this time in the app\login\ directory, and enter the code below:

"use client";
import Login from "./_components";
import { useState, useEffect } from "react";

export default function Root() {
  const [docEnv, setDocEnv] = useState(false);

  useEffect(() => {
    if (typeof document !== "undefined") {
      setDocEnv(true);
    }
  }, []);

  return <div className="relative">{docEnv && <Login />}</div>;
}

Enter fullscreen mode Exit fullscreen mode
  • The "use client"; directive ensures that the code is executed only on the client side, not during server-side rendering.

  • useState hook initializes docEnv to false, which indicates whether the document environment has been initialized, while the useEffect hook checks if document is defined to confirm the code is running in a client environment. If so, it sets docEnv to true.

  • The Login component is rendered conditionally only if docEnv is true, ensuring it only loads after the document environment is ready.

Step 4: Building the Login component logic to integrate with FACEIO

Before we proceed with the logic component, follow this steps to setup your account on FACEIO:

FaceIO Console

  • On the FACEIO dashboard, click on New Application to create a FACEIO application, and follow the steps in the application wizard.

When asked to select a Facial Recognition Engine for the application, choose PixLab Insight. The engine will be used for mapping each enrolled user’s face in real-time into a mathematical feature vector, known as a biometric hash, which is, in turn, stored in a sand-boxed binary index. This is used to protect the data of users when they are authenticated with FACEIO.

facio app

  • Your new FACEIO app will have an ID number; take note of it, as we'll use it to integrate FACEIO into the web application.

FACEIO appID

Also note the API key, as it'll be used in the application

API Key

  • In the root directory of your Next.js project, create a new file named .env.local and enter this to define the environment variable:
NEXT_PUBLIC_FACE_IO_ID=your_faceio_app_id_here
NEXT_PUBLIC_KEY=your_faceio_api_key
Enter fullscreen mode Exit fullscreen mode

Replace "your_faceio_app_id_here" with your FACEIO application ID and "your_faceio_api_key" with your FACEIO API key, then add .env.local to your .gitignore file

Now that's done, let's proceed

To set up the Login component, we must integrate the FACEIO SDK and handle user input efficiently.

Run this command in your terminal to install the FACEIO/fiojs NPM package installed for handling facial authentication.

npm i @faceio/fiojs
Enter fullscreen mode Exit fullscreen mode

When it is installed, create an index.jsx file in the app\login\_components directory and initialize FACEIO.

Enter the code like so:

import faceIO from "@faceio/fiojs";
let faceio = new faceIO(process.env.NEXT_PUBLIC_KEY);
Enter fullscreen mode Exit fullscreen mode

In the code above, we import the faceIO SDK and then initialize it to a variable.

The API key is stored in the environment variables (.env) file under NEXT_PUBLIC_KEY.

Next, set up the form input states to capture the user's email and PIN. These form inputs are managed by creating a state variable called payload. The variable will hold the email and pin values.

We define an onChange function that dynamically updates the payload state based on the user’s input to handle changes in the input fields.

Enter this code in the index.jsx file stated above

const [payload, setPayload] = useState({ email: "", pin: "" });

const onChange = (e) => {
  setPayload({ ...payload, [e.target.name]: e.target.value });
};
Enter fullscreen mode Exit fullscreen mode
  • The useState hook initializes payload with empty strings for email and pin.
  • The onChange function updates the payload state with the values entered in the form fields, setting either email or pin based on the name attribute of the input element.

This configuration prepares the Login component for user authentication and input - which is the SignUp/Login process in the voting web app.

Step 5: Handling User Authentication Logic

With the onChange function in place to handle user input, the next step is to implement user authentication. This involves creating a function that interacts with the FACEIO SDK to authenticate users based on their email and PIN.

First, define the AuthenticateUser function. This function will send the user's credentials to FACEIO for authentication.

Still in the index.jsx file, enter this code:

 const AuthenticateUser = async () => {
  try {
    const userInfo = await faceio.enroll({
      locale: "auto",
      token: process.env.NEXT_PUBLIC_FACE_IO_ID,  // we use the environment variable here

      payload: {
        email: payload.email,
        pin: payload.pin,
      },
    });

     // Store user info in localStorage
    localStorage.setItem("userinfo", JSON.stringify(userInfo));

    // Navigate to the home page upon successful authentication
    if (userInfo) {
      router.push("/");
    }

    console.log(userInfo);
  } catch (error) {
    console.error("Enrollment failed:", error);
  }
};


Enter fullscreen mode Exit fullscreen mode

In this function:

  • The faceio.enroll method sends the payload containing the email and PIN to FACEIO for authentication.
  • Upon successful authentication, the returned userInfo is stored in localStorage for session management.
  • If the authentication is successful, the user is redirected to the home page using router.push("/").
  • If an error occurs, it is logged to the console.

Next, we need a way to trigger this authentication process when the user submits the login form. To do this, implement the handleSubmit function:

Yes 😎, enter this code in the same index.jsx file @app\login\_components

  const handleSubmit = async (e) => {
  e.preventDefault(); // Prevent default form submission behavior

  // Check if both email and PIN fields are filled
  if (!payload?.email || !payload?.pin) {
    toast.error("Please fill in the user credentials");
  } else {
    // Proceed with authentication if fields are valid
    AuthenticateUser();
  }
};

Enter fullscreen mode Exit fullscreen mode

In this function:

  • The e.preventDefault() call prevents the default form submission, which would otherwise reload the page.
  • It checks if both the email and pin fields are filled. If either is missing, an error message is displayed using toast.error.
  • If both fields are provided, the AuthenticateUser function is called to process the authentication.

Step 6: Redirecting after the FACEIO Authentication

After a user successfully logs in, we must ensure they are redirected to the homepage if their authentication is valid.

This process involves checking if the userInfo data, which is stored in localStorage by FACEIO, exists. If it does, the user should be redirected to the homepage.

Enter the code below @index.jsx file to implement this in the useEffect hook:

useEffect(() => {
  // Ensure that the window object is defined before accessing localStorage
  if (typeof window !== "undefined") {
    // Retrieve user information from localStorage
    const userinfo = localStorage.getItem("userinfo");

    // Redirect to the home page if userInfo exists
    if (userinfo !== null) {
      router.push("/");
    }
  }
}, [router]);

Enter fullscreen mode Exit fullscreen mode
  • The window object is defined to ensure the code runs only on the client side. This avoids issues during server-side rendering.
  • userInfo data is retrieved from localStorage, which was set by FACEIO during the authentication process. FACEIO’s enroll method not only performs facial recognition but also stores user information, ensuring secure access.
  • If the userInfo data is found (i.e., it is not null), it indicates that FACEIO has successfully authenticated the user. As a result, we use router.push("/") to redirect the user to the home page.

Step 7: Integrating Components and UI Elements

Next, we'll integrate components and some UI elements for the application.

In the app directory, create a register/_components directory and then an index.jsx file. This will contain the main components of the login interface.

First, let’s define the structure of the Login component. This component renders the Navbar and MainContent components, which together form the layout of the login page.

"use client";
import React, { useEffect, useState } from "react";
import { Camera, Lock } from "lucide-react";
import faceIO from "@faceio/fiojs";
import { useRouter } from "next/navigation";
import Link from "next/link";
import toast from "react-hot-toast";
Enter fullscreen mode Exit fullscreen mode

In the code above, we import necessary modules and components. faceIO from @faceio/fiojs is imported to handle the facial recognition authentication.

const Login = () => {
  return (
    <div className="w-full">
      <Navbar />
      <MainContent />
      {/* <Footer/> */}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

The Login component uses a layout that includes a Navbar and MainContent section. You can also include a footer if needed.

Navbar Component

const Navbar = () => {
  const navbarlist = ["About", "Contact"];

  return (
    <div className="w-full py-6 bg-[#000] flex">
      <div className="w-full px-8 flex items-center justify-between">
        <span className="text-2xl font-bold flex items-center gap-3 text-white">
          <Lock />
          Secure Voting
        </span>
        <div className="flex items-center gap-8 justify-end">
          <div className="hidden lg:flex items-center gap-8">
            {navbarlist?.map((nav, index) => {
              return (
                <Link
                  key={index}
                  className="text-base font-normal text-[#fff]"
                  href={"/"}
                >
                  {nav}
                </Link>
              );
            })}
          </div>
        </div>
      </div>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

The Navbar component provides navigation links like "About" and "Contact." It uses Link from Next.js to handle routing.

MainContent Component

const MainContent = () => {
  const router = useRouter();
  const [payload, setPayload] = useState({ email: "", pin: "" });
  const [isSigningUp, setIsSigningUp] = useState(false); // New state variable

  const onChange = (e) => {
    setPayload({ ...payload, [e.target.name]: e.target.value });
  };

  const faceio = new faceIO(process.env.NEXT_PUBLIC_KEY);

  let faceInfo;
  if (typeof window !== "undefined") {
    faceInfo = localStorage.getItem("faceId");
  }

  const toggleSignUpPage = () => {
    setIsSigningUp((prevState) => !prevState);
  };

  const registerNewUser = async () => {
    try {
      const userInfo = await faceio.enroll({
        locale: "auto",
        token: "fioaf212",
        payload: {
          email: payload.email,
          pin: payload.pin,
        },
      });

      localStorage.setItem("userinfo", JSON.stringify(userInfo));
      if (userInfo) {
        router.push("/");
      }

      console.log(userInfo);
    } catch (error) {
      console.error("Enrollment failed:", error);
    }
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    if (!payload?.email && !payload?.pin) {
      toast.error("Please fill in the user credentials");
    } else {
      registerNewUser();
    }
  };

  useEffect(() => {
    if (typeof window !== "undefined") {
      const userinfo = localStorage.getItem("userinfo");
      if (userinfo !== null) {
        router.push("/");
      }
    }
  }, [router]);

  return (
    <div className="w-full min-h-[100vh] items-center justify-center py-24 flex flex-col gap-4">
      <div className="w-[90%] rounded-2xl md:w-[500px] mx-auto border py-12 px-8 flex flex-col gap-8 ">
        <span className="text-3xl md:text-4xl font-bold gap-3 text-[#000]">
          Sign Up
          <span className="block font-normal text-base pt-2 text-grey">
            Create your account
          </span>
        </span>
        <form onSubmit={handleSubmit} className="flex flex-col gap-4">
          {LoginFormInputData.map((form, index) => (
            <label
              key={index}
              htmlFor={form?.label}
              className="text-base flex flex-col gap-2 font-semibold"
            >
              {form?.label}
              <input
                type={form?.type}
                onChange={onChange}
                name={form?.name}
                value={payload[form?.name]}
                placeholder={form?.placeholder}
                className="font-normal px-6 rounded-md w-full h-[50px] border outline-none"
              />
            </label>
          ))}

          <button
            type="submit"
            className="p-3 hover:opacity-[.6] cursor-pointer px-4 rounded-sm text-base text-center font-semibold text-[#fff] bg-[#000]"
          >
            Sign up
          </button>
        </form>
        <div className="flex px-4">
          <span className="flex text-base items-center gap-2">
            Already a member?{" "}
            <Link href={"/login"} className="font-bold underline">
              Sign In
            </Link>
          </span>
        </div>
      </div>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

In the MainContent component:

  • faceIO is initialized using the key from the environment variables.
  • useState manages form inputs (payload) and a state for toggling sign-up pages (isSigningUp).
  • onChange function updates the form state dynamically as the user types.
  • registerNewUser function handles the registration process using FACEIO's enroll method. If successful, it stores the user information in localStorage and redirects to the home page.
  • handleSubmit function prevents the default form submission behavior, validates the inputs, and calls registerNewUser.
  • useEffect checks if userInfo is present in localStorage and redirects to the home page if it is.

This setup ensures that FACEIO’s authentication is integrated into the login page, handling user input and redirection based on the authentication status.

Running the application

Run this command in your terminal to start the application:

npm run dev
Enter fullscreen mode Exit fullscreen mode

Then copy this URL and open it in your browser:
http://localhost:3000

The secure voting app loads like so:

sign up


To proceed, enter your email, set a password (that you can remember), and click on Sign Up

Then, the FACEIO process begins like so:

prompt

Accept the terms & conditions, allow access to your camera, and get authenticated:

Accept

You will also be prompted to set a PIN code of at least 4 digits and no more than 16 digits

auth

When the authentication is successful, and FACEIO confirms you are above 18 years, you can proceed to view the community voting page! 😎

vote now

In your FACEIO console dashboard, you can also view the recently enrolled users and their indexed facial IDs to monitor and keep track of the application.

console

And that's it! We have successfully integrated facial authentication using FACEIO into a voting application!

To clone the project and run it locally, open your terminal and run this command:

git clone https://github.com/Tabintel/secure-voting
Enter fullscreen mode Exit fullscreen mode

Then run npm install to install all dependencies needed for the project and npm run dev to run the web app.

Get the full source code on GitHub.

If you’re looking to add cutting-edge security features to your own projects, sign up on FACEIO today. It’s a powerful tool for easily integrating facial recognition into your applications.

Thank you for reading! 🍻

Resources

Top comments (0)