dmytroshved's avatar

VueJS 3: Storing Token & User. Code Analyzing

Hey everyone

I am analyzing the code of RealWorld Example (Vue 3 + Pinia) and I'm trying to understand the author's logic for storing the user and token.

Below is a summary of what I observed.

1. User storage in localStorage

The app uses a custom Storage class to store the user object (from what I understood including the token) in localStorage:

// src/utils/storage.ts

export const userStorage = new Storage<User>('user')

Source code of userStorage

2. Global access via Pinia

The user is exposed through a Pinia store:

// src/store/user.ts

const user = ref(userStorage.get())
const isAuthorized = computed(() => !!user.value)

function updateUser(userData?: User | null) {
  if (userData) {
    userStorage.set(userData)
    api.setSecurityData(userData.token)
    user.value = userData
  } else {
    userStorage.remove()
    api.setSecurityData(null)
    user.value = null
  }
}

Notes:

  • user.value provides reactive access to the user data.

  • isAuthorized is a computed property based on whether a user exists (boolean true/false).

  • updateUser updates both Pinia and localStorage with edited user data.

Source code of Pinia storage

3. Token storage in JavaScript memory

The HttpClient class, used for API requests, has a private securedData property that holds the token.

// src/services/api.ts

export class HttpClient<SecurityDataType = unknown> {
  public baseUrl: string = "https://api.realworld.show/api";
  private securityData: SecurityDataType | null = null; // this line 
  private securityWorker?: ApiConfig<SecurityDataType>["securityWorker"];
  private abortControllers = new Map<CancelToken, AbortController>();
  private customFetch = (...fetchParams: Parameters<typeof fetch>) =>
    fetch(...fetchParams);

  //...

  public setSecurityData = (data: SecurityDataType | null) => {
    this.securityData = data;
  };

Source code of api.ts (the file is pretty big (800+ lines of code), so you can find it by: Ctrl + F and typing HttpClient.)

During login, the token is stored both in localStorage (via userStorage) and in JavaScript memory (securityData):

// src/store/user.ts 

updateUser(result.data.user) // sets token in both places

4. Token recovery after reload

On application start, a plugin (set-authorization-token.ts) reads the token from localStorage and writes it to HttpClient.securityData

// src/plugins/set-authorization-token.ts

import { api } from 'src/services'
import { userStorage } from 'src/store/user'

export default function setAuthorizationToken(): void {
  const token = userStorage.get()?.token
  if (token !== undefined)
    api.setSecurityData(token)
}
// src/main.ts

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import registerGlobalComponents from './plugins/global-components'
import setAuthorizationToken from './plugins/set-authorization-token'
import { router } from './router'

const app = createApp(App)

app.use(createPinia())
app.use(router)

setAuthorizationToken()
registerGlobalComponents(app)

app.mount('#app')

Authentication flow:

  • The user visits Login.vue and logs in via api.users.login, which returns a user object (with token?).

  • updateUser(user) saves user in user.ts and userStorage, and copies token into HttpClient.securityData.

  • On page reload, setAuthorizationToken() reads the token from userStorage and restores it to HttpClient.securityData.

  • API requests use HttpClient.securityData in the Authorization header:

// src/services/index.ts

import { CONFIG } from 'src/config'
import type { GenericErrorModel, HttpResponse } from 'src/services/api'
import { Api, ContentType } from 'src/services/api'

export const limit = 10

export const api = new Api({
  baseUrl: `${CONFIG.API_HOST}/api`,
  securityWorker: token => token ? { headers: { Authorization: `Token ${String(token)}` } } : {},
  baseApiParams: {
    headers: {
      'content-type': ContentType.Json,
    },
    format: 'json',
  },
})

// ...

Source code of index.ts

My questions

  1. If the token is already in userStorage, why does the author also store it in JavaScript memory (securityData)?
  2. Is this approach secure? I understand that storing tokens in userStorage is vulnerable to XSS attacks, but since this repo has many stars I still don't understand the logic.
  3. How is the token actually protected here, given that it persists in userStorage and only copied into JavaScript memory on initialization?

I'm trying to understand the rationale and security implications of this design. Is this a common pattern? Are there better practices for SPA token management?

Would be grateful for your help

1 like
2 replies
martinbean's avatar

@dmytro_shved Doing authorisation checks on the client is never secure. This is the entire reason Sanctum and its cookie-based authentication was created.

2 likes
vincent15000's avatar

@dmytro_shved Furthermore with Sanctum you don't have to store the token, it's done automatically via Laravel internal mechanisms.

1 like

Please or to participate in this conversation.