Last active
April 13, 2023 12:06
-
-
Save jengel3/5cc40d23b2620683c1f2862de7f72b9a to your computer and use it in GitHub Desktop.
Vue/Nuxt JWT Authentication Implementation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 !== '' | |
}, | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 | |
}) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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) | |
}) | |
}) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"status": "failed", | |
"text_code": "TOKEN_EXPIRED", | |
"message": "The JWT token is expired", | |
"status_code": 401 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const email_address = 'me@example.com' | |
const password = 'abc123' | |
await $store.dispatch('auth/login', { email_address, password }) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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') | |
} | |
} | |
}, | |
} | |
// ... |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const { data: { ... } } = await $axios.get('/api/my-account') |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.any advice?
Also tried :
Cannot read property 'query' of undefined