NextAuth is Magic (If You Understand JWTs)
Handling authentication used to mean writing complex Passport.js strategies, setting manual HttpOnly cookies, and dreading every OAuth callback implementation. Now, NextAuth (Auth.js) turns Google/GitHub logins into a 5-minute setup.
But what happens when you need to connect your Next.js frontend to a separate custom Node.js backend using a JWT?
1. The Custom Provider Workflow
If you own the backend (like our standard user-service at XRide Labs), you don't just want NextAuth to use a generic database provider. You need NextAuth to send credentials to your backend, and retrieve an access token.
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
export const authOptions = {
providers: [
CredentialsProvider({
name: "Your App",
credentials: {
email: { label: "Email", type: "text" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
// Send to your separate Node or Python backend
const res = await fetch("https://api.yourdomain.com/v1/auth/login", {
method: "POST",
body: JSON.stringify(credentials),
headers: { "Content-Type": "application/json" },
});
const user = await res.json();
if (res.ok && user) return user;
return null;
},
}),
],
callbacks: {
// We append the backend token into the NextAuth JWT
async jwt({ token, user }) {
if (user) {
token.accessToken = user.token;
token.role = user.role;
}
return token;
},
// And then pass it to the Session so the client can read it
async session({ session, token }) {
session.accessToken = token.accessToken;
session.user.role = token.role;
return session;
},
},
};2. Refresh Token Rotation
A common mistake is returning an access token valid for 30 days. Never do this! If a token is stolen, the attacker has a full month to freely hit your API.
Best Practice:
- Access tokens should live 15-30 minutes only.
- A Refresh Token (stored securely in an HttpOnly cookie if possible) lives for 7-30 days.
- In NextAuth, you can intercept the
jwtcallback to check if the currentaccessTokenis expired, and if so, fire a silent request to your backend's/refreshendpoint to get a new one before returning the token to the user.
3. Protecting Server Actions
In Next.js 14, standard pages can check session validity, but Server Actions are open to the world directly.
YOU MUST AUTHENTICATE YOUR SERVER ACTIONS:
// app/actions.ts
"use server";
import { getServerSession } from "next-auth";
import { authOptions } from "./api/auth/[...nextauth]/route";
export async function deleteUserResource(id: string) {
const session = await getServerSession(authOptions);
if (!session || !session.user) {
throw new Error("Unauthorized! Action denied.");
}
// Verify Role-Based Access Control
if (session.user.role !== "ADMIN") {
throw new Error("Insufficient permissions.");
}
// Proceed with DB mutation
}Security isn't a feature you bolt on later. Build your platforms securely from day one.