Skip to content

Instantly share code, notes, and snippets.

@jengel3
Last active April 13, 2023 12:06
Show Gist options
  • Star 13 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save jengel3/5cc40d23b2620683c1f2862de7f72b9a to your computer and use it in GitHub Desktop.
Save jengel3/5cc40d23b2620683c1f2862de7f72b9a to your computer and use it in GitHub Desktop.
Vue/Nuxt JWT Authentication Implementation
// 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 !== ''
},
}
// plugins/axios.js
// expose the store, axios client and redirect method from the Nuxt context
// https://nuxtjs.org/api/context/
export default function ({ store, app: { $axios }, redirect }) {
$axios.onRequest((config) => {
// check if the user is authenticated
if (store.state.auth.access_token) {
// set the Authorization header using the access token
config.headers.Authorization = 'Bearer ' + store.state.auth.access_token
}
return config
})
}
// plugins/axios.js
// expose the store, axios client and redirect method from the Nuxt context
// https://nuxtjs.org/api/context/
export default function ({ store, app: { $axios }, redirect }) {
const IGNORED_PATHS = ['/auth/login', '/auth/logout', '/auth/refresh']
$axios.onRequest((config) => {
// check if the user is authenticated
if (store.state.auth.access_token) {
// set the Authorization header using the access token
config.headers.Authorization = 'Bearer ' + store.state.auth.access_token
}
return config
})
$axios.onError((error) => {
return new Promise(async (resolve, reject) => {
// ignore certain paths (i.e. paths relating to authentication)
const isIgnored = IGNORED_PATHS.some(path => error.config.url.includes(path))
// get the status code from the response
const statusCode = error.response ? error.response.status : -1
// only handle authentication errors or errors involving the validity of the token
if ((statusCode === 401 || statusCode === 422) && !isIgnored) {
// API should return a reason for the error, represented here by the text_code property
// Example API response:
// {
// status: 'failed',
// text_code: 'TOKEN_EXPIRED',
// message: 'The JWT token is expired',
// status_code: 401
// }
// retrieve the text_code property from the response, or default to null
const { data: { text_code } = { text_code: null } } = error.response || {}
// get the refresh token from the state if it exists
const refreshToken = store.state.auth.refresh_token
// determine if the error is a result of an expired access token
// also ensure that the refresh token is present
if (text_code === 'TOKEN_EXPIRED' && refreshToken) {
// see below - consider the refresh process failed if this is a 2nd attempt at the request
if (error.config.hasOwnProperty('retryAttempts')) {
// immediately logout if already attempted refresh
await store.dispatch('auth/logout')
// redirect the user home
return redirect('/')
} else {
// merge a new retryAttempts property into the original request config to prevent infinite-loop if refresh fails
const config = { retryAttempts: 1, ...error.config }
try {
// attempt to refresh access token using refresh token
await store.dispatch('auth/refresh')
// re-run the initial request using the new request config after a successful refresh
// this response will be returned to the initial calling method
return resolve($axios(config))
} catch (e) {
// catch any error while refreshing the token
await store.dispatch('auth/logout')
// redirect the user home
return redirect('/')
}
}
} else if (text_code === 'TOKEN_INVALID') {
// catch any other JWT-related error (i.e. malformed token) and logout the user
await store.dispatch('auth/logout')
// redirect the user home
return redirect('/')
}
}
// ignore all other errors, let component or other error handlers handle them
return reject(error)
})
})
}
{
"status": "failed",
"text_code": "TOKEN_EXPIRED",
"message": "The JWT token is expired",
"status_code": 401
}
const email_address = 'me@example.com'
const password = 'abc123'
await $store.dispatch('auth/login', { email_address, password })
// store/index.js
// ....
export const actions = {
// https://nuxtjs.org/guide/vuex-store/#the-nuxtserverinit-action
// automatically refresh the access token on the initial request to the server, if possible
async nuxtServerInit ({ dispatch, commit, state }) {
const { access_token, refresh_token } = state.auth
if (access_token && refresh_token) {
try {
// refresh the access token
await dispatch('auth/refresh')
} catch (e) {
// catch any errors and automatically logout the user
await dispatch('auth/logout')
}
}
},
}
// ...
// 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)
}
const { data: { ... } } = await $axios.get('/api/my-account')
@MarvinKweyu
Copy link

Hi, @jengel3 , thank you for this 100%. Would you mind sharing on local-storage.js ? I have a problem accessing the headers, even after a copy paste. Strange.

Cannot read property 'headers' of undefined 

any advice?
Also tried :

req.query.cookie

Cannot read property 'query' of undefined

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment