Yoann Buzenet
Yoann Buzenet

Reputation: 879

Next-auth - How to update the session client side?

I manage to update my session serverside, with new user data, but event after assigning it to my session object in [...nextauth.js], my session client side remains the old one. It doesn't refresh, event if I use getSession().

This code works for the back-end :

callbacks.session = async function session({ session, token }) {
  // we can fetch info from back end here to add it to the session   
  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;
};

How can I refresh the session in front-end ?

Upvotes: 22

Views: 63973

Answers (8)

Farhan Hossen
Farhan Hossen

Reputation: 1

Looks like the next Auth update function only update the client side session on that particular browser from where it triggered. And on other logged in devices you still have stale data.

Upvotes: 0

Ahmet Firat Keler
Ahmet Firat Keler

Reputation: 4065

2024 UPDATE

I will explain how you can update session (client side and server side), how you can notify other components about changes and how you can reflect these changes to UI.

Please note that I use Credentials Provider with npm versions below

"next": "^14.1.0",
"next-auth": "^5.0.0-beta.4",

Updating Session (Client Side)

To update session in a component client side, we should first wrap its parent with SessionProvider

"use client"

import { SessionProvider } from "next-auth/react"

const ParentComponent = () => {
  return (
    <SessionProvider>
      <ComponentInWhichWeUpdateSession />
    </SessionProvider>
  )
}

and then access the state using useSession hook like that

"use client"

import { useSession } from "next-auth/react"

const ComponentInWhichWeUpdateSession = () => {
  const { data: session, update } = useSession()

  const email = session?.user?.email || undefined

  return (
    <>
      {email?.toString().split("@")[0]}
      <button 
        onClick={() => {
        }}
      >
        Update Email
      </button>
    </>
  )
}

Before we proceed, we should make an addition in our jwt callback just like in this example

async jwt({ token, user, account, trigger, session }) {
  if (account && user) {
    token.id = user.id
    //...

    const decodedAccessToken = JSON.parse(Buffer.from(user.accessToken.split(".")[1], "base64").toString())
    if (decodedAccessToken) {
      token.accessTokenExpires = decodedAccessToken["exp"] * 1000
      //...
    }

    const decodedIdToken = JSON.parse(Buffer.from(user.idToken.split(".")[1], "base64").toString())
    if (decodedIdToken) {
      token.email = decodedIdToken["email"]
      //...
    }
  }

  // ---> ADDITION <---
  if (trigger == "update") {
    if (session?.user?.email) {
      token.email = session.user.email
    }
  }
  //

  if (token.accessTokenExpires && (Date.now() < Number(token.accessTokenExpires))) {
    const { refreshToken, ...rest } = token

    return rest
  }

  return await refreshAccessToken(token)
}

Now we can finalize our code to accomplish what we are after; updating session email

"use client"

import { useSession } from "next-auth/react"

const ComponentInWhichWeUpdateSession = () => {
  const { data: session, update } = useSession()

  const email = session?.user?.email || undefined

  return (
    <>
      {email?.toString().split("@")[0]}
      <button 
        onClick={() => {
          // we update session email client side
          update({
            user: {
              ..user,
              email: "[email protected]"
            }
          })
        }}
      >
        Update Email
      </button>
    </>
  )
}

Updating Session (Server Side)

To update session server side, we use a different approach.

In our auth.ts file, we export auth and unstable_update method just like we export handlers

export const config = {
  providers: [
    CredentialsProvider({
      //...
    })
  ],
  secret: process.env.AUTH_SECRET,
  //...
  callbacks: {
    //...
  },
  //...
} satisfies NextAuthConfig

export const { auth, handlers, unstable_update } = NextAuth(config)

We can then get/update our session using a server action

import { auth, unstable_update } from "auth"

const ComponentInWhichWeUpdateSession = () => {
  const session = await auth()

  const user = session?.user

  return (
    <>
      <form
        action={async () => {
          // we update session email server side
          "use server"

          if (user) {
            await unstable_update({
              user: {
                ...user,
                email: "[email protected]",
              }
            })
          }
        }}
      >
        <button>
          Update email
        </button>
      </form>
    </>
  )
}

Notify other components about changes

We basically dispatch and event for this purpose

"use client"

import { useSession } from "next-auth/react"

const ComponentInWhichWeUpdateSession = () => {
  const { data: session, update } = useSession()

  const email = session?.user?.email || undefined

  return (
    <>
      {email?.toString().split("@")[0]}
      <button 
        onClick={() => {
          // we update session email client side
          update({
            user: {
              ..user,
              email: "[email protected]"
            }
          })
            // notify other components
            .then(() => window.dispatchEvent(new CustomEvent("emailUpdated")))
        }}
      >
        Update Email
      </button>
    </>
  )
}

Reflect changes to UI

There are multiple ways to do that such as registering an event listener or polling session using intervals like here

"use client"

import { useSession } from "next-auth/react"

const ComponentInWhichWeReflectChanges = () => {
  const { data: session, update } = useSession()

  const email = session?.user?.email || undefined

  // polling session every hour
  useEffect(() => {
    const interval = setInterval(() => {
      if (window.navigator.onLine)
        update()
      }, 1000 * 60 * 60)
      return () => clearInterval(interval)
  }, [update])

  // registering an event listener
  useEffect(() => {
    const emailUpdatedEventHandler = () => update()

    window.addEventListener("emailUpdated", emailUpdatedEventHandler, false)
    return () => {
      window.removeEventListener("emailUpdated", emailUpdatedEventHandler)
    }
  }, [update])

  return (
    <>
      <p>{email}</p>
    </>
  )
}

That's it. Please comment if you have any questions.

Upvotes: 4

Yoann Buzenet
Yoann Buzenet

Reputation: 879

UPDATE 2023: There is now an official way to do it as of v4.22.1. See here and the updated docs

You now get an update function like this:

  const { update } = useSession();

Note: update fn doesn't sync up the session object between tabs, so if you have multiple tabs open, each tab needs to do reloadSession() separately.

PREVIOUS ANSWER:

There is no easy official way to do it, it is still in discussion among contributors.

That said, there is a way. Next-auth does refresh session if you click on another tab, then on the one of your app. It happens, it is a javascript event we can reproduce.

So just define a method to do it :

const reloadSession = () => {
  const event = new Event("visibilitychange");
  document.dispatchEvent(event);
};

Call it, and .. you're done. Session did refresh.

reloadSession();

For more information, this behaviour comes from this part of the code

Upvotes: 43

apl-by
apl-by

Reputation: 108

If you want to update the session using getSession() then try this:

import { getSession, getCsrfToken } from 'next-auth/react'
... 
const csrfToken = await getCsrfToken()
const updatedSession = await getSession({ req: {
                  body: {
                    csrfToken,
                    data: { user: { accessToken: "newToken" } },
                  },
                },
              })
...

and in callbacks:

 async jwt({ token, session, trigger }) {
   if (trigger === 'update' && session?.user?.accessToken) {
   token.accessToken= session.user.accessToken
  }

  return token
},

async session({ session, token }) {
  session.user = token
  return session
},

Upvotes: 1

Ahmed Sbai
Ahmed Sbai

Reputation: 16249

The update() method is now available.

The useSession() hook exposes a update(data?: any): Promise<Session | null> method that can be used to update the session, without reloading the page.
You can optionally pass an arbitrary object as the first argument, which will be accessible on the server to merge with the session object.
If you are not passing any argument, the session will be reloaded from the server. (This is useful if you want to update the session after a server-side mutation, like updating in the database.)

import { useSession } from "next-auth/react"

export default function Page() {
  const { data: session, status, update } = useSession()

  if (status === "authenticated") {
    return (
      <>
        <p>Signed in as {session.user.name}</p>
        
        {/* Update the value by sending it to the backend. */}
        <button onClick={() => update({ name: "John Doe" })}>
          Edit name
        </button>
        {/*
          * Only trigger a session update, assuming you already updated the value server-side.
          * All `useSession().data` references will be updated.
          */}
        <button onClick={() => update()}>
          Edit name
        </button>
      </>
    )
  }

  return <a href="/api/auth/signin">Sign in</a>
}

Upvotes: 3

Henrique Tom&#233;
Henrique Tom&#233;

Reputation: 71

Just in case that anyone needs this. Here it is how to update the session when you make a change.

On your component you will do:

import { useSession } from "next-auth/react";
...

const {data: session, update: sessionUpdate} = useSession()
...

function handleUpdateSession() {
  sessionUpdate({
    info: //Your info here...
  })
}

And in your [...nextauth] you do something like:

callbacks: {
    jwt({ token, trigger, session }) {
      if(trigger === 'update') {
        if(session.info) {
          // update your token whatever you like
        }
      }
      
      return token;
    }
}

trigger are triggered in 3 scenarios (Signin, SignUp and Update) and session receives what you send on your update call. In this case it will be just an object like:

{
  info: some info here
}

Upvotes: 7

Shawn Mclean
Shawn Mclean

Reputation: 57479

Just incase anyone comes here to figure out how to get the session info into an analytics platform like Heap, I ended up creating a component that uses useSession.

export default function HeapAnalytics() {
  const { data: session, status } = useSession();
  const [scriptLoaded, setScriptLoaded] = useState(false);

  useEffect(() => {
    if (status === "authenticated" && window.heap) {
      console.info("Identifying Heap User...");
      window.heap.identify(session.user.email);
      window.heap.addUserProperties({
        name: session.user.name,
        userId: session.user.id,
      });
    }
  }, [scriptLoaded, session, status]);

  const scriptReady = () => {
    if (window.heap) {
      setScriptLoaded(true);
    }
  };

  return (
    <Script
      id="heap-analytics"
      strategy="afterInteractive"
      dangerouslySetInnerHTML={{
        __html: `
      window.heap=window.heap||[],heap.load=function(e,t){window.heap.appid=e,window.heap.config=t=t||{};var r=document.createElement("script");r.type="text/javascript",r.async=!0,r.src="https://cdn.heapanalytics.com/js/heap-"+e+".js";var a=document.getElementsByTagName("script")[0];a.parentNode.insertBefore(r,a);for(var n=function(e){return function(){heap.push([e].concat(Array.prototype.slice.call(arguments,0)))}},p=["addEventProperties","addUserProperties","clearEventProperties","identify","resetIdentity","removeEventProperty","setEventProperties","track","unsetEventProperty"],o=0;o<p.length;o++)heap[p[o]]=n(p[o])};
      heap.load("${config.tags.HEAP_ANALYTICS}");
      `,
      }}
      onReady={scriptReady}
    />
  );
}

Full documentation: https://samelogic.com/blog/heap-analytics-in-nextjs-with-nextauthjs

Upvotes: 0

Baldr&#225;ni
Baldr&#225;ni

Reputation: 5638

Using the hack of simulating a tab switch does not works anymore in the v4.

What you could do is to update the callback to session?update.

const createOptions = (req) => ({
  // ...
  callbacks: {
    async jwt({ token, ...params }) {
      if (req.url === "/api/auth/session?update") {
          const response = await fetch(`${process.env.NEXT_PUBLIC_URL}/api/users/get/${token.email}`);
          const newUser = await response.json();
          token.hasAcceptedTerms = newUser.hasAcceptedTerms;
      }
      return token;
    },
    async session({ session, token }) {
        if (session.user != null && token.hasAcceptedTerms != null) {
            session.user.hasAcceptedTerms = token?.hasAcceptedTerms;
        }

        return Promise.resolve(session);
     },
  },
});

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

Then in your client you can do a call

  await axios.get('/api/auth/session?update');

Tribute goes to this answer on github.

Upvotes: 6

Related Questions