Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

make updating session easier #2269

Closed
balazsorban44 opened this issue Jun 28, 2021 Discussed in #2267 · 40 comments
Closed

make updating session easier #2269

balazsorban44 opened this issue Jun 28, 2021 Discussed in #2267 · 40 comments
Labels
enhancement New feature or request

Comments

@balazsorban44
Copy link
Member

Discussed in #2267

Originally posted by phileasmah June 28, 2021

Summary 馃挱

Include a setState to mutate the session state when using the useSession() hook.

Description 馃摀

In my app, I'm trying to build a settings page for profiles, like changing the display name etc, but I found that after changing the display name (name in database has been changed), the session state in useSession() stayed the same.

So, I was trying to find a way to mutate the session object of the current user and stumbled upon getSession() which (correct me if I'm wrong) is suppose to get a new session and replace it with the current one but according to this issue it doe not work as intended.

I was looking at the source code and thought wouldn't it be easier to just allow us to mutate the session state directly? (or are there security concerns I'm not aware of? In which case, it would be interesting to know). Looking at the code, it should be pretty easy to implement.

An advantage when compared to getSession() would obviously be that 1 less call needs to be made to the database, but again, I'm not sure if I am glossing over any security concerns etc

There are probably other related issues, which could be resolved with this. I'll try to gather them below, but if anyone reading this knows any, please comment below, and I would also like to hear your thoughts on this!

@balazsorban44
Copy link
Member Author

balazsorban44 commented Jun 28, 2021

Hi, and thanks for opening this. So when the session value changes and is being persisted either in a database or in the HttpOnly session cookie as JWT, the value can only be written server-side, so a trip to the server is inevitable. But I think we could expose a new function, something like updateSession, or better:

const { data: session, update } = useSession()
...
const handleUpdate = async () => {
  update((prev) => {...prev: foo: "new session value"})
}

update() could take either a new session object, or a callback, where the argument is the previous session for convenience (mimicking the setState function of useState), that would not only update the session and persist it server-side, but would also update the session in the client-side cache automatically.

I am currently refactoring the useSession() API in #2236, that would allow us to easily return other properties from useSession.

Would like to know what you think, and if you would be willing to open a PR for this @phileasmah

@balazsorban44 balazsorban44 added the enhancement New feature or request label Jun 28, 2021
@phileasmah
Copy link

Hi, glad you liked my idea, didn't know it could only be written server-side, but yea, what you suggested was exactly what I was thinking about!

I'm just a student but would love to take another look and possibly open a PR for this. I'm trying to set up my local environment and was just wondering how I should go about trying to implement this with your refactoring in #2236 or should I just try to implement this without your refactor?

@balazsorban44
Copy link
Member Author

I'll try to get it done this week, so hopefully you will be able to use the next branch to work against then. I think first we have to discuss how we should go about the API to support both database persisted and JWT cookie sessions.

I think we will need a new endpoint for this, or we have to extend this one: https://github.com/nextauthjs/next-auth/blob/main/src/server/routes/session.js

@phileasmah
Copy link

I'm guessing we could just have a if/else check like in the current session.js route and mutate the data inside based on what type of session it is (?).

Personally, I think a new endpoint would be better overall for readability, maintenance and maybe performance(?) since I don't think much would be needed to change data in the session as opposed to validating/refreshing it.

@dkleber89

This comment has been minimized.

@balazsorban44
Copy link
Member Author

@dkleber89 Please choose the alternative, or consider contributing through a PR 馃檹. The show must go on!

(For a more serious answer, this is not mine or anyone's application/library. We are open-source, with a license that lets you do anything at all with the source code. If you feel the development is a bit slow on features you need for your company's success, please ask them to consider sponsoring us, drive this conversation with your implementation suggestions, or create the feature yourself in your own fork, and open a PR back here, so others can make use of it as well! Please remember that for now only we work on this project in our free time, and although we would like to prioritize every user's wishes, we simply cannot. Thanks for understanding 馃挌)

@dkleber89
Copy link

Thanks for your feedback ... It was not a reproach or something else ... Was only a question for my interrest ... I know also the open source meaning and how it works but unfortunately this discussion is a hard one in the company i work :-(

@balazsorban44
Copy link
Member Author

balazsorban44 commented Jul 15, 2021

I see. This is mainly only an issue with the JWT persisted session flow. If you use a database, you can update the data directly, and if you configured it correctly, I believe the getSession call should update it already. When using JWT, the data is stored in a cookie, and you have to trigger the sign-in flow again to get the new user data. You can get around this by extending your jwt callback in a way that checks the /userinfo endpoint of your Identity Provider on every invocation for example. This kind of destroys the performance boost of what the JWT persisted session provides (no expensive calls to external services).

With the proposed method in this discussion, it would be up to you to update the cookie on-demand, for example when you know that the user info has changed.

@huksley
Copy link

huksley commented Jul 15, 2021

I am using JWT and have a need to update the session (after onboarding). I implemented this "hack" to get proper cookie with JWT. Current /api/auth/session endpoint updates JWT using normal flow, but only designed to be used as XHR request.

No relogin by user needed.

Update api/auth/[...nextauth].ts handler:

export default async (req: NextApiRequest, res: NextApiResponse) => {
  if (req.url === "/api/auth/session?update") {
    // Session call updates cookie, redirect to / afterwards
    const endResponse = res.end as (cb?: () => void) => void;
    res.end = (cb: () => void) => {
      res.setHeader("Location", "/");
      res.status(301);
      return endResponse.apply(res, [cb]);
    };
    return NextAuth(req, res, options);
  } else {
    return NextAuth(req, res, options);
  }
}

When needed, forward user to /api/auth/session?update to update session and it will go back to / after updating a cookie.

@balazsorban44
Copy link
Member Author

balazsorban44 commented Jul 15, 2021

Not sure I understand @huksley. How would that retrieve the new information about a user from an OAuth provider? I'm not really sure what it actually does either. So if NextAuth calls res.end, you redirect to the homepage? Why? Could you please explain? 馃槉

@atawfique21

This comment has been minimized.

@balazsorban44
Copy link
Member Author

So this issue was actually meant to discuss how we could improve the current implementation, not individual questions for one's own problems. I understand it is frustrating, but please open a discussion with your problem rather than asking for help here as this does not accelerate the resolving of this issue. We welcome any implementation suggestions or PRs regarding this. Thanks for understanding! 馃檹馃挌

@balazsorban44
Copy link
Member Author

@phileasmah #2236 is now merged, so if you want to have a look at it and create a PR, please do. I'll try looking at this myself whenever I get the time.

@huksley
Copy link

huksley commented Jul 15, 2021

@balazsorban44 If you need to fetch something from OAuth provider, you can do it manually or in callbacks.jwt handler.

Current implementation of /api/auth/session recreates JWT token and puts it in session AND outputs JSON when you call it - see

// Refresh JWT expiry by re-signing it, with an updated expiry date

My "hack" is to avoid outputting JSON and instead forward user to other page, as needed.

@balazsorban44
Copy link
Member Author

And what other page would you send the user? and why? I am just curious to understand.

@huksley
Copy link

huksley commented Jul 15, 2021

I think this is out of scope of this issue,

but for my use case is I avoid hitting the DB on every API request, instead I keep in JWT token everything needed to check permissions. For this I have user.companyId which is created by user after login using OAuth. In this scenario I need a way to update the JWT token without relogin. After that I redirect to dashboard.

@romseguy
Copy link

romseguy commented Jul 18, 2021

@huksley it's working for me but only the first time. Flow is : 1) user submits profile form 2) redirection with router.push("/api/auth/session?update")

Next times req.url value is /api/auth/session

Making a XHR to /api/auth/session?update doesn't update the token either. Wouldn't that be the best way to solve this issue? That way we could just do:

const createOptions = req => ({
  // ...
  callbacks: {
    async jwt(params) {
      if (req.url === "/api/auth/session?update") {
         // hit the DB and return token with updated values
      }
    }
  }
});

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

@dextermb
Copy link

dextermb commented Aug 2, 2021

I'd like to see something such as:

const [session, isLoading, refresh] = useSession({ setLoadingAfterInit: false })

This way when a user update their username or other attributes we can trigger the refresh to instantly update the data rather than waiting for the next cycle.

@appy79
Copy link

appy79 commented Sep 7, 2021

I am new to next auth and now need to mutate the session. It's easier store the new value to db and then do something like this I think.
const newSession = session; newSession.user.name = newName; updateSession(newSession);

Or
We also could have an option for refresh that will ask us to give some way to get the latest user from the source. Like for credentials provider we can simply give a way to call up the database and get the updated user.

@fredguest
Copy link

@balazsorban44 hey I just wanted to check if this feature is definitively on the roadmap or not, it seems like things may have gotten bogged down in the implementation details. FWIW, the implementation you described here #2269 (comment) sounds great to me and would be extremely useful.

@balazsorban44
Copy link
Member Author

Yeah, I do plan to add something like this, but I don't have an ETA. Currently busy getting v4 and all the relevant database adapters to stable. 馃憤

I don't want to start working on new features at this point, because then I would never release v4. 馃槄 I have a TON of ideas for improvements... 馃憖

@fredguest
Copy link

Ok cool, thanks for confirming. Appreciate your work and looking forward to the updates!

@aboveyunhai
Copy link

@balazsorban44 Just want to get a confirm. It is impossible to mutate session without logout/login currently for V3 right ?

@balazsorban44
Copy link
Member Author

#2269 (comment)

@hyunoosung
Copy link

I'd like to see this feature added soon where my application constantly updates user's profile and I don't want to hit the profile apis on every session requests.

@JesseVelden
Copy link

JesseVelden commented Oct 4, 2021

@huksley it's working for me but only the first time. Flow is : 1) user submits profile form 2) redirection with router.push("/api/auth/session?update")

Next times req.url value is /api/auth/session

Making a XHR to /api/auth/session?update doesn't update the token either. Wouldn't that be the best way to solve this issue? That way we could just do:

const createOptions = req => ({
  // ...
  callbacks: {
    async jwt(params) {
      if (req.url === "/api/auth/session?update") {
         // hit the DB and return token with updated values
      }
    }
  }
});

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

Actually this method is working for me if you combine it with a session callback. So a simple working example:

const createOptions = (req) => ({
  // ...
  callbacks: {
    async jwt({ token, ...params }) {
      if (req.url === "/api/auth/session?update") {
        // hit the DB and return token with updated values. e.g.:
        token.name = "Updated Mr. Johnson";
      }
      return token;
    },
    async session({ session, token }) {
        console.log(token);
        session.user = {
            ...session.user,
            name: token.name
        }
        return session;
    },
  },
});

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

Then somewhere in your client you can make an AJAX call to the /api/auth/session?update route.

@stale stale bot added the stale Did not receive any activity for 60 days label Dec 3, 2021
@nextauthjs nextauthjs deleted a comment from stale bot Dec 3, 2021
@balazsorban44 balazsorban44 removed the stale Did not receive any activity for 60 days label Dec 3, 2021
@nemanjam
Copy link

How to update user object inside the session after user edits his profile? Is this the official way to do it, it's really ugly?

if (req.url === "/api/auth/session?update") {
...

@aboveyunhai
Copy link

aboveyunhai commented Dec 11, 2021

How to update user object inside the session after user edits his profile? Is this the official way to do it, it's really ugly?

if (req.url === "/api/auth/session?update") {
...

I end up don't store any user info into the session except the crtical rotation and secret token, those will be stably rotate checked via next auth, then retrieved and replaced from auth api whenever user open the app.

Use another user api to retrieve up to date user info as application change. Save it somewhere else temporarily acrossing the entire app using, or in local storage if you really want to. There are many ways to avoid abusing the user info api, which I found much more stable comparing to hack through the session here. And avoid the hazard of messing with token.

@balazsorban44
Copy link
Member Author

balazsorban44 commented Dec 11, 2021

local storage if you really want to.

Please don't use local storage for storing user information.

@aboveyunhai
Copy link

aboveyunhai commented Dec 12, 2021

local storage if you really want to.

Please don't use local storage for storing user information.

You are right. I probably should state more explicitly the data and the degree of security measure without misleading others.
I don't store it into local storage obviously, but the user data in my mean is the purely public profile that everyone is able to see. No personal authentication or login block require. Never participate into any api check and for display purpose only. Even they are inside nextauth session, use it as auth check is also questionable. These data should be different from the "user data" generated by next-auth.
I had seen ppl just want to dump all trivial data inside next-auth, and want to update the session. Next-auth indeed made life much more easier, I just wish ppl has some understanding inside its own data and some of logics behinds the package.

@MamoshiSE
Copy link

const createOptions = (req) => ({

I am doing like this, but everytime i navigate to another page/refresh, the token and session values go back the previous ones. Even though the database has been updated.

@YoannBuzenet
Copy link

YoannBuzenet commented Dec 18, 2021

I've added a XHR call in session callback to get new data from DB each time. But data on front only update if I click on another tab then on the Nextjs one. Is there a way to make this directly happen on my page ?

Calling getSession() clientside does trigger the callback but doesn't update it on the page.

callbacks.session = async function session({ session, token }) {
  session.user = token.user;
  session.jti = token.jti;

  // If user is logged, we refresh his session
  if (token?.user?.id) {
    const url = routes.api.entities.shop.get.byId.build(token?.user?.id);
    let apiResp = await axios.get(url, {});
    session.user = { ...apiResp.data };
    token.user = { ...apiResp.data };
  }

  return session;
};

@baptisteArno
Copy link

I've added a XHR call in session callback to get new data from DB each time. But data on front only update if I click on another tab then on the Nextjs one. Is there a way to make this directly happen on my page ?

Calling getSession() clientside does trigger the callback but doesn't update it on the page.

callbacks.session = async function session({ session, token }) {
  session.user = token.user;
  session.jti = token.jti;

  // If user is logged, we refresh his session
  if (token?.user?.id) {
    const url = routes.api.entities.shop.get.byId.build(token?.user?.id);
    let apiResp = await axios.get(url, {});
    session.user = { ...apiResp.data };
    token.user = { ...apiResp.data };
  }

  return session;
};

Same here!

@YoannBuzenet
Copy link

Have a look here : #596

@balazsorban44
Copy link
Member Author

balazsorban44 commented Jan 16, 2022

Hi everyone, I just wanted to let you know, that I've had some progress on this. Please see #3648, there is an experimental release available there as well.

The API is const { updateUser } = useSession() where

const updateUser = (user: Partial<User>) => Session["user"]

Example to update user.name:

<form
  onSubmit={async (e) => {
    e.preventDefault()
    const form = new FormData(e.target)
    const user = Object.fromEntries(form.entries())
    await updateUser(user)
    e.target.reset()
  }}
>
  <input name="name" />
  <button>Update</button>
</form>

There are caveats though:

  1. only works when using an Adapter (strategy: "database"): It might not actually be worth updating a session when it is not persisted, since a re-login might invalidate it anyway. (Say you log in with Twitter, you change the user name. You sign out, sign in with Twitter again, the name is back to the one from Twitter.)
  2. no e-mail update support (needs more work, must go through verification, probably via Email Provider)
  3. no account update support: There is no separate "Account" concept on the client.
  4. the session must contain the user object: There is no separate concept of "User" on the client.

1 is probably debatable, if someone gives me a good argument I can think about it
2 this could be addressed in another feature request

@robertwt7
Copy link

This is awesome! can't wait for this to be released :)

@balazsorban44
Copy link
Member Author

I'm still thinking through 3. and 4. It feels a bit weird, but I don't think we could enforce a format like that without a breaking change... 馃

For 3, we might just introduce a new hook that could return the access_token for any of the Accounts saved in the db, finally adding token rotation support at the same time.

For 4, we could add a feature flag maybe, and a warning/error when using that method incorrectly (without having a user in session).

@jaxomlotus
Copy link

That sounds great. Thanks!

@cubesnyc
Copy link

cubesnyc commented Feb 8, 2022

So to be clear, does this mean that it is currently not possible to mutate the values within JWT, and persist them?

@nemanjam
Copy link

nemanjam commented Feb 9, 2022

So to be clear, does this mean that it is currently not possible to mutate the values within JWT, and persist them?

So far I store only immutable user properties in session like userId and sync the rest of the user object with GET user/:id endpoint and React Query. Is there a better way?

@nextauthjs nextauthjs locked and limited conversation to collaborators Feb 12, 2022
@balazsorban44 balazsorban44 converted this issue into discussion #3941 Feb 12, 2022

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.