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

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

Closed
blms opened this issue Aug 21, 2020 · 36 comments
Closed
Labels
bug Something isn't working question Ask how to do something or how something works

Comments

@blms
Copy link
Contributor

blms commented Aug 21, 2020

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 blms added the question Ask how to do something or how something works label Aug 21, 2020
@iaincollins iaincollins added the incomplete Insufficient reproduction. Without more info, we won't take further actions/provide help. label Aug 24, 2020
@ValentinH
Copy link
Contributor

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?

@blms blms changed the title UI only updates on focus change despite getSession() call getSession call does not trigger session object update until window loses focus Nov 30, 2020
@iaincollins iaincollins added the bug Something isn't working label Dec 1, 2020
@iaincollins
Copy link
Member

@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 iaincollins added priority Priority fix or enhancement and removed incomplete Insufficient reproduction. Without more info, we won't take further actions/provide help. labels Dec 1, 2020
@Dev-Songkran
Copy link

Hi, is there a temporary solution?

@ValentinH
Copy link
Contributor

ValentinH commented Jan 5, 2021

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

@blms
Copy link
Contributor Author

blms commented Jan 5, 2021

I'm using this workaround for now: #371 (comment)

@balazsorban44
Copy link
Member

balazsorban44 commented Jan 7, 2021

@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?

// Ignore storage events fired from the same window that created them
if (__NEXTAUTH._clientId === message.clientId) {
return
}

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.

@thattimc
Copy link

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')
}

@ayushkamadji
Copy link

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
}

@paul-vd
Copy link
Contributor

paul-vd commented May 23, 2021

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
}

@zanami
Copy link

zanami commented Jun 4, 2021

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?

@balazsorban44
Copy link
Member

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)

@zanami
Copy link

zanami commented Jun 7, 2021

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

@mihaic195
Copy link

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

@zanami
Copy link

zanami commented Sep 21, 2021

@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.

@malioml
Copy link

malioml commented Oct 2, 2021

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

@PhilippLgh
Copy link

PhilippLgh commented Oct 14, 2021

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)
}

@jacklimwenjie
Copy link

@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?

@YoannBuzenet
Copy link

YoannBuzenet commented Dec 18, 2021

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 ?

@killjoy2013
Copy link

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?

@mAAdhaTTah
Copy link

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?

@1finedev
Copy link

any luck on this so far?

@mikarty
Copy link

mikarty commented Jun 10, 2022

Any solutions/workarounds for latest versions? :)

@1finedev
Copy link

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

@IRediTOTO
Copy link

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 :(

@dextermb
Copy link

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())
}

@leephan2k1
Copy link

leephan2k1 commented Sep 20, 2022

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!

@PhilippLgh
Copy link

PhilippLgh commented Sep 28, 2022

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 })

@kachar
Copy link

kachar commented Oct 11, 2022

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.

@sanbornhilland
Copy link

sanbornhilland commented Dec 20, 2022

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 #596 (comment) 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 #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?

@rolanday
Copy link

+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.

@atomcat1978
Copy link

Such an old and important issue, and no progress at all? :(

@kachar
Copy link

kachar commented Jan 20, 2023

Indeed it's so essential but it seems the dev team is focused on NextAuth.js to Auth.js migration.

@BrahimiMehdi
Copy link

+1 , Been having the same issue over here
Still no good way to actually refresh the token is kind of a big deal which may make me have to drop next-auth on future and current projects

@rolanday
Copy link

rolanday commented Feb 3, 2023

Related thread here: #4229

@dextermb
Copy link

dextermb commented Feb 3, 2023

Heads up, my earlier suggestion still works ("next-auth": "^4.19.1").

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())
}

@balazsorban44
Copy link
Member

Hi all, let's continue the discussion in #3941, which I think should address this. Here is our latest update: #3941 (comment)

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

Successfully merging a pull request may close this issue.