DEV Community

Teddy ASSIH
Teddy ASSIH

Posted on

Shipping a Nextjs app in two days with Hanko Auth elements

Hanko Gif


As part of building modern web applications, authentication is a critical aspect, and developers often seek secure, user-friendly, and flexible solutions to handle it. In my recent Next.js project, I chose to integrate Hanko, an authentication provider that allow you integrate auth and passkeys in minutes.

In this article, I’ll walk you through how I used Hanko in my app to implement secure, both password and passwordless authentication for users, cause yes, Hanko can do both :).


Why I Chose Hanko

Hanko is a powerful authentication solution that provides:

  • Free 10,000 monthly active users: You can start for free with passkeys, social logins, email passcodes, optional passwords and user management.
  • Passwordless authentication: No need to store passwords, reducing security risks.
  • Easy to understand an clear documentation: Hanko has probably the best documentation I've ever seen. It guets you started quickly with their framework specific quickstarts so that you can ship fast.

Given the project’s focus on security and ease of use, Hanko was the perfect choice for my app.


What is my App about?

To Don't App is a not so common to do App where instead of adding the tasks you have to do, you add the ones or the habits you have to avoid. The Hanko auth here helps keeping the user data safe as it is persisted in a mongodb database.
It's Live here🎉.

Step-by-Step Integration of Hanko in my App

Here’s how I integrated Hanko into my To Don't app:

1. Setting Up the Next.js Project

I started by creating the Next.js project. If you haven't done so already, you can set up a new Next.js project using the following commands:

npx create-next-app@latest next-todont-app
cd next-todont-app
Enter fullscreen mode Exit fullscreen mode

2. Creating an Account on Hanko

To integrate Hanko, I first needed to set up a Hanko account. I signed up on their platform and created a new project to obtain the necessary API URL for authentication.

1. Sign Up at Hanko.

2. Create a new project and retrieve your API URL.

3. Installing Hanko SDK

I installed the Hanko SDK in my Next.js app to handle the authentication flow.

npm install @teamhanko/hanko-elements
Enter fullscreen mode Exit fullscreen mode

This package provides the client-side tools for registering, authenticating, and managing users.

4. Setting environment variables

I created an .env.local file in my project root and added my Hanko API URL:

NEXT_PUBLIC_HANKO_API_URL=https://f4****-4802-49ad-8e0b-3d3****ab32.hanko.io
Enter fullscreen mode Exit fullscreen mode

This ensures that the authentication service can securely communicate with Hanko's API.

5. Building the Authentication Pages

Login

To allow users to register and login, I created a page integrating the <hanko-auth> web component that adds these two interfaces.

First, I imported the register function from @teamhanko/hanko-elements into my Next.js component. Called it with the Hanko API URL as an argument to register <hanko-auth>. Once done, I added it in my JSX. Here's a snippet for the component:
components/HankoAuth.tsx

"use client";

import { useEffect, useCallback, useState } from "react";
import { useRouter } from "next/navigation";
import { register, Hanko } from "@teamhanko/hanko-elements";

const hankoApi = process.env.NEXT_PUBLIC_HANKO_API_URL;

export default function HankoAuth() {
  const router = useRouter();

  const [hanko, setHanko] = useState<Hanko>();

  useEffect(() => setHanko(new Hanko(hankoApi)), []);

  const redirectAfterLogin = useCallback(() => {
    // successfully logged in, redirect to a page in your application
    router.replace("/dashboard");
  }, [router]);

  useEffect(
    () =>
      hanko?.onSessionCreated(() => {
        redirectAfterLogin();
      }),
    [hanko, redirectAfterLogin]
  );

  useEffect(() => {
    register(hankoApi).catch((error) => {
      // handle error
    });
  }, []);

  return <hanko-auth />;
}
Enter fullscreen mode Exit fullscreen mode

Next, I added the component to the login page:
app/login/page.tsx

import HankoAuth from "@/components/HankoAuth";

export default function LoginPage() {
 return (
   <div className="flex justify-center items-center h-screen">
    <HankoAuth />
   </div>
 );
}
Enter fullscreen mode Exit fullscreen mode

And BOOM 🎉. Just like that, the sign in and sign up features are done. Here is what it looks like 👇

Login Page

Profile Page

For users to see their account infos, I created a page using the <hanko-profile> component. This component provides an interface, where users can manage their email addresses and passkeys.
components/HankoProfile.jsx

"use client"

import { useEffect } from "react";
import { register } from "@teamhanko/hanko-elements";

const hankoApi = process.env.NEXT_PUBLIC_HANKO_API_URL;

export default function HankoProfile() {
  useEffect(() => {
    register(hankoApi).catch((error) => {
      // handle error
    });
  }, []);

return <hanko-profile />;
}
Enter fullscreen mode Exit fullscreen mode

Here is Profile page:
app/profile/page.tsx

import React from "react";
import HankoProfile from "@/components/profile";

export default function ProfilePage() {
  return (
    <div>
        <HankoProfile />
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Look at this beauty :):

profile page

Logout

Now for the Logout. I used @teamhanko/hanko-elements to easily manage user logouts. Below, I created a logout button component I used on my dashboard.
components/LogoutButton.tsx

"use client";

import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { Hanko } from "@teamhanko/hanko-elements";

const hankoApi = process.env.NEXT_PUBLIC_HANKO_API_URL;

export default function LogoutBtn() {
  const router = useRouter();
  const [hanko, setHanko] = useState<Hanko>();

  useEffect(() => setHanko(new Hanko(hankoApi ?? "")), []);

  const logout = async () => {
    try {
      await hanko?.user.logout();
      router.push("/login");
      router.refresh();
      return;
    } catch (error) {
      console.error("Error during logout:", error);
    }
  };

  return <button onClick={logout}>Logout</button>;
}
Enter fullscreen mode Exit fullscreen mode
Customization

To make it look seggsy, I customized the Hanko elements to make the app's UI uniform. To achieve this, i created a dedicated CSS file for the Hanko elements. For further explanation, I really recommend these two articles: Hanko Docs and Esther-Lita post
src/app/hanko.css

:root {
    --color: #111827;
    --color-shade-1: #222222;
    --color-shade-2: #eff5ff;
    --brand-color: #000000e8;
    --background-color: #ffffff;
    --headline1-font-size: 29px;
    --headline2-font-size: 16px;
    --headline1-margin: 10px 40px;
    --border-radius: 6px;
    --font-size: 14px;
    --item-height: 36px;
    --link-color: #111827;
    --divider-padding: 0 15px;
}

hanko-auth {
    --container-max-width: 640px;
    --container-padding: 40px;
}

hanko-profile {
    --container-max-width: 550px;
    --container-padding: 30px;
}


hanko-profile::part(container) {
    border: solid 1px #d8dee3;
    border-radius: 6px;
}

hanko-auth::part(button):hover {
    background-color: #848587;
    border: #848587 solid 1px;
}


hanko-auth::part(headline1),
hanko-profile::part(headline1),
hanko-auth::part(headline2),
hanko-profile::part(headline2) {
    color: #1f2328;
    line-height: 36px;
    text-align: center;
}

hanko-auth::part(form-item) {
    min-width: 100%;
}

hanko-auth::part(input),
hanko-profile::part(input) {
    background-color: #ffffff;
    margin-bottom: 12px;
}

hanko-auth::part(secondary-button) {
    border-color: var(--brand-color--);
}

hanko-auth::part(divider) {
    margin: 24px 0;
    color: var(--brand-color);
}
Enter fullscreen mode Exit fullscreen mode

Here is what it looks like now:
Login:

Login after custom

Profile:

profile after custom

6. Securing API Routes

I ensured that sensitive API routes (like those managing to-do items) were protected by checking whether the user was authenticated before allowing access. This was done by adding middleware that verifies if there's valid JWT and using the jose library. However, you’re free to choose any other suitable library.
middleware.ts

import { NextResponse, NextRequest } from "next/server";

import { jwtVerify, createRemoteJWKSet } from "jose";

const hankoApiUrl = process.env.NEXT_PUBLIC_HANKO_API_URL;

export async function middleware(req: NextRequest) {
  const hanko = req.cookies.get("hanko")?.value;

  const JWKS = createRemoteJWKSet(
    new URL(`${hankoApiUrl}/.well-known/jwks.json`)
  );

  try {
    const verifiedJWT = await jwtVerify(hanko ?? "", JWKS);
  } catch {
    return NextResponse.redirect(new URL("/login", req.url));
  }
}

export const config = {
  matcher: ["profile","/dashboard"],
};
Enter fullscreen mode Exit fullscreen mode

This middleware ensures that only authenticated users can access certain API endpoints.

7. Getting User data

To Link th "to-donts" to a specific user, I needed to get the user Id from Hanko. To achieve that, I created a custom hook to fetch the current user’s data using @teamhanko/hanko-elements. The useUserData hook leverages the hanko.user.getCurrent() method to retrieve the currently logged-in user’s data. I just needed the user Id, but you can get more.
hooks/useUserData.ts

import { useState, useEffect } from "react";
import { Hanko } from "@teamhanko/hanko-elements";

const hankoApi = process.env.NEXT_PUBLIC_HANKO_API_URL || "";

// I just needed the user id here
interface HankoUser {
  id: string;
  loading: boolean;
  error: string | null;
}

export function useUserData(): HankoUser {
  const [hanko, setHanko] = useState<Hanko>();
  const [userState, setUserState] = useState<HankoUser>({
    id: "",
    loading: true,
    error: null,
  });

  useEffect(() => setHanko(new Hanko(hankoApi)), []);

  useEffect(() => {
    hanko?.user
      .getCurrent()
      .then(({ id }) => {
        setUserState({ id, loading: false, error: null });
      })
      .catch((error) => {
        setUserState((prevState) => ({ ...prevState, loading: false, error }));
      });
  }, [hanko]);

  return userState;
}
Enter fullscreen mode Exit fullscreen mode

And here, I used it in my dashboard page:
src/app/dashboard/page.tsx

"use client"

import { useUserData } from "@/hooks/useUserData";
import { useSessionData } from "@/hooks/useSessionData";

const Dashboard = () => {
  const { id, loading: userDataLoading, error: userDataError } = useUserData();

  if (userDataLoading) {
    return <div>Loading...</div>;
  }

  return (
    <div>
      <div>User id: {id}</div>
    </div>
  );
};

export default Dashboard;
Enter fullscreen mode Exit fullscreen mode

8. Deploying on Vercel

After testing the app locally, I deployed it to Vercel for production. Here's how I did it:

  1. Commit and push the code to a GitHub repository.
  2. Link the repository to Vercel for automatic deployment.
  3. Configure the environment variables (Hanko API URL and database related variables) in the Vercel dashboard.
  4. Deploy the app.

Integrating Hanko in my Next.js app made the authentication process much more secure, user-friendly and fast. With the rise of passwordless authentication and biometrics, Hanko’s approach provides an excellent way to enhance security while offering users a simple and modern login experience.

If you're looking for a fast authentication solution in your Next.js project, I highly recommend giving Hanko a try! Try Hanko now.

Top comments (0)