Authentication

Auth.js v5 with Google OAuth, Magic Links, JWT sessions, and Edge-compatible middleware.

Included

  • • Google OAuth sign-in
  • • Magic Link email authentication
  • • JWT session strategy
  • • Edge-compatible middleware
  • • Automatic user creation
  • • Welcome emails on signup

Security

  • • Cryptographic JWT verification
  • • CSRF protection
  • • OAuth account linking protection
  • • Admin role detection
  • • 30-day session expiry

Architecture

Auth is split into multiple files to support both Edge Runtime (middleware) and Node.js Runtime (server actions).

src/
├── auth.config.ts— Edge-compatible config
├── auth.edge.ts— Lightweight (~88KB)
├── auth.ts— Full auth + Prisma
└── proxy.ts— Route protection

Why the split? Vercel Edge Functions have a 1MB limit. Importing Prisma would exceed this, so middleware uses a lightweight auth instance.

JWT Sessions

Sessions use JWT strategy — user data is stored in a signed cookie, not the database. Verification is cryptographic, not a DB lookup.

1// auth.ts
2session: {
3 strategy: "jwt",
4 maxAge: 30 * 24 * 60 * 60, // 30 days
5},
6
7callbacks: {
8 jwt({ token, user }) {
9 if (user) token.id = user.id;
10 return token;
11 },
12 session({ session, token }) {
13 session.user.id = token.id;
14 return session;
15 },
16}

Configuration

.env
# Required
AUTH_SECRET="your-secret-key" # openssl rand -base64 32
# Google OAuth
AUTH_GOOGLE_ID="your-client-id.googleusercontent.com"
AUTH_GOOGLE_SECRET="your-client-secret"
# Magic Links
AUTH_RESEND_KEY="re_..."

Sign-in Methods

Google OAuth

1import { signIn } from "~/auth";
2
3async function googleSignIn() {
4 "use server";
5 await signIn("google");
6}

Magic Links

1import { signIn } from "~/auth";
2
3async function emailSignIn(formData: FormData) {
4 "use server";
5 await signIn("resend", formData);
6}

Route Protection

Proxy (Edge)

Protects routes by verifying JWT. Redirects unauthenticated users.

1// src/proxy.ts
2export { proxy } from "~/auth.edge";
3
4export const config = {
5 matcher: ["/account", "/admin"],
6};

Server Components

1import { auth } from "~/auth";
2import { redirect } from "next/navigation";
3
4export default async function ProtectedPage() {
5 const session = await auth();
6 if (!session) redirect("/auth/signin");
7
8 return <div>Welcome, {session.user?.name}</div>;
9}

Admin Access

Admin status is checked server-side, not in middleware, because it requires checking an email list.

1// src/app/admin/page.tsx
2import { auth } from "~/auth";
3import { checkIsAdmin } from "~/server/actions/admin";
4
5export default async function AdminPage() {
6 const session = await auth();
7 if (!session) redirect("/auth/signin");
8
9 const isAdmin = await checkIsAdmin();
10 if (!isAdmin) return <div>Access Denied</div>;
11
12 return <AdminDashboard />;
13}

Next: Stripe Payments

Set up subscriptions and billing

Continue →