question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

CredentialsProvider session token cookie not updated on getServerSideProps

See original GitHub issue

Environment

System: OS: Windows 10 10.0.19042 CPU: (8) x64 Intel® Xeon® 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.

Issue Analytics

  • State:closed
  • Created 2 years ago
  • Reactions:2
  • Comments:11 (4 by maintainers)

github_iconTop GitHub Comments

6reactions
cassioseffrincommented, Jul 16, 2022

To use getServerSession, export the configuration object in [...nextauth].ts like this:

export const authOptions: NextAuthOptions = {
  secret: process.env.TOKEN_SECRET,
  // other configs...
}
export default NextAuth(authOptions);

In index.tsx:

 const session = await getServerSession(ctx, authOptions);

It’s save my day! Thks so much! may it would be very welcome to official docs of next-auth!

2reactions
killjoy2013commented, Feb 27, 2022

@ThangHuuVu @balazsorban44 getServerSession works the way I need, thank you a lot 👍

One point I observed is, I need to supply default token properties explicitly;

const defaultToken = {
  name: '',
  email: '',
  picture: '',
};

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

If I try to create the token without them;

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

I received

error - Error: Error serializing `.session.user.name` returned from `getServerSideProps` in "/".
Reason: `undefined` cannot be serialized as JSON. Please use `null` or omit this value.

Maybe those default props can be supplied internally. So, we wouldn’t have to supply them.

Thanks again & regards

Read more comments on GitHub >

github_iconTop Results From Across the Web

How to force new token/session in next-auth server-side after ...
I'm using the credentials provider of Next-Auth, where the sign in would set the user into the token, then into the session. The...
Read more >
getserversideprops nextauth - You.com | The AI Search ...
Since the session token cookie is not updated, this rotation logic fails because the token received in jwt callback is always the same....
Read more >
Working with NextAuth.js & User Generated Content - Hygraph
userId = token.sub;. return Promise.resolve(session);. },. },. Now back inside of pages/api/account.js let's update our getServerSideProps ...
Read more >
How to implement NextAuth credentials provider with external ...
NextAuth credentials provider with custom backend and login page ... return token; }, async session({ session, token }) { session.user.
Read more >
Building an authentication API with NextAuth.js
Create a new file called […nextauth].js in pages/api/auth and paste this ... export async function getServerSideProps(ctx) { const session ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found