This article is written for Nuxt 2.x and will not work with Nuxt 3.x.
In this article, I'm going to show you a way of handling JWT authentication tokens and refresh tokens within Nuxt. We will not use the official nuxt/auth package in this tutorial, since the current version, as of writing this article, is not very stable and not yet fully documented and tested.
First things first, we're going to need to install some dependencies for handling the tokens and cookies for storing the keys.
$ yarn add cookie-universal-nuxt @nuxtjs/axios vuex-persistedstate
The cookie-universal-nuxt and vuex-persistedstate packages are responsible for setting and updating the cookies we're using to store the access and refresh tokens from the API. @nuxtjs/axios is used to make API calls with the tokens and/or to retrieve the tokens.
To make authorized API-Calls we need to store the access and refresh tokens somewhere. We will make use of the Vuex Store inside Nuxt for this, and we will use the vuex-persistedstate package to persist the keys on reloads through cookies.
import createPersistedState from "vuex-persistedstate";
export default ({ isDev, store, app }) => {
createPersistedState({
key: "auth",
paths: ["auth.accessToken", "auth.refreshToken"],
storage: {
getItem: (key) => app.$cookies.get(key),
setItem: (key, value) =>
app.$cookies.set(key, value, {
path: "/",
expires: new Date(Date.now() + 14 * 864e5),
secure: !isDev,
}),
removeItem: (key) => app.$cookies.remove(key),
},
assertStorage() {
return !!app.$cookies;
},
})(store);
};
Here we're setting up the vuex-persistedstate plugin to use our cookie library for persisting the tokens in our application. Within paths
we're defining that only our tokens should be persisted within the cookie to not exceed the size limit of cookies. The cookie in our example is valid for 14 Days.
Don't forget to add the plugin to your nuxt.config.js
file:
plugins: ["./plugins/vuex-persistedstate"];
Now that we have configured our persisting plugin for the vuex store, we also need to write our mutations and actions to handle the token related tasks. For this we're going to create a file within the store
directory of nuxt called auth.js
.
export const state = () => ({
accessToken: null,
refreshToken: null,
});
export const getters = {
isAuthenticated(state) {
return !!state.accessToken;
},
};
export const mutations = {
setTokens(state, { accessToken, refreshToken = null }) {
state.accessToken = accessToken;
if (refreshToken) {
state.refreshToken = refreshToken;
}
},
setUser(state, user) {
state.user = user;
},
logout(state) {
state.accessToken = null;
state.refreshToken = null;
state.user = null;
},
};
export const actions = {
async login({ commit, dispatch }, { username, password }) {
const res = await this.$axios.$post("/auth/login", {
username,
password,
});
commit("setTokens", res);
await dispatch("getUser");
},
async register({ commit, dispatch }, { username, password }) {
const res = await this.$axios.$post("/auth/register", {
username,
password,
});
commit("setTokens", res);
await dispatch("getUser");
},
async getUser({ commit }) {
const res = await this.$axios.$get("/auth/user");
commit("setUser", res);
},
async refresh({ state, commit }) {
const res = await this.$axios.$post("/auth/refresh", {
refreshToken: state.refreshToken,
});
commit("setTokens", res);
},
};
In this file we defined a few actions for handling our token requests.
login
is responsible for retrieving both tokens from the API by providing the user credentials via a POST request.register
is doing more or less the same as login, except that a new user gets created with the given credentials.getUser
is the API call responsible for retrieving the user information after the tokens have been received.refresh
is refreshing our access token by providing the refresh token to the API. In response, we will receive a new access token.Now that we have our tokens stored and persisted, it's time to make authenticated requests to the API. Axios has a built-in feature called "interceptors" which we're going to use to authenticate our API Calls dynamically with the stored token.
export default function ({ store, app: { $axios }, redirect }) {
$axios.onRequest((config) => {
// check if the user is authenticated
if (store.state.auth.accessToken) {
// set the Authorization header using the access token
config.headers.Authorization = "Bearer " + store.state.auth.accessToken;
}
return config;
});
}
Now axios will check on each request if the user has an access token and will send the Authorization-header payload to that access token automatically.
Again, don't forget to add the axios plugin to your nuxt.config.js
:
plugins: ["./plugins/vuex-persistedstate", "./plugins/axios"];
Now we're all set-up and could end the tutorial here, but what happens if the token is expired or invalid?
When access tokens expire or become invalid but the application still needs to access a protected resource, the application faces the problem of getting a new access token without forcing the user to once again grant permission. To solve this problem, OAuth 2.0 introduced an artifact called a refresh token. A refresh token allows an application to obtain a new access token without prompting the user.
-- Understanding refresh tokens via Auth0
To handle this we can update our axios interceptor to handle the returned API Error and automatically try to refresh our access token, before trying the request again. For this we're modifying our axios.js
plugin file:
export default function ({ store, app: { $axios }, redirect }) {
$axios.onRequest((config) => {
// check if the user is authenticated
if (store.state.auth.accessToken) {
// set the Authorization header using the access token
config.headers.Authorization = "Bearer " + store.state.auth.accessToken;
}
return config;
});
$axios.onError(async (error) => {
const statusCode = error.response ? error.response.status : -1;
if (statusCode === 401 || statusCode === 422) {
const refreshToken = store.state.auth.refreshToken;
if (
error.response.data.errorCode === "JWT_TOKEN_EXPIRED" &&
refreshToken
) {
if (
Object.prototype.hasOwnProperty.call(error.config, "retryAttempts")
) {
store.commit("auth/logout");
return redirect("/anmelden");
}
const config = { retryAttempts: 1, ...error.config };
try {
await store.dispatch("auth/refresh");
return Promise.resolve($axios(config));
} catch (e) {
store.commit("auth/logout");
return redirect("/anmelden");
}
}
store.commit("auth/logout");
return redirect("/anmelden");
}
return Promise.reject(error);
});
}
Here we added the onError
handler to listen for token related error codes from the API, try to refresh the access token using our refresh token and retrying the failed request again. If the token couldn't be refreshed, or an error occurred we're login out the user from the application.
Since our access token shouldn't be valid for a long time, we can safely assume that if the user reloads the page or first connects to the server again, that our access token is expired. By using the nuxtServerInit hook in our store we can automatically try to refresh the access token. To achieve this, we're updating our store like this:
export const actions = {
async nuxtServerInit({ state, commit, dispatch }) {
const cookie = this.$cookies.get("auth");
if (cookie) {
commit("auth/setTokens", cookie.auth);
}
const { accessToken, refreshToken } = state.auth;
if (accessToken && refreshToken) {
try {
await dispatch("auth/refresh");
} catch (e) {
commit("auth/logout");
}
}
},
};
With this updated action our server will now attempt to refresh the access token on every server request automatically.
Now we have a fully working and mostly automated solution for handling JWT in Nuxt. Our tokens are persisted in a cookie and are available for client- and serverside requests.