Thanks to visit codestin.com
Credit goes to github.com

Skip to content

CredentialsProvider session token cookie not updated on getServerSidePropsΒ #4075

@killjoy2013

Description

@killjoy2013

Environment

System:
OS: Windows 10 10.0.19042
CPU: (8) x64 Intel(R) Xeon(R) CPU E3-1505M v6 @ 3.00GHz
Memory: 5.44 GB / 15.86 GB
Binaries:
Node: 14.17.2 - C:\Program Files\nodejs\node.EXE
Yarn: 1.22.4 - C:\Program Files (x86)\Yarn\bin\yarn.CMD
npm: 6.14.13 - C:\Program Files\nodejs\npm.CMD
Browsers:
Edge: Spartan (44.19041.1266.0)
Internet Explorer: 11.0.19041.1202
npmPackages:
next: ~12.1.0 => 12.1.0
next-auth: ~4.2.1 => 4.2.1
react: 17.0.2 => 17.0.2

Reproduction URL

https://github.com/killjoy2013/nextauth-credential-rotate

Describe the issue

Hi,
Using CredentialsProvider and need to rotate the token. My [...nextauth.ts] is below;

import NextAuth from 'next-auth';
import { JWT, JWTEncodeParams, JWTDecodeParams } from 'next-auth/jwt';
import CredentialsProvider from 'next-auth/providers/credentials';
import jsonwebtoken from 'jsonwebtoken';

function createToken(username: string) {
  return {
    username,
    willExpire: Date.now() + parseInt(process.env.TOKEN_REFRESH_PERIOD) * 1000,
  };
}

function refreshToken(token) {
  const { iat, exp, ...others } = token;

  return {
    ...others,
    willExpire: Date.now() + parseInt(process.env.TOKEN_REFRESH_PERIOD) * 1000,
  };
}

export default NextAuth({
  secret: process.env.TOKEN_SECRET,
  jwt: {
    secret: process.env.TOKEN_SECRET,
    maxAge: parseInt(process.env.TOKEN_MAX_AGE),
    encode: async (params: JWTEncodeParams): Promise<string> => {
      const { secret, token } = params;
      let encodedToken = '';
      if (token) {
        const jwtClaims = {
          username: token.username,
          willExpire: token.willExpire,
        };

        encodedToken = jsonwebtoken.sign(jwtClaims, secret, {
          expiresIn: parseInt(process.env.TOKEN_REFRESH_PERIOD),
          algorithm: 'HS512',
        });
      } else {
        console.log('TOKEN EMPTY. SO, LOGOUT!...');
        return '';
      }
      return encodedToken;
    },
    decode: async (params: JWTDecodeParams) => {
      const { token, secret } = params;
      const decoded = jsonwebtoken.decode(token);

      return { ...(decoded as JWT) };
    },
  },
  session: {
    maxAge: parseInt(process.env.TOKEN_MAX_AGE),
    updateAge: 0,
    strategy: 'jwt',
  },
  callbacks: {
    async jwt({ token, user, account }) {
      if (user) {
        token = createToken(user.username as string);
      }

      let left = ((token.willExpire as number) - Date.now()) / 1000;
      console.log({
        now: Date.now(),
        willExpire: token.willExpire,
        left,
      });

      if (left > 0) {
        return token;
      } else {
        let newToken = await refreshToken(token);
        return { ...newToken };
      }
    },
    async session({ session, token, user }) {
      // Send properties to the client, like an access_token from a provider.
      session.username = token.username;
      session.willExpire = token.willExpire;
      return session;
    },
  },
  providers: [
    CredentialsProvider({
      name: 'LDAP',
      credentials: {
        username: { label: 'Username', type: 'text' },
        password: { label: 'Password', type: 'password' },
      },
      async authorize(credentials) {
        const { username, password } = credentials;

        if (!username || !password) {
          throw new Error('enter username or password');
        }
        try {
          let token = createToken(username);
          return token;
        } catch (error) {
          console.log(error);
          throw new Error('Authentication error');
        }
      },
    }),
  ],
});

TOKEN_MAX_AGE is 3600 seconds and TOKEN_REFRESH_PERIOD is 60 seconds. So, in 60 seconds after JWT is created, token is supposed to get rotated. Initial token creation is in createToken and tried to simulate token refresh in refreshToken. I put username & willExpire as claims. In Jwt callback, a token is created and returned. Then, encode runs and encoded token returned. Both home page (index.tsx) and cities.tsx has

  const session = await getSession({ req });
  const token = await getToken({
    req,
    secret: process.env.TOKEN_SECRET,
    raw: true,
  });

in their getServerSideProps. So, each navigation to home & cities page triggers jwt callback and even though encode function encodes and return a new token, next-auth.session-token cookie never gets updated. It's just created when user first login, and stays always the same. Normally, I'll decide token rotation in jwt as in token rotation sample in tutorials like this;

 let left = ((token.willExpire as number) - Date.now()) / 1000;
      console.log({
        now: Date.now(),
        willExpire: token.willExpire,
        left,
      });

      if (left > 0) {
        return token;
      } else {
        let newToken = await refreshToken(token);
        return { ...newToken };
      }

Since the session token cookie is not updated, this rotation logic fails because the token received in jwt callback is always the same.

How to reproduce

npm i & npm run dev

navigate to http://localhost:3000/

you'll be redirected to login page. Provide a dummy username and a password

You'll be redirected to home page. Now you can display next-auth.session-token cookie now.

click Cities in the left menu and you'll navigate to cities page.

click toolbar to go back home page.

In every navigation between pages, getServerSideProps runs with getSession and getToken inside.

Note that, next-auth.session-token is not changing :-(

Close the browser and wait until token refresh period ( 60 seconds ) has passed. Then open a browser and navigate to http://localhost:3000/cities

Now, notice that, next-auth.session-token cookie still has the old token from last login before closing the browser.

Expected behavior

next-auth.session-token is supposed to get updated with the returned token from encode.
On the client side, we can force the token to update using

  const event = new Event('visibilitychange');
  document.dispatchEvent(event);

However, users can directly navigate to http://localhost:3000/cities In this case, on getServerSideProps an updated token is supposed to be obtained because this token will be used in a graphql query that will be run from serverside.

Metadata

Metadata

Assignees

No one assigned

    Labels

    docsRelates to documentation

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions