How to set a fixed session length in next-auth
Next-auth can be used as a wrapper for existing backends/APIs which already have an auth setup, it can be fast to setup and reduces boilerplate.
With this type of setup however you may run into an issue where the current next-auth session is valid but the wrapped API session is not. This can cause various issues like private pages being accessible (valid next-auth session) but data failing to be fetched within those pages (invalid api session). Ideally we want our next-auth session to match the fixed session length of our wrapped API.
Next-auth has a somewhat confusingly named maxAge option available but fails to document well that the next-auth session will continue to be extended on any user request given the maxAge has not been reached since the last request and it doesn't appear to have an option to disable this behaviour 😢 (but better-auth does).
Thankfully, the fix is pretty simple 😄 and only requires adding a new property to the token on creation, then adding this to the session so it can be accessed elsewhere.
export const { handlers, signIn, signOut, auth } = NextAuth({
session: {
strategy: "jwt",
},
callbacks: {
jwt: async ({ token }) => {
// next-auth automatically extends jwt expiry by default and has no way to disable it
// so we are doing our own expiry logic here
// set a fixed expiry date when the jwt is first created
if (!token.fixedExpiry) {
const days = 30 // fixed session length goes here
const expiryDate = new Date();
expiryDate.setDate(expiryDate.getDate() + days);
token.fixedExpiry = expiryDate.toISOString();
}
return token;
},
session: async ({ session, token }) => {
if (session.user) {
if (!session.fixedExpiry && token.fixedExpiry) {
// attach the fixed expiry date to the session so it be read elsewhere (like middleware)
session.fixedExpiry = token.fixedExpiry;
}
}
return session;
},
},
You can also add the following to the bottom of the same file to extend the libraries' types to be aware of what we've added
declare module "next-auth" {
interface Session extends DefaultSession {
fixedExpiry?: string; // custom field
}
}
declare module "next-auth/jwt" {
interface JWT extends DefaultJWT {
fixedExpiry?: string; // custom field
}
}
Now that we have set this custom field we can use it to enforce our fixed session length, here are some ideas ⤵️
in middleware.ts:
export const middleware = auth(async (req) => {
if (
req.auth?.fixedExpiry &&
// prevent infinite redirect loop
!req.nextUrl.pathname.startsWith("/login") &&
!req.nextUrl.pathname.startsWith("/logout")
) {
if (new Date(req.auth.fixedExpiry) < new Date())
// ^ check our fixedExpiry
{
const logoutUrl = new URL("/logout", req.nextUrl.origin);
return NextResponse.redirect(logoutUrl);
}
}
return NextResponse.next();
});
We redirect to /logout if the fixedExpiry has been reached, that means we need to handle the actual logout logic on the logout page, something like this:
export default function Page() {
useEffect(() => {
// attempt to logout from your wrapped api/backend
void accountsLogout().finally(() => {
// logout from next-auth session regardless if above fails
void signOut_ServerAction();
// could use either client or server method here https://authjs.dev/getting-started/session-management/login?framework=Next.js%2520%28Client%29
});
}, []);
return "Logging out...";
}
Thats it!
If you have not setup next-auth yet, better-auth is probably better
Subscribe
Email X LinkedIn
Related GitHub issues
- https://github.com/nextauthjs/next-auth/discussions/10807
- https://github.com/nextauthjs/next-auth/discussions/7957
- https://github.com/nextauthjs/next-auth/discussions/9595
- https://github.com/nextauthjs/next-auth/discussions/2790