Introduction
Next.js has made significant strides in its architecture, particularly with the introduction of server components. While these changes bring about powerful features, they also introduce complexities, especially when implementing authentication. In this blog, I'll discuss a streamlined approach to handling JWT authentication in Next.js, addressing challenges like local storage access and refresh token management.
The Challenge
In Next.js, server components are rendered by default. This poses a challenge for JWT authentication since server components cannot directly access browser storage (localStorage or sessionStorage). Additionally, storing and managing refresh tokens securely becomes a critical concern.
My Solution
To tackle these issues, I devised a method that leverages Next.js server actions and a session provider for client components. This approach ensures secure token management and seamless integration between server and client components.
First, create an authentication action using Next.js server actions. This action acts as a proxy server, handling credential validation, token issuance, and session management.
// auth.action.ts
"use server";
import { SignJWT, jwtVerify } from "jose";
import { cookies } from "next/headers";
import { NextRequest, NextResponse } from "next/server";
import { unstable_noStore as noStore, revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { r } from "@/config/request";
import { handleUnauthorized } from "@/lib/middleware";
const secretKey = process.env.SESSION_SECRET;
const key = new TextEncoder().encode(secretKey);
export async function encrypt(payload: any) {
return await new SignJWT(payload)
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt()
.setExpirationTime("30h")
.sign(key);
}
export async function decrypt(input: string): Promise<any> {
const { payload } = await jwtVerify(input, key, {
algorithms: ["HS256"],
});
return payload;
}
export async function logout() {
cookies().set("session", "", { expires: new Date(0) });
redirect("/");
}
export async function getSession() {
noStore();
const session = cookies().get("session")?.value;
if (!session) return;
try {
return await decrypt(session);
} catch (err) {
return;
}
}
export async function updateSession({
session,
request,
}: {
session: any;
request: NextRequest;
}) {
try {
const { accessToken: token, user: { refreshToken } } = session;
const newSession = await r.post({
endpoint: "/auth/refreshtoken",
token,
payload: { refreshToken },
});
const expires = new Date(Date.now() + 30 * 60 * 60 * 1000);
const res = NextResponse.next();
res.cookies.set({
name: "session",
value: await encrypt(newSession),
httpOnly: true,
expires: expires,
});
return res;
} catch (err) {
console.log("Error while refreshing token:", err);
return handleUnauthorized(request);
}
}
type Response = {
message?: string;
error?: string;
code: number;
};
export const login = async (payload: any): Promise<Response> => {
try {
const res = await fetch(
`${process.env.NEXT_PUBLIC_BACKEND_URL}/auth/signin`,
{
method: "POST",
body: JSON.stringify(payload),
headers: { "Content-Type": "application/json" },
}
);
if (!res.ok) {
const data = await res.json()
const session = await encrypt(data);
cookies().set("session", session, {
httpOnly: true,
expires: new Date(Date.now() + 24 * 60 * 1000),
});
revalidatePath("/", "layout");
return { code: res.status, message: "You have been logged in successfully !!" };
}
} catch (err: any) {
console.log(err);
return { code: err.code || 500, error: err.message || "couldn't connect to server" };
}
};
Next, set up middleware to handle session validation and token refresh.
// middleware.ts
import { NextRequest, NextResponse } from "next/server";
import { handleUnauthorized, isPrivateRoute } from "./lib/middleware";
import { getSession, updateSession } from "./actions/auth.action";
export async function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname;
if (/\.(png|svg|jpg|webp|mp3|geojson)$/.test(pathname)) return;
if (isPrivateRoute(pathname)) {
const session = await getSession();
if (!session) {
return handleUnauthorized(request);
}
return updateSession({ request, session });
}
return NextResponse.next();
}
export const config = {
matcher: ["/((?!_next|api|favicon.ico).*)"],
};
Create a session provider to manage session state in client components.
// SessionProvider.tsx
"use client";
import React, { createContext, useContext, ReactNode } from "react";
interface SessionCtxProps {
session: any;
}
const SessionCtx = createContext<SessionCtxProps | undefined>(undefined);
const SessionProvider = ({
children,
session,
}: {
children: ReactNode;
session: any;
}) => {
return (
<SessionCtx.Provider value={{ session }}>{children}</SessionCtx.Provider>
);
};
const useSession = (): SessionCtxProps => {
const context = useContext(SessionCtx);
if (!context) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
};
export { SessionProvider, useSession };
Finally, integrate the session provider in your layout to pass the session to client components.
// layout.tsx
import { getSession } from "./actions/auth.action";
import { SessionProvider } from "./components/SessionProvider";
const Layout = async ({ children }: { children: ReactNode }) => {
const session = await getSession();
return (
<html lang="en">
<body>
<SessionProvider session={session}>
{children}
</SessionProvider>
</body>
</html>
);
};
export default Layout;
"use client";
import { useSession } from "@/providers/SessionProvider";
import React from "react";
const UserPill = () => {
const {
session: { user },
} = useSession();
return (
<div className="bg-muted w-fit flex items-center gap-2 py-1 p-1 rounded-full">
<img
className="h-[35px] w-[35px] rounded-full border-primary object-cover object-center"
src={user?.imgurl ? user.imgurl : "/avatar.jpg"}
alt=""
height={35}
width={35}
/>
<div>
<p className="font-semibold text-sm">{user?.name}</p>
<p className="text-xs text-muted-foreground">@{user?.name}</p>
</div>
</div>
);
};
export default UserPill;
By following this approach, you can implement robust JWT authentication in your Next.js applications, overcoming the challenges posed by server and client component integration.