13. Authentication & Authorization
Server‑Side Session Storage
In a typical Next.js application, user state is kept on the server. A random, cryptographically‑strong sessionId is generated upon login and sent to the client inside an HttpOnly cookie. On every subsequent request the middleware reads the cookie, looks up the session in a SessionStore (e.g., a database or Redis), and attaches the associated user object to req.
Example middleware (Express‑style pseudo‑code):
if (req.cookies.sessionId) { const session = await SessionStore.find(req.cookies.sessionId); req.user = session.user; }
The cookie should be configured with security‑focused attributes:
- HttpOnly – prevents client‑side JavaScript access, mitigating XSS theft.
- Secure – ensures the cookie is transmitted only over HTTPS.
- SameSite –
StrictorLaxreduces CSRF risk by limiting cross‑site sending. - Path=
/– makes the cookie available site‑wide. - Max‑Age – lifetime in seconds (e.g.,
3600for one hour).
An example cookie string:
session=abc123def456; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=86400
Cookies: Key‑Value Pairs and Flags
Cookies are automatically attached to HTTP requests by the browser. Their syntax is a series of name=value pairs separated by semicolons, followed by optional attributes.
| Attribute | Purpose |
|---|---|
| HttpOnly | Blocks access via document.cookie (XSS protection). |
| Secure | Sent only over TLS‑encrypted connections. |
| SameSite | Values: Strict, Lax, None (with Secure). Controls cross‑site request inclusion. |
| Path | URL path that must match for the cookie to be sent. |
| Max-Age / Expires | Defines lifetime; Max-Age=3600 means one hour. |
JSON Web Tokens (JWT)
A JWT is a compact, URL‑safe token consisting of three Base64Url‑encoded parts separated by dots:
header.payload.signature
Header
Contains the token type and signing algorithm.
{"alg":"HS256","typ":"JWT"}
Payload (Claims)
Holds user‑identifying information and metadata.
{"sub":"1234567890","name":"Alice","iat":1717000000,"role":"admin"}
Signature
Created by hashing the encoded header and payload with a secret (or private key). For HS256:
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
Verification recomputes the signature and compares it to the supplied one; if they match, the payload is trusted.
Example token (HS256 with secret mysecret):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaWNlIiwiaWF0IjoxNzE3MDAwMDAwLCJyb2xlIjoiYWRtaW4ifQ.HMACSHA256Signature
Auth.js (NextAuth) Overview
NextAuth.js (now @auth/core + next-auth) simplifies authentication in Next.js by providing providers, session management, and JWT handling out of the box.
Setup
- Install the packages:
- Create an API route that exports
authOptionsand the NextAuth handler.
npm install next-auth @auth/core
Example using the app router (app/api/auth/[...nextauth]/route.ts):
import NextAuth from "next-auth";
import GoogleProvider from "next-auth/providers/google";
import CredentialsProvider from "next-auth/providers/credentials";
import { compare } from "bcrypt";
export const authOptions = {
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_ID!,
clientSecret: process.env.GOOGLE_SECRET!,
}),
CredentialsProvider({
name: "Credentials",
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" },
},
authorize: async (credentials) => {
const user = await db.user.findUnique({
where: { email: credentials?.email },
});
if (user && await compare(credentials?.password ?? "", user.password)) {
return { id: user.id, email: user.email, role: user.role };
}
return null;
},
}),
],
secret: process.env.NEXTAUTH_SECRET,
callbacks: {
jwt: ({ token, user }) => {
if (user) token.role = user.role;
return token;
},
session: ({ session, token }) => {
if (session.user) session.user.role = token.role as string;
return session;
},
},
};
export default NextAuth(authOptions);
Google Login
When the user clicks “Sign in with Google”, NextAuth redirects them to Google’s OAuth 2.0 consent screen. After approval, Google redirects back with an authorization code. NextAuth exchanges this code for an access token and ID token, extracts the user’s profile (name, email, picture), and either stores a JWT in a cookie or creates a server‑side session, depending on the configuration.
GitHub Login
Similar to Google; typical scopes are read:user and user:email. The returned data includes the GitHub username, avatar URL, and primary email address.
Credentials Login
Allows a custom email/password flow. The developer supplies an authorize callback that validates credentials against a data store (often using bcrypt for password hashing). On success, NextAuth returns a user object that is serialized into a session or JWT.
Authorization: Roles, Permissions, and Protected Routes
Roles
Roles are coarse‑grained access levels stored as a string or enum (e.g., "admin", "moderator", "user"). They are frequently included as a claim in the JWT or session object (role: "admin").
Permissions
Permissions are fine‑grained actions such as "create:post", "delete:comment", "read:own_profile". They can be derived from a role‑permission matrix or stored as an array in the user record (permissions: ["create:post","edit:own_post"]).
Protected Routes with NextAuth Middleware
NextAuth provides a withAuth higher‑order function that can be used in middleware.ts to protect pages or API routes.
import { withAuth } from "next-auth/middleware";
export default withAuth(
function (req) {
// Optional custom logic
},
{
callbacks: {
authorized: ({ token }) => !!token && token.role