Day 9: Authentication and Middleware
What You'll Learn Today
- Middleware basics
- Redirects and rewrites
- Authentication with NextAuth.js (Auth.js)
- Protected routes
- Session management
What is Middleware?
Middleware is code that runs before a request completes. Used for auth checks, redirects, header manipulation, etc.
flowchart LR
subgraph Request["Request"]
USER["User"]
end
subgraph Middleware["Middleware"]
CHECK["Condition Check"]
REDIRECT["Redirect"]
REWRITE["Rewrite"]
HEADERS["Header Manipulation"]
end
subgraph Response["Response"]
PAGE["Page"]
end
USER --> CHECK
CHECK --> REDIRECT
CHECK --> REWRITE
CHECK --> HEADERS
REDIRECT --> PAGE
REWRITE --> PAGE
HEADERS --> PAGE
style Request fill:#3b82f6,color:#fff
style Middleware fill:#8b5cf6,color:#fff
style Response fill:#22c55e,color:#fff
Basic Middleware
// middleware.ts (place at project root)
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
console.log("Middleware executed:", request.nextUrl.pathname);
return NextResponse.next();
}
// Specify matching paths
export const config = {
matcher: [
// Exclude static files and API
"/((?!api|_next/static|_next/image|favicon.ico).*)",
],
};
Redirects
Redirect users to different pages based on conditions.
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Redirect old URL to new URL
if (pathname === "/old-page") {
return NextResponse.redirect(new URL("/new-page", request.url));
}
// Pages requiring authentication
const token = request.cookies.get("token");
if (pathname.startsWith("/dashboard") && !token) {
return NextResponse.redirect(new URL("/login", request.url));
}
return NextResponse.next();
}
Redirect Types
| Method | Status | Use Case |
|---|---|---|
redirect() |
307 (temporary) | Temporary redirect |
redirect() with status |
301 (permanent) | SEO URL changes |
// Permanent redirect (301)
return NextResponse.redirect(new URL("/new-page", request.url), {
status: 301,
});
Rewrites
Render different pages without changing the URL.
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// A/B testing
if (pathname === "/") {
const bucket = Math.random() < 0.5 ? "a" : "b";
return NextResponse.rewrite(new URL(`/home/${bucket}`, request.url));
}
// Region-specific content
const country = request.geo?.country || "US";
if (pathname === "/pricing") {
return NextResponse.rewrite(
new URL(`/pricing/${country.toLowerCase()}`, request.url)
);
}
return NextResponse.next();
}
Header Manipulation
Add or modify headers on requests and responses.
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
// Add response headers
const response = NextResponse.next();
response.headers.set("x-custom-header", "my-value");
// Add security headers
response.headers.set(
"Content-Security-Policy",
"default-src 'self'"
);
return response;
}
NextAuth.js (Auth.js)
NextAuth.js (now Auth.js) is an authentication library for Next.js.
Setup
npm install next-auth
// src/app/api/auth/[...nextauth]/route.ts
import NextAuth from "next-auth";
import GithubProvider from "next-auth/providers/github";
import GoogleProvider from "next-auth/providers/google";
import CredentialsProvider from "next-auth/providers/credentials";
const handler = NextAuth({
providers: [
// GitHub login
GithubProvider({
clientId: process.env.GITHUB_ID!,
clientSecret: process.env.GITHUB_SECRET!,
}),
// Google login
GoogleProvider({
clientId: process.env.GOOGLE_ID!,
clientSecret: process.env.GOOGLE_SECRET!,
}),
// Email/password login
CredentialsProvider({
name: "Credentials",
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
// Validate user
const user = await validateUser(
credentials?.email,
credentials?.password
);
if (user) {
return user;
}
return null;
},
}),
],
pages: {
signIn: "/login",
error: "/auth/error",
},
callbacks: {
async session({ session, token }) {
if (session.user) {
session.user.id = token.sub!;
}
return session;
},
},
});
export { handler as GET, handler as POST };
Environment Variables
# .env.local
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=your-secret-key
GITHUB_ID=your-github-client-id
GITHUB_SECRET=your-github-client-secret
GOOGLE_ID=your-google-client-id
GOOGLE_SECRET=your-google-client-secret
SessionProvider Setup
Wrap with SessionProvider to use sessions in client components.
// src/app/providers.tsx
"use client";
import { SessionProvider } from "next-auth/react";
export function Providers({ children }: { children: React.ReactNode }) {
return <SessionProvider>{children}</SessionProvider>;
}
// src/app/layout.tsx
import { Providers } from "./providers";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}
Getting Authentication State
In Server Components
// src/app/dashboard/page.tsx
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
export default async function DashboardPage() {
const session = await getServerSession();
if (!session) {
redirect("/login");
}
return (
<div>
<h1>Dashboard</h1>
<p>Welcome, {session.user?.name}</p>
</div>
);
}
In Client Components
// src/components/UserMenu.tsx
"use client";
import { useSession, signIn, signOut } from "next-auth/react";
export function UserMenu() {
const { data: session, status } = useSession();
if (status === "loading") {
return <div>Loading...</div>;
}
if (!session) {
return (
<button
onClick={() => signIn()}
className="px-4 py-2 bg-blue-600 text-white rounded"
>
Login
</button>
);
}
return (
<div className="flex items-center gap-4">
<span>{session.user?.name}</span>
<button
onClick={() => signOut()}
className="px-4 py-2 bg-gray-200 rounded"
>
Logout
</button>
</div>
);
}
Protecting Routes with Middleware
Use Middleware to protect routes that require authentication.
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { getToken } from "next-auth/jwt";
export async function middleware(request: NextRequest) {
const token = await getToken({
req: request,
secret: process.env.NEXTAUTH_SECRET,
});
const { pathname } = request.nextUrl;
// Protected routes
const protectedPaths = ["/dashboard", "/settings", "/profile"];
const isProtected = protectedPaths.some((path) =>
pathname.startsWith(path)
);
if (isProtected && !token) {
const loginUrl = new URL("/login", request.url);
loginUrl.searchParams.set("callbackUrl", pathname);
return NextResponse.redirect(loginUrl);
}
// Authenticated users accessing login page
if (pathname === "/login" && token) {
return NextResponse.redirect(new URL("/dashboard", request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard/:path*", "/settings/:path*", "/profile/:path*", "/login"],
};
flowchart TB
subgraph Middleware["Middleware"]
CHECK["Check Token"]
PROTECTED{"Protected Route?"}
HAS_TOKEN{"Has Token?"}
end
subgraph Actions["Actions"]
REDIRECT_LOGIN["Redirect to Login"]
REDIRECT_DASHBOARD["Redirect to Dashboard"]
CONTINUE["Continue"]
end
CHECK --> PROTECTED
PROTECTED -->|Yes| HAS_TOKEN
PROTECTED -->|No| CONTINUE
HAS_TOKEN -->|No| REDIRECT_LOGIN
HAS_TOKEN -->|Yes| CONTINUE
style Middleware fill:#8b5cf6,color:#fff
style Actions fill:#22c55e,color:#fff
Implementing Login Page
// src/app/login/page.tsx
"use client";
import { signIn } from "next-auth/react";
import { useSearchParams } from "next/navigation";
import { useState } from "react";
export default function LoginPage() {
const searchParams = useSearchParams();
const callbackUrl = searchParams.get("callbackUrl") || "/dashboard";
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const result = await signIn("credentials", {
email,
password,
redirect: false,
});
if (result?.error) {
setError("Invalid email or password");
} else {
window.location.href = callbackUrl;
}
};
return (
<div className="min-h-screen flex items-center justify-center">
<div className="max-w-md w-full p-6 border rounded-lg">
<h1 className="text-2xl font-bold mb-6 text-center">Login</h1>
{error && (
<div className="mb-4 p-3 bg-red-100 text-red-600 rounded">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block mb-1">Email</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full border rounded p-2"
required
/>
</div>
<div>
<label className="block mb-1">Password</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full border rounded p-2"
required
/>
</div>
<button
type="submit"
className="w-full bg-blue-600 text-white py-2 rounded"
>
Login
</button>
</form>
<div className="mt-6">
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t" />
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-white text-gray-500">or</span>
</div>
</div>
<div className="mt-4 space-y-2">
<button
onClick={() => signIn("github", { callbackUrl })}
className="w-full border py-2 rounded flex items-center justify-center gap-2"
>
Login with GitHub
</button>
<button
onClick={() => signIn("google", { callbackUrl })}
className="w-full border py-2 rounded flex items-center justify-center gap-2"
>
Login with Google
</button>
</div>
</div>
</div>
</div>
);
}
Summary
| Concept | Description |
|---|---|
| Middleware | Code that runs before requests |
| Redirect | Forward to different URL |
| Rewrite | Change content without changing URL |
| NextAuth.js | Auth library for Next.js |
| getServerSession | Get session on server |
| useSession | Get session on client |
Key Points
- Keep Middleware lightweight: Runs on Edge Runtime
- Easy auth with NextAuth.js: Supports multiple providers
- Protect at Middleware level: Centralized route control
- Session available everywhere: Server and Client support
Practice Exercises
Exercise 1: Basic
Use Middleware to redirect paths starting with /admin to /login if not authenticated.
Exercise 2: Intermediate
Implement GitHub login with NextAuth.js. After login, redirect to dashboard and display user info.
Challenge
Implement a login page supporting both email/password and GitHub authentication. Include error handling and loading states.
References
Coming Up Next: In Day 10, we'll learn about "Deployment and Production." We'll explore deploying to Vercel, environment variables, and performance optimization.