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.
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
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',
],
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