Browse Source

fix: refresh-token based token generation issue

pull/9415/head
Pranav C 3 months ago
parent
commit
0e7a9dce6b
  1. 1
      packages/nc-gui/components/dlg/TableDescriptionUpdate.vue
  2. 50
      packages/nc-gui/composables/useApi/interceptors.ts
  3. 7
      packages/nc-gui/composables/useGlobal/actions.ts
  4. 16
      packages/nc-gui/composables/useGlobal/index.ts
  5. 8
      packages/nc-gui/store/notification.ts
  6. 1
      packages/nc-gui/utils/ncUtils.ts

1
packages/nc-gui/components/dlg/TableDescriptionUpdate.vue

@ -1,6 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { TableType } from 'nocodb-sdk' import type { TableType } from 'nocodb-sdk'
import type { ComponentPublicInstance } from '@vue/runtime-core'
interface Props { interface Props {
modelValue?: boolean modelValue?: boolean

50
packages/nc-gui/composables/useApi/interceptors.ts

@ -1,10 +1,13 @@
import type { Api } from 'nocodb-sdk' import type { Api } from 'nocodb-sdk'
import { useStorage } from '@vueuse/core'
const DbNotFoundMsg = 'Database config not found' const DbNotFoundMsg = 'Database config not found'
let refreshTokenPromise: Promise<string> | null = null let refreshTokenPromise: Promise<string> | null = null
export function addAxiosInterceptors(api: Api<any>) { export function addAxiosInterceptors(api: Api<any>) {
const isTokenRefreshInProgress = useStorage(TOKEN_REFRESH_PROGRESS_KEY, false)
const state = useGlobal() const state = useGlobal()
const router = useRouter() const router = useRouter()
const route = router.currentRoute const route = router.currentRoute
@ -45,7 +48,10 @@ export function addAxiosInterceptors(api: Api<any>) {
return response return response
}, },
// Handle Error // Handle Error
(error) => { async (error) => {
// if cancel request then throw error
if (error.code === 'ERR_CANCELED') return Promise.reject(error)
if (error.response && error.response.data && error.response.data.msg === DbNotFoundMsg) return router.replace('/base/0') if (error.response && error.response.data && error.response.data.msg === DbNotFoundMsg) return router.replace('/base/0')
// Return any error which is not due to authentication back to the calling service // Return any error which is not due to authentication back to the calling service
@ -55,10 +61,33 @@ export function addAxiosInterceptors(api: Api<any>) {
// Logout user if token refresh didn't work or user is disabled // Logout user if token refresh didn't work or user is disabled
if (error.config.url === '/auth/token/refresh') { if (error.config.url === '/auth/token/refresh') {
state.signOut() state.signOut(undefined, true)
return Promise.reject(error) return Promise.reject(error)
} }
// if no active refresh token request in the current session then check the local storage
if (!refreshTokenPromise && isTokenRefreshInProgress.value) {
// if token refresh is already in progress, wait for it to finish and then retry the request if token is available
await until(isTokenRefreshInProgress).toMatch((v) => !v, { timeout: 10000 })
isTokenRefreshInProgress.value = false
// check if the user is signed in by checking the token presence and retry the request with the new token
if (state.token.value) {
return new Promise((resolve, reject) => {
const config = error.config
config.headers['xc-auth'] = state.token.value
axiosInstance
.request(config)
.then((response) => {
resolve(response)
})
.catch((error) => {
reject(error)
})
})
}
}
let refreshTokenPromiseRes: (token: string) => void let refreshTokenPromiseRes: (token: string) => void
let refreshTokenPromiseRej: (e: Error) => void let refreshTokenPromiseRej: (e: Error) => void
@ -86,15 +115,20 @@ export function addAxiosInterceptors(api: Api<any>) {
// ignore since it could have already been handled and redirected to sign in // ignore since it could have already been handled and redirected to sign in
}) })
} else { } else {
isTokenRefreshInProgress.value = true
refreshTokenPromise = new Promise<string>((resolve, reject) => { refreshTokenPromise = new Promise<string>((resolve, reject) => {
refreshTokenPromiseRes = resolve refreshTokenPromiseRes = resolve
refreshTokenPromiseRej = reject refreshTokenPromiseRej = reject
}) })
// set a catch on the promise to avoid unhandled promise rejection // set a catch on the promise to avoid unhandled promise rejection
refreshTokenPromise.catch(() => { refreshTokenPromise
.catch(() => {
// ignore // ignore
}) })
.finally(() => {
isTokenRefreshInProgress.value = false
})
} }
// Try request again with new token // Try request again with new token
@ -124,9 +158,17 @@ export function addAxiosInterceptors(api: Api<any>) {
}) })
}) })
.catch(async (refreshTokenError) => { .catch(async (refreshTokenError) => {
// skip signout call if request cancelled
if (refreshTokenError.code === 'ERR_CANCELED') {
// reject the refresh token promise and reset
refreshTokenPromiseRej(refreshTokenError)
refreshTokenPromise = null
return Promise.reject(refreshTokenError)
}
await state.signOut({ await state.signOut({
redirectToSignin: !route.value.meta.public, redirectToSignin: !route.value.meta.public,
}) }, true);
// reject the refresh token promise and reset // reject the refresh token promise and reset
refreshTokenPromiseRej(refreshTokenError) refreshTokenPromiseRej(refreshTokenError)

7
packages/nc-gui/composables/useGlobal/actions.ts

@ -1,5 +1,5 @@
import { getActivePinia } from 'pinia' import { getActivePinia } from 'pinia'
import type { Actions, AppInfo, SignOutParams, State } from './types' import type { Actions, AppInfo, State } from './types'
import type { NcProjectType } from '#imports' import type { NcProjectType } from '#imports'
export function useGlobalActions(state: State): Actions { export function useGlobalActions(state: State): Actions {
@ -8,10 +8,13 @@ export function useGlobalActions(state: State): Actions {
} }
/** Sign out by deleting the token from localStorage */ /** Sign out by deleting the token from localStorage */
const signOut: Actions['signOut'] = async ({ redirectToSignin, signinUrl = '/signin' }: SignOutParams = {}) => { const signOut: Actions['signOut'] = async ({ redirectToSignin, signinUrl = '/signin', skipApiCall = false }: SignOutParams = {}) => {
try { try {
// call and invalidate refresh token only if user manually triggered logout
if (!skipApiCall) {
const nuxtApp = useNuxtApp() const nuxtApp = useNuxtApp()
await nuxtApp.$api.auth.signout() await nuxtApp.$api.auth.signout()
}
} catch { } catch {
// ignore error // ignore error
} finally { } finally {

16
packages/nc-gui/composables/useGlobal/index.ts

@ -42,22 +42,6 @@ export const useGlobal = createGlobalState((): UseGlobalReturn => {
const actions = useGlobalActions(state) const actions = useGlobalActions(state)
/** try to refresh token before expiry (5 min before expiry) */
watch(
() =>
!!(
state.jwtPayload.value &&
state.jwtPayload.value.exp &&
state.jwtPayload.value.exp - 5 * 60 < state.timestamp.value / 1000
),
async (expiring: boolean) => {
if (getters.signedIn.value && state.jwtPayload.value && expiring) {
await actions.refreshToken()
}
},
{ immediate: true },
)
watch( watch(
state.jwtPayload, state.jwtPayload,
(nextPayload) => { (nextPayload) => {

8
packages/nc-gui/store/notification.ts

@ -2,8 +2,11 @@ import { defineStore } from 'pinia'
import type { NotificationType } from 'nocodb-sdk' import type { NotificationType } from 'nocodb-sdk'
import axios, { type CancelTokenSource } from 'axios' import axios, { type CancelTokenSource } from 'axios'
import { CancelToken } from 'axios' import { CancelToken } from 'axios'
import { useStorage } from '@vueuse/core'
export const useNotification = defineStore('notificationStore', () => { export const useNotification = defineStore('notificationStore', () => {
const isTokenRefreshInProgress = useStorage(TOKEN_REFRESH_PROGRESS_KEY, false)
const readNotifications = ref<NotificationType[]>([]) const readNotifications = ref<NotificationType[]>([])
const unreadNotifications = ref<NotificationType[]>([]) const unreadNotifications = ref<NotificationType[]>([])
@ -198,7 +201,10 @@ export const useNotification = defineStore('notificationStore', () => {
try { try {
if (newToken && newToken !== oldToken) { if (newToken && newToken !== oldToken) {
await init() await init()
} else if (!newToken) { }
// clear polling if there is no refresh token request in progress
// and access token is removed
else if (!newToken && !isTokenRefreshInProgress.value) {
clearPolling() clearPolling()
} }
} catch (e) { } catch (e) {

1
packages/nc-gui/utils/ncUtils.ts

@ -1 +1,2 @@
export const isEeUI = false export const isEeUI = false
export const TOKEN_REFRESH_PROGRESS_KEY = 'nc_token_refresh_progress'

Loading…
Cancel
Save