atinux / nuxt-auth-utils

Minimal Auth module for Nuxt 3.
MIT License
810 stars 74 forks source link

Is session refresh implemented? #91

Open septatrix opened 3 months ago

septatrix commented 3 months 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 3 months ago

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

atinux commented 3 months 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 3 months 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 3 months 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 3 months ago

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

PGLongo commented 3 months 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 3 months ago

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

PGLongo commented 3 months 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 3 months 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 months ago

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

PGLongo commented 2 months ago

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

thijsw commented 2 months 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 months ago

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

daniandl commented 2 months 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 :)

marr commented 2 weeks ago

Does the refreshCookie util help with the stale cookie issue reported by @thijsw and @daniandl? Documentation is here https://nuxt.com/docs/api/utils/refresh-cookie

sdevogel commented 1 week ago

Hi, currently also running into the same problem. When I replaceUserSession with new experiationDate the cookie is not updated and the session data also stays the same. Any idea how to replace the session when the refresh token is still valid? Thanks!

carlos-duran commented 1 week ago

@sdevogel are you calling await fetch() from useUserSession() in the client after you replaced the session in the server?

sdevogel commented 1 week ago

@sdevogel are you calling await fetch() from useUserSession() in the client after you replaced the session in the server?

No, how would the client know that the server replaced the session? Maybe asking for the obvious but I'm a bit lost. Thanks a lot for your help :) @carlos-duran