Updated 8 August 2026
Own Auth with Next.js App Router
Add authentication to a Next.js App Router application by mounting createOwnAuthHandler in one catch-all Route Handler. It validates auth requests, sets HttpOnly session and MFA cookies, applies CSRF checks, and returns the responses consumed by the Own Auth React client.
Install Own Auth
npm install own-authConnect Postgres
DATABASE_URL=postgres://user:password@localhost:5432/myappnpx own-auth migrateCreate the auth instance
OWN_AUTH_TOKEN_PEPPER=replace-with-a-long-random-secretimport "server-only";
import { createOwnAuth } from "own-auth";
const tokenPepper = process.env.OWN_AUTH_TOKEN_PEPPER!;
export const auth = createOwnAuth({
tokenPepper,
});Mount the HTTP handler
import { createOwnAuthHandler } from "own-auth/http";
import { auth } from "@/lib/auth";
const handler = createOwnAuthHandler(auth);
export const GET = handler;
export const POST = handler;The route exposes the Own Auth HTTP API under /api/auth. Sign-up, sign-in, sign-out, session reads, password recovery, magic links, OAuth, passkeys, and MFA all use this handler.
Add the React client
"use client";
import { createOwnAuthReactClient } from "own-auth/react";
export const authClient = createOwnAuthReactClient();The client calls /api/auth and uses the cookies set by the handler. Raw session and MFA challenge tokens never enter Client Component state; the MFA result carries the available methods and expiry.
Add email and password
"use client";
import { authClient } from "@/lib/auth-client";
export function signUp(name: string, email: string, password: string) {
return authClient.signUpEmailPassword({ name, email, password });
}
export async function signIn(email: string, password: string) {
const result = await authClient.signInEmailPassword({ email, password });
if (result.status === "mfa_required") {
return { ...result, next: "/mfa" };
}
const { id, email, name, imageUrl } = result.user;
return { next: "/dashboard", user: { id, email, name, imageUrl } };
}Build the sign-in form
Submit with the React client, then navigate with the App Router. The form stays in-app instead of reloading the document, and an MFA result continues to the challenge screen.
"use client";
import { type FormEvent, useState } from "react";
import { useRouter } from "next/navigation";
import { signIn } from "@/lib/email-password";
export function SignInForm() {
const router = useRouter();
const [error, setError] = useState<string | null>(null);
async function submit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
setError(null);
const data = new FormData(event.currentTarget);
try {
const result = await signIn(
String(data.get("email") ?? ""),
String(data.get("password") ?? ""),
);
router.push(result.next);
router.refresh();
} catch {
setError("Unable to sign in with those credentials.");
}
}
return (
<form onSubmit={submit}>
<label>
Email
<input name="email" type="email" autoComplete="email" required />
</label>
<label>
Password
<input
name="password"
type="password"
autoComplete="current-password"
required
/>
</label>
{error ? <p role="alert">{error}</p> : null}
<button type="submit">Sign in</button>
</form>
);
}Read the current session
Server Components and Server Actions can verify the handler's session cookie directly with auth.getCurrentSession.
import "server-only";
import { cache } from "react";
import { cookies } from "next/headers";
import { defaultSessionCookieName } from "own-auth/http";
import { auth } from "@/lib/auth";
export const getCurrentAuth = cache(async () => {
const token = (await cookies()).get(defaultSessionCookieName)?.value;
return token ? auth.getCurrentSession(token) : null;
});Protect a Server Component
import { redirect } from "next/navigation";
import { getCurrentAuth } from "@/lib/current-auth";
export default async function DashboardPage() {
const current = await getCurrentAuth();
if (!current) redirect("/signin");
return <h1>Welcome, {current.user.name ?? current.user.email}</h1>;
}Protect a Server Action
Pass the handler's session cookie into server-only Own Auth methods. This password action validates only that both strings are present; Own Auth enforces the password policy.
"use server";
import { cookies } from "next/headers";
import { AuthError } from "own-auth";
import { defaultSessionCookieName } from "own-auth/http";
import { auth } from "@/lib/auth";
export async function changePassword(formData: FormData) {
const token = (await cookies()).get(defaultSessionCookieName)?.value;
if (!token) throw new Error("Unauthorized");
const currentPassword = String(formData.get("currentPassword") ?? "");
const newPassword = String(formData.get("newPassword") ?? "");
if (!currentPassword || !newPassword) {
return { error: "Both password fields are required" };
}
try {
await auth.changePassword({
sessionToken: token,
currentPassword,
newPassword,
});
return { changed: true };
} catch (error) {
if (error instanceof AuthError) {
return { error: error.safeMessage };
}
throw error;
}
}Sign out
"use client";
import { useRouter } from "next/navigation";
import { authClient } from "@/lib/auth-client";
export function SignOutButton() {
const router = useRouter();
async function signOut() {
await authClient.signOut();
router.push("/signin");
router.refresh();
}
return <button onClick={signOut}>Sign out</button>;
}Add magic links
Email-and-password authentication does not need APP_URL. Add it with magic links because Own Auth puts the public application origin in the emailed sign-in URL. Pass it as baseUrl in the existing auth instance and configure the application's email provider before requesting a link.
APP_URL=https://app.example.comimport "server-only";
import { createOwnAuth } from "own-auth";
const tokenPepper = process.env.OWN_AUTH_TOKEN_PEPPER!;
const appUrl = process.env.APP_URL!;
export const auth = createOwnAuth({
tokenPepper,
baseUrl: appUrl,
});Request and verify links through the same browser client. Complete verification sets the HttpOnly session cookie.
"use client";
import { authClient } from "@/lib/auth-client";
export async function requestMagicLink(email: string) {
return authClient.requestMagicLink({ email });
}
export async function verifyMagicLink(token: string) {
const result = await authClient.verifyMagicLink({ token });
if (result.status === "mfa_required") {
return { ...result, next: "/mfa" };
}
const { id, email, name, imageUrl } = result.user;
return { next: "/dashboard", user: { id, email, name, imageUrl } };
}Add OAuth
Once a provider is configured on the Own Auth instance, start its redirect flow from the client. The callback returns through /api/auth and sets the same session cookie.
"use client";
import { authClient } from "@/lib/auth-client";
export function signInWithGoogle() {
return authClient.signInWithOAuth({
provider: "google",
destination: "/dashboard",
});
}