Cookie Based Authentication Using Flask and Vue.js: Part 2
This is part 2 of how to integrate cookie based authentication into your Flask/Vue application.
- Part 1: Flask
- Part 2: Vue.js (you are here)
- Part 3: Connecting your applications in a development environment
This series is from the perspective of running 2 applications, one for your Vue frontend and the other your Flask backend.
Disclaimer: This is not the definitive way to do this nor is it the best way. But it is 1 way of doing it so enjoy.
Assumptions and setup
I’ve created my Vue application using the vue-cli tool. I find it easier to work with when getting a project up and running quickly. The structure will be written from this angle.
To simplify the structure of this article, I will assume you have a basic understanding of how Vue works.
Getting Started
If you are using vue-cli to create your project, it should have a project structure similar to this:
Vue-cli also installs vue-router and vuex, two great libraries I’ll be using in this article.
The last library you will want to install is axios, a great library for working with HTTP requests. This is optional as you can simply use the JavaScript native HTTP API, Fetch, but my personal preference is to use axios.
$ npm install axios
Setting up axios instance
If you’re coming from part 1, you will know I’ve created a flask API that I’m using to authenticate users. The first thing I will want to do is create an axios instance that has parameters configured to my authentication API. This essentially allows me to create a single object that can only communicate with my authentication endpoints, is decoupled from my Vue instance and I can import into any file. I can even call it something like “authService”. Neat.
I create a .env
file in the root of my project, where I can store the route to my authentication endpoint.
# .envVUE_APP_AUTH_URL=https://api.myapplication.com/auth // Your URL here
The VUE_APP_*
prefix is necessary, as is explained here.
Now for the fun part. In my src/
folder I create a file for my axios instance.
# src/api.jsimport axios from 'axios';
const authService = axios.create({
baseURL: process.env.VUE_APP_AUTH_URL,
withCredentials: true,
xsrfCookieName: 'csrf_access_token'
});export { authService };
I’ve created the instance with a couple parameters configured for working with cookies as well as interacting with my authentication API. For an explanation of the parameters:
- baseURL: This is prepended to any URLs provided to the instance
- withCredentials: This determines whether cross site requests are made with credentials (see: cookies) or not. This is only necessary with cross site or cross origin requests, but since I am working with 2 separate applications (Flask + Vue) running on separate ports on my development machine, its necessary.
- xsrfCookieName: The name of the cookie used for storing our csrf token. This is configured in our flask application. The default is shown above but if you change it in your flask application config file, change it here too.
We will look at sending requests and managing responses but as it is now the axios instance should be able to communicate with our flask application as well as handle any cookies it receives.
Vuex Module
I decided to use vuex to manage the state of a user and actions to handle our authentication requests. By namespacing this state and actions as a vuex module, this allows for a single file where all our main authentication interactions with our backend take place. I create a directory for my vuex store and a sub-directory for the vuex modules, removing the store.js
file that came with our default vue-cli installation.
Project Structure
src
|---assets
|---components
|---store
| |---modules
| | |---auth.js
| |---index.js
|---views
|---App.vue
|---api.js
|---main.js
|---router.js### src/store/index.jsimport Vue from 'vue';
import Vuex from 'vuex';
import auth from './modules/auth';
Vue.use(Vuex);
export default new Vuex.Store({
modules: {
auth
}
});
### src/store/modules/auth.jsimport { authService } from '@/api'
const namespaced = true;
const state = {
user: {},
isLoggedIn: false
};
const getters = {
isLoggedIn: state => state.isLoggedIn,
user: state => state.user
};
const actions = {
async registerUser({ dispatch }, user) {
await authService.post('/register', user)
await dispatch('fetchUser')
},
async loginUser({ dispatch }, user) {
await authService.post('/login', user)
await dispatch('fetchUser')
},
async fetchUser({ commit }) {
await authService.get('/user')
.then(({ data }) => commit('setUser', data))
},
async logoutUser({ commit }) {
await authService.post('/logout');
commit('logoutUserState');
}
};
const mutations = {
setUser(state, user) {
state.isLoggedIn = true;
state.user = user;
},
logoutUserState(state) {
state.isLoggedIn = false;
state.user = {};
}
};
export default {
namespaced,
state,
getters,
actions,
mutations
};
Whew. The usefulness of the axios instance becomes really apparent here. Instead of manually typing our authentication URL over and over again or specifying cookie specific parameters for our axios instance, we set it once and its done. We now simply specify the route we are hitting and enjoy our clean code.
Error Handling
You might be thinking: “But what about errors? What if the cookies have expired?”. This is where another cool feature of the axios instance come into play.
Response Interceptors
Due to axios being a JavaScript promise based library, for the asynchronous use case, we can intercept requests or responses before they are handled by a then
or catch
. This is very useful for our use case where we want to intercept a 401 Unauthorized and utilize the refresh token flow we setup in the last article or return the error.
To do this we add the following interceptor to our src/api.js
file:
const COOKIE_EXPIRED_MSG = 'Token has expired'authService.interceptors.response.use((response) => {
return response
}, async (error) => {
const error_message = error.response.data.msg
switch (error.response.status) {
case 401:
if (!error.config.retry && error_message === COOKIE_EXPIRED_MSG) {
error.config.retry = true
authService.defaults.xsrfCookieName = 'csrf_refresh_token';
await authService.post('/refresh_token')
authService.defaults.xsrfCookieName = 'csrf_access_token';
return authService(error.config)
}
break;
case 404:
router.push('/404');
break;
default:
break;
}
return error.response;
});
Here is a breakdown of what is going on in this snippet:
- I intercept an error response and store the contents of the error message as a constant. Here I am looking at ‘error.response.data.msg’ for my error message. This is custom and specific to how I return errors from my flask application. You may use ‘error.response.data.message’ or ‘error.response.data.error’ depending on what you have used. Don’t blindly copy and paste!
- I use a switch statement to make the conditional logic for the different status codes more obvious.
- I check if this error request is a retry from a previous interception and if our error message is the same as our cookie expiration message. This is hard-coded and I’ve used the default flask-jwt-extended error message as seen in the last article of this series. if you’ve changed this value in your flask configuration file, change it here too.
- If the conditional is true meaning this is not a retry request and the error message refers to an expired cookie, I change the retry parameter of our request config to true. I then change the xsrfCookieName parameter we set when the instance is created to refer to our refresh token instead.
- I send a POST request to my refresh_token endpoint with the updated authService instance.
- I then revert the xsrfCookieName change and return the error requests config, updated with our retry parameter.
By doing it like this, we have our refresh tokens working and intercept requests so that they are automatically refreshed once our backend notices it has expired. There are more efficient ways of doing this, such as intercepting the cookie expiration in our flask application and returning a new cookie along with our response, all in one request. I’ll leave it to you to figure out how to integrate this. Here’s a hint.
Finishing Up
We have all our pieces in place in the forms of the authService instance and our vuex store. Now we need to tie it all together in a view.
### src/views/login.vue<template>
<form @submit="login">
<input v-model="username" placeholder="Username">
<input v-model="password" type="password">
<input type="submit" value="Submit">
</form>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
export default {
data: () => ({
user: {
username: null,
password: null
}
}),
computed: {
...mapGetters({
authUser: 'auth/user'
})
},
methods: {
...mapActions({
loginUser: 'auth/loginUser'
}),
async login() {
await this.loginUser(this.user)
.then(() => {
if (this.authUser.authenticated) {
this.$router.push('/secure')
} else {
// Handle error
this.user = {
username: null,
password: null
}
}
})
}
}
}
</script>
Conclusion
So now we have a backend that can handle authentication via cookies and a frontend that integrates with it. With this functionality in place, the series is pretty much done. The last part will be on integrating the two applications together on a development machine. Working with 2 applications in 2 languages is enough fun as it is but throw in CORS errors and browsers considering ports different origins for a whole new meaning of fun. Next time on drag…