How can I implement JWT authentication with App Router in Next.js 16? #96472
SummaryI'm building a Next.js 16 application using the App Router. I want to implement JWT authentication for protected routes but I'm unsure of the recommended approach. Could someone explain the best practice? Additional informationNext.js: 16.x
Node.js: 22.x
OS: Windows 11
No runtime errors.
I'm looking for the recommended implementation pattern.ExampleCan you check in my( GitHub page https://github.com/vigneshwaran1702 ) |
Replies: 4 comments
|
One thing to check is whether the issue is caused by stale dependencies or cached build artifacts. Try:
If the problem persists, please share the relevant code and error output. |
|
Deleting For Next.js 16, the recommended approach is to use an established authentication library such as Auth.js, Better Auth, Clerk, or another supported provider instead of implementing the complete JWT lifecycle yourself. The important architecture is:
If you implement the JWT yourself, verify its signature and expiry on the server and still perform authorization at each protected operation. The official guide documents this separation: Next.js authentication. |
|
For Next.js 16, use an auth library like Auth.js instead of building JWT auth from scratch. Store the session/JWT in an Docs: |
|
I would add one important constraint to this architecture: don't make the JWT/session itself the source of truth for effective permissions. For multi-layer authorization like this, I would separate it into: The key is that For example: type Capability =
| "analytics.read"
| "analytics.write"
| "billing.read"
| "billing.manage";
type ResourcePolicy = {
path: string;
required: Capability[];
};
function resolveCapabilities({
org,
user,
}: {
org: Set<Capability>;
user: Set<Capability>;
}) {
return new Set(
[...user].filter(permission => org.has(permission))
);
}Then keep one registry for application resources: export const resources = {
analytics: {
path: "/dashboard/analytics",
required: ["analytics.read"],
},
billing: {
path: "/dashboard/billing",
required: ["billing.read"],
},
} satisfies Record<
string,
{
path: string;
required: Capability[];
}
>;From there, the same policy can drive:
But these are still different enforcement points, not different authorization systems. For example: export function canAccess(
capabilities: Set<Capability>,
required: Capability[]
) {
return required.every(permission =>
capabilities.has(permission)
);
}A Server Component can use it: const capabilities = await getEffectiveCapabilities(userId);
if (
!canAccess(
capabilities,
resources.analytics.required
)
) {
notFound();
}And the menu can derive directly from the exact same registry: const menu = Object.entries(resources)
.filter(([, resource]) =>
canAccess(capabilities, resource.required)
)
.map(([key, resource]) => ({
key,
href: resource.path,
}));That removes the need to maintain: as separate mappings. I would also avoid putting the full permission matrix into a long-lived JWT. If an admin removes access while a token is still valid, a JWT containing stale permissions can continue advertising capabilities that no longer exist. Instead, I would keep the token focused mostly on identity/session information and resolve authorization server-side, optionally caching the result with a short TTL or an authorization/version key. Something like: The frontend can receive a sanitized capability projection for UX purposes, but the backend or DAL must re-check authorization before any sensitive operation. For backend-driven routes specifically, I would strongly prefer: rather than manually reproducing backend paths and permission names in several frontend files. If the backend contract can be represented by OpenAPI, JSON Schema, or another machine-readable schema, generating the TypeScript resource types/metadata from it also prevents frontend/backend authorization drift. So my rule would be:
Middleware/Proxy can perform cheap optimistic checks, Server Components can prevent unauthorized pages from rendering, and Server Actions/Route Handlers/DAL should perform the final security check. The UI should reflect authorization — never be the authorization boundary itself. |
One thing to check is whether the issue is caused by stale dependencies or cached build artifacts.
Try:
.nextnpm installorpnpm install)If the problem persists, please share the relevant code and error output.