Usage
This guide covers authentication implementation across your application. Learn how to handle authentication server-side with server components, client-side with React hooks, using middleware for route protection, and integrating with React Router. Includes practical code examples for common authentication workflows.
Server-side
Server-side authentication allows you to protect routes, handle user sessions, and implement authentication flows using server components and server actions. You can retrieve the current session with auth()
and implement sign-in/sign-out functionality.
import { cookies, headers } from 'next/headers'
import { auth, signIn, signOut } from '@/server/auth'
export default async function SSRPage() {
const session = await auth({ headers: await headers() })
return (
<main className="flex min-h-dvh flex-col items-center justify-center gap-4">
<div className="max-w-md overflow-hidden">
<pre>{JSON.stringify(session, null, 2)}</pre>
</div>
{session.user ? (
<form
action={async () => {
'use server'
await signOut({ headers: await headers() })
;(await cookies()).delete('auth_token')
}}
>
<button>Sign Out</button>
</form>
) : (
<form
action={async (formData: FormData) => {
'use server'
const { sessionToken, expires } = await signIn({
email: formData.get('email') as string,
password: formData.get('password') as string,
})
;(await cookies()).set('auth_token', sessionToken, {
path: '/',
httpOnly: true,
sameSite: 'lax' as const,
secure: process.env.NODE_ENV === 'production',
expires,
})
}}
>
<input name="email" type="email" placeholder="Email" />
<input name="password" type="password" placeholder="Password" />
<button>Sign In</button>
</form>
)}
</main>
)
}
Client-side
Client-side authentication provides access to session data and authentication methods in client components. Start by adding the SessionProvider
to your root layout, then use the useSession
hook to access authentication state and functions in any client component.
import { SessionProvider } from '@/hooks/use-session'
export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="en" suppressHydrationWarning>
<body
className={cn(
'flex min-h-dvh flex-col font-sans antialiased',
geistSans.variable,
geistMono.variable,
)}
>
<SessionProvider>{children}</SessionProvider>
</body>
</html>
)
}
Now, you can use the useSession
hook to access the session in your components.
'use client'
export const Auth: React.FC = () => {
const { session, status, signIn, signOut } = useSession()
if (status === 'loading') return <div>Loading...</div>
if (status === 'unauthenticated')
return (
<div>
<p>Not authenticated</p>
<Button onClick={() => signIn('discord')}>Sign In with Discord</Button>
</div>
)
return (
<div>
<div className="max-w-md overflow-x-auto">
<pre>{JSON.stringify(session, null, 2)}</pre>
</div>
<Button onClick={signOut}>Logout</Button>
</div>
)
}
Middleware
Middleware authentication enables route protection, automatic redirects, and CSRF protection at the Edge. This allows you to control access to protected routes and enforce security policies before requests reach your application.
To use authentication in middleware, your database must support Edge Runtime (e.g., Neon PostgreSQL, Supabase, Vercel PostgreSQL). Standard database connections may not work in Edge environments.
import type { MiddlewareConfig, NextRequest } from 'next/server'
import { NextResponse } from 'next/server'
import { auth } from '@/server/auth'
const authRoutes: string[] = ['/login', '/register']
const protectedRoutes: string[] = []
export default async function middleware(req: NextRequest) {
const { pathname } = new URL(req.url)
const session = await auth({ headers: req.headers })
if (req.method === 'GET') {
if (
!session.user &&
protectedRoutes.some((route) => pathname.startsWith(route))
) {
const url = new URL('/login', req.url)
url.searchParams.set('redirect_to', pathname)
return NextResponse.redirect(url)
}
if (session.user && authRoutes.some((route) => pathname.startsWith(route)))
return NextResponse.redirect(new URL('/', req.url))
return NextResponse.next()
}
/**
* CSRF Protection Implementation
*
* Provides protection against Cross-Site Request Forgery by:
* - Comparing Origin header with Host header to verify same-origin requests
* - Blocking requests with missing or mismatched headers (403 Forbidden)
* - Bypassing protection for authenticated users
*
* Security approach:
* - Validates Origin as proper URL and matches against request host
* - Basic protection complemented by CSRF tokens and SameSite cookies
*
* Note: Consider additional validation for authenticated users or
* implementing request-specific CSRF tokens for enhanced security.
*/
if (session.user) return NextResponse.next()
const originHeader = req.headers.get('Origin') ?? ''
const hostHeader =
req.headers.get('Host') ?? req.headers.get('X-Forwarded-Host') ?? ''
if (!originHeader || !hostHeader)
return new NextResponse(null, { status: 403 })
let originUrl: URL
try {
originUrl = new URL(originHeader)
} catch {
return new NextResponse(null, { status: 403 })
}
if (originUrl.host !== hostHeader)
return new NextResponse(null, { status: 403 })
return NextResponse.next()
}
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico, sitemap.xml, robots.txt (metadata files)
*/
'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
/*
* Match all request paths starting with:
* - api (API routes)
*/
'/api/(.*)',
],
} satisfies MiddlewareConfig
React router
Set up
import type { Route } from './+types/api.auth.$'
import { handlers } from '@/server/auth'
const { GET, POST } = handlers
export const loader = ({ request }: Route.LoaderArgs) => GET(request)
export const action = ({ request }: Route.ActionArgs) => POST(request)
Get session
import { auth } from '@/server/auth'
export const loader = async ({ request }: Route.LoaderArgs) => {
const session = await auth(request)
return { session }
}
export default function IndexPage({
loaderData: { session },
}: Route.ComponentProps) {
return (
<main>
<pre>{JSON.stringify(session, null, 2)}</pre>
</main>
)
}
Server Authentication Actions
Sign up function example (Drizzle)
'use server'
import { Password } from '@/server/auth/core/password'
import { db } from '@/server/db'
import { accounts, users } from '@/server/db/schema'
export const signUp = async (input) => {
const user = await db.query.users.findFirst({
where: (users, { eq }) => eq(users.email, input.email),
})
if (user) throw new Error('User already exists')
const [newUser] = await db
.insert(users)
.values({
name: input.name,
email: input.email,
image: '',
})
.returning({ id: users.id })
await db
.insert(accounts)
.values({
provider: 'credentials',
accountId: newUser?.id ?? '',
userId: newUser?.id ?? '',
password: await new Password().hash(input.password),
})
.returning()
return true
}
Change password function example (Drizzle)
'use server'
import { headers } from 'next/headers'
import { and, eq } from 'drizzle-orm'
import { auth } from '@/server/auth'
import { Password } from '@/server/auth/core/password'
import { db } from '@/server/db'
import { accounts } from '@/server/db/schema'
export const changePassword = async (input) => {
const session = await auth({ headers: await headers() })
const { currentPassword, newPassword } = input
const userId = ctx.session.user.id
const account = await ctx.db.query.accounts.findFirst({
where: (accounts, { and, eq }) =>
and(eq(accounts.provider, 'credentials'), eq(accounts.accountId, userId)),
})
if (!account) {
await ctx.db.insert(accounts).values({
provider: 'credentials',
accountId: userId,
userId: userId,
password: await password.hash(newPassword),
})
} else {
if (!(await password.verify(account.password ?? '', currentPassword ?? '')))
throw new Error('Current password is incorrect')
await ctx.db
.update(accounts)
.set({ password: await password.hash(newPassword) })
.where(
and(
eq(accounts.provider, 'credentials'),
eq(accounts.accountId, account.accountId),
),
)
}
return true
}