Next.js

View Example Next.js Project →

For best results we recommend using the App Router.

Installation

npm install @0x57/passkey @0x57/passkey-react

Create or modify your .env.local file with the following:

SECRET_COOKIE_PASSWORD="testtesttesttesttesttesttesttest"
HEX57_KEY="<env token>"

Next we’ll want to add session capabilities to our app.

Adding Secure Sessions

Our recommendation is to use the iron-session package to add secure sessions to your Next.js app. We’ll also use the server-only package as extra assurance we aren’t leaking sensitive information to the client.

Start by installing it by running the following command:

npm install iron-session server-only

Then create a new file named lib/session.ts:

import { getIronSession, type IronSessionData } from "iron-session";
import { cookies } from "next/headers";
import "server-only";
 
declare module "iron-session" {
	interface IronSessionData {
		accountId?: string;
		challenge?: string;
	}
}
 
const sessionOptions = {
	password: process.env.SECRET_COOKIE_PASSWORD ?? "",
	cookieName: "session",
	cookieOptions: {
		secure: process.env.NODE_ENV === "production",
	},
};
 
export async function getSession() {
	const session = await getIronSession<IronSessionData>(
		cookies(),
		sessionOptions,
	);
 
	return session;
}

We’ll be storing the currently logged in Account ID and WebAuth challenge. The information will be persisted using Next.js cookie primitives. Next, we can begin working on our registration system.

Account Sign Ups

In order to create a new WebAuthn credential we’ll need to generate a challenge to use. Start by creating a new file named actions/challenge.ts with the following:

"use server";
 
import { generateChallenge } from "@0x57/passkey";
import { getSession } from "../lib/session";
 
export default async function createChallenge() {
	const session = await getSession();
	const challenge = generateChallenge();
 
	session.challenge = challenge;
	await session.save();
 
	return challenge;
}

We are generating a random sequence of bytes and storing it in a secure session to read from later.

Next let’s build the register webpage. We’ll be building a server rendered page that mounts a client component form. Create a new file named app/register/page.tsx with the following:

import createChallenge from "../../actions/challenge";
import RegisterForm from "./RegisterForm";
 
export default function RegisterPage() {
	return (
	  <Fragment>
		  <h2>Register</h2>
			<RegisterForm createChallenge={createChallenge} />
			<Link href="/login">Login</a>
		</Fragment>
	);
}

The RegisterForm component is more complex. We’ll want to prompt the user for an email, a username, and then have a form submit handler.

"use client";
 
import {
	useChallengeAction,
	useRegister,
	useWebAuthnAvailability,
} from "@0x57/passkey-react";
import { redirect } from "next/navigation";
import { FormEvent, useState } from "react";
import registerAction from "../../actions/register";
 
export default function RegisterForm({
	createChallenge,
}: {
	createChallenge: () => Promise<string>;
}) {
	const isAvailable = useWebAuthnAvailability();
	const challenge = useChallengeAction(createChallenge);
	const [email, setEmail] = useState("");
	const [username, setUsername] = useState("");
 
	const register = useRegister({
		challenge: challenge ?? "",
		relyingParty: {
			id: "localhost",
			name: "0x57 Example App",
		},
		action: registerAction,
		onSuccess: () => {
			redirect("/profile");
		},
		onError: (result) => {
			console.error({ result });
		},
	});
 
	const onSubmit = (event: FormEvent) => {
		event.preventDefault();
		register({
			email,
			username,
		});
	};
 
	return (
		<form onSubmit={onSubmit}>
			<label htmlFor="email">Email address</label>
			<input
				id="email"
				name="email"
				type="email"
				autoComplete="email"
				value={email}
				onChange={(e) => setEmail(e.target.value)}
				required
			/>
 
			<label htmlFor="username">Username</label>
			<input
				id="username"
				name="username"
				type="text"
				autoComplete="username"
				value={username}
				onChange={(e) => setUsername(e.target.value)}
				required
			/>
 
			<button
				type="submit"
				disabled={!isAvailable}
			>
				Register
			</button>
		</form>
	);
}

Finally we’ll create the registration action we referenced in the form. Create a new file named actions/register.ts:

"use server";
 
import { isRedirectError } from "next/dist/client/components/redirect";
import { redirect } from "next/navigation";
import hex57 from "../lib/0x57";
import { getSession } from "../lib/session";
 
export default async function register(formData: FormData) {
	const session = await getSession();
	const { accountId, challenge } = session;
 
	if (accountId != null) {
		redirect("/dashboard");
	}
 
	if (challenge == null) {
		return { error: "Invalid credentials - please refresh and try again." };
	}
 
	try {
		const credential = formData.get("credential");
		if (credential == null) {
			return { error: "Invalid credential - please refresh and try again." };
		}
 
		const result = await hex57.register({
			challenge,
			credential: credential.toString(),
			username: formData.get("username")?.toString(),
			email: formData.get("email")?.toString(),
		});
 
		session.accountId = result.accountId;
		await session.save();
		redirect("/profile");
	} catch (err) {
		if (isRedirectError(err)) {
			throw err;
		}
 
		console.error(err);
		return { error: true };
	}
}

Here we will check to see if a user is logged in (and prevent a new registration if they are), validate the input data, and then call 0x57 to create a new Account. If the result is successful, we will save their new ID in the session (logging them in) and redirect them to a protected page.

Adding Account Sign Out

Logging a user out of your service is incredibly easy with Next.js route handlers. You can create a new file named app/logout/route.ts and export a GET request handler:

import { redirect } from "next/navigation";
import { getSession } from "../..//lib/session";
 
export async function GET() {
	const session = await getSession();
	session.destroy();
	redirect("/");
}

We destroy the current session and redirect the user to the homepage.

Creating Account Sign In

The final piece is adding a login page. The structure will be similar to the registration flow - we’ll create a server component page, a client component form, and then an action form submission handler.

Start by creating a new file named app/login/page.tsx:

import createChallenge from "../../actions/challenge";
import LoginForm from "./LoginForm";
 
export default function LoginPage() {
		return (
	  <Fragment>
		  <h2>Register</h2>
			<LoginForm createChallenge={createChallenge} />
			<Link href="/login">Login</a>
		</Fragment>
	);
}}

Next, create a new file named app/login/LoginForm.tsx:

"use client";
 
import {
	useChallengeAction,
	useLogin,
	useWebAuthnAvailability,
} from "@0x57/passkey-react";
import { redirect } from "next/navigation";
import loginAction from "../../actions/login";
 
export default function LoginForm({
	createChallenge,
}: {
	createChallenge: () => Promise<string>;
}) {
	const isAvailable = useWebAuthnAvailability();
	const challenge = useChallengeAction(createChallenge);
 
	const onSubmit = useLogin({
		challenge: challenge ?? "",
		relyingPartyId: "localhost",
		action: loginAction,
		onSuccess: () => {
			redirect("/profile");
		},
		onError: (result) => {
			console.error({ result });
		},
	});
 
	return (
		<form onSubmit={onSubmit}>
			<button
				type="submit"
				disabled={!isAvailable}
			>
				Login
			</button>
		</form>
	);
}

We don’t need to prompt for a username or email because WebAuthn counts as the entire login mechanism! The final piece is creating the submission handler. Create a new file named /actions/login.ts:

"use server";
 
import { isRedirectError } from "next/dist/client/components/redirect";
import { redirect } from "next/navigation";
import hex57 from "../lib/0x57";
import { getSession } from "../lib/session";
 
export default async function login(formData: FormData) {
	const session = await getSession();
	const { accountId, challenge } = session;
 
	if (accountId != null) {
		redirect("/profile");
	}
 
	if (challenge == null) {
		return { error: "Invalid credentials - please refresh and try again." };
	}
 
	try {
		const result = await hex57.login({
			challenge,
			credential: JSON.parse(formData.get("credential")?.toString() ?? ""),
		});
 
		session.accountId = result.id;
		await session.save();
		redirect("/profile");
	} catch (err) {
		if (isRedirectError(err)) {
			throw err;
		}
 
		console.error(err);
		return { error: true };
	}
}

This action is almost exactly the same as the registration handler. The key difference is instead of calling the registration 0x57 API we’re calling the session API via hex57.login!

Requiring Users be Logged In

You can require users be logged in by checking the session value - no need to call 0x57 APIs!

import { redirect } from "next/navigation";
import { getSession } from "../../lib/session";
 
export default async function LockedPage() {
	const session = await getSession();
 
	if (!session.userId) {
		redirect("/login");
	}
 
	return (
		<section>
			<p>You are logged in!</p>
		</section>
	);
}
 

Adding a Profile Page

If you want to display additional information about the logged in user, you can call the 0x57 API like this:

import { redirect } from "next/navigation";
import hex57 from "../../lib/0x57";
import { getSession } from "../../lib/session";
 
export default async function ProfilePage() {
	const session = await getSession();
 
	if (!session.userId) {
		redirect("/login");
	}
 
	const user = await hex57.getAccount(session.userId);
 
	return (
		<section>
			<h1 className="text-4xl font-extrabold mb-8">User Profile Page</h1>
			<pre>{JSON.stringify(user, null, 2)}</pre>
		</section>
	);
}