Atinux / nuxt-auth-utils

Minimal Auth module for Nuxt 3.
MIT License
617 stars 60 forks source link

Is session refresh implemented? #91

Open septatrix opened 1 month ago

septatrix commented 1 month ago

I saw the "offline_access" scope being used for the OAuth0 provider but no reference to refresh tokens in the codebase. Are refresh tokens implemented/utilized? Or is the session from the OAuth2 provider only used once and afterwards everything is delegated to h3?

amandesai01 commented 1 month ago

I believe there should be a refresh token implementation. Is this open to PR?

Atinux commented 1 month ago

Refresh tokens are not implemented so far as we just give back to the session what's needed and some OAuth does not handle refresh tokens.

Do you have an example of an implementation you would like to see?

PGLongo commented 1 month ago

@septatrix I have successfully achieved the refresh of the session with the session hook for 'fetch'. If the session has expired and I have a valid refresh token, then the refresh workflow is initiated to obtain a new valid token.

septatrix commented 1 month ago

@septatrix I have successfully achieved the refresh of the session with the session hook for 'fetch'. If the session has expired and I have a valid refresh token, then the refresh workflow is initiated to obtain a new valid token.

Would you mind sharing the code for that?

silvio-e commented 1 month ago

@PGLongo I would be also very interested in that! 😊

PGLongo commented 1 month ago

Sure! Here I refresh the Microsoft Oauth. Note that in the auth handler I have stored the expirationDate in the session.user

// server/plugins/session.ts

import { useRuntimeConfig } from '#imports'
import type { OAuthMicrosoftConfig } from '~/server/api/auth/login.get'

export default defineNitroPlugin(() => {
  sessionHooks.hook('fetch', async (session, event) => {
    const now = new Date()
    const expirationDate = new Date(session.user.expirationDate)
    const jwt = getCookie(event, 'jwt')
    console.log(expirationDate < now, expirationDate, now)
    if (expirationDate < now || !jwt) {
      const config = useRuntimeConfig(event).oauth?.microsoft as OAuthMicrosoftConfig

      const tokenEndpoint = `https://login.microsoftonline.com/${config.tenant!}/oauth2/v2.0/token`
      const params = new URLSearchParams()
      const refreshToken = getCookie(event, 'refresh-token') || ''

      params.append('client_id', config.clientId!)
      params.append('client_secret', config.clientSecret!)
      params.append('refresh_token', refreshToken)
      params.append('grant_type', 'refresh_token')

      const data = await $fetch(tokenEndpoint, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded'
        },
        body: params
      })
      const now = new Date()
      session.user.expirationDate = new Date(now.getTime() + data.expires_in * 1000)
      await setCookie(event, 'jwt', data.access_token, { httpOnly: true, secure: true, maxAge: data.expires_in })
    }
  })

  sessionHooks.hook('clear', async (session, event) => {
    await deleteCookie(event, 'jwt')
    await deleteCookie(event, 'refresh-token')
  })
})
silvio-e commented 1 month ago

@PGLongo Awesome! Very kind of you! Thank you very much!

PGLongo commented 1 month ago

@PGLongo Awesome! Very kind of you! Thank you very much!

Sharing is caring! 😊

If you have any questions or need further assistance, feel free to reach out. Happy coding!

thijsw commented 1 month ago

Thank you, @PGLongo, for providing an example implementation of the refresh dynamic!

Inspired by your code, I created a similar plugin that refreshes the tokens when the access token expires. The problem I'm facing is that the sealed session cookie is never updated, so the original contents remain unchanged. After the access token expires the first time, it refreshes the tokens on every subsequent page refresh. Do you have a solution for this issue?

My code:

// server/plugins/session.ts

export default defineNitroPlugin(() => {
  sessionHooks.hook('fetch', async (session, event) => {
    const authenticationConfig = getAuthenticationConfig(event) // Configuration helper
    const now = new Date()
    const expirationDate = new Date(session.expirationDate)

    if (expirationDate < now) {
      // Refresh session
      const body = new FormData()
      body.append('grant_type', 'refresh_token')
      body.append('refresh_token', session.refreshToken)
      body.append('response_type', 'id_token')
      body.append('client_id', authenticationConfig.clientId)
      body.append('client_secret', authenticationConfig.clientSecret)
      body.append('scope', authenticationConfig.scope)

      const token = await $fetch<AccessToken>(authenticationConfig.tokenURL, {
        method: 'post',
        body
      })

      session.accessToken = token.access_token
      session.refreshToken = token.refresh_token
      session.expirationDate = new Date(now.getTime() + token.expires_in * 1000)
    }
  })
})
doubleujay commented 3 weeks ago

@PGLongo do you have an answer on the question of @thijsw ? We are really looking forward to it.

PGLongo commented 3 weeks ago

@doubleujay and @thijsw are you refreshing the token on server side?

thijsw commented 3 weeks ago

@PGLongo Yes, what I'm trying to accomplish is that the tokens is refreshed server-side (when expired) and the new updated value stored in the encrypted cookie. This new, just retrieved, token should be used by all API calls within the same request cycle. However, using my previous shared code, this doesn't work. The cookie doesn't get updated, so when I refresh the page the old token is being refreshed again. Do you have a suggestion how I could address this?

doubleujay commented 2 weeks ago

@PGLongo Is the reply of @thijsw enough to give more insight?

daniandl commented 1 week ago

Inspired by your code, I created a similar plugin that refreshes the tokens when the access token expires. The problem I'm facing is that the sealed session cookie is never updated, so the original contents remain unchanged. After the access token expires the first time, it refreshes the tokens on every subsequent page refresh. Do you have a solution for this issue?

I can maybe provide some insight here, dealing with this myself at the moment. The reason the cookie may not be getting refreshed is because the set-cookie header is not forwarded on fetch calls. This eventually lead me to a fetch wrapper snippet like this:

const res = await $fetch.raw<T>(request, {
  ...opts,
  headers: { ...opts?.headers, ...useRequestHeaders(['cookie']) },
})

// forward cookies into SSR response
const cookies = (res.headers.get('set-cookie') || '').split(',')
for (const cookie of cookies)
  appendResponseHeader(event, 'set-cookie', cookie)

// Return the data of the response
return res._data

The cookie now get set refreshed properly for me but pressing back on the browser messes this up and theres an nuxt internal error. All this trouble kind of made me rethink my token flow, I do not see a clean refresh token implementation possible on Nuxt, or SSR flows in general, where server side refreshes are needed. My conclusion here is to just ditch the refresh flow for a regular access token + auto session extension on activity + revocation flow.

Hope it helps a bit at least :)