-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
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
Comments
Yes if using JSON Web Tokens see the callback documentation, specifically the Alternatively, if using Database Sessions (and not JWT) you can add logic in the There are a lot of closed issues related to this and they contain example code. (We will probably have tutorials at some point). |
@iaincollins, I'm not seeing how the 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? |
@dkreft As well as being called on sign in, the 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 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. |
@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 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. |
If you make changes to the token in the The 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 |
I had a question about this. I'm currently using the 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 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 I'm wondering, is this the proper usage of the And is there a way I can update my UI with the new |
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 When 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? 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 |
@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; 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:
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 |
Turns out that server-side 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? |
@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:
I suspect / hope this is a weird situation from development mode, but I think it's worth some investigation to confirm that. |
I am having the same issue using the Credentials provider. I am expecting the global session state to be updated 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. |
Hmm if you have a provider configured in There could be edge case issues, but uses a fairly well supported mechanism for this. Calling If you have a provider in |
Thanks for the quick reply. I have added provider in This is a demo video to show the issue. https://www.loom.com/share/9e4837edbd274ec892e5853e6a9c9341 |
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 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
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. 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: 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! |
You can do this via the session callback
If you call 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>
)
} |
Perhaps I'm missing something here but if I mutate the session returned from 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()
} |
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 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! :) |
Thanks @dsavage311! Avoiding unnecessary API call is exactly what I'm trying to do. This clears up some things but in regards to |
@justinwhall it looks like the 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 |
Ah, I missed that. Thanks @elilambnz! I think my issue is around this. For instance p 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. |
@justinwhall the way you're accessing |
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 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 |
@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 // 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)
} |
@justinwhall So I'm actually just using the React 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 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);
}, |
@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 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 Another (less clean) approach is manually triggering a
@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. |
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. |
@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? |
@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. |
@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? |
you can control the number of writes to the database easily with the |
@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... |
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. |
You can use getSession() to get the updated session. reference - https://next-auth.js.org/getting-started/client#getsession |
I see this was closed - does that mean we're closer to a solution? |
This comment was marked as spam.
This comment was marked as spam.
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 |
Updating the session may be possible by directly modifying |
@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. |
@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? |
Agree. A quick online search, however, shows that this is a long existing question with several GitHub issues and the
Do you know at which line this error appears? I tested it myself and it works nicely. |
It happens at this line:
I also changed it to include my JWT_SECRET (and confirmed the value is passed in)
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. |
@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. |
@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!) |
@larsqa I was able to get the token contents by adding raw=true to the options passed to getToken
This only gives me the raw token however - next I'll need to figure out how to decode it. |
@jaxomlotus |
I wrapped the approach from @larsqa's link in a method, called 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:
|
How about this ? |
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
}, |
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.
The text was updated successfully, but these errors were encountered: