Authentication in SPAs is often a hot topic, for those unsure of how to implement a full-featured authentication system – registration, login and access token refreshing via refresh tokens.
In this article, we will discuss how to implement an endpoint instead of implementing a JWT (Json web tokens) API. So we have the flexibility to write our own JWT API.
Refer to JWT Laravel at https://sal.vn/oMStwi .
Implementation
For the front end, we will use the following packages: vuex-persistedstate , js-cookie and @nuxtjs/axios . We need to allow them to store tokens and user information to be able to access both the server and the client side-by-side, so authentication can be done in both.
Install packages:
1 2 | npm install --save vuex-persistedstate js-cookie @nuxtjs/axios |
VueX State Persistence
To make authenticated API calls from the server and the browser (client), we need to make sure the tokens are accessible between the two points. Vuex-persistedstate simplifies this with js-cookie support that will persist tokens on tcookie.
After installing the packages, we need to configure vuex-persistedstate as a plugin.
plugins/local-storage.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | <span class="token keyword">import</span> createPersistedState <span class="token keyword">from</span> <span class="token string">'vuex-persistedstate'</span> <span class="token keyword">import</span> <span class="token operator">*</span> <span class="token keyword">as</span> Cookies <span class="token keyword">from</span> <span class="token string">'js-cookie'</span> <span class="token keyword">import</span> cookie <span class="token keyword">from</span> <span class="token string">'cookie'</span> <span class="token comment">// access the store, http request and environment from the Nuxt context</span> <span class="token comment">// https://nuxtjs.org/api/context/</span> <span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token punctuation">(</span> <span class="token parameter"><span class="token punctuation">{</span> store <span class="token punctuation">,</span> req <span class="token punctuation">,</span> isDev <span class="token punctuation">}</span></span> <span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token function">createPersistedState</span> <span class="token punctuation">(</span> <span class="token punctuation">{</span> key <span class="token operator">:</span> <span class="token string">'authentication-cookie'</span> <span class="token punctuation">,</span> <span class="token comment">// choose any name for your cookie</span> paths <span class="token operator">:</span> <span class="token punctuation">[</span> <span class="token comment">// persist the access_token and refresh_token values from the "auth" store module</span> <span class="token string">'auth.access_token'</span> <span class="token punctuation">,</span> <span class="token string">'auth.refresh_token'</span> <span class="token punctuation">,</span> <span class="token punctuation">]</span> <span class="token punctuation">,</span> storage <span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token comment">// if on the browser, parse the cookies using js-cookie otherwise parse from the raw http request</span> <span class="token function-variable function">getItem</span> <span class="token operator">:</span> <span class="token parameter">key</span> <span class="token operator">=></span> process <span class="token punctuation">.</span> client <span class="token operator">?</span> Cookies <span class="token punctuation">.</span> <span class="token function">getJSON</span> <span class="token punctuation">(</span> key <span class="token punctuation">)</span> <span class="token operator">:</span> cookie <span class="token punctuation">.</span> <span class="token function">parse</span> <span class="token punctuation">(</span> req <span class="token punctuation">.</span> headers <span class="token punctuation">.</span> cookie <span class="token operator">||</span> <span class="token string">''</span> <span class="token punctuation">)</span> <span class="token punctuation">[</span> key <span class="token punctuation">]</span> <span class="token punctuation">,</span> <span class="token comment">// js-cookie can handle setting both client-side and server-side cookies with one method</span> <span class="token comment">// use isDev to determine if the cookies is accessible via https only (i.e. localhost likely won't be using https)</span> <span class="token function-variable function">setItem</span> <span class="token operator">:</span> <span class="token punctuation">(</span> <span class="token parameter">key <span class="token punctuation">,</span> value</span> <span class="token punctuation">)</span> <span class="token operator">=></span> Cookies <span class="token punctuation">.</span> <span class="token function">set</span> <span class="token punctuation">(</span> key <span class="token punctuation">,</span> value <span class="token punctuation">,</span> <span class="token punctuation">{</span> expires <span class="token operator">:</span> <span class="token number">14</span> <span class="token punctuation">,</span> secure <span class="token operator">:</span> <span class="token operator">!</span> isDev <span class="token punctuation">}</span> <span class="token punctuation">)</span> <span class="token punctuation">,</span> <span class="token comment">// also allow js-cookie to handle removing cookies</span> <span class="token function-variable function">removeItem</span> <span class="token operator">:</span> <span class="token parameter">key</span> <span class="token operator">=></span> Cookies <span class="token punctuation">.</span> <span class="token function">remove</span> <span class="token punctuation">(</span> key <span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">)</span> <span class="token punctuation">(</span> store <span class="token punctuation">)</span> <span class="token punctuation">}</span> |
Next, add this plugin to nuxt.config.js:
1 2 3 4 | plugins <span class="token operator">:</span> <span class="token punctuation">[</span> <span class="token string">'~/plugins/local-storage'</span> <span class="token punctuation">,</span> <span class="token punctuation">]</span> <span class="token punctuation">,</span> |
VueX Store
We need to set up VueX store, which will store data about the user, access token and refresh token. These will include actions for calling the API to register, login, and refresh the user, as well as mutations to pass the returned data to the state.
store/auth.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | <span class="token comment">// reusable aliases for mutations</span> <span class="token keyword">export</span> <span class="token keyword">const</span> <span class="token constant">AUTH_MUTATIONS</span> <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token constant">SET_USER</span> <span class="token operator">:</span> <span class="token string">'SET_USER'</span> <span class="token punctuation">,</span> <span class="token constant">SET_PAYLOAD</span> <span class="token operator">:</span> <span class="token string">'SET_PAYLOAD'</span> <span class="token punctuation">,</span> <span class="token constant">LOGOUT</span> <span class="token operator">:</span> <span class="token string">'LOGOUT'</span> <span class="token punctuation">,</span> <span class="token punctuation">}</span> <span class="token keyword">export</span> <span class="token keyword">const</span> <span class="token function-variable function">state</span> <span class="token operator">=</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">(</span> <span class="token punctuation">{</span> access_token <span class="token operator">:</span> <span class="token keyword">null</span> <span class="token punctuation">,</span> <span class="token comment">// JWT access token</span> refresh_token <span class="token operator">:</span> <span class="token keyword">null</span> <span class="token punctuation">,</span> <span class="token comment">// JWT refresh token</span> id <span class="token operator">:</span> <span class="token keyword">null</span> <span class="token punctuation">,</span> <span class="token comment">// user id</span> email_address <span class="token operator">:</span> <span class="token keyword">null</span> <span class="token punctuation">,</span> <span class="token comment">// user email address</span> <span class="token punctuation">}</span> <span class="token punctuation">)</span> <span class="token keyword">export</span> <span class="token keyword">const</span> mutations <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token comment">// store the logged in user in the state</span> <span class="token punctuation">[</span> <span class="token constant">AUTH_MUTATIONS</span> <span class="token punctuation">.</span> <span class="token constant">SET_USER</span> <span class="token punctuation">]</span> <span class="token punctuation">(</span> <span class="token parameter">state <span class="token punctuation">,</span> <span class="token punctuation">{</span> id <span class="token punctuation">,</span> email_address <span class="token punctuation">}</span></span> <span class="token punctuation">)</span> <span class="token punctuation">{</span> state <span class="token punctuation">.</span> id <span class="token operator">=</span> id state <span class="token punctuation">.</span> email_address <span class="token operator">=</span> email_address <span class="token punctuation">}</span> <span class="token punctuation">,</span> <span class="token comment">// store new or updated token fields in the state</span> <span class="token punctuation">[</span> <span class="token constant">AUTH_MUTATIONS</span> <span class="token punctuation">.</span> <span class="token constant">SET_PAYLOAD</span> <span class="token punctuation">]</span> <span class="token punctuation">(</span> <span class="token parameter">state <span class="token punctuation">,</span> <span class="token punctuation">{</span> access_token <span class="token punctuation">,</span> refresh_token <span class="token operator">=</span> <span class="token keyword">null</span> <span class="token punctuation">}</span></span> <span class="token punctuation">)</span> <span class="token punctuation">{</span> state <span class="token punctuation">.</span> access_token <span class="token operator">=</span> access_token <span class="token comment">// refresh token is optional, only set it if present</span> <span class="token keyword">if</span> <span class="token punctuation">(</span> refresh_token <span class="token punctuation">)</span> <span class="token punctuation">{</span> state <span class="token punctuation">.</span> refresh_token <span class="token operator">=</span> refresh_token <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">,</span> <span class="token comment">// clear our the state, essentially logging out the user</span> <span class="token punctuation">[</span> <span class="token constant">AUTH_MUTATIONS</span> <span class="token punctuation">.</span> <span class="token constant">LOGOUT</span> <span class="token punctuation">]</span> <span class="token punctuation">(</span> <span class="token parameter">state</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span> state <span class="token punctuation">.</span> id <span class="token operator">=</span> <span class="token keyword">null</span> state <span class="token punctuation">.</span> email_address <span class="token operator">=</span> <span class="token keyword">null</span> state <span class="token punctuation">.</span> access_token <span class="token operator">=</span> <span class="token keyword">null</span> state <span class="token punctuation">.</span> refresh_token <span class="token operator">=</span> <span class="token keyword">null</span> <span class="token punctuation">}</span> <span class="token punctuation">,</span> <span class="token punctuation">}</span> <span class="token keyword">export</span> <span class="token keyword">const</span> actions <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token keyword">async</span> <span class="token function">login</span> <span class="token punctuation">(</span> <span class="token parameter"><span class="token punctuation">{</span> commit <span class="token punctuation">,</span> dispatch <span class="token punctuation">}</span> <span class="token punctuation">,</span> <span class="token punctuation">{</span> email_address <span class="token punctuation">,</span> password <span class="token punctuation">}</span></span> <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// make an API call to login the user with an email address and password</span> <span class="token keyword">const</span> <span class="token punctuation">{</span> data <span class="token operator">:</span> <span class="token punctuation">{</span> data <span class="token operator">:</span> <span class="token punctuation">{</span> user <span class="token punctuation">,</span> payload <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token keyword">this</span> <span class="token punctuation">.</span> $axios <span class="token punctuation">.</span> <span class="token function">post</span> <span class="token punctuation">(</span> <span class="token string">'/api/auth/login'</span> <span class="token punctuation">,</span> <span class="token punctuation">{</span> email_address <span class="token punctuation">,</span> password <span class="token punctuation">}</span> <span class="token punctuation">)</span> <span class="token comment">// commit the user and tokens to the state</span> <span class="token function">commit</span> <span class="token punctuation">(</span> <span class="token constant">AUTH_MUTATIONS</span> <span class="token punctuation">.</span> <span class="token constant">SET_USER</span> <span class="token punctuation">,</span> user <span class="token punctuation">)</span> <span class="token function">commit</span> <span class="token punctuation">(</span> <span class="token constant">AUTH_MUTATIONS</span> <span class="token punctuation">.</span> <span class="token constant">SET_PAYLOAD</span> <span class="token punctuation">,</span> payload <span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token punctuation">,</span> <span class="token keyword">async</span> <span class="token function">register</span> <span class="token punctuation">(</span> <span class="token parameter"><span class="token punctuation">{</span> commit <span class="token punctuation">}</span> <span class="token punctuation">,</span> <span class="token punctuation">{</span> email_addr <span class="token punctuation">,</span> password <span class="token punctuation">}</span></span> <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// make an API call to register the user</span> <span class="token keyword">const</span> <span class="token punctuation">{</span> data <span class="token operator">:</span> <span class="token punctuation">{</span> data <span class="token operator">:</span> <span class="token punctuation">{</span> user <span class="token punctuation">,</span> payload <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token keyword">this</span> <span class="token punctuation">.</span> $axios <span class="token punctuation">.</span> <span class="token function">post</span> <span class="token punctuation">(</span> <span class="token string">'/api/auth/register'</span> <span class="token punctuation">,</span> <span class="token punctuation">{</span> email_address <span class="token punctuation">,</span> password <span class="token punctuation">}</span> <span class="token punctuation">)</span> <span class="token comment">// commit the user and tokens to the state</span> <span class="token function">commit</span> <span class="token punctuation">(</span> <span class="token constant">AUTH_MUTATIONS</span> <span class="token punctuation">.</span> <span class="token constant">SET_USER</span> <span class="token punctuation">,</span> user <span class="token punctuation">)</span> <span class="token function">commit</span> <span class="token punctuation">(</span> <span class="token constant">AUTH_MUTATIONS</span> <span class="token punctuation">.</span> <span class="token constant">SET_PAYLOAD</span> <span class="token punctuation">,</span> payload <span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token punctuation">,</span> <span class="token comment">// given the current refresh token, refresh the user's access token to prevent expiry</span> <span class="token keyword">async</span> <span class="token function">refresh</span> <span class="token punctuation">(</span> <span class="token parameter"><span class="token punctuation">{</span> commit <span class="token punctuation">,</span> state <span class="token punctuation">}</span></span> <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">const</span> <span class="token punctuation">{</span> refresh_token <span class="token punctuation">}</span> <span class="token operator">=</span> state <span class="token comment">// make an API call using the refresh token to generate a new access token</span> <span class="token keyword">const</span> <span class="token punctuation">{</span> data <span class="token operator">:</span> <span class="token punctuation">{</span> data <span class="token operator">:</span> <span class="token punctuation">{</span> payload <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token keyword">this</span> <span class="token punctuation">.</span> $axios <span class="token punctuation">.</span> <span class="token function">post</span> <span class="token punctuation">(</span> <span class="token string">'/api/auth/refresh'</span> <span class="token punctuation">,</span> <span class="token punctuation">{</span> refresh_token <span class="token punctuation">}</span> <span class="token punctuation">)</span> <span class="token function">commit</span> <span class="token punctuation">(</span> <span class="token constant">AUTH_MUTATIONS</span> <span class="token punctuation">.</span> <span class="token constant">SET_PAYLOAD</span> <span class="token punctuation">,</span> payload <span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token punctuation">,</span> <span class="token comment">// logout the user</span> <span class="token function">logout</span> <span class="token punctuation">(</span> <span class="token parameter"><span class="token punctuation">{</span> commit <span class="token punctuation">,</span> state <span class="token punctuation">}</span></span> <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token function">commit</span> <span class="token punctuation">(</span> <span class="token constant">AUTH_MUTATIONS</span> <span class="token punctuation">.</span> <span class="token constant">LOGOUT</span> <span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token punctuation">,</span> <span class="token punctuation">}</span> <span class="token keyword">export</span> <span class="token keyword">const</span> getters <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token comment">// determine if the user is authenticated based on the presence of the access token</span> <span class="token function-variable function">isAuthenticated</span> <span class="token operator">:</span> <span class="token punctuation">(</span> <span class="token parameter">state</span> <span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token keyword">return</span> state <span class="token punctuation">.</span> access_token <span class="token operator">&&</span> state <span class="token punctuation">.</span> access_token <span class="token operator">!==</span> <span class="token string">''</span> <span class="token punctuation">}</span> <span class="token punctuation">,</span> <span class="token punctuation">}</span> |
Next, we need to create a Form Components for the login and registration page. We will cover this part in detail later. Basically, our form should call authentication module actions to login or register user information.
1 2 3 4 5 | <span class="token keyword">const</span> email_address <span class="token operator">=</span> <span class="token string">'me@example.com'</span> <span class="token keyword">const</span> password <span class="token operator">=</span> <span class="token string">'abc123'</span> <span class="token keyword">await</span> $store <span class="token punctuation">.</span> <span class="token function">dispatch</span> <span class="token punctuation">(</span> <span class="token string">'auth/login'</span> <span class="token punctuation">,</span> <span class="token punctuation">{</span> email_address <span class="token punctuation">,</span> password <span class="token punctuation">}</span> <span class="token punctuation">)</span> |
Authenticated API Requests
In this section, we will use the built-in Interceptors feature of Axios, which allows us to change requests and responses as well as handle all returned errors. @nuxtjs /axios gives the full set: https://axios.nuxtjs.org/extend/#adding-interceptors
We will use a request interceptor to attach an access token to each request.
plugins/axios.js
1 2 3 4 5 6 7 8 9 10 11 12 13 | <span class="token comment">// expose the store, axios client and redirect method from the Nuxt context</span> <span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">function</span> <span class="token punctuation">(</span> <span class="token parameter"><span class="token punctuation">{</span> store <span class="token punctuation">,</span> app <span class="token operator">:</span> <span class="token punctuation">{</span> $axios <span class="token punctuation">}</span> <span class="token punctuation">,</span> redirect <span class="token punctuation">}</span></span> <span class="token punctuation">)</span> <span class="token punctuation">{</span> $axios <span class="token punctuation">.</span> <span class="token function">onRequest</span> <span class="token punctuation">(</span> <span class="token punctuation">(</span> <span class="token parameter">config</span> <span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token comment">// check if the user is authenticated</span> <span class="token keyword">if</span> <span class="token punctuation">(</span> store <span class="token punctuation">.</span> state <span class="token punctuation">.</span> auth <span class="token punctuation">.</span> access_token <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// set the Authorization header using the access token</span> config <span class="token punctuation">.</span> headers <span class="token punctuation">.</span> Authorization <span class="token operator">=</span> <span class="token string">'Bearer '</span> <span class="token operator">+</span> store <span class="token punctuation">.</span> state <span class="token punctuation">.</span> auth <span class="token punctuation">.</span> access_token <span class="token punctuation">}</span> <span class="token keyword">return</span> config <span class="token punctuation">}</span> <span class="token punctuation">)</span> <span class="token punctuation">}</span> |
This plugin is quite simple, it will capture every request and if the user is authenticated, will add an Authorization header.
Add nuxt.config.js
:
1 2 3 4 5 | plugins: [ '~/plugins/local-storage', '~/plugins/axios', ], |
Refresh Tokens
For security reasons, an access token should not last too long and should be easily revoked if necessary. When the access token expires or is invalid but the application still needs protection, the application needs to generate a new access token that the user does not need to grant access again.
To solve this problem, we can modify the interceptor plugin, adding an error handler to automatically generate a new access token in case it expires.
In case the access token expires, the API will need to notify the client that the token is invalid and needs to be refreshed. Usually we will return with a status code of 401.
1 2 3 4 5 6 7 | <span class="token punctuation">{</span> <span class="token property">"status"</span> <span class="token operator">:</span> <span class="token string">"failed"</span> <span class="token punctuation">,</span> <span class="token property">"text_code"</span> <span class="token operator">:</span> <span class="token string">"TOKEN_EXPIRED"</span> <span class="token punctuation">,</span> <span class="token property">"message"</span> <span class="token operator">:</span> <span class="token string">"The JWT token is expired"</span> <span class="token punctuation">,</span> <span class="token property">"status_code"</span> <span class="token operator">:</span> <span class="token number">401</span> <span class="token punctuation">}</span> |
At this point, the client, aware of the token’s expiration, can move on to renew the token, before retrying the same request.
Usually the Refresh Token endpoint provides a refresh_token value via a POST request, so the API will need to generate a new access token to return to the client. If the new token is expired, revoked or invalid, it may return an error code so that the client side cannot re-validate and need to logout.
We will need to modify the interceptor plugin to catch the plugins/axios.js
error:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | <span class="token comment">// expose the store, axios client and redirect method from the Nuxt context</span> <span class="token comment">// https://nuxtjs.org/api/context/</span> <span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">function</span> <span class="token punctuation">(</span> <span class="token parameter"><span class="token punctuation">{</span> store <span class="token punctuation">,</span> app <span class="token operator">:</span> <span class="token punctuation">{</span> $axios <span class="token punctuation">}</span> <span class="token punctuation">,</span> redirect <span class="token punctuation">}</span></span> <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">const</span> <span class="token constant">IGNORED_PATHS</span> <span class="token operator">=</span> <span class="token punctuation">[</span> <span class="token string">'/auth/login'</span> <span class="token punctuation">,</span> <span class="token string">'/auth/logout'</span> <span class="token punctuation">,</span> <span class="token string">'/auth/refresh'</span> <span class="token punctuation">]</span> $axios <span class="token punctuation">.</span> <span class="token function">onRequest</span> <span class="token punctuation">(</span> <span class="token punctuation">(</span> <span class="token parameter">config</span> <span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token comment">// check if the user is authenticated</span> <span class="token keyword">if</span> <span class="token punctuation">(</span> store <span class="token punctuation">.</span> state <span class="token punctuation">.</span> auth <span class="token punctuation">.</span> access_token <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// set the Authorization header using the access token</span> config <span class="token punctuation">.</span> headers <span class="token punctuation">.</span> Authorization <span class="token operator">=</span> <span class="token string">'Bearer '</span> <span class="token operator">+</span> store <span class="token punctuation">.</span> state <span class="token punctuation">.</span> auth <span class="token punctuation">.</span> access_token <span class="token punctuation">}</span> <span class="token keyword">return</span> config <span class="token punctuation">}</span> <span class="token punctuation">)</span> $axios <span class="token punctuation">.</span> <span class="token function">onError</span> <span class="token punctuation">(</span> <span class="token punctuation">(</span> <span class="token parameter">error</span> <span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token keyword">new</span> <span class="token class-name">Promise</span> <span class="token punctuation">(</span> <span class="token keyword">async</span> <span class="token punctuation">(</span> <span class="token parameter">resolve <span class="token punctuation">,</span> reject</span> <span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <span class="token comment">// ignore certain paths (i.e. paths relating to authentication)</span> <span class="token keyword">const</span> isIgnored <span class="token operator">=</span> <span class="token constant">IGNORED_PATHS</span> <span class="token punctuation">.</span> <span class="token function">some</span> <span class="token punctuation">(</span> <span class="token parameter">path</span> <span class="token operator">=></span> error <span class="token punctuation">.</span> config <span class="token punctuation">.</span> url <span class="token punctuation">.</span> <span class="token function">includes</span> <span class="token punctuation">(</span> path <span class="token punctuation">)</span> <span class="token punctuation">)</span> <span class="token comment">// get the status code from the response</span> <span class="token keyword">const</span> statusCode <span class="token operator">=</span> error <span class="token punctuation">.</span> response <span class="token operator">?</span> error <span class="token punctuation">.</span> response <span class="token punctuation">.</span> status <span class="token operator">:</span> <span class="token operator">-</span> <span class="token number">1</span> <span class="token comment">// only handle authentication errors or errors involving the validity of the token</span> <span class="token keyword">if</span> <span class="token punctuation">(</span> <span class="token punctuation">(</span> statusCode <span class="token operator">===</span> <span class="token number">401</span> <span class="token operator">||</span> statusCode <span class="token operator">===</span> <span class="token number">422</span> <span class="token punctuation">)</span> <span class="token operator">&&</span> <span class="token operator">!</span> isIgnored <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// API should return a reason for the error, represented here by the text_code property</span> <span class="token comment">// Example API response: </span> <span class="token comment">// { </span> <span class="token comment">// status: 'failed', </span> <span class="token comment">// text_code: 'TOKEN_EXPIRED',</span> <span class="token comment">// message: 'The JWT token is expired',</span> <span class="token comment">// status_code: 401</span> <span class="token comment">// }</span> <span class="token comment">// retrieve the text_code property from the response, or default to null</span> <span class="token keyword">const</span> <span class="token punctuation">{</span> data <span class="token operator">:</span> <span class="token punctuation">{</span> text_code <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token punctuation">{</span> text_code <span class="token operator">:</span> <span class="token keyword">null</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token operator">=</span> error <span class="token punctuation">.</span> response <span class="token operator">||</span> <span class="token punctuation">{</span> <span class="token punctuation">}</span> <span class="token comment">// get the refresh token from the state if it exists</span> <span class="token keyword">const</span> refreshToken <span class="token operator">=</span> store <span class="token punctuation">.</span> state <span class="token punctuation">.</span> auth <span class="token punctuation">.</span> refresh_token <span class="token comment">// determine if the error is a result of an expired access token</span> <span class="token comment">// also ensure that the refresh token is present</span> <span class="token keyword">if</span> <span class="token punctuation">(</span> text_code <span class="token operator">===</span> <span class="token string">'TOKEN_EXPIRED'</span> <span class="token operator">&&</span> refreshToken <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// see below - consider the refresh process failed if this is a 2nd attempt at the request</span> <span class="token keyword">if</span> <span class="token punctuation">(</span> error <span class="token punctuation">.</span> config <span class="token punctuation">.</span> <span class="token function">hasOwnProperty</span> <span class="token punctuation">(</span> <span class="token string">'retryAttempts'</span> <span class="token punctuation">)</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// immediately logout if already attempted refresh</span> <span class="token keyword">await</span> store <span class="token punctuation">.</span> <span class="token function">dispatch</span> <span class="token punctuation">(</span> <span class="token string">'auth/logout'</span> <span class="token punctuation">)</span> <span class="token comment">// redirect the user home</span> <span class="token keyword">return</span> <span class="token function">redirect</span> <span class="token punctuation">(</span> <span class="token string">'/'</span> <span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span> <span class="token comment">// merge a new retryAttempts property into the original request config to prevent infinite-loop if refresh fails</span> <span class="token keyword">const</span> config <span class="token operator">=</span> <span class="token punctuation">{</span> retryAttempts <span class="token operator">:</span> <span class="token number">1</span> <span class="token punctuation">,</span> <span class="token operator">...</span> error <span class="token punctuation">.</span> config <span class="token punctuation">}</span> <span class="token keyword">try</span> <span class="token punctuation">{</span> <span class="token comment">// attempt to refresh access token using refresh token</span> <span class="token keyword">await</span> store <span class="token punctuation">.</span> <span class="token function">dispatch</span> <span class="token punctuation">(</span> <span class="token string">'auth/refresh'</span> <span class="token punctuation">)</span> <span class="token comment">// re-run the initial request using the new request config after a successful refresh</span> <span class="token comment">// this response will be returned to the initial calling method</span> <span class="token keyword">return</span> <span class="token function">resolve</span> <span class="token punctuation">(</span> <span class="token function">$axios</span> <span class="token punctuation">(</span> config <span class="token punctuation">)</span> <span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span> e <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// catch any error while refreshing the token</span> <span class="token keyword">await</span> store <span class="token punctuation">.</span> <span class="token function">dispatch</span> <span class="token punctuation">(</span> <span class="token string">'auth/logout'</span> <span class="token punctuation">)</span> <span class="token comment">// redirect the user home</span> <span class="token keyword">return</span> <span class="token function">redirect</span> <span class="token punctuation">(</span> <span class="token string">'/'</span> <span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token keyword">if</span> <span class="token punctuation">(</span> text_code <span class="token operator">===</span> <span class="token string">'TOKEN_INVALID'</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// catch any other JWT-related error (i.e. malformed token) and logout the user</span> <span class="token keyword">await</span> store <span class="token punctuation">.</span> <span class="token function">dispatch</span> <span class="token punctuation">(</span> <span class="token string">'auth/logout'</span> <span class="token punctuation">)</span> <span class="token comment">// redirect the user home</span> <span class="token keyword">return</span> <span class="token function">redirect</span> <span class="token punctuation">(</span> <span class="token string">'/'</span> <span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token comment">// ignore all other errors, let component or other error handlers handle them</span> <span class="token keyword">return</span> <span class="token function">reject</span> <span class="token punctuation">(</span> error <span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token punctuation">)</span> <span class="token punctuation">}</span> |
The plugin has been noted here very specifically, but basically the new interceptor checks if the error is related to an expired token and then tries to refresh the access token if it is.
Successful processing of the Promise will return a copy of the original request, leaving the calling function completely unaware that the token was refreshed before its response was received. However, if the refresh processing fails, the Interceptor will automatically logout and navigate to the home page.
1 2 | <span class="token keyword">const</span> <span class="token punctuation">{</span> data <span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token operator">...</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token keyword">await</span> $axios <span class="token punctuation">.</span> <span class="token function">get</span> <span class="token punctuation">(</span> <span class="token string">'/api/my-account'</span> <span class="token punctuation">)</span> |
Nuxt provides nuxtServerInit hook for SSR request to server. We can automatically refresh the access token when the user is logged in with the first connection to the server.
With SPA it won’t be necessary to refresh as often, so when it happens, we can offer a short-term token. To do this, add to the root store:
store/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <span class="token comment">// ....</span> <span class="token keyword">export</span> <span class="token keyword">const</span> actions <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token comment">// https://nuxtjs.org/guide/vuex-store/#the-nuxtserverinit-action</span> <span class="token comment">// automatically refresh the access token on the initial request to the server, if possible</span> <span class="token keyword">async</span> <span class="token function">nuxtServerInit</span> <span class="token punctuation">(</span> <span class="token parameter"><span class="token punctuation">{</span> dispatch <span class="token punctuation">,</span> commit <span class="token punctuation">,</span> state <span class="token punctuation">}</span></span> <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">const</span> <span class="token punctuation">{</span> access_token <span class="token punctuation">,</span> refresh_token <span class="token punctuation">}</span> <span class="token operator">=</span> state <span class="token punctuation">.</span> auth <span class="token keyword">if</span> <span class="token punctuation">(</span> access_token <span class="token operator">&&</span> refresh_token <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">try</span> <span class="token punctuation">{</span> <span class="token comment">// refresh the access token</span> <span class="token keyword">await</span> <span class="token function">dispatch</span> <span class="token punctuation">(</span> <span class="token string">'auth/refresh'</span> <span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span> e <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// catch any errors and automatically logout the user</span> <span class="token keyword">await</span> <span class="token function">dispatch</span> <span class="token punctuation">(</span> <span class="token string">'auth/logout'</span> <span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">,</span> <span class="token punctuation">}</span> <span class="token comment">// ...</span> |
Now, when the user navigates the app through a URL or an external link, we will automatically refresh their access token if they have previously logged in.
Conclusion
In this article, we will only briefly discuss the implementation of Authentication in SPAs but provide us with the necessary concepts to be able to implement a universal client and server-side JWT authentication in Nuxt. In the following article, we will build a specific application that includes more about Authentication in SPAs.