Skip to content

Authentication Token Management

14.4.1. JWT Storage Strategy

  • Access Tokens must be stored in memory (Pinia Store). localStorage must not be used as it is vulnerable to XSS (Cross-Site Scripting) attacks.
  • Refresh Tokens must be managed as HttpOnly cookies. They must not be accessible from client-side JavaScript.
Token TypeStorage LocationRationale
Access TokenPinia Store (memory)Prevents theft during XSS attacks
Refresh TokenHttpOnly cookieBlocks JavaScript access
  • The Auth Store (src/stores/useAuthStore.ts) must define the accessToken state along with setAccessToken and clearAccessToken actions. Since tokens stored in memory are lost on page refresh, they must be reissued using the Refresh Token at application initialization.

14.4.2. Token Refresh

  • Upon receiving a 401 response, an automatic Access Token refresh must be attempted using the Refresh Token.
  • Additional requests occurring during the refresh process must be queued and retried sequentially after the refresh completes.
  • If the refresh request itself fails, all queued requests must be rejected and the user must be logged out.
typescript
// src/api/interceptors/tokenRefresh.ts
import type { AxiosError, InternalAxiosRequestConfig } from 'axios'
import apiClient from '@/api/client'
import { useAuthStore } from '@/stores/useAuthStore'

let isRefreshing = false
let pendingRequests: Array<{ resolve: Function; reject: Function }> = []

export async function handleTokenRefresh(error: AxiosError): Promise<unknown> {
  const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean }
  if (error.response?.status !== 401 || originalRequest._retry) {
    return Promise.reject(error)
  }

  if (isRefreshing) {
    return new Promise((resolve, reject) => {
      pendingRequests.push({ resolve, reject })
    })
  }

  originalRequest._retry = true
  isRefreshing = true

  try {
    const { data } = await apiClient.post<{ accessToken: string }>('/auth/refresh')
    useAuthStore().setAccessToken(data.accessToken)
    pendingRequests.forEach(({ resolve }) => resolve(apiClient(originalRequest)))
    pendingRequests = []
    originalRequest.headers.Authorization = `Bearer ${data.accessToken}`
    return apiClient(originalRequest)
  } catch (refreshError) {
    pendingRequests.forEach(({ reject }) => reject(refreshError))
    pendingRequests = []
    useAuthStore().clearAccessToken()
    window.location.href = '/login'
    return Promise.reject(refreshError)
  } finally {
    isRefreshing = false
  }
}

14.4.3. Automatic 401 Redirect

  • When token refresh fails, the user must be redirected to the login page.
  • During the redirect, the current page path must be preserved as a returnUrl query parameter.
  • The returnUrl must be validated to ensure it starts with /. Redirects to external URLs must be prevented.
typescript
// src/api/utils/redirect.ts
import router from '@/router'

export function redirectToLogin(): void {
  const currentPath = router.currentRoute.value.fullPath
  if (currentPath === '/login') return
  router.push({ path: '/login', query: { returnUrl: currentPath } })
}
  • After successful login, the returnUrl query must be read to navigate back to the original path. The returnUrl must be validated to ensure it starts with /.

14.4.4. Logout Processing

  • The following items must be processed sequentially during logout.
OrderActionBehavior on Failure
1Call server logout APIIgnore and proceed to next step
2Delete Access TokenMandatory (always executed)
3Reset user StoreMandatory (always executed)
4Redirect to login pageMandatory (always executed)
typescript
// src/composables/useLogout.ts
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/useAuthStore'
import { useUserStore } from '@/stores/useUserStore'
import apiClient from '@/api/client'

export function useLogout() {
  const router = useRouter()
  async function logout(): Promise<void> {
    try { await apiClient.post('/auth/logout') } catch { /* Ignore failure */ }
    useAuthStore().clearAccessToken()
    useUserStore().$reset()
    await router.push('/login')
  }

  return { logout }
}
  • Even if the server logout API call fails, client-side cleanup must be completed.

TIENIPIA QUALIFIED STANDARD