Clerk Auth

This quick start guide will walk you through how to quickly get setup with the Passport SDK with Clerky account, register and authenticate users, and start signing messages and transactions. You can skip to the Complete Setup, and then follow the section step-by-step where each code snippet will be explained. You can also quickly pull the completed repository here https://github.com/0xpass/passport-clerk-quickstart and follow along.

Project setup

First we'll setup a Next.js project, with tailwind, we can do this with the following command, and walk through the configuration wizard, making sure to chose the app-router, and tailwind.

npx create-next-app@latest

Setup and Dependencies

Firstly, we'll ensure we have all the requires dependencies. So we're going to install the Passport SDK @0xpass/passport to interact with Passport protocol, and @clerk/nextjs for third party authentication. We'll also install some helper packages @0xpass/key-signer to handle our signature management for delegated registration and authentication.

npm install @0xpass/passport @0xpass/key-signer @clerk/nextjs@4.2.9 viem@2.9.4

Environment Variables and Generating Developer Keys

In your project root, copy and paste the environment variables below.

NEXT_PUBLIC_SCOPE_ID=""
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=""
CLERK_SECRET_KEY=""
PRIVATE_KEY=""

Going one by one,

NEXT_PUBLIC_SCOPE_ID can get obtained from Configuring your scope

You should grab your own NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY, and CLERK_SECRET_KEY from your Clerk account.

For Developer Key (PRIVATE_KEY), follow Generating DOA Keys for generating your own keys. We'll refer to the private key generated in this step as PRIVATE_KEY environment variable in the example below.

Backend APIs

Firstly let's setup backend API for creating an account in passport protocol upon successful authentication via Clerk.

Authentication Flow:

Signing Flow:

This is not a part of backend, but you need a page that takes the callback from clerk, and redirects to /api/user-callback. In app/auth/callback we'll have the following snippet so that frontend calls the backend endpoint with user id and email address upon successful sign-in.

"use client";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
import { useUser } from "@clerk/nextjs";

export default function CallbackPage() {
  const router = useRouter();
  const { isLoaded, isSignedIn, user } = useUser();

  useEffect(() => {
    if (isLoaded && isSignedIn) {
      fetch("/api/user-callback", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          userId: user.id,
          emailAddress: user.emailAddresses[0].emailAddress,
        }),
      })
        .then((response) => response.json())
        .then(() => {
          router.push("/");
        })
        .catch((error) => {
          console.error("Error:", error);
        });
    }
  }, [isLoaded, isSignedIn, user, router]);

  return <div>Processing...</div>;
}

We'll create a file at:app/api/user-callback/route.ts Which will create a Passport instance with KeySigner with your private key. PRIVATE_KEY environment variable can point to a file path or the actual private key. Note that the private key should only be handled in your backend, and can't be exposed publicly.

import { NextResponse } from "next/server";
import { KeySigner } from "@0xpass/key-signer";
import { Passport } from "@0xpass/passport";

export async function POST(req: Request) {
  const payload = await req.json();
  const keySigner = new KeySigner(process.env.PRIVATE_KEY!, true);

  const passport = new Passport({
    scope_id: process.env.NEXT_PUBLIC_SCOPE_ID!,
    signer: keySigner,
  });

  await passport.setupEncryption();
  const data = await passport.delegatedRegisterAccount({
    username: payload.emailAddress,
  });

  return NextResponse.json(data);
}

After authenticating with Clerk, you can directly interact with Passport with the username (email from Clerk) and your developer keys.

Now we'll implement get-accountendpioint at app/api/get-account/route.ts. For retrieving your user's email address you can use currentUser() function which will grab user information from the session established with Clerk.

import { currentUser } from "@clerk/nextjs/server";
import { KeySigner } from "@0xpass/key-signer";
import { Passport } from "@0xpass/passport";

export async function GET() {
  try {
    const user = await currentUser();
    if (!user) {
      return new Response("Unauthorized", { status: 401 });
    }

    const keySigner = new KeySigner(process.env.PRIVATE_KEY!, true);
    const passport = new Passport({
      scope_id: process.env.NEXT_PUBLIC_SCOPE_ID!,
      signer: keySigner,
    });

    passport.setUserData({ username: user.emailAddresses[0].emailAddress });
    await passport.setupEncryption();
    const addresses = await passport.getAddresses();
    return new Response(JSON.stringify(addresses), { status: 200 });
  } catch (error) {
    console.log(error);
    return new Response("Something went wrong", { status: 500 });
  }
}

Lastly, we're using signing message as an example of user action. In app/api/sign/route.ts, implement the following function.

import { NextResponse } from "next/server";
import { currentUser } from "@clerk/nextjs/server";
import { KeySigner } from "@0xpass/key-signer";
import { Passport } from "@0xpass/passport";
import { stringToHex } from "viem";

export async function POST(req: Request) {
  const payload = await req.json();
  const { type, data } = payload;

  const user = await currentUser();

  if (!user) {
    return new Response("Unauthorized", { status: 401 });
  }

  const keySigner = new KeySigner(process.env.PRIVATE_KEY!, true);
  const passport = new Passport({
    scope_id: process.env.NEXT_PUBLIC_SCOPE_ID!,
    signer: keySigner,
  });

  passport.setUserData({ username: user.emailAddresses[0].emailAddress });
  await passport.setupEncryption();
  const signature = await passport.signMessage(stringToHex(data));
  return NextResponse.json({ signature });
}

Middleware

Under app directory, we need a middleware to keep the authenticated user context across the backend apis and the frontend.

import { authMiddleware } from "@clerk/nextjs/server";

export default authMiddleware({
  publicRoutes: ["/", "/auth/callback", "/lambda"],
});

export const config = {
  matcher: ["/((?!.+\\.[\\w]+$|_next).*)", "/", "/(api|trpc)(.*)"],
};

Frontend

Clerk Providers

First we'll need to make sure we wrap our application inside the ClerkProvider . We can do this by creating a new file providers.tsx inside the src directory, and populating it as below.

"use client";
import { ClerkProvider } from "@clerk/nextjs";

export function Providers({ children }: { children: JSX.Element }) {
  return <ClerkProvider>{children}</ClerkProvider>;
}

Then we can now wrap our app with our Provider by going to src/layout.tsx and adjusting it so it looks like this

import { Inter } from "next/font/google";
import { Providers } from "./providers";
import localFont from "next/font/local";
import "./globals.css";

const myFont = localFont({ src: "../../public/fonts/PPNeueMontreal-Book.otf" });

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <Providers>
        <body className={`${myFont.className} bg-black text-white`}>
          {children}
        </body>
      </Providers>
    </html>
  );
}

Now in our page.tsx we can setup our state variables for, tracking registering, and authenticating users.

"use client";
import { useState } from "react";
import { SignUpButton, useUser, SignOutButton } from "@clerk/nextjs";
import { useEffect } from "react";

export default function Page() {
  const { isSignedIn, isLoaded } = useUser();
  const [authenticated, setAuthenticated] = useState(false);
  const [signMessageLoading, setSignMessageLoading] = useState(false);
  const [message, setMessage] = useState("");
  const [messageSignature, setMessageSignature] = useState({
    signature: "",
    timeTaken: 0,
  });
  const [address, setAddress] = useState<string>();
  const [addressLoading, setAddressLoading] = useState(false);

For fetching the address of the current user, we'll use the function below

  const fetchDelegatedAddress = async () => {
    setAddressLoading(true);
    try {
      if (!addressLoading) {
        const response = await fetch("/api/get-account", {
          method: "GET",
        });

        if (response.ok) {
          const addresses = await response.json();
          setAddress(addresses.result[0]);
        }
      }
    } catch (error) {
      console.log(error);
    } finally {
      setAddressLoading(false);
    }
  };

and we'll trigger this function once when the page loads:

  useEffect(() => {
    if (isSignedIn) {
      fetchDelegatedAddress();
    }
  }, [isSignedIn]);

We also need a handler for when user click on sign button:

  async function delegatedSignMessage(message: string) {
    setSignMessageLoading(true);
    try {
      const startTime = performance.now();
      let response = await fetch("/api/sign", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          type: "message",
          data: message,
        }),
      });

      if (response.ok) {
        const { signature } = await response.json();
        const timeTaken = performance.now() - startTime;
        setMessageSignature({
          signature: signature.result,
          timeTaken: timeTaken,
        });
      } else {
        throw Error(`HTTP error: ${response}`);
      }
    } catch (e) {
      console.error(e);
    } finally {
      setSignMessageLoading(false);
    }
  }

Lastly, we'll work on the presentation of this page.

Before we get to the main part, we'll display a spinner to wait for Clerk to determine whether this person is authenticated or not.

  if (!isLoaded) {
    return (
      <div className="h-screen w-full flex items-center justify-center">
        <svg
          className="animate-spin h-12 w-12 text-white"
          xmlns="http://www.w3.org/2000/svg"
          fill="none"
          viewBox="0 0 24 24"
        >
          <circle
            className="opacity-25"
            cx="12"
            cy="12"
            r="10"
            stroke="currentColor"
            strokeWidth="4"
          ></circle>
          <path
            className="opacity-75"
            fill="currentColor"
            d="M22 12c0-5.522-4.477-10-10-10-1.065 0-2.098.166-3.051.47l1.564 1.564A8 8 0 0112 4c4.418 0 8 3.582 8 8h-2z"
          ></path>
        </svg>
      </div>
    );
  }

Lastly we have a simple button and input interfaces for signing in and submitting signature requests!


  return (
   <div className="flex flex-col items-center justify-center min-h-screen py-2 bg-white text-black">
      <div
        className={`text-2xl font-bold mb-8 ${
          authenticated ? "text-green-500" : "text-red-500"
        }`}
      >
        {authenticated ? "Authenticated" : "Not authenticated"}
      </div>
      <div className="text-center">
        <h1 className="text-3xl font-bold underline">
          Passport Protocol with Clerk Auth Quickstart
        </h1>
        <p className="mt-2 text-lg">
          This is a quickstart guide for the Passport Protocol SDK.
        </p>

        <div className="flex flex-col mt-4 space-y-4">
          {authenticated ? (
            <>
              <div className="flex flex-col space-y-4">
                <div className="flex items-center justify-between">
                  <div className="font-bold">Address</div>
                  <div>{address}</div>
                </div>
              </div>

              {messageSignature && (
                <div className="flex flex-col space-y-4 max-w-[60ch] break-words">
                  <div className="font-bold">Message Signature</div>
                  <div>{messageSignature.signature}</div>
                </div>
              )}

              <input
                value={message}
                onChange={(e) => setMessage(e.target.value)}
                className="border border-1 rounded p-2 border-black mb-4 ml-2 text-center"
                placeholder="Message to sign"
              />
              <button
                onClick={async () => await delegatedSignMessage(message)}
                disabled={signMessageLoading}
                className="border border-1 rounded p-2 border-black mb-4 ml-2"
              >
                {signMessageLoading ? "Signing..." : "Sign Message"}
              </button>
            </>
          ) : (
            <div className="mb-12 flex flex-col space-y-2 mt-8">
              <SignUpButton
                mode="modal"
                afterSignInUrl="/auth/callback"
                afterSignUpUrl="/auth/callback"
              >
                <button className="border border-1 rounded p-2 border-black mb-4 w-full">
                  Sign Up / In With Clerk
                </button>
              </SignUpButton>
              <SignOutButton>
                <button className="border border-1 rounded p-2 border-black mb-4 w-full">
                  Sign Out (Clear Cache)
                </button>
              </SignOutButton>
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

Complete Setup

Now with all of this setup we can setup our UI too, so the overall setup looks as below. In the final part of our setup, we've created the UI layer to interact with the above functions. We have buttons to register and authenticate, which dynamically interact with the state, to show loading states, as well as authenticated states, and finally when a message is signed, the user is also able to see the signed message signature. Please refer to https://github.com/0xpass/passport-clerk-quickstart for the complete setup.

You should be able to clone, update environment variable and run the example in less than a minute.

Last updated