Authentication in SPAs is often a hot topic, and even more-so for those who aren’t sure of the best method for implementing an authentication system with all the necessary features and one that can handle the most common edge cases.

Although we won’t cover writing the backend JWT or authentication implementation, there are various articles that describe the best methods for various languages and frameworks, depending on your needs. Regardless of your choice, you’ll want to make sure to implement the three core features necessary for the frontend — registration, login and access token refreshing via refresh tokens. We’ll discuss a basic implementation of some of these endpoints, but they are still completely flexible. It’s also assumed that you have already setup a basic Nuxt application in universal rendering mode.

Getting Started

For the frontend, we’ll be making use of three essential packages — vuex-persistedstate, js-cookie and @nuxtjs/axios. The first allows us to persist Nuxt module state into a store of our choosing (cookies!), which will allow us to store tokens and user data accessible to both the Nuxt server, as well as the client, which in turn allows for authenticated calls from both ends. The second will make parsing cookies easier, while the last is a common Nuxt package, providing an all-inclusive package for HTTP calls.

Let’s start by installing those.

npm install --save vuex-persistedstate js-cookie @nuxtjs/axios

VueX State Persistence

To make authenticated API calls from both the server and browser (client), we need to ensure that the tokens are accessible from both ends. vuex-persistedstate simplifies this, and with the help of js-cookie will persist the tokens to a cookie.

After installing the packages, we’ll need to configure the vuex-persistedstate with a plugin.

// plugins/local-storage.js

import createPersistedState from 'vuex-persistedstate'
import * as Cookies from 'js-cookie'
import cookie from 'cookie'

// access the store, http request and environment from the Nuxt context
// https://nuxtjs.org/api/context/
export default ({ store, req, isDev }) => {
  createPersistedState({
    key: 'authentication-cookie', // choose any name for your cookie
    paths: [
      // persist the access_token and refresh_token values from the "auth" store module
      'auth.access_token',
      'auth.refresh_token',
    ],
    storage: {
      // if on the browser, parse the cookies using js-cookie otherwise parse from the raw http request
      getItem: key => process.client ? Cookies.getJSON(key) : cookie.parse(req.headers.cookie || '')[key],
      // js-cookie can handle setting both client-side and server-side cookies with one method
      // use isDev to determine if the cookies is accessible via https only (i.e. localhost likely won't be using https)
      setItem: (key, value) => Cookies.set(key, value, { expires: 14, secure: !isDev }),
      // also allow js-cookie to handle removing cookies
      removeItem: key => Cookies.remove(key)
    }
  })(store)
}

plugins/local-storage.js — configuring vuex-persistedstate

Don’t forget to add this plugin to your nuxt.config.js!

plugins: [
  '~/plugins/local-storage',
],

VueX Store

We’ll also need to setup the VueX store, which will be where we store data about the user, the access token and refresh token. We’ll also need to include actions for making API calls to register, login and refresh a user, as well as mutations to commit the returned data to the state.

Although the structure is easily modifiable, you should end up with something like this…

// store/auth.js

// reusable aliases for mutations
export const AUTH_MUTATIONS = {
  SET_USER: 'SET_USER',
  SET_PAYLOAD: 'SET_PAYLOAD',
  LOGOUT: 'LOGOUT',
}

export const state = () => ({
  access_token: null, // JWT access token
  refresh_token: null, // JWT refresh token
  id: null, // user id
  email_address: null, // user email address
})

export const mutations = {
  // store the logged in user in the state
  [AUTH_MUTATIONS.SET_USER] (state, { id, email_address }) {
    state.id = id
    state.email_address = email_address
  },

  // store new or updated token fields in the state
  [AUTH_MUTATIONS.SET_PAYLOAD] (state, { access_token, refresh_token = null }) {
    state.access_token = access_token

    // refresh token is optional, only set it if present
    if (refresh_token) {
      state.refresh_token = refresh_token
    }
  },

  // clear our the state, essentially logging out the user
  [AUTH_MUTATIONS.LOGOUT] (state) {
    state.id = null
    state.email_address = null
    state.access_token = null
    state.refresh_token = null
  },
}

export const actions = {
  async login ({ commit, dispatch }, { email_address, password }) {
    // make an API call to login the user with an email address and password
    const { data: { data: { user, payload } } } = await this.$axios.post(
      '/api/auth/login', 
      { email_address, password }
    )

    // commit the user and tokens to the state
    commit(AUTH_MUTATIONS.SET_USER, user)
    commit(AUTH_MUTATIONS.SET_PAYLOAD, payload)
  },

  async register ({ commit }, { email_addr, password }) {
    // make an API call to register the user
    const { data: { data: { user, payload } } } = await this.$axios.post(
      '/api/auth/register', 
      { email_address, password }
    )

    // commit the user and tokens to the state
    commit(AUTH_MUTATIONS.SET_USER, user)
    commit(AUTH_MUTATIONS.SET_PAYLOAD, payload)
  },

  // given the current refresh token, refresh the user's access token to prevent expiry
  async refresh ({ commit, state }) {
    const { refresh_token } = state

    // make an API call using the refresh token to generate a new access token
    const { data: { data: { payload } } } = await this.$axios.post(
      '/api/auth/refresh', 
      { refresh_token }
    )

    commit(AUTH_MUTATIONS.SET_PAYLOAD, payload)
  },

  // logout the user
  logout ({ commit, state }) {
    commit(AUTH_MUTATIONS.LOGOUT)
  },
}

export const getters = {
  // determine if the user is authenticated based on the presence of the access token
  isAuthenticated: (state) => {
    return state.access_token && state.access_token !== ''
  },
}

store/auth.js — the essential implementation of the auth store module

Now that we have a state in place, you’ll need to create form components for the login and registration pages, which also won’t be covered here. Essentially, your forms should call the authentication module actions to login or register the user.

const email_address = 'me@example.com'
const password = 'abc123'

await $store.dispatch('auth/login', { email_address, password })

A sample login API call

With the state and forms in place, and the ability to authenticate users, we can implement authenticated API requests!

#jwt #vue #nuxt #security #javascript

How to Implement JWT Authentication in Vue/Nuxt Applications
21.00 GEEK