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.ts2session: {3 strategy: "jwt",4 maxAge: 30 * 24 * 60 * 60, // 30 days5},67callbacks: {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
# RequiredAUTH_SECRET="your-secret-key" # openssl rand -base64 32# Google OAuthAUTH_GOOGLE_ID="your-client-id.googleusercontent.com"AUTH_GOOGLE_SECRET="your-client-secret"# Magic LinksAUTH_RESEND_KEY="re_..."
Sign-in Methods
Google OAuth
1import { signIn } from "~/auth";23async function googleSignIn() {4 "use server";5 await signIn("google");6}
Magic Links
1import { signIn } from "~/auth";23async 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.ts2export { proxy } from "~/auth.edge";34export const config = {5 matcher: ["/account", "/admin"],6};
Server Components
1import { auth } from "~/auth";2import { redirect } from "next/navigation";34export default async function ProtectedPage() {5 const session = await auth();6 if (!session) redirect("/auth/signin");78 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.tsx2import { auth } from "~/auth";3import { checkIsAdmin } from "~/server/actions/admin";45export default async function AdminPage() {6 const session = await auth();7 if (!session) redirect("/auth/signin");89 const isAdmin = await checkIsAdmin();10 if (!isAdmin) return <div>Access Denied</div>;1112 return <AdminDashboard />;13}
Next: Stripe Payments
Set up subscriptions and billing