nuxt-auth-utils icon indicating copy to clipboard operation
nuxt-auth-utils copied to clipboard

setUserSession does not update session immediately after login in 'fetch' hook

Open vanling opened this issue 10 months ago • 12 comments

Last working version: v0.5.11

Issue description: After updating past v0.5.11, setUserSession is no longer updating the session immediately after login when adding user data in the fetch hook. The session does update, but only after the fact and only visible after page reload.

Expected behavior: as in v0.5.11 the session immediately reflected the updated user data after calling setUserSession in the hook.

Actual behavior: After login, the session, in the frontend/app does not immediately contain the updated user data. Reloading the page makes the session data appear.

Code:

export default defineNitroPlugin(() => {
  sessionHooks.hook('fetch', async (session, event) => {
    console.log('fetch', session) // gets filled with basic info during login
    if (session && session.secure && session.secure.loginid) {
     // normally i $fetch the user data from api here, so after login or on a page reload its complete
      await setUserSession(event, {
        user: { name: 'test' },
      })
      // expect session to contain user.name = 'test'
      console.log('after fetch', session.user?.name === 'test')

    }
  })
})

refreshing on http://localhost:3000/api/_auth/session after shows user.name = 'test'.

Another way to quickly test this: Open http://localhost:3000/api/_auth/session use replaceUserSession or setUserSession in the hook, change some values in code and you than need to refresh 2 times to see the changes in session

vanling avatar Feb 18 '25 09:02 vanling

I'm facing the same issue. In my case I'm using the fetch hook to persist the access token and refresh token after refreshing them. Unfortunately, rolling back to v0.5.11 did not fix this for me.

import type { H3Event } from 'h3'
import { jwtDecode } from 'jwt-decode'
import type { UserSession } from '#auth-utils'

function shouldRefreshToken(accessToken: string): boolean {
  const decodedToken = jwtDecode(accessToken)
  return decodedToken.iat + 10000 < Date.now() // for debugging purpose
  // const ONE_HOUR = 60 * 60 * 1000
  // return Date.now() + ONE_HOUR >= decodedToken.exp * 1000
}

async function refreshTokens(refreshToken: string) {
  const config = useRuntimeConfig()

  return $fetch(`https://${config.oauth.auth0.domain}/oauth/token`, {
    method: 'POST',
    headers: { 'content-type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'refresh_token',
      client_id: config.oauth.auth0.clientId,
      client_secret: config.oauth.auth0.clientSecret,
      refresh_token: refreshToken,
    }).toString(),
  })
}

export default defineNitroPlugin(() => {
  sessionHooks.hook('fetch', async (session: UserSession, event: H3Event) => {
    if (!session?.tokens) {
      return
    }

    const { tokens } = session
    if (shouldRefreshToken(tokens.access_token)) {
      try {
        console.log('old refresh token', tokens.refresh_token) // Correctly prints the old refresh token the first time this hook is called, but stays the same after that

        const newTokens = await refreshTokens(tokens.refresh_token)

        await setUserSession(event, {
          tokens: newTokens,
        })

        console.log('new refresh token', newTokens.refresh_token) // Correctly prints the new refresh token

        session.tokens = newTokens
      } catch (error) {
        console.error('Failed to refresh tokens:', error)
        await clearUserSession(event)
      }
    }
  })
})

Console output:

// First output right after logging in
old refresh token v1.[...]-8qRLSJWJhQSoZbiz6z5l1I_fpQ
new refresh token v1.[...]-jn_Nrbwlr6dv1JYVdqAtTkRYtCgyeE

// Next time when we call /api/_auth/session
old refresh token v1.[...]-8qRLSJWJhQSoZbiz6z5l1I_fpQ

 ERROR  Failed to refresh tokens: [POST] "https://[auth0domain]/oauth/token": 403 Forbidden

Crease29 avatar Feb 18 '25 20:02 Crease29

Thanks @Crease29, your code gave me an idea to try and I found a 'solution' that worked for my case.

instead of

await setUserSession(event, {
   user: { name: 'test' },
})

I use

session.user =  { name: 'test' }

Still think its a bug tho that setUserSession is not working anymore.

Edit: @atinux, if I'm mistaken in thinking that I should use setUserSession in the fetch hook and should instead set data directly on the session variable, feel free to close this ticket.

vanling avatar Feb 19 '25 06:02 vanling

Glad that it helped you, @vanling 😅 I'm still struggling getting this to work. I really hope @atinux has a brilliant idea what's going on.

Crease29 avatar Feb 20 '25 17:02 Crease29

Found this https://github.com/atinux/nuxt-auth-utils/issues/314#issuecomment-2589502413, which might be helpful.

Edit: I've implemented it as a custom refresh endpoint and it seems to work fine :)

/server/api/auth/refresh.get.ts:

import { UserSession } from '#auth-utils'
import { jwtDecode } from 'jwt-decode'

function isTokenExpired(token: string): boolean {
  const decoded = jwtDecode(token)
  const ONE_HOUR = 60 * 60 * 1000
  const expiresAt = (decoded?.exp ?? 0) * 1000

  return Date.now() + ONE_HOUR >= expiresAt
}

async function refreshTokens(refreshToken: string) {
  const config = useRuntimeConfig()

  return $fetch(`https://${config.oauth.auth0.domain}/oauth/token`, {
    method: 'POST',
    headers: { 'content-type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'refresh_token',
      client_id: config.oauth.auth0.clientId,
      client_secret: config.oauth.auth0.clientSecret,
      refresh_token: refreshToken,
    }).toString(),
  })
}

export default defineEventHandler(async (event) => {
  const session: UserSession = await getUserSession(event)
  if (!session?.tokens) {
    return
  }

  const { tokens } = session
  const isAccessTokenExpired = isTokenExpired(tokens.access_token)
  if (!isAccessTokenExpired) {
    // If the access token is still valid, we don't need to refresh it
    return
  }

  try {
    const newTokens = await refreshTokens(tokens.refresh_token)

    session.tokens = newTokens
    await setUserSession(event, {
      tokens: {
        access_token: newTokens.access_token,
        refresh_token: newTokens.refresh_token,
      },
    })
  } catch (error) {
    console.error('Failed to refresh tokens:', error)
    await clearUserSession(event)
  }
})

app.vue (script setup):

onMounted(() => {
  $fetch('/api/auth/refresh')
})

Crease29 avatar Feb 23 '25 07:02 Crease29

I have the same issue. I do check if email is verified with the auth provider, and if it comes back as verified, I updated the session, but now, session shows as updated on the server, but on the app it still sees only the old session. Worked before. Also checked the session size and that too is under the limit. So far only hard rest in form of logout login works.

BerzinsU avatar Mar 12 '25 06:03 BerzinsU

@atinux any idea what could be the issue here? I'm also still having issues getting the updated session on the client side after refreshing the session.

Crease29 avatar Apr 02 '25 05:04 Crease29

If you make an API call that update the session / cookie on server-side, you need to tell the Vue composable to refresh it self:

+ const { fetch: refreshSession } = useUserSession()
onMounted(() => {
  $fetch('/api/auth/refresh')
+ .then(refreshSession)
})

atinux avatar Apr 02 '25 07:04 atinux

If you make an API call that update the session / cookie on server-side, you need to tell the Vue composable to refresh it self:

  • const { fetch: refreshSession } = useUserSession() onMounted(() => { $fetch('/api/auth/refresh')
  • .then(refreshSession) })

And how about the initial approach using the session fetch hook?

export default defineNitroPlugin(() => {
  sessionHooks.hook('fetch', async (session, event) => {
    // ...
  })
})

We would expect that whatever we're setting in the session in that hook is available after the response of the session fetch request.

Crease29 avatar Apr 02 '25 07:04 Crease29

If you make an API call that update the session / cookie on server-side, you need to tell the Vue composable to refresh it self:

  • const { fetch: refreshSession } = useUserSession() onMounted(() => { $fetch('/api/auth/refresh')
  • .then(refreshSession) })

And how about the initial approach using the session fetch hook?

export default defineNitroPlugin(() => { sessionHooks.hook('fetch', async (session, event) => { // ... }) }) We would expect that whatever we're setting in the session in that hook is available after the response of the session fetch request.

We don't have custom endpoint either, already calling fetch() from useUserSession(). Should this not update the cookies?

DavidDeSloovere avatar Jun 02 '25 15:06 DavidDeSloovere

seems like there's an issue with setUserSession when used in fetch hook.

the following does not work:

export default defineNitroPlugin(() => {
  sessionHooks.hook('fetch', async (session, event) => {
    const user = getUser()
    await setUserSession(event, {
      user: user,
      })
    })
})

This works:

export default defineNitroPlugin(() => {
  sessionHooks.hook('fetch', async (session, event) => {
    const user = getUser()
    session.user = user
  })
})

costaryka avatar Jun 12 '25 15:06 costaryka

I think this is linked to these comments

https://github.com/atinux/nuxt-auth-utils/issues/427#issuecomment-3309446157 https://github.com/atinux/nuxt-auth-utils/issues/427#issuecomment-3309554934

NicholasAntidormi avatar Sep 18 '25 20:09 NicholasAntidormi

I'm curious if anyone has seen the proposed solution (e.g. session.user = ...) work in a Nuxt4 ecosystem?

I was previously using this approach; however when upgrading to Nuxt4 it's no longer working; my server-side APIs don't have the updated session information.

kbarnesMCC avatar Nov 04 '25 18:11 kbarnesMCC