import { NextRequestWithAuth } from 'next/server';
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { verifySession } from './lib/verify-session';
import { isProtected, isAuthPage } from './lib/route-matchers';
export async function middleware(req: NextRequest) {
const { pathname } = req.nextUrl;
const token = req.cookies.get('session')?.value;
const session = token ? await verifySession(token) : null;
if (isProtected(pathname) && !session) {
const loginUrl = new URL('/login', req.url);
loginUrl.searchParams.set('from', pathname + req.nextUrl.search);
return NextResponse.redirect(loginUrl);
}
if (isAuthPage(pathname) && session) {
return NextResponse.redirect(new URL('/dashboard', req.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*', '/account/:path*', '/login', '/register'],
};
import { jwtVerify } from 'jose';
export interface SessionClaims {
sub: string;
email: string;
role: 'user' | 'admin';
}
const secret = new TextEncoder().encode(process.env.SESSION_SECRET);
export async function verifySession(token: string): Promise<SessionClaims | null> {
try {
const { payload } = await jwtVerify(token, secret, {
algorithms: ['HS256'],
issuer: 'app.auth',
});
if (typeof payload.sub !== 'string' || typeof payload.email !== 'string') {
return null;
}
return {
sub: payload.sub,
email: payload.email as string,
role: (payload.role as SessionClaims['role']) ?? 'user',
};
} catch {
// Signature mismatch, expired token, or malformed input all fall through.
return null;
}
}
const PROTECTED_PREFIXES = ['/dashboard', '/account'];
const AUTH_PAGES = ['/login', '/register'];
export function isProtected(pathname: string): boolean {
return PROTECTED_PREFIXES.some(
(prefix) => pathname === prefix || pathname.startsWith(prefix + '/'),
);
}
export function isAuthPage(pathname: string): boolean {
return AUTH_PAGES.includes(pathname);
}
export function safeRedirectTarget(from: string | null): string {
if (!from || !from.startsWith('/') || from.startsWith('//')) {
return '/dashboard';
}
return from;
}
This snippet shows how route protection is centralized in Next.js Middleware so that unauthenticated requests never reach protected pages. The core idea is that Middleware runs on the Edge runtime before rendering, which makes it the correct place to enforce access decisions without duplicating checks in every page or layout.
In middleware.ts, the config.matcher limits execution to the paths that actually need gating, avoiding overhead on static assets and public routes. The handler reads the session token from an HTTP-only cookie and calls verifySession to validate it. When a token is missing or invalid on a protected route, the code builds a redirect to /login and preserves the originally requested URL in a from query parameter so the user can be returned after signing in. A subtle but important detail is the inverse rule: if an already-authenticated user hits /login, they are redirected away to the dashboard, preventing a logged-in user from seeing the auth screen.
The verifySession helper uses jose rather than jsonwebtoken because the Edge runtime lacks Node's crypto module, so a Web Crypto-compatible library is required. jwtVerify checks both the signature and the expiry, and the function returns null on any failure so callers treat verification as a simple boolean-like gate. The secret is encoded once at module scope to avoid re-encoding on every request.
Matching decisions rely on isProtected and isAuthPage in route-matchers.ts, which keep the path logic declarative and testable in isolation. Using prefix matching here means nested routes like /dashboard/settings are covered automatically.
The trade-offs are worth noting. Middleware verification is stateless: it trusts the JWT signature but cannot know if a session was revoked server-side, so short token lifetimes or an additional server check for sensitive actions are advisable. Because Middleware runs at the edge, it must avoid Node-only APIs and heavy dependencies. The from redirect parameter should be validated before use to prevent open-redirect attacks. This pattern fits apps that want one authoritative access-control layer while keeping pages focused on rendering.
Related snips
payload = {
sub: user.id,
iss: 'https://auth.example.com',
aud: 'codesnips-api',
exp: 15.minutes.from_now.to_i,
iat: Time.now.to_i,
JWT issuance and verification without common footguns
from django.urls import path
from . import views
app_name = 'blog'
urlpatterns = [
Django URL namespacing and reverse lookups
#!/usr/bin/env bash
set -euo pipefail
export VAULT_ADDR="https://vault.internal:8200"
export VAULT_TOKEN="${VAULT_TOKEN:?missing VAULT_TOKEN}"
Secrets management with environment isolation and Vault
import { randomBytes, createHash } from "crypto";
import jwt from "jsonwebtoken";
import { RefreshTokenStore } from "./store";
const ACCESS_SECRET = process.env.ACCESS_SECRET!;
const ACCESS_TTL = "15m";
JWT access + refresh token rotation (conceptual)
package files
import (
"context"
"time"
Presigned S3 upload URLs (AWS SDK v2)
<%# private stream: turbo signs the serialized record name %>
<%= turbo_stream_from current_user %>
<section class="notifications">
<h1>Notifications</h1>
Turbo Streams + authorization: signed per-user stream name
Share this code
Here's the card — post it anywhere.