Skip to content

Session / token data disappears after restart #1027

Open
@dschreij

Description

@dschreij

Environment

  • Operating System: Darwin
  • Node Version: v22.14.0
  • Nuxt Version: 3.17.5
  • CLI Version: 3.25.1
  • Nitro Version: 2.11.12
  • Package Manager: pnpm@10.11.1
  • Builder: -
  • User Config: ssr, compatibilityDate, modules, app, future, devtools, imports, watch, runtimeConfig, plugins, auth, lodash, i18n, vuetify, piniaOrm, pinia, css, eslint, sentry, sourcemap
  • Runtime Modules: @nuxt/eslint@1.3.0, @nuxt/image@1.10.0, @nuxt/scripts@0.11.5, @nuxt/test-utils@3.17.2, @nuxt/test-utils/module@3.17.2, @nuxtjs/i18n@9.4.0, @pinia/nuxt@0.10.1, @pinia-orm/nuxt@1.10.2, @sidebase/nuxt-auth@0.10.1, @vueuse/nuxt@13.0.0, @zadigetvoltaire/nuxt-gtm@0.0.13, nuxt-lodash@2.5.3, vuetify-nuxt-module@0.18.6, @formkit/auto-animate/nuxt@0.8.2, nuxt-authorization@0.3.3, @sentry/nuxt/module@9.27.0
  • Build Modules: -

Reproduction

This is difficult to reproduce, as this happens quite sporadically, and cannot be provided as a repo, as the problem might be related to containerization.

  • Start up the app running in a docker container / kubernetes pod
  • Start a session by logging in to your app in the browser
  • Do some stuff
  • Restart the docker container / kubernetes pod
  • Do some stuff in the app again
  • Observe that calling `getToken({event}) in server side functions returns null for a while, but after waiting a few moments / a few refreshes of the browser the app starts to function as normal again, and the session information can be retrieved again...

Describe the bug

It appears as if shortly after a containerized app is restarted, the token or session information is temporarily lost. We have a server catchall endpoint that proxies calls to our api service, and before doing so, adds the token information to the header, e.g.

// /server/api/[...].ts
import { getToken } from '#auth'
import * as Sentry from '@sentry/nuxt'

const config = useRuntimeConfig()

export default defineEventHandler(async (event) => {
  const apiUrl = config.apiUrl
  const path = event.path.replace(/^\/api/, '')
  const token = await getToken({ event })
  
  if (!token) {
    Sentry.captureException(new Error('No token found for API request'), {
      extra: {
        data: { token, path, method: event.method },
      },
    })
  }
  const headers = getRequestHeaders(event)
  if (token?.access_token) {
    headers.authorization = `Bearer ${token.access_token}`
  }
  await proxyRequest(event, `${apiUrl}${path}`, { headers })
})

This works great 95% of the times. However, occasionally, and shortly after a restart of the app, the getToken({event}) call returns null, while the user definitely has a session with a token still available. If you shortly wait and refresh the browser. The app starts to work as usual again and the token is found agai

I am not sure if this is is persistence problem (as it does not occur locally / non-containerized, and thus might be a thing to do with statefulness / persistence on disk?) or whether sessions are slow to start up after the application boots? So I guess my main question is where to look, as I have too little knowledge about the intrinsics of session handling by sidebase/auth

This behavior causes some users to experience 401 unauthorized errors, which we like to spare them. So it would be nice to know how to get the sessions active again after a boot up as quickly as possible :)

Additional context

Our NuxtAuthHandler code is below. We use Auth0 as our IDP. We have implemented a refresh token procedure which works great as far as we can tell:

// /server/api/auth/[...].ts

import { Buffer } from 'node:buffer'
import { NuxtAuthHandler } from '#auth'
import * as Sentry from '@sentry/nuxt'
import Auth0Provider from 'next-auth/providers/auth0'
import { isTokenValid } from '~/shared/utils/auth'

const config = useRuntimeConfig()

function convertExpiresInToExpiresAt(expiresIn: unknown): number {
  if (typeof expiresIn === 'number') {
    // Convert expires_in to expires_at timestamp
    return Math.floor(Date.now() / 1000) + expiresIn
  }
  return 0
}

export default NuxtAuthHandler({
  secret: config.authSecret,
  providers: [
      Auth0Provider.default({
      clientId: config.auth0.clientId,
      clientSecret: config.auth0.clientSecret,
      issuer: config.auth0.domain,
      authorization: {
        params: {
          scope: 'openid profile email offline_access',
          audience: config.auth0.audience,
          prompt: 'login',
        },
      },
    }),
  ],
  callbacks: {
    async jwt({ token, account, profile }) {
      token.error = undefined // Reset error on each login attempt
      if (account) {
        if (profile) {
          Sentry.setUser({
            id: profile.sub,
            email: profile.email,
            username: profile.name,
          })
        }
        // First login, store the tokens in the JWT
        return {
          ...token,
          access_token: account.access_token,
          id_token: account.id_token,
          refresh_token: account.refresh_token,
          expires_at: account.expires_at,
        }
      }
      else if (isTokenValid(token)) {
        // Subsequent logins, but the `access_token` is still valid
        return token
      }
      else {
        // Subsequent logins, but the `access_token` has expired, try to refresh it
        if (!token.refresh_token)
          throw new TypeError('Missing refresh_token')
        try {
          const response = await fetch(`${config.auth0.domain}/oauth/token`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
            body: new URLSearchParams({
              client_id: config.auth0.clientId,
              client_secret: config.auth0.clientSecret,
              grant_type: 'refresh_token',
              refresh_token: token.refresh_token,
            }),
          })

          if (!response.ok)
            throw new Error(response.statusText)

          const tokens = await response.json()

          const newToken = tokens as {
            access_token: string
            expires_in: number
            refresh_token?: string
            id_token?: string
          }

          return {
            ...token,
            access_token: newToken.access_token,
            expires_at: convertExpiresInToExpiresAt(newToken.expires_in),
            id_token: newToken.id_token ? newToken.id_token : token.id_token,
            refresh_token: newToken.refresh_token,
          }
        }
        catch (error) {
          console.error('Error refreshing access_token: ', error)
          // If we fail to refresh the token, return an error so we can handle it on the page
          Sentry.captureException(error, {
            tags: {
              module: 'auth',
              action: 'refresh_token',
            },
          })
          token.error = 'RefreshTokenError'
          return token
        }
      }
    },
    session({ session, token }) {
      // Add Auth0 permissions to session cookie
      if (token?.access_token && typeof token.access_token === 'string') {
        const accessTokenData = JSON.parse(
          Buffer.from(token.access_token.split('.')[1], 'base64').toString(),
        )
        const sessUser = session.user
        if (!sessUser) {
          Sentry.captureException(new Error('Session user is undefined'), {
            tags: { module: 'auth' },
            extra: { session },
          })
          return session
        }
        sessUser.permissions = accessTokenData.permissions
        sessUser.location = accessTokenData['https://cargoplot.com/geo']
      }
      session.error = token.error
      return session
    },
  },
})

Logs

Metadata

Metadata

Assignees

No one assigned

    Labels

    questionA question about NuxtAuth

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions