Authentication Token Management
14.4.1. JWT Storage Strategy
- Access Tokens must be stored in memory (Pinia Store).
localStoragemust 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 Type | Storage Location | Rationale |
|---|---|---|
| Access Token | Pinia Store (memory) | Prevents theft during XSS attacks |
| Refresh Token | HttpOnly cookie | Blocks JavaScript access |
- The Auth Store (
src/stores/useAuthStore.ts) must define theaccessTokenstate along withsetAccessTokenandclearAccessTokenactions. 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
returnUrlquery parameter. - The
returnUrlmust 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
returnUrlquery must be read to navigate back to the original path. ThereturnUrlmust be validated to ensure it starts with/.
14.4.4. Logout Processing
- The following items must be processed sequentially during logout.
| Order | Action | Behavior on Failure |
|---|---|---|
| 1 | Call server logout API | Ignore and proceed to next step |
| 2 | Delete Access Token | Mandatory (always executed) |
| 3 | Reset user Store | Mandatory (always executed) |
| 4 | Redirect to login page | Mandatory (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.