Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Is mutating the session possible? #371

Closed
dkreft opened this issue Jun 30, 2020 · 57 comments
Closed

Is mutating the session possible? #371

dkreft opened this issue Jun 30, 2020 · 57 comments
Labels
bug Something isn't working question Ask how to do something or how something works

Comments

@dkreft
Copy link

dkreft commented Jun 30, 2020

Following-up on #325 because I'm not sure if GitHub sends you notifications after an issue has been closed.

I don't see a way to update data in the session after login. If I'm to store my tokens in the session, I need to be able to write to it after I refresh my access token. I've tried Googling for a solution, but to no avail. Is there something else I'm missing?

Thanks.

@dkreft dkreft added the question Ask how to do something or how something works label Jun 30, 2020
@iaincollins
Copy link
Member

Yes if using JSON Web Tokens see the callback documentation, specifically the jwt() callback.

Alternatively, if using Database Sessions (and not JWT) you can add logic in the session() callback; but you will need to add logic required to write to the database yourself there.

There are a lot of closed issues related to this and they contain example code.

(We will probably have tutorials at some point).

@dkreft
Copy link
Author

dkreft commented Jul 1, 2020

@iaincollins, I'm not seeing how the jwt() callback is relevant. The doc says that the callback is invoked "whenever a JSON Web Token is created or updated" but it does not give any indication of how to initiate such a change. I get that this is called internally after a successful login--but I'm outside of that context when I need to update the tokens stored in the JWT.

Using a database session is not an option--all our communications with the database happen through a web service.

I did a search (e.g. "update jwt," "update session") through the closed issues for something that seemed relevant, but none of the issue titles gave strong clues that they were at all relevant except for maybe #224, but that turned out not to be helpful. Since you're familiar with the issues in your queue, perhaps you would be willing to throw me a bone and direct me towards one or two tickets that would prove helpful?

@iaincollins
Copy link
Member

@dkreft As well as being called on sign in, the jwt callback is invoked session is checked (e.g via getSession(), useSession(), calling /api/auth/session directly, etc. If you stick a console.log() statement in the call back you should be able to see every time it's called.

The relevant bit from the callback documentation linked to above is in the session callback documentation:

"The JWT callback is invoked before the session() callback is called, so anything you add to the
JWT will be immediately available in the session callback."

An example use case for this is getting the latest access token for a third party service before returning it in the session object to the client - useful for services that have short lived / rotating access tokens.

You can then cherry pick what data is exposed from the encrypted JWT to the unencrypted session object is passed to the client - e.g. you might want to expose an access token so the client can make requests in the browser via AJAX, but you might not want to expose a server side API key you have stored in the JWT.

Note: If you don't do anything in the JWT callback, it the session/token expiry will still be updated/extended automatically any time the session is accessed from the client, but the contents of the JWT won't otherwise change.

I've amended the callback documentation to spell this out more clearly.

@dkreft
Copy link
Author

dkreft commented Jul 2, 2020

@iaincollins, Thank you. I'm a little closer to understanding what's going on here but I'm still not seeing the desired results.

When I augment the session with the updated tokens (and add a new datum so I can more easily detect the changes), I can see that the session token is updated with the new data the next time I call getSession(), but the key part I'm missing here is that the updates are not actually persisted across page requests—so when I refresh the page, the JWT with the expired access token is retrieved and loaded into the session, not the updated version.

Here's the relevant bit of code I've got to handle refreshing and updating the session:

async function getTokens({ refreshAuthToken }) {
  const session = await getSession()

  const { user } = session
  const { tokens } = user

  if ( !isTokenStale(tokens.accessToken) ) {
    return tokens
  }

  if ( isTokenStale(tokens.refreshToken) ) {
    console.debug('Refresh token is stale')
    return
  }

  const newTokens = await refreshAuthToken({
    refreshToken: tokens.refreshToken,
  })

  session.user.tokens = {
    ...newTokens,
    refreshedAt: new Date(),
  }

  // After re-fetching the session, I can see in `updatedSession` the values
  // that I added in the line above. So that's good...
  const updatedSession = await getSession()
  console.log('updatedSession: %o', updatedSession)
  
  return newTokens
}

I also added some debugging to my callbacks so I can see when they're invoked:

    callbacks: {
      jwt(token, profile) {
        console.log('JWT callback invoked with %o', { token, profile })
        return Promise.resolve(token)
      },
      session(session, token) {
        console.log('session callback invoked with %o', { session, token })
        session.user.tokens = token.user.tokens

        return Promise.resolve(session)
      }
    },

I never see my updates reflected in these two methods.

So, the $64,000 question remains....how do I set the changes I made to my session persisted so that when I reload the page, I'm not dealing with stale tokens?

Sorry if I seem really obtuse...I'm not stupid, just blonde, so please go easy on me; and thank you for your continued patience with me.

@iaincollins
Copy link
Member

If you make changes to the token in the jwt callback, they should be persisted in the JSON Web Token (i.e. whatever is returned from it should be saved back to the token). This should be true both on sign in and any other time it is called.

The session callback works differently, it is stateless and the response is not 'saved' anywhere, just returned to the client.

They serve slightly different purposes, but can be used together or separately.

For example, it's possible to use the session callback without using JSON Web Tokens - for example, to get additional data from another database table and return it in the session.

Similarly, some use cases only the the JWT callback and code the token in API routes to perform actions server side and don't need to expose additional data to the client via the session callback.

@blms
Copy link
Contributor

blms commented Jul 7, 2020

For example, it's possible to use the session callback without using JSON Web Tokens - for example, to get additional data from another database table and return it in the session.

I had a question about this. I'm currently using the session callback to update session.user.name when the user submits a form. I have written the session callback as such:

session: async (session) => {
  const { email } = session.user;
  const newSession = session;
  const url = `${process.env.SITE}/api/user/${email}`;
  const res = await fetch(url, {
    method: 'GET',
    headers: { 'Content-Type': 'application/json' },
  });
  if (res.status === 200) {
    const user = await res.json();
    newSession.user.name = user.name;
  }
  return Promise.resolve(newSession);
},

And a snippet from my form onSubmit:

const res = await fetch('/api/users', {
  method: 'PATCH',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(body),
});
if (res.status === 200) {
  await res.json();
  getSession();
}

It is working in the sense that the session is being updated after the form is submitted. However, the UI isn't updating after my form is submitted and I call getSession. I have to navigate to a new page for that.

I'm wondering, is this the proper usage of the session callback—to send a GET request to my API route every time?

And is there a way I can update my UI with the new session.user.name as soon as the form is submitted/session is updated?

@yournatalita
Copy link

yournatalita commented Jul 14, 2020

I came with the same issue right now and I have agree there is no clear answer for me too.

As the people above I use custom auth (mine with credentials) but the auth schema is common and looks a lot like Spotify's.

You get the access_token and refresh_token on first sign in. access_token is added to every endpoint in Authorization header.

When access_token expires and you get 401 error from one of API endpoint, you have a second chance to regenerate tokens with refresh_token and rerun the point. When you call refresh endpoint, if server responses with 20X app generates all new tokens, so old ones are useless. You need to update session store or jwt but how can I do it?

If session() callback fires everytime I am asking for session info - i can't ask for a refresh token everytime user hard reloads page or opening new tab, if session() callback was designed for it how can I pass data directly to it, ex. getSession(myNewData) or something like that? It looks like it needs to be a base feature.

Or is every custom non-database authorization designed to have no ability to mutate session at all?
Or is there another more efficient way to work with refresh tokens out-of box?

Edit: I just had an idea to write expire timestamp in my session token after login and check in session() everytime I get there if refresh needs to be done (and refresh there if so). So when my api comes back with 401 I'll ask for getSession() and it refresh itself.

Sorry to bother you, but since I have the lack of experience in auth I find it tricky so I'll be happy if you answer me: Is it a good workaround for my case or it'll cause me troubles in feature?

@iaincollins
Copy link
Member

Edit: I just had an idea to write expire timestamp in my session token after login and check in session() everytime I get there if refresh needs to be done (and refresh there if so). So when my api comes back with 401 I'll ask for getSession() and it refresh itself.

Sorry to bother you, but since I have the lack of experience in auth I find it tricky so I'll be happy if you answer me: Is it a good workaround for my case or it'll cause me troubles in feature?

That is a great solution and is exactly what we want to do in NextAuth.js some time :-)

I was actually just discussing token refreshing in #425 - in the context of databases - but the same logic should apply to tokens used without a database. I very much hope this is something we support natively soon - e.g. with simple functions like getToken('twitter') in the client.

@yournatalita
Copy link

yournatalita commented Jul 15, 2020

@iaincollins thank you, but I think topic's issue is not clear for me at the moment.

Idea: I update token in jwt callback and resolve session with actual token;
My code, simplified

callbacks: {
    session: async (session: any, token: any) => {
      console.log('SESSION CALLBACK', session, token);
      return Promise.resolve({
        ...session,
        ...token
      });
    },
    jwt: async (token: any) => {
      console.log('JWT CALLBACK START', token);
   
      let newSession = {
        accessExpires: token.accessExpires || getExpireISOCurrentTime(),
        ...token
      };

      const isTimeAfter = isAfter(new Date(), new Date(newSession.accessExpires));

      if (token && isTimeAfter) {
        const result = await sendRefresh(token);
        const data = result.data;

        if (data.access_token) {
          newSession = {
            user: data,
            accessExpires: getExpireISOCurrentTime()
          };
        }
      }

      console.log('NEW TOKEN JWT', newSession);
      return Promise.resolve(newSession);
    }
  }

And this is the log i get after it, with user interactions described since login, simplified:

[user logins]
> JWT CALLBACK START { user: { refresh_token: "vI" } }
> NEW TOKEN JWT { accessExpires:  "2020-07-15T13:31:27.257Z", user: { refresh_token: "vI" }}

[page reloads and redirect after success login]
> JWT CALLBACK START { accessExpires:  "2020-07-15T13:31:27.257Z", user: { refresh_token: "vI" }} <---- JWT is UPDATED
> NEW TOKEN JWT { accessExpires:  "2020-07-15T13:31:27.257Z", user: { refresh_token: "vI" }} 
> SESSION CALLBACK { ... accessExpires:  "2020-07-15T13:31:27.257Z", user: { refresh_token: "vI" }} <---- on CLIENT I GET THIS

[timeout comes]
[user reloads page]
> JWT CALLBACK START { accessExpires:  "2020-07-15T13:31:27.257Z", user: { refresh_token: "vI" }} <---- JWT is EXPIRED
> NEW TOKEN JWT { accessExpires:  "2020-07-15T13:41:50.470Z", user: { refresh_token: "qU" }} <---- JWT is UPDATED
> SESSION CALLBACK { ... accessExpires:  "2020-07-15T13:31:27.257Z", user: { refresh_token: "qU" }} <---- i get correct JWT in session callback and I pull it on client

> JWT CALLBACK START { accessExpires:  "2020-07-15T13:31:27.257Z", user: { refresh_token: "vI" }} <---- JWT is called again?, but why is it old?
> NEW TOKEN JWT { accessExpires:  "2020-07-15T13:31:27.257Z", user: { refresh_token: "vI" }} <--- my code crashes since it expired
> SESSION CALLBACK { ... accessExpires:  "2020-07-15T13:31:27.257Z", user: { refresh_token: "vI" }} <---- saves as old

Why my JWT token callback calls twice both times and why on the second time it's stale? it updates perfectly after await function but then comes back with old one
What do I miss?

@yournatalita
Copy link

yournatalita commented Jul 15, 2020

Turns out that server-side getSession is stale in my case.

Is this expected behavior or my code is wrong? After removing server-side check of session described here #345 and here #347 I got rid of second stale jwt callback calls.

SSR and Server-Side redirects are very handy, so can I ask if this bug, wrong usage or expected behaviour? Any workarounds?
To be honest, this behaviour is not obvious at first sight and needs to be described.

@iaincollins iaincollins added the bug Something isn't working label Jul 20, 2020
@iaincollins
Copy link
Member

@yournatalita Hmm thank you for raising this.

This sounds like it could be a bug but I'm not entirely sure what is going on.

I don't know why and old token would get presented, unless it was some sort of artefact of development mode (where weird things happen, like routes getting called more than once and taking some time to get compiled by Next.js the first time).

If you get the time and are so inclined, please feel free to raise bug report. I've flagged this with bug to make sure it's tracked in the mean time, but we should move that into a separate issue.

It would be helpful to know:

  1. Was this in v2 or in v3 beta?
  2. Was this locally / in development mode or in production?

I suspect / hope this is a weird situation from development mode, but I think it's worth some investigation to confirm that.

@davepoon
Copy link

And is there a way I can update my UI with the new session.user.name as soon as the form is submitted/session is updated?

I am having the same issue using the Credentials provider. I am expecting the global session state to be updated const [session] = useSession(); after the form is submitted using my custom React Hook Form const res = await fetch("/api/auth/callback/credentials", requestOptions);

To solve that, now I have to force to refresh the page with router.reload() in order to get the session update after logging in.

@iaincollins
Copy link
Member

Hmm if you have a provider configured in _app.js the session state should update automatically anytime you call getSession - or if other events occur, such as the window losing and gaining focus, or sign in with another window.

There could be edge case issues, but uses a fairly well supported mechanism for this.

Calling getSession() on the client should be enough, you should need to force reload the page, though the provided helper methods actually do that to avoid any jankyness that apps typically display (with the option to use your own sign in function handle it more elegantly left for advanced users).

If you have a provider in _app.js and call getSession() after the sign in form has returned and it's still not working I'd love to take a look.

@davepoon
Copy link

davepoon commented Oct 1, 2020

Thanks for the quick reply.

I have added provider in _app.tsx, and I am using a modal box for the login form. And I am expecting after the login form has been submitted successfully, and the modal box will be closed, and then the header login button detect the session update from const [session] = useSession();, and then button will be replaced with a Logout button without a page refresh.
I have also tried to call getSession(), the returned value is up to date, but the useSession hook session state is still not up to date unless reload the page or as you said the window losing and gaining focus after coming back from another window, and then the session state will be updated.

This is a demo video to show the issue. https://www.loom.com/share/9e4837edbd274ec892e5853e6a9c9341
Probably next-auth hasn't been designed to work in this way. But if we have a hook or function to get the session update instantly within the same window or screen, this would be great to address an use case like this.

@dsavage311
Copy link

dsavage311 commented Nov 3, 2020

I'm running into a similar issue described above, but I don't have the same ability as others to depend on a timeout to refresh the session. The issue I'm running into is not finding a way to update the user object that is returned from getSession().

I'm using the credentials provider so that I can authenticate with our custom API endpoint on another server. On a successful login, I fetch some basic user information from our API as well to populate the user object that is attached to the session.

The use case I'm having difficulty understanding how to implement properly is: if the logged in user updates their name or profile image, I can't find a way to update this information in the returned object from getSession() without:

  1. The user logging out and logging back in to re-fetch the information from the API
  2. Querying our API to fetch profile information in the session / jwt callback

Option 1 isn't really acceptable for obvious reasons: you shouldn't have to force the user to logout just to display their new profile image / name change.
Option 2 isn't acceptable either, as multiple getSession calls are made on a regular basis from the client, and would quickly add an enormous amount of traffic to our APIs.

Ideally, I'd be able to add some kind of flag to the session to trigger a refetch of data during the jwt / session callback as described by @yournatalita: getSession(myNewData).

My only option I can see now is to add some sort of timeout to the session so I can throttle my api call checks, but this still isn't really ideal and adds more traffic than is necessary to our APIs, along with causing at least some kind of delay to updating the user's image / name depending on how long I add the timeout for.

Is there some way I can alter just one property on the session object from the client so it knows to refetch user data from our API? Or is there a REST endpoint that's not documented where I can update the session from the client? Or just pass some bit of data to the jwt / session callback? I've been searching through all the issues and docs and I've come up empty-handed.

Any help is greatly appreciated!

@iaincollins
Copy link
Member

The issue I'm running into is not finding a way to update the user object that is returned from getSession().

You can do this via the session callback

Ideally, I'd be able to add some kind of flag to the session to trigger a refetch of data during the jwt / session callback as described

If you call getSession() on the client it should update the session state in the current window and in all other open windows.

e.g. you can do this after an operation like saving changes to a profile - e.g. updating name or profile image.

If you want to control how the session on the client is updated, you can can control the cache behaviour by passing options in app.js

import { Provider } from 'next-auth/client'

export default function App ({ Component, pageProps }) {
  return (
    <Provider session={pageProps.session}
      options={{ 
        clientMaxAge: 60     // Re-fetch session if cache is older than 60 seconds
      }}
      >
      <Component {...pageProps} />
    </Provider>
  )
}

@justinwhall
Copy link
Contributor

justinwhall commented Nov 4, 2020

Perhaps I'm missing something here but if I mutate the session returned from getSession() I do not see a change in the session callback.

    session: async (session, user, sessionToken) => {
       // session and user remain the same.
      return Promise.resolve(session)
    },
    jwt: async (token, user, account, profile, isNewUser) => {
      // user remains the same. 
      if (profile) {
        token.id = user.id
      }
      return Promise.resolve(token)
    }
export default async (req, res) => {
  const session = await getSession({ req })
  session.user.foo = 'foobar'
  await getSession({ req })
  res.end()
}

@dsavage311
Copy link

If you want to control how the session on the client is updated, you can can control the cache behaviour by passing options in app.js

import { Provider } from 'next-auth/client'

export default function App ({ Component, pageProps }) {
  return (
    <Provider session={pageProps.session}
      options={{ 
        clientMaxAge: 60     // Re-fetch session if cache is older than 60 seconds
      }}
      >
      <Component {...pageProps} />
    </Provider>
  )
}

This was the big piece I was missing. I noticed the client would call session almost constantly when even just regaining focus to the browser window or just clicking around throughout the app, so I thought it would be constantly polling my API to fetch the data I needed. Adding the caching options instantly fixed that issue and allowed me to use the session callback as you mentioned.

For someone else that might be running into this issue, here's how I went about solving it:

_app.js

import { Provider } from 'next-auth/client';

const sessionOptions = {
  clientMaxAge: 2 * 60 * 60, // Re-fetch session if cache is older than 2 hours
  keepAlive: 60 * 60 // Send keepAlive message every hour
};

function App({ Component, pageProps }) {
  const [session, setSession] = useState(pageProps.session);

  return (
    <Provider options={sessionOptions} session={pageProps.session}>
      <Header {...pageProps} session={session} />
      <Component {...pageProps} updateSession={setSession} />
    </Provider>
  )
}

export default App;

pages/api/auth/[...nextauth].js

...
const options = {
  callbacks: {
    jwt: async (token, authToken) => {
      let resolvedToken = token;

      if (authToken) {
        resolvedToken = { ...authToken, ...token };
      }

      return Promise.resolve(resolvedToken);
    },
    session: async (session, jwt) => {
      if (jwt.authToken) {
        try {
           // fetches the user record from our API
          const contact = await getContact(jwt.authToken);
          
          // Only store the data we need in the session.user object
          const minimalContact = {
            email: contact.email,
            image: contact.profile_img.thumbnail ? contact.profile_img.thumbnail : null,
            role: contact.role,
            name: contact.first_name
          };

          session.user = minimalContact;
          return Promise.resolve(session);
        } catch (err) {
          return Promise.resolve(session);
        }
      }

      return Promise.resolve(session);
    },
    ...
    providers: [
       Providers.Credentials({
         authorize: async (credentials) => {
           // my API returns an authToken to be used on all subsequent API calls
           const authToken = await login(credentials);
           
           // No authToken implies the user entered incorrect credentials
           if (authToken === null) {
             return Promise.resolve(null);
           }
           
           // email is also useful for identifying the user in my application for subsequent API calls

           return Promise.resolve({ authToken, email: credentials.email });
         },
         credentials: {
           email: { label: 'Email', type: 'email', placeholder: 'Email' } ,
           password: { label: 'Password', type: 'password' }
         },
         name: 'Credentials'
       })
     ]
  },
  ...

editProfile.js

  import { getSession } from 'next-auth/client';
  ....
  // Note updateSession is passed in from my _app.js to the page. 
  // This is so in the header I can display the updated profile image and username 
  // (if changed on a profile update)
  export default function EditProfile({ updateSession }) {
      const saveData = (data) => {
        saveContact(data).then(() => {
           getSession().then((session) => {
              updateSession(session);
            })
          });
        })
      };
   }
  ...

Hope that helps someone else that might be in the same spot struggling with how to do something like this. I'm not sure I did it the best way possible but it works as I would want it to without unnecessary API calls! :)

@justinwhall
Copy link
Contributor

Thanks @dsavage311! Avoiding unnecessary API call is exactly what I'm trying to do. This clears up some things but in regards to updateSession, I understand you passing it in from _app.js but where does it come from before that / what does it look like? The adapter you are using? If so, how does one access that method?

@elilambnz
Copy link

@justinwhall it looks like the updateSession is updating the session that gets passed into the <Header> component. This isn't an ideal solution for my use case, as I'd like to update the session globally without passing down method through props.

I'm still working on a robust solution to update an existing user's access token which is stored in session, but can't quite figure out how the API wants me to update the session directly. Using the session callback as suggested above seems to result in a stale session when subsequently calling getSession() after mutating. If I figure it out, I'll put an update here.

@justinwhall
Copy link
Contributor

justinwhall commented Nov 6, 2020

@justinwhall it looks like the updateSession is updating the session that gets passed into the <Header> component. This isn't an ideal solution for my use case, as I'd like to update the session globally without passing down method through props.

Ah, I missed that. Thanks @elilambnz! I think my issue is around this. For instance ppageProps.session is always undefined for me.

function App({ Component, pageProps }) {
  console.log(pageProps.session) // always undefined
  return (
    <Provider options={sessionOptions} session={pageProps.session}>
      <Header {...pageProps} session={session} />
      <Component {...pageProps} updateSession={setSession} />
    </Provider>
  )
}

I think this might be an issue with my adapter. I'm using the custom adapter option with Fauna. https://gist.github.com/justinwhall/4895b7d6115b2acf7201808109338da7

Edit: Erm, maybe not. The createSession method is only for DB session not jwt.

@elilambnz
Copy link

@justinwhall the way you're accessing pageProps.session above looks correct, this might be a separate issue. I'll add my solution to updating the session in a separate comment, if that doesn't solve it perhaps open a new issue for this problem.

@elilambnz
Copy link

An example of updating the session in the context of extending a user upon signin can be found here.

However, this doesn't cover conditionally updating the session. As far as I'm aware, there's not currently a method exposed to update the session. However, whenever the session is accessed, the jwt() callback is invoked before the session() as per the docs. In the context of refreshing an accessToken, which is my use case for updating the session, we can use the callbacks to check the token expiry and pass new data to the session. As for updating user information, i.e. when a user is editing their profile, you could perhaps adapt this solution to check for a difference in user data when the callbacks are invoked, then pass the data into the session.

callbacks: {
  jwt: async (token, user, account, profile, isNewUser) => {
    // The user argument is only passed the first time this callback is called on a new session, after the user signs in
    if (user) {
      // Add a new prop on token for user data
      token.data = user
    }

    // Don't access user as it's only available once, access token.data instead
    if (token.data?.accessToken) {
      const decodedJwt = jwt_decode(token.data.accessToken)
      const almostNow = moment().add(5, 'minutes').valueOf() / 1000

      if (decodedJwt.exp !== undefined && decodedJwt.exp < almostNow) {
        // Token almost expired, refresh
        try {
          const newToken = await refreshToken(token.data.refreshToken)
          token.data.accessToken = newToken
        } catch (error) {
          console.error(error, 'Error refreshing access token')
        }
      }
    }

    return Promise.resolve(token)
  },
  session: async (session, user) => {
    // `user` is the jwt token
    if (user.data) {
      // Assign access token to session
      session.accessToken = user.data.accessToken
    return Promise.resolve(session)
  },
},

Here I am using the credentials provider with JWT. If you're specifically after a solution to refresh access tokens, this approach assumes that you have an API or are using a service which provides accessToken and refreshToken JWTs, and also provides functionality to refresh a token.

@justinwhall
Copy link
Contributor

@elilambnz Your logic makes perfect sense. Appreciate your response here. And yes, I am doing what you are suggesting. Updating a user's profile. The above makes sense as you can check to see if a token is expired and then, do stuff...

In the case of a user, say, updating their profile picture, there's no way to update the session because no matter what I do, the session param is always the same. In fact, all the params for the session and jwt call back are always the same. For instance:

// some API route or page.
export default async (req, res) => {
  const session = await getSession({ req }) // get session
  session.user.foo = 'foobar' // mutate session
  // do other stuff that doesn't matter...
}
    // no matter how I mutate the session, params are always the same. Specifically, I'd expect  `session.user.foo` === 'foorbar'
    session: async (session, user, sessionToken) => {
      session.user.id = user.id
      return Promise.resolve(session)
    },
    jwt: async (token, user, account, profile, isNewUser) => {
      if (profile) {
        token.id = user.id
      }
      return Promise.resolve(token)
    }

@dsavage311
Copy link

dsavage311 commented Nov 9, 2020

Thanks @dsavage311! Avoiding unnecessary API call is exactly what I'm trying to do. This clears up some things but in regards to updateSession, I understand you passing it in from _app.js but where does it come from before that / what does it look like? The adapter you are using? If so, how does one access that method?

@justinwhall So I'm actually just using the React useState hook in _app.js. The initial value is what is passed in from pageProps, but after that I'm manually updating it. So I pass the useState update function down to my page components as updateSession.

import { useState } from 'react';

function App({ Component, pageProps }) {
  const [session, setSession] = useState(pageProps.session); // this is all I'm doing

  return (
    <Provider options={sessionOptions} session={pageProps.session}>
      <Header {...pageProps} session={session} />
      <Component {...pageProps} updateSession={setSession} />
    </Provider>
  )
}

For clarity's sake, I probably should have passed it into my component as setSession. So, pageProps gives the initial value from the SSR page, but then when I do something in the UI like update the user's image or name, I call the next-auth's getSession and use it in that passed down setSession from the useState react hook (although it would be referred to as updateSession in my above code). Since I really only care about the current session in the header, header is always using the session I am holding in state.

Hopefully that clears up your question. Updating the actual session object happens in the callbacks on the server side in the nextAuth config. The only place you can mutate session is in these callbacks. I do the actual mutation in the session callback, but if you notice I also spread that data across the jwt token. I'm not sure that's done in the "proper way", but that effectively preserves the data that I'm trying to save in the session:

  callbacks: {
    jwt: async (token, authToken) => {
      let resolvedToken = token;

      if (authToken) {
        resolvedToken = { ...authToken, ...token };
      }

      return Promise.resolve(resolvedToken);
    },
    session: async (session, jwt) => {
      if (jwt.authToken) {
        try {
           // fetches the user record from our API
          const contact = await getContact(jwt.authToken);
          
          // Only store the data we need in the session.user object
          const minimalContact = {
            email: contact.email,
            image: contact.profile_img.thumbnail ? contact.profile_img.thumbnail : null,
            role: contact.role,
            name: contact.first_name
          };

          session.user = minimalContact;
          return Promise.resolve(session);
        } catch (err) {
          return Promise.resolve(session);
        }
      }

      return Promise.resolve(session);
    },

@elilambnz
Copy link

In the case of a user, say, updating their profile picture, there's no way to update the session because no matter what I do, the session param is always the same. In fact, all the params for the session and jwt call back are always the same.

@justinwhall I believe this is due to the nature of JS (and admittedly an oversight of mine earlier). In your example, you're essentially taking a copy of session and updating the value. So session.user.foo will be foobar in the scope of that function, but the original session object is unchanged.

Since the library doesn't appear to expose a method for updating the session directly, that leaves us a couple of options. We could provide a mutateable session object to the <Provider> and make an updateSession() function for mutating this value (@dsavage311 has provided an example of this while I was typing all of this out). With the introduction of React Hooks, you could possibly provide this functionality globally without passing it down as props, which is something I'm trying to avoid myself.

Another (less clean) approach is manually triggering a signin after updating the user. Assuming the API providing user data has the new changes, the new user object will be loaded into the session if you're saving user details in the callbacks.

Updating the actual session object happens in the callbacks on the server side in the nextAuth config. The only place you can mutate session is in these callbacks. I do the actual mutation in the session callback, but if you notice I also spread that data across the jwt token. I'm not sure that's done in the "proper way", but that effectively preserves the data that I'm trying to save in the session

@dsavage311 since these callbacks are invoked quite regularly, do you find that this approach spams your API? Or are you only fetching the session in one place within your app? I'm accessing the session from a couple of places and I'd imagine this approach would generate a lot of surplus API calls.

@TimBenjamin
Copy link

I know this thread is a little old, but it's all that came up for me in my searching for session mutation. Therefore I will post my solution here for the next person who goes on this quest.

I'm using v4, with the EmailProvider only, using JWT and a DynamoDB backend. (Yes, I had to roll my own DynamoDBAdapter because there is not a v4 one available at the time of writing).

I needed to modify the session in order to store some values the user set after they signed in. The app already saves those values to the user database (i.e. in dynamodb), when the user changes them in the app, where they will be retrieved next time the user signs in and starts a new session. However, my problem was: how to put these values in the current session immediately after the user sets them, without requiring the user to sign out and sign in again? Not clear at all from the docs.

The answer was, in the end, quite simple, but it took me an effort to get there. Note that this is not the documented way to set up your [...nextauth].js -

./pages/api/auth/[...nextauth].js

export default async function handler(req, res) {
  return NextAuth(req, res, {
    ...
    callbacks: {
      ...
      async jwt({ token, user, account, profile, isNewUser }) {
        // NB: we can update the session with this callback.
        // call /api/auth/session?update and that will invoke a reload of the user data from dynamodb.
        if(url.parse(req.url, true).query.update !== undefined) {
          const client = new AWS.DynamoDB.DocumentClient()
          const data = await client.get({
            TableName: "next-auth-users",
            Key: {
              pk: `USER#${token.userId}`,
              sk: `USER#${token.userId}`,
            },
          }).promise()
          token.user = data.Item
        } else {
          // anything else, etc
          user && (token.user = user)
        }
        ...
        return token
      },
      async session({ session, token, user }) {
        // And here we make sure that the data we put in the token make it through to the session.
        // NB this callback gets called right after the jwt callback, with the token that it modified with the updated user data
        ...
        session.user = token.user
        ...
        return session
      },
      ...

So I used that like this on the client side:

...
// update database with new user data foo: bar
fetch('/api/data/userWatchList', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    foo: bar,
    userId: session.user.id
  }),
}).then(() => {
  // Now we cause the jwt callback handler to retrieve the new user data and save it in the session
  fetch('/api/auth/session?update', {
    method: "GET",
    credentials: "include"
  })
})
...

It feels sub-optimal to make two database transactions to do this, but it works, and it's not completely madly inefficient.

@MamoshiSE
Copy link

@TimBenjamin Hey your solution seems the best so far, but I wanted to do it from server-side - call '/api/auth/session?update' from another api route e.g: /api/user.js but when I call from here it doesnt show any query, any idea how to solve this?

@TimBenjamin
Copy link

@MamoshiSE You can't call the route from another route and expect it to work like a client-server session, at least not without passing cookies and your JWT around. You are better to either 1) call each api independently from the client side or 2) do your api to api calls without depending on the user's session or 3) on the client side use a global state (e.g. with Redux or a react context provider) to keep all that data and not use the session.

@MamoshiSE
Copy link

@TimBenjamin I see. I am trying with passing around cookies, but do you know if there is a way in the nextauth file inside the jwt callback to know from which "route" the call is being made? I am for example using const session = await getSession({ req }); inside one of my api routes which leads to the JWT callback being triggered everytime this api route is used, which leads to unessecary api call to my db. Is there a way in the nextauth I can know where the jwt callback was triggered from and exclude that api route for example?

@balazsorban44
Copy link
Member

you can control the number of writes to the database easily with the updateAge session option https://next-auth.js.org/configuration/options#session

@alur2191
Copy link

alur2191 commented Dec 10, 2021

@TimBenjamin Hi there! Trying to implement your solution, and it seems to be working to a certain point. User submits data to the DB:

await fetch("/api/company/create", {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
}).then(() => {
    fetch('/api/auth/session?update', {
        method: "GET",
        credentials: "include"
    })
})

Router.push('/')

Here's how [...nextauth].js handles "/api/auth/session?update" link:

const createOptions = (req) => ({
   // session settings and providers
    ],callbacks: {
        jwt: async (token, user) => {
            if(req.url === "/api/auth/session?update"){
                const userRes = await prisma.user.findUnique({
                    where: {
                        email: token.email
                    },
                    include:{
                        company:true
                    }
                })
                userRes.company && (token.companyId = userRes.company.id)
            }
            if(user){
                token.activated = user.activated
                if(user.companyId){
                    token.companyId = user.companyId
                }
            }
            
            return token;
        },
        session: async (session, user) => {
            if(user){
                session.user.companyId = user.companyId
                
                if(session.user.activated){
                    session.user.activated = user.activated
                }
                console.log(session);
            }

            return session;
        }
    },
})

export default async (req, res) => {
    return NextAuth(req, res, createOptions(req));
};

Console.log returns the session with updated data. So far it works as it should. What I'm trying to do from here is to show/hide links from the navbar component.

/components/layout/navbar.js

import classes from './navbar.module.css'
import {useSession, signOut} from 'next-auth/client'
import Link from 'next/link'


function Navbar () {
    const [session, loading] = useSession()

    console.log('Component session:',session);
    function logoutHandler() {
        signOut()
    }
    
    return(
    <nav className={classes.navbar}>
        <img src="/images/logo.png" alt="" style={{width: 100, height: 40}}/>
        <ul>
            <Link href={{
            pathname: "/"}}><a>link</a></Link>
            {session&&session.user.companyId &&<Link href={{
            pathname: "/link/form"
            }}
            ><a>link</a></Link>}
            {session&&!session.user.activated &&<Link href={{
            pathname: "/link/"
            }}
            ><a>link</a></Link>}
            {!session &&  !loading && (<Link href={{pathname:"/auth"}}><a>link</a></Link>)}
            {session && <li><button onClick={logoutHandler}>link</button></li>}
        </ul>
    </nav>
    )
}


export default Navbar;

Console.log doesn't return updated session data. I have to refresh, or open another tab and go back for the links to appear. I'm new to Next.JS and my shortfall might be with Next.JS and not Next-Auth. I'd really appreciate some help! I've been trying to figure this one out...

@MamoshiSE
Copy link

I know this thread is a little old, but it's all that came up for me in my searching for session mutation. Therefore I will post my solution here for the next person who goes on this quest.

I'm using v4, with the EmailProvider only, using JWT and a DynamoDB backend. (Yes, I had to roll my own DynamoDBAdapter because there is not a v4 one available at the time of writing).

I needed to modify the session in order to store some values the user set after they signed in. The app already saves those values to the user database (i.e. in dynamodb), when the user changes them in the app, where they will be retrieved next time the user signs in and starts a new session. However, my problem was: how to put these values in the current session immediately after the user sets them, without requiring the user to sign out and sign in again? Not clear at all from the docs.

The answer was, in the end, quite simple, but it took me an effort to get there. Note that this is not the documented way to set up your [...nextauth].js -

./pages/api/auth/[...nextauth].js

export default async function handler(req, res) {
  return NextAuth(req, res, {
    ...
    callbacks: {
      ...
      async jwt({ token, user, account, profile, isNewUser }) {
        // NB: we can update the session with this callback.
        // call /api/auth/session?update and that will invoke a reload of the user data from dynamodb.
        if(url.parse(req.url, true).query.update !== undefined) {
          const client = new AWS.DynamoDB.DocumentClient()
          const data = await client.get({
            TableName: "next-auth-users",
            Key: {
              pk: `USER#${token.userId}`,
              sk: `USER#${token.userId}`,
            },
          }).promise()
          token.user = data.Item
        } else {
          // anything else, etc
          user && (token.user = user)
        }
        ...
        return token
      },
      async session({ session, token, user }) {
        // And here we make sure that the data we put in the token make it through to the session.
        // NB this callback gets called right after the jwt callback, with the token that it modified with the updated user data
        ...
        session.user = token.user
        ...
        return session
      },
      ...

So I used that like this on the client side:

...
// update database with new user data foo: bar
fetch('/api/data/userWatchList', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    foo: bar,
    userId: session.user.id
  }),
}).then(() => {
  // Now we cause the jwt callback handler to retrieve the new user data and save it in the session
  fetch('/api/auth/session?update', {
    method: "GET",
    credentials: "include"
  })
})
...

It feels sub-optimal to make two database transactions to do this, but it works, and it's not completely madly inefficient.

Hey, I have done similar to you it works fine until I for example refresh the page/navigate to another page, then the session values just go back to the previous values even though the database has been updated accordingly and ?update was called when a change was made. I can't figure out how to solve this except having to call the DB every single time in nextauth to update the session.

@bna-ch
Copy link

bna-ch commented Jan 6, 2022

@TimBenjamin Hi there! Trying to implement your solution, and it seems to be working to a certain point. User submits data to the DB:

await fetch("/api/company/create", {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
}).then(() => {
    fetch('/api/auth/session?update', {
        method: "GET",
        credentials: "include"
    })
})

Router.push('/')

Here's how [...nextauth].js handles "/api/auth/session?update" link:

const createOptions = (req) => ({
   // session settings and providers
    ],callbacks: {
        jwt: async (token, user) => {
            if(req.url === "/api/auth/session?update"){
                const userRes = await prisma.user.findUnique({
                    where: {
                        email: token.email
                    },
                    include:{
                        company:true
                    }
                })
                userRes.company && (token.companyId = userRes.company.id)
            }
            if(user){
                token.activated = user.activated
                if(user.companyId){
                    token.companyId = user.companyId
                }
            }
            
            return token;
        },
        session: async (session, user) => {
            if(user){
                session.user.companyId = user.companyId
                
                if(session.user.activated){
                    session.user.activated = user.activated
                }
                console.log(session);
            }

            return session;
        }
    },
})

export default async (req, res) => {
    return NextAuth(req, res, createOptions(req));
};

Console.log returns the session with updated data. So far it works as it should. What I'm trying to do from here is to show/hide links from the navbar component.

/components/layout/navbar.js

import classes from './navbar.module.css'
import {useSession, signOut} from 'next-auth/client'
import Link from 'next/link'


function Navbar () {
    const [session, loading] = useSession()

    console.log('Component session:',session);
    function logoutHandler() {
        signOut()
    }
    
    return(
    <nav className={classes.navbar}>
        <img src="/images/logo.png" alt="" style={{width: 100, height: 40}}/>
        <ul>
            <Link href={{
            pathname: "/"}}><a>link</a></Link>
            {session&&session.user.companyId &&<Link href={{
            pathname: "/link/form"
            }}
            ><a>link</a></Link>}
            {session&&!session.user.activated &&<Link href={{
            pathname: "/link/"
            }}
            ><a>link</a></Link>}
            {!session &&  !loading && (<Link href={{pathname:"/auth"}}><a>link</a></Link>)}
            {session && <li><button onClick={logoutHandler}>link</button></li>}
        </ul>
    </nav>
    )
}


export default Navbar;

Console.log doesn't return updated session data. I have to refresh, or open another tab and go back for the links to appear. I'm new to Next.JS and my shortfall might be with Next.JS and not Next-Auth. I'd really appreciate some help! I've been trying to figure this one out...

You can use getSession() to get the updated session. reference - https://next-auth.js.org/getting-started/client#getsession

@jaxomlotus
Copy link

I see this was closed - does that mean we're closer to a solution?

@TimMTech

This comment was marked as spam.

@seenasabti
Copy link

seenasabti commented Jan 2, 2023

I need to simply update the session object after some user action. e.g. after a user uploads a new image, or updates their name. I should not have to reload the user object on every useSession call. I dont see any solutions here or anywhere on the web on how to accomplish this.

@larsqa
Copy link

larsqa commented Jan 16, 2023

Updating the session may be possible by directly modifying next-auth's encrypted JWT cookie value; however, this requires some custom encryption & security logic, which should be used carefully!
An example can be found here: https://stackoverflow.com/a/75134483/3673659

@jaxomlotus
Copy link

@larsqa thank you - that's super helpful.

I still maintain that this should ideally be supported by next-auth directly without any custom tampering given the security risks custom workarounds can introduce. And given that most sites with login also store some form of user modifiable profile data, a method to update the JWT token would be used by many next-auth users. If you have every one of these sites writing their own custom implementation, it's an invitation for errors and the types of security breaches next-auth aims to avoid.

@jaxomlotus
Copy link

@larsqa fyi - when implementing that stackexchange thread you linked it triggers a "TypeError: JWTs must be decrypted first" error. Is there something else that must be added to it?

@larsqa
Copy link

larsqa commented Jan 17, 2023

@jaxomlotus

... ideally be supported by next-auth directly ... If you have every one of these sites writing their own custom implementation, it's an invitation for errors and the types of security breaches next-auth aims to avoid.

Agree. A quick online search, however, shows that this is a long existing question with several GitHub issues and the next-auth never addressed this issue.


... when implementing that stackexchange thread you linked it triggers a "TypeError: JWTs must be decrypted first" error.

Do you know at which line this error appears? I tested it myself and it works nicely.

@jaxomlotus
Copy link

jaxomlotus commented Jan 17, 2023

@larsqa

Do you know at which line this error appears? I tested it myself and it works nicely.

It happens at this line:

// 1. Get current token
const token = await getToken({ req });

I also changed it to include my JWT_SECRET (and confirmed the value is passed in)

// 1. Get current token
const token = await getToken({ req, JWT_SECRET });

But for some reason this still happens. This is in Next-Auth 3.17.2 and on Windows 11, so maybe that has something to do with it. I can never get getToken to send me back the token contents now that I think of it.

@seenasabti
Copy link

@jaxomlotus fwiw, my solution was to use encrypted cookies to decouple the session object from user data. The session object is now only used to convey signed in/out status. I'm dismayed by how rigid the architecture of next-auth is, but maybe that's by design.

@jaxomlotus
Copy link

@seenasabti that's a great idea. Do you know of any good open source examples I could look at to understand the best way to accomplish this? I'm pretty new to encryption / decryption (which was why working with next-auth was so appealing in the first place!)

@jaxomlotus
Copy link

@larsqa I was able to get the token contents by adding raw=true to the options passed to getToken

const token = await getToken({ req, JWT_SECRET,raw:true });

This only gives me the raw token however - next I'll need to figure out how to decode it.

@larsqa
Copy link

larsqa commented Jan 18, 2023

@jaxomlotus getToken is one of next-auth's function, like getSession. If that doesn't work, then you might have another issue out of scope of this Github issue.
Have a look at their examples: https://next-auth.js.org/tutorials/securing-pages-and-api-routes#using-gettoken

@advename
Copy link

I wrapped the approach from @larsqa's link in a method, called updateToken, that can now be used similarly to getToken. Use at your own risk

import { EncryptJWT } from "jose";
import hkdf from "@panva/hkdf";
import { serialize } from "cookie";
import { getToken } from "next-auth/jwt";
import { NextResponse } from "next/server";

/**
 * Update the "next-auth" encrypted JWT token, stored client side in an HttpOnly cookie.
 * Does not work when the session strategy is set to "database".
 * WARNING: Tampering with security implementations is commonly a bad. Use this at your own risk!
 * 
 * @example
 * Use in Next's API routes.
 * 
 *              export default async function handler(req, res) {
 *                  await updateToken({req,res, transformToken: (token) => {
 *                              // Example structure of the "token"
 *                              // JWT claims are stripped out, i.e. "sub", "iat", "exp", "jti"
 *                              // {
 *                              //     email: 'example@example.com',
 *                              //     isPremiumUser: false
 *                              // }
 *                          return { ...token, isPremium: true }; // apply changes and return updated token
 *                      },
 *                  });
 *              
 *                  res.status(200).json({ message: "Successfully updated user to premium" });
 *              }
 * 
 * 
 * @param {object} param0
 * @param {NextRequest | NextApiRequest} param0.req Pass on the request instance. Used to get the current JWE value from the cookie
 * @param {NextResponse | NextApiResponse} param0.res Pass on the response instance. Used to update the new JWE value in the cookie
 * @param {function(object): object} param0.transformToken Callback method where you may update the token, except current JWT claims. We only want to update values, not implementation logic.
 * @param {string} [param0.cookieName="next-auth.session-token"]
 * @param {string} [param0.secret=process.env.NEXTAUTH_SECRET]
 */
export const updateToken = async ({
    req,
    res,
    transformToken,
    cookieName = "next-auth.session-token",
    secret = process.env.NEXTAUTH_SECRET,
}) => {
    // Get the current token, and destructure initial JWT claims
    const { sub, iat, exp, jti, ...token } = await getToken({
        req,
        cookieName,
        secret,
    });

    const newToken = transformToken(token); // allows only to update your custom token properties

    const newJWE = await encode({ sub, iat, exp, jti, ...newToken }, secret);

    const newCookie = serialize(cookieName, newJWE, {
        httpOnly: true,
        path: "/",
        maxAge: token.exp,
        sameSite: "lax",
    });
    res.setHeader("Set-Cookie", newCookie);
};

/**
 * ===============================
 * HELPER METHODS
 * ===============================
 * Copied and modified from "next-auth"
 */

/**
 * Modify "next-auth" encode method to manullay set JWT claims
 * @see https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/jwt/index.ts#L17
 */
async function encode(token, secret) {
    const encryptionSecret = await getDerivedEncryptionKey(secret); // Create a cryptographical random secret key using a predictable random secret.
    return await new EncryptJWT(token)
        .setProtectedHeader({ alg: "dir", enc: "A256GCM" })
        .setSubject(token.sub) // set "sub" Subject claim
        .setIssuedAt(token.iat) // adds "iat" to the JWT with current timestamp
        .setExpirationTime(token.exp) // adds "exp" to the JWT - timestamp format
        .setJti(token.jti) // set the "jti claim" - allows for replay protection/blacklist
        .encrypt(encryptionSecret);
}

/**
 * Copy & Paste from "next-auth" since it's not exported
 * Create a cryptographical random secret key using a predictable random secret.
 * @see https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/jwt/index.ts#L119
 * @param {string} secret Random string secret, e.g. "Whale-Banana-69"
 * @returns {Promise<Uint8Array(32)>} Actual cryptographical randomnes, e.g. transforms above into: "Uint8Array(32) [189,67,29,228,...]" which
 */
async function getDerivedEncryptionKey(secret) {
    return await hkdf(
        "sha256",
        secret,
        "",
        "NextAuth.js Generated Encryption Key",
        32
    );
}

@cubesnyc
Copy link

cubesnyc commented Mar 8, 2023

I wrapped the approach from @larsqa's link in a method, called updateToken, that can now be used similarly to getToken. Use at your own risk

import { EncryptJWT } from "jose";
import hkdf from "@panva/hkdf";
import { serialize } from "cookie";
import { getToken } from "next-auth/jwt";
import { NextResponse } from "next/server";

/**
 * Update the "next-auth" encrypted JWT token, stored client side in an HttpOnly cookie.
 * Does not work when the session strategy is set to "database".
 * WARNING: Tampering with security implementations is commonly a bad. Use this at your own risk!
 * 
 * @example
 * Use in Next's API routes.
 * 
 *              export default async function handler(req, res) {
 *                  await updateToken({req,res, transformToken: (token) => {
 *                              // Example structure of the "token"
 *                              // JWT claims are stripped out, i.e. "sub", "iat", "exp", "jti"
 *                              // {
 *                              //     email: 'example@example.com',
 *                              //     isPremiumUser: false
 *                              // }
 *                          return { ...token, isPremium: true }; // apply changes and return updated token
 *                      },
 *                  });
 *              
 *                  res.status(200).json({ message: "Successfully updated user to premium" });
 *              }
 * 
 * 
 * @param {object} param0
 * @param {NextRequest | NextApiRequest} param0.req Pass on the request instance. Used to get the current JWE value from the cookie
 * @param {NextResponse | NextApiResponse} param0.res Pass on the response instance. Used to update the new JWE value in the cookie
 * @param {function(object): object} param0.transformToken Callback method where you may update the token, except current JWT claims. We only want to update values, not implementation logic.
 * @param {string} [param0.cookieName="next-auth.session-token"]
 * @param {string} [param0.secret=process.env.NEXTAUTH_SECRET]
 */
export const updateToken = async ({
    req,
    res,
    transformToken,
    cookieName = "next-auth.session-token",
    secret = process.env.NEXTAUTH_SECRET,
}) => {
    // Get the current token, and destructure initial JWT claims
    const { sub, iat, exp, jti, ...token } = await getToken({
        req,
        cookieName,
        secret,
    });

    const newToken = transformToken(token); // allows only to update your custom token properties

    const newJWE = await encode({ sub, iat, exp, jti, ...newToken }, secret);

    const newCookie = serialize(cookieName, newJWE, {
        httpOnly: true,
        path: "/",
        maxAge: token.exp,
        sameSite: "lax",
    });
    res.setHeader("Set-Cookie", newCookie);
};

/**
 * ===============================
 * HELPER METHODS
 * ===============================
 * Copied and modified from "next-auth"
 */

/**
 * Modify "next-auth" encode method to manullay set JWT claims
 * @see https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/jwt/index.ts#L17
 */
async function encode(token, secret) {
    const encryptionSecret = await getDerivedEncryptionKey(secret); // Create a cryptographical random secret key using a predictable random secret.
    return await new EncryptJWT(token)
        .setProtectedHeader({ alg: "dir", enc: "A256GCM" })
        .setSubject(token.sub) // set "sub" Subject claim
        .setIssuedAt(token.iat) // adds "iat" to the JWT with current timestamp
        .setExpirationTime(token.exp) // adds "exp" to the JWT - timestamp format
        .setJti(token.jti) // set the "jti claim" - allows for replay protection/blacklist
        .encrypt(encryptionSecret);
}

/**
 * Copy & Paste from "next-auth" since it's not exported
 * Create a cryptographical random secret key using a predictable random secret.
 * @see https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/jwt/index.ts#L119
 * @param {string} secret Random string secret, e.g. "Whale-Banana-69"
 * @returns {Promise<Uint8Array(32)>} Actual cryptographical randomnes, e.g. transforms above into: "Uint8Array(32) [189,67,29,228,...]" which
 */
async function getDerivedEncryptionKey(secret) {
    return await hkdf(
        "sha256",
        secret,
        "",
        "NextAuth.js Generated Encryption Key",
        32
    );
}

Thank you for this. Its strange that this is still not supported natively, considering how much of next-auth revolves around JWT.

Alternatively, I have found that this also works:

add this in next-auth.configconfig

export const NextAuthUpdateConfig = {
	...NextAuthConfig,
	callbacks: {
		...NextAuthConfig.callbacks,
		jwt: async (obj) => {
			return NextAuthConfig.callbacks.jwt({ ...obj, refresh: true });
		},
	},
};

and in the jwt callback you can now test if the refresh flag is set and force a DB call with every call of getServerSession(req, res, NextAuthUpdateConfig)

@aakash19here
Copy link

How about this ?

https://git.new/token-rotation

@estani
Copy link

estani commented Jan 11, 2025

for v5.beta.25 (not sure since when it works) using jwt session (documentation is still terrible... but it works and makes sense, aka might be stable):

client side need to send the changes (Couldn't find a way to do it server side, but it would be almost the same):

'use client'
import { SessionProvider, useSession } from "next-auth/react"

import React from 'react'
function WrappedButton() {
  // this is not a session, but a session context! (i.e. you have here the `update`function) not like `await auth();`
  const session = useSession();

  async function onClick() {
    // forcing the jwt callback to be called with this payload
    await session.update({ newData: 'example'});
  }
  return <button onClick={onClick}>test</button>
}

// need sessionProvider to access useSession context
export function ChangeSessionButton() {
  return (
    <SessionProvider>
      <WrappedButton />
    </SessionProvider>
  )
}

This call will trigger a call to the jwt callback, with the payload as "session" (even though is not the session). So you can change the token there:

//... 
// you might want to add `async jwt({...` if you need to do some async work while updating the token
// note: session is not the session, but the payload as sent to update()
jwt({ token, user, trigger, session }) {
      switch (trigger) {
        case 'update':
           console.log('here we have the payload', session); // {newData: 'example'}
           token.modifyMeAsYouNeed = session.newData; // or whatever you need to check, do...
          break;
        // case 'signIn': ...
      }

      return token
    },
session({ session, token }) {
      // Enhance session with whatever you need from token
      session.modifyMeAsYouNeed = token.modifyMeAsYouNeed; // 'example'
     // ... 
      return session
    },

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working question Ask how to do something or how something works
Projects
None yet
Development

No branches or pull requests