next-auth icon indicating copy to clipboard operation
next-auth copied to clipboard

getSession call does not trigger session object update until window loses focus

Open blms opened this issue 5 years ago • 26 comments
trafficstars

I'm currently trying to update some page components when the session changes after an onClick-triggered Promise is fulfilled. Here the function DeleteGroup returns a Promise. I print the value of the session before the Promise is returned, and then print it again after it is fulfilled by calling getSession().

<Button
  onClick={() => {
    console.log(session);
    DeleteGroup(value.id).then(async () => console.log(await getSession()));
  }}
>

According to the console, everything I've logged looks right. The initial console.log(session) output shows groups as an Array of length 2, while the second console.log(await getSession()) output shows it as length 1.

Screen Shot 2020-08-20 at 6 02 35 PM

However, until the window loses focus, my UI continues to show 2 groups despite the console output clearly showing the result of the getSession call having only 1 group. My UI is immediately synced once the window loses focus, so I do not believe there is anything wrong with the way I've written my UI components. If that is the case, why wouldn't my getSession() call immediately update the UI accordingly?

Apologies if this is a repeat; I find session updating to be the trickiest part of Next-Auth.

blms avatar Aug 21 '20 00:08 blms

I just noticed the same issue: if I call getSession() to trigger a session update, the call is correctly sent by the context is not updated. I think I understand why but I'm not sure how to fix it.

The first thing would be that we need to pass {triggerEvent: true} to getSession() so that it updates the localStorage to inform other windows: this line.

However, the code responsible to update the session following such a message has an explicit check to not update the session if the window is coming from the same window: this line

The problem is that the only way to update the Context is to call the internal _getSession of the _useSessionHook() via __NEXTAUTH._getSession().

I think this part of the doc is misleading: image

It's actually "you can can trigger an update of the session object across all other tabs/windows".

Are we trying to do something that is not intended or is there a way to imperatively trigger a session update that would update the context of the current window?

ValentinH avatar Nov 18 '20 19:11 ValentinH

@ValentinH @blms Thanks to you both!

This is helpful and I think we understand it well enough to fix now.

It's supposed to work as described in the docs, but I think is inadvertently relying on localStorage to trigger the event and the way localStorage events work is they only trigger updates in windows other than the window that triggered the event.

iaincollins avatar Dec 01 '20 16:12 iaincollins

Hi, is there a temporary solution?

Dev-Songkran avatar Jan 05 '21 09:01 Dev-Songkran

For now, I'm doing a full-reload navigation as a workaround.

ValentinH avatar Jan 05 '21 10:01 ValentinH

I'm using this workaround for now: https://github.com/nextauthjs/next-auth/issues/371#issuecomment-723719264

blms avatar Jan 05 '21 16:01 blms

@ValentinH @blms Thanks to you both!

This is helpful and I think we understand it well enough to fix now.

It's supposed to work as described in the docs, but I think is inadvertently relying on localStorage to trigger the event and the way localStorage events work is they only trigger updates in windows other than the window that triggered the event.

@iaincollins couldn't this be the cause?

https://github.com/nextauthjs/next-auth/blob/65504d691736087195ef4cb220d528064751ba70/src/client/index.js#L52-L55

Although MDN says:

The storage event of the Window interface fires when a storage area (localStorage) has been modified in the context of another document.

"another document" might just mean how you described it.

balazsorban44 avatar Jan 07 '21 22:01 balazsorban44

In case someone looking for a workaround. I just replace useSession hook and use useSWR library instead.

const { data, error } = useSWR('/api/auth/session', async (url) => {
  const res = await fetch(url)
  if (!res.ok) {
    throw new Error()
  }
  return res.json()
})

// For loading
if (!error && !data) {
}

// For no session
if (!data) {
}

// For session existing
if (data) {
}

// Make change on user 
async function handleSubmit(e: React.SyntheticEvent) {
  // e.g. update user profile

  // Get the latest session
  mutate('/api/auth/session')
}

thattimc avatar Jan 14 '21 15:01 thattimc

I'm using this workaround right now (see demo request handler below for GET). Essentially this handler is doing what next auth session endpoint does but also modifying the session/jwt payload with an updated user data. My use case is using next endpoints as proxy for my backend endpoints for user info update. So manually updating session and jwt with the updated information is mandatory. @iaincollins Is it possible in future releases to expose some of the libs like cookie, logger, default-events, etc? As of now I'm importing directly from the node_modules path 😅

import * as cookie from '../../../node_modules/next-auth/dist/server/lib/cookie'
import dispatchEvent from '../../../node_modules/next-auth/dist/server/lib/dispatch-event'
import logger from '../../../node_modules/next-auth/dist/lib/logger'
import parseUrl from '../../../node_modules/next-auth/dist/lib/parse-url'
import * as events from '../../../node_modules/next-auth/dist/server/lib/default-events'
import jwtDefault from 'next-auth/jwt'

const secret = process.env.SECRET

export default async function userHandler(req, res) {
  const { query: { id }, method } = req


  switch (method) {
    case 'GET':
      const dummyUserData = { name: "UPDATE USERNAME HERE"}
      await handleUserGet(req, res, dummyUserData)
      break
    case 'PUT':
      // Update or create data in your database
      res.status(200).json({ id, name: name || `User ${id}` })
      break
    default:
      res.setHeader('Allow', ['GET', 'PUT'])
      res.status(405).end(`Method ${method} Not Allowed`)
  }
}

async function handleUserGet(req, res, updatedUser) {
  // Simulate NextAuth Request Options Generation
  const maxAge = 30 * 24 * 60 * 60 // Sessions expire after 30 days of being idle; taken from NextAuthHandler
  const { baseUrl } = parseUrl(process.env.NEXTAUTH_URL || process.env.VERCEL_URL)
  const isHTTPS = baseUrl.startsWith("https://")
  const cookies = { ...cookie.defaultCookies(isHTTPS) }
  const jwt = {
    secret,
    maxAge,
    encode: jwtDefault.encode,
    decode: jwtDefault.decode
  }
  const sessionToken = req.cookies[cookies.sessionToken.name]

  if (!sessionToken) {
    return res.json({})
  }

  // JWT Session refresh sequence
  try {
    const decodedJwt = await jwt.decode({ ...jwt, token: sessionToken })
    console.log("decoded")
    console.log(decodedJwt)

    const jwtPayload = await jwtCallback(decodedJwt)
    console.log("aftercallback")
    console.log(jwtPayload)

    const updatedJwtPayload = { ...jwtPayload, ...updatedUser }
    console.log("update")
    console.log(updatedJwtPayload)

    const newSessionExpiry = createSessionExpiryDate(maxAge)
    const defaultSessionPayload = createDefaultSessionPayload(updatedJwtPayload, newSessionExpiry)
    const sessionPayload = await sessionCallback(defaultSessionPayload, updatedJwtPayload)

    const newEncodedJwt = await jwt.encode({ ...jwt, token: updatedJwtPayload })
    cookie.set(res, cookies.sessionToken.name, newEncodedJwt, { expires: newSessionExpiry, ...cookies.sessionToken.options })
    await dispatchEvent(events.session, { session: sessionPayload, jwt: updatedJwtPayload })
  } catch (error) {
    logger.error('JWT_SESSION_ERROR', error)
    cookie.set(res, cookies.sessionToken.name, '', { ...cookies.sessionToken.options, maxAge: 0 })
  }

  res.status(200).json({message: "foobar"})
}

function createDefaultSessionPayload(decodedJwt, sessionExpires) {
  return {
    user: {
      name: decodedJwt.name || null,
      email: decodedJwt.email || null,
      image: decodedJwt.picture || null
    },
    expires: sessionExpires
  }
}

function createSessionExpiryDate(sessionMaxAge) {
  const expiryDate = new Date()
  expiryDate.setTime(expiryDate.getTime() + (sessionMaxAge * 1000))
  return expiryDate.toISOString()
}

// TODO Move these callbacks to a shared config file
async function sessionCallback(session, token) {
  console.log("session callback")
  if (token && token.accessToken) {
    session.accessToken =  token.accessToken
  }
  return session
}
async function jwtCallback(token, user, account, profile, isNewUser) {
  console.log("jwt callback")
  if (user && user.token) {
    token.accessToken = user.token
  }
  return token
}

ayushkamadji avatar Feb 20 '21 08:02 ayushkamadji

Any news on the issue? following @timshingyu I'm also doing it via useSwr now:

import useSWR, { mutate } from 'swr'

// allows to mutate and revalidate
export const mutateSession = (data?: Session, shouldRevalidate?: boolean) => mutate('/api/auth/session', data, shouldRevalidate)

// parse the response
const fetcher = (url) => fetch(url).then((r) => r.json())

export function useUser({ redirectTo = '', redirectIfFound = false } = {}) {
  const [, loading] = useSession() // check if provider is still loading (avoid redirecting)
  const { data: session, isValidating } = useSWR<Session>('/api/auth/session', fetcher)

  const hasSession = Boolean(session?.user)
  const isLoading = loading || (!session && isValidating)

  useEffect(() => {
    if (!redirectTo || isLoading) return
    if (
      // If redirectTo is set, redirect if the user was not found.
      (redirectTo && !redirectIfFound && !hasSession) ||
      // If redirectIfFound is also set, redirect if the user was found
      (redirectIfFound && hasSession)
    ) {
      Router.push(redirectTo)
    }
  }, [redirectTo, redirectIfFound, hasSession, isLoading])

  return session?.user ?? null
}

paul-vd avatar May 23 '21 17:05 paul-vd

I believe this and #371 deal with a number of different issues.

I need to update jwt and session (with jwt:true) after user has logged in, whenever user updates some underlaying data (name or image, for example) via a separate api route handler. This definitely not the case when SWR can help, because /api/auth/session always returns whatever was stored in jwt callback initially. Looks like the jwt callback only receives 'user' on signIn (so, once per session).

So, what's the simplest way to make jwt and/or session update without logging out/in (not an option, obviously) and without querying database for changes every time the jwt callback is called?

zanami avatar Jun 04 '21 08:06 zanami

I think triggering a re-signin is the easiest method here. this will invoke the jwt callback with the new data, and a second login on most OAuth providers is barely visible as it will be a fast redirect (unless some params explicitly require the user to give their credentials again)

balazsorban44 avatar Jun 04 '21 20:06 balazsorban44

@balazsorban44 not really viable in case of credentials or email signin and I'm trying to offer both along with some oAuth providers.

zanami avatar Jun 07 '21 01:06 zanami

@zanami did you find the right solution for this? I'm in the same situation as you...

mihaic195 avatar Sep 20 '21 16:09 mihaic195

@zanami did you find the right solution for this? I'm in the same situation as you...

Nope, sorry, I gave up and dropped NextAuth.

zanami avatar Sep 21 '21 12:09 zanami

Hi. are there any solution already for this to update client session with getSession?

malioml avatar Oct 02 '21 12:10 malioml

One way to trigger the internal session re-validation on the active(!) window (thx to @ValentinH for providing some insights) might be to manually trigger a visibilitychange event

https://github.com/nextauthjs/next-auth/blob/main/src/client/index.js#L72

  document.addEventListener(
    "visibilitychange",
    () => {
      !document.hidden && __NEXTAUTH._getSession({ event: "visibilitychange" })
    },
    false
  )

this seems to be a working hack for me that does not require a full page reload or signIn

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

PhilippLgh avatar Oct 14 '21 15:10 PhilippLgh

@zanami did you find the right solution for this? I'm in the same situation as you...

Nope, sorry, I gave up and dropped NextAuth.

@zanami Do you mind sharing how are you implementing your authentication module now?

jacklimwenjie avatar Oct 15 '21 05:10 jacklimwenjie

Ok, it works.

Doing as @PhilippLgh said :
event triggering :

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

just works ! it does refresh the session.

@balazsorban44 Could this be a way ?

YoannBuzenet avatar Dec 18 '21 16:12 YoannBuzenet

Ok, it works.

Doing as @PhilippLgh said : event triggering :

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

just works ! it does refresh the session.

@balazsorban44 Could this be a way ?

Can you plaese be mode specific? How to use it in serverside getSession?

killjoy2013 avatar Feb 20 '22 12:02 killjoy2013

One way to trigger the internal session re-validation on the active(!) window (thx to @ValentinH for providing some insights) might be to manually trigger a visibilitychange event

https://github.com/nextauthjs/next-auth/blob/main/src/client/index.js#L72

  document.addEventListener(
    "visibilitychange",
    () => {
      !document.hidden && __NEXTAUTH._getSession({ event: "visibilitychange" })
    },
    false
  )

this seems to be a working hack for me that does not require a full page reload or signIn

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

This hack stopped working in NextAuth@4. Not 100% sure why, but haven't been able to get an alternative hack working yet. Anyone have a workaround yet for this in v4?

mAAdhaTTah avatar May 07 '22 13:05 mAAdhaTTah

any luck on this so far?

1finedev avatar May 21 '22 13:05 1finedev

Any solutions/workarounds for latest versions? :)

mikarty avatar Jun 10 '22 15:06 mikarty

Only solution so fat is to call your db and update the session yourself in the session callback...

1finedev avatar Jun 16 '22 08:06 1finedev

Only solution so fat is to call your db and update the session yourself in the session callback...

Call database many time not really good, btw I use JWT too :(

IRediTOTO avatar Jul 15 '22 16:07 IRediTOTO

This hack stopped working in NextAuth@4.

Seems to work for me. Here's my set up.

"next-auth": "^4.10.3"
export default function refreshSession () {
  const event = new Event('visibilitychange')
  document.dispatchEvent(event)
}

Here's how I'm using it.

function create (data) {
  return post('/groups', { name: data.name })
    .then(() => refreshSession())
}

dextermb avatar Aug 26 '22 19:08 dextermb

It seems that dispatch event visibilitychange is not helpful for version 4. (I'm using ^4.6.1)

The client session state is lost every time the tab is switched or hangs too long.

Is there any solution to this problem? Thanks!

leephan2k1 avatar Sep 20 '22 03:09 leephan2k1

The new implementation uses a BroadcastChannel based on the localStorage API. This could work (not tested):

export default function refreshSession () {
  const message = { event: 'session', data: { trigger: "getSession" } }
  localStorage.setItem(
    "nextauth.message",
    JSON.stringify({ ...message, timestamp: Math.floor(Date.now() / 1000) })
  )
}

Alternatively use getSession({ broadcast: true })

PhilippLgh avatar Sep 28 '22 15:09 PhilippLgh

The solution with new Event('visibilitychange') works only when the flag refetchOnWindowFocus is set to true on SessionProvider.

In case we don't want to auto-update the session when a user switches windows we're kind of stuck.

@PhilippLgh Looks like it's also possible to use the exported BroadcastChannel function directly.

import { BroadcastChannel, BroadcastMessage } from 'next-auth/client/_utils'

const reloadSession = () => {
  const { post } = BroadcastChannel()
  const message: Partial<BroadcastMessage> = {
    event: 'session',
    data: { trigger: 'getSession' },
  }
  post(message)
}

Unfortunately, it doesn't really trigger XHR to update the session.

Even if we add await getSession() it seems the SessionProvider doesn't propagate the changes on the client.

const reloadSession = async () => {
  const { post } = BroadcastChannel()
  const message: Partial<BroadcastMessage> = {
    event: 'session',
    data: { trigger: 'getSession' },
  }
  post(message)

  await getSession({
    event: 'session',
    broadcast: true,
    triggerEvent: true,
  })
}

It's possible that PR #4744 fixes it, but it's still pending review.

kachar avatar Oct 11 '22 11:10 kachar

I want to give this issue a bump and see if we can move this along. Not being able to refresh sessions and have updates propagate is a big problem for us. ~~Unfortunately the solutions provide either don't seem to work or require reloading the page which is a non-starter for long-running real-time WebSocket based applications.~~ (I spoke too soon. This does seem to work https://github.com/nextauthjs/next-auth/issues/596#issuecomment-943453568 but it is a very ugly hack). It seems that session refreshing is one of, if not the most frequent pain points for using NextAuth, as everyone is converging on these same questions in a number of places.

The work that's been done on NextAuth provides a ton of value and I really appreciate it. Unfortunately, not having token refresh is a pretty critical flaw that makes the rest of the library not particularly useful in all but the simplest of cases. It looks like https://github.com/nextauthjs/next-auth/pull/4744 has been open for some time. Could I kindly ask that it's either merged or it's made clear why it can't be merged so that we can work on an updated solution?

sanbornhilland avatar Dec 20 '22 18:12 sanbornhilland

+1. NextAuth gets me to the 99 yard line and then fumbles the ball by not being able to trigger a session state update from within the client (w/o needing to resort to fragile hackery). Calling getSession exhibits all the right behaviors throughout the stack except for updating state :0( Frankly, I'm surprised to see how old this issue is (and others like it) w/o any resolution for such an essential capability. NextAuth is an overall excellent, well-maintained library IMHO, and super appreciate the contrib, but I may need to bail on it over this issue given the inaction.

rolanday avatar Dec 22 '22 08:12 rolanday