Raghav
Raghav

Reputation: 371

How to sync up the expiration time of Next Auth Session and a token coming from the server as I chose Credentials in provider of next-Auth

I have implemented a next-auth authentication system for my Next.js app. In the providers, I have chosen credentials because I have a node.js backend server.

The problem that I am facing is the expiration of next auth session is not in sync up with the expiration of jwt token on my backend. This is leading to inconsistency. Kindly help me out.

Below is my next auth code

import NextAuth, {
  NextAuthOptions,
  Session,
  SessionStrategy,
  User,
} from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import { login } from "@actions/auth";
import { toast } from "react-toastify";
import { JWT } from "next-auth/jwt";
import { NextApiRequest, NextApiResponse } from "next";
import { SessionToken } from "next-auth/core/lib/cookie";

// For more information on each option (and a full list of options) go to
// https://next-auth.js.org/configuration/options
const nextAuthOptions = (req: NextApiRequest, res: NextApiResponse) => {
  return {
    providers: [
      CredentialsProvider({
        name: "Credentials",
        credentials: {
          email: { label: "Email", type: "text" },
          password: { label: "Password", type: "password" },
        },
        async authorize(
          credentials: Record<"email" | "password", string> | undefined,
          req
        ): Promise<Omit<User, "id"> | { id?: string | undefined } | null> {
          // Add logic here to look up the user from the credentials supplied
          const response = await login(
            credentials?.email!,
            credentials?.password!
          );
          const cookies = response.headers["set-cookie"];

          res.setHeader("Set-Cookie", cookies);
          if (response) {
            var user = { token: response.data.token, data: response.data.user };
            return user;
          } else {
            return null;
          }
        },
      }),
    ],
    refetchInterval: 1 * 24 * 60 * 60,
    secret: process.env.NEXTAUTH_SECRET,
    debug: true,
    session: {
      strategy: "jwt" as SessionStrategy,
      maxAge: 3 * 24 * 60 * 60,
    },
    jwt: {
      maxAge: 3 * 24 * 60 * 60,
    },
    callbacks: {
      jwt: async ({ token, user }: { token: JWT; user?: User }) => {
        user && (token.accessToken = user.token);
        user && (token.user = user.data);
        return token;
      },
      session: async ({ session, token }: { session: Session; token: JWT }) => {
        session.user = token.user;
        session.accessToken = token.accessToken;
        return session;
      },
    },
  };
};
export default (req: NextApiRequest, res: NextApiResponse) => {
  return NextAuth(req, res, nextAuthOptions(req, res));
};

Upvotes: 24

Views: 47743

Answers (4)

NickAtServerli
NickAtServerli

Reputation: 11

Not sure if this helps anyone, but I could not figure out how to keep the app from issuing new tokens every time I idled or left the page and came back, so I gave up on trying to sync them. And instead am just including them in the token as additional properties. So my middleware can know when the token is expired.

import { getToken } from 'next-auth/jwt';
import { NextResponse } from 'next/server';
import { DateTime } from 'luxon';

export async function middleware(req) {
  const token = await getToken({ req, secret: process.env.JWT_SECRET });
  const { pathname } = req.nextUrl;
  const origin = req.nextUrl.origin;

  if (pathname === '/auth/sign-in') {
    return NextResponse.next();
  }

  const isTokenExpired =
    token?.apiExp && DateTime.fromSeconds(token.apiExp) < DateTime.now();

  const isPublicRoute =
    pathname.includes('/api/auth') ||
    pathname.includes('.png') ||
    pathname.includes('.svg') ||
    pathname.includes('/favicon.ico') ||
    pathname.includes('jpg') ||
    pathname.includes('_next');

  if (isPublicRoute) {
    return NextResponse.next();
  }

  if (!token || isTokenExpired) {
    console.log('Not signed in or token expired, redirecting to signIn');
    return NextResponse.redirect(origin + '/auth/sign-in');
  }

  return NextResponse.next();
}
in [...nextauth]

const callbacks = {
  async jwt({ token, user }) {
    if (user) {
      token.id = user.id;
      token.email = user.email;
      token.accountId = user.accountId;
      token.apiIat = user.iat;
      token.apiExp = user.exp;
    }
    return token;
  },
Token {
  email: '[email protected]',
  sub: '2',
  id: 2,
  accountId: 1,
  apiIat: 1694127090,
  apiExp: 1694130690,
  iat: 1694127090,
  exp: 1694130690,
  jti: '82342349c-e222a-409a-b91f-c00327367d0f'
}
Token {
  email: '[email protected]',
  sub: '2',
  id: 2,
  accountId: 1,
  apiIat: 1694127090,
  apiExp: 1694130690,
  iat: 1694127094,
  exp: 1696719094,
  jti: '123456-1234-1234-1234-1234567890'
}

Upvotes: 1

danilibros
danilibros

Reputation: 193

I have a similar setup: NextAuth (version 4) with Next.js (version 13 with App Router) on the client using credential authentication with a jwt and a separate backend session token.

This is how we keep the sessions in sync:

  1. As others mentioned, in the NextAuthOptions, set the maxAge property to the same expiration time as the token on the back end server.

    const nextAuthOptions = {
      providers: [...],
      session: {
        strategy: 'jwt',
        maxAge: 4 * 60 * 60 // 4 hours
      },
      ...
    }
    
  2. At the top level of the route tree for your authenticated pages, check if your client-side session is about to expire and if so, refresh the token. I refresh the token on the client-side with the NextAuth useSession update function and send a request to the backend API to update the token expiration on the server. This is added to the layout.tsx file in the top level hierarchy for any views that should be authenticated to see.

    --layout.tsx--

    'use client';
    import { useSession } from 'next-auth/react';
    
    export default function Layout() {
      const { data: session, status, update } = useSession();
    
      useEffect(() => {
        const interval = setInterval(() => {
          update(); // extend client session
          // TODO request token refresh from server
        }, 1000 * 60 * 60)
        return () => clearInterval(interval)
      }, [update]); 
      return (
        {children}
      )
    }
    
  3. If you also want to add functionality to determine if the user is idle or not before extending their session, you can use react-idle-timer.

    --full layout.tsx file--

    'use client';
    import React, { useEffect } from 'react';
    import { useSession, signOut } from 'next-auth/react';
    import { useIdleTimer } from 'react-idle-timer';
    
    export default function Layout({ children }: { children: React.ReactNode 
    }) {
      const { data: session, status, update } = useSession();
      const CHECK_SESSION_EXP_TIME = 300000; // 5 mins
      const SESSION_IDLE_TIME = 300000; // 5 mins 
      const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL;
    
      const onUserIdle = () => {
        console.log('IDLE');
      };
    
      const onUserActive = () => {
        console.log('ACTIVE');
      };
    
      const { isIdle } = useIdleTimer({
        onIdle: onUserIdle,
        onActive: onUserActive,
        timeout: SESSION_IDLE_TIME, //milliseconds
        throttle: 500
      });
    
      useEffect(() => {
        const checkUserSession = setInterval(() => {
          const expiresTimeTimestamp = Math.floor(new Date(session?.expires || '').getTime());
          const currentTimestamp = Date.now();
          const timeRemaining = expiresTimeTimestamp - currentTimestamp;
    
          // If the user session will expire before the next session check
          // and the user is not idle, then we want to refresh the session
          // on the client and request a token refresh on the backend
          if (!isIdle() && timeRemaining < CHECK_SESSION_EXP_TIME) {
            update(); // extend the client session
    
            // request refresh of backend token here
    
          } else if (timeRemaining < 0) {
            // session has expired, logout the user and display session expiration message
            signOut({ callbackUrl: BASE_URL + '/login?error=SessionExpired' });
          }
        }, CHECK_SESSION_EXP_TIME);
    
        return () => {
          clearInterval(checkUserSession);
        };
      }, [update]); 
      return (
        <main>
          {children}
        </main>
      );
    }
    

Upvotes: 12

Zakkkk
Zakkkk

Reputation: 11

I found a solution you can try to save the jwt cookie inside session token and read it from there and they will expiry both at the same time check this out for more info https://github.com/nextauthjs/next-auth/discussions/1290

Upvotes: 0

Youzef
Youzef

Reputation: 858

In your options, there is the maxAge property. Set it to be equal to whatever time you have set in your backend server. The time is in seconds, so yours is currently set to 3days.

See here

Upvotes: 1

Related Questions