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

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

Open
vanling opened this issue Feb 18, 2025 · 4 comments

Comments

@vanling
Copy link

vanling commented Feb 18, 2025

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

@Crease29
Copy link

Crease29 commented Feb 18, 2025

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

@vanling
Copy link
Author

vanling commented Feb 19, 2025

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.

@Crease29
Copy link

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
Copy link

Crease29 commented Feb 23, 2025

Found this #314 (comment), 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')
})

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants