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
Asked Answered
A

4

19

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));
};
Alkylation answered 5/3, 2022 at 13:46 Comment(3)
Were you able to sale this issue? I'm stuck at the same placeTeak
Nope. No luck in this yet. The session keeps on updating and they are not syncing upAlkylation
What about now, I have the same annoying issue :'[Jori
B
9

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>
      );
    }
    
Bosson answered 22/8, 2023 at 22:0 Comment(0)
D
2

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

Directoire answered 22/3, 2022 at 11:59 Comment(2)
maxAge: Seconds - How long until an idle session expires and is no longer valid. That wont work, if the user isn't idle.Thetisa
what if maxAge is dynamic? Its value comes from BE ?Elyot
N
1

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'
}
Needlepoint answered 7/9, 2023 at 23:35 Comment(0)
F
0

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

Fluoroscopy answered 11/7, 2023 at 21:32 Comment(1)
As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center.Aircool

© 2022 - 2024 — McMap. All rights reserved.