JWT Authentication
Welcome to the sixth installment to this multi-part tutorial series on full-stack web development using Vue.js and Flask. In this post I will be demonstrating a way to use JSON Web Token (JWT) authentication.
The code for this post can be found on my GitHub account under the branch SixthPost.
Series Content
- Seup and Getting to Know VueJS
- Navigating Vue Router
- State Management with Vuex
- RESTful API with Flask
- AJAX Integration with REST API
- JWT Authentication (you are here)
- Deployment to a Virtual Private Server
Basic Introduction to JWT Authentication
Similar to some of the other posts in this series I will not be going into significant details on the theory of how JWT works. Instead I will be taking a pragmatic approach and demonstrating its implementation specifics using the technologies of interest within Flask and Vue.js. If you are interested in gaining a deeper understanding of JWTs I refer you to Scott Robinson's excellent post here on StackAbuse, where he explains the low-level details of the technique.
In the basic sense a JWT is an encoded JSON object used to convey information between two systems which is composed of a header, a payload, and a signature in the form of [HEADER].[PAYLOAD].[SIGNATURE]
all contained in the HTTP header as "Authorization: Bearer [HEADER].[PAYLOAD].[SIGNATURE]". The process starts with the client (requesting system) authenticating with the server (a service with a desired resource) which generates a JWT that is only valid for a specific amount of time. The server then returns this as a signed and encoded token for the client to store and use for verification in later communications.
JWT authentication works quite well for SPA applications like the one being built out in this series and have gained significant popularity among developers implementing them.
Implementing JWT Authentication in the Flask RESTful API
On the Flask side of things I will be using the Python package PyJWT to handle some of the particulars around creating, parsing, and validating JWTs.
(venv) $ pip install PyJWT
With the PyJWT package installed I can move on to implementing the pieces necessary for authentication and verification in the Flask application. To start, I will give the application the ability to create new registered users which will be represented by a User
class. As with all the other classes in this application the User
class will reside in the models.py
module.
First item to do is to import a couple of functions, generate_password_hash
and check_password_hash
from the werkzeug package's security
module which I will use to generate and verify hashed passwords. There is no need to install this package as it comes with Flask automatically.
"""
models.py
- Data classes for the surveyapi application
"""
from datetime import datetime
from flask_sqlalchemy import SQLAlchemy
from werkzeug.security import generate_password_hash, check_password_hash
db = SQLAlchemy()
Directly below the above code I define the User
class, which inherits from the SQLAlchemy Model
class similar to the others defined in earlier posts. This User
class needs to contain an auto generated integer primary key class field called id
then two string fields called email
and password
with the email configured to be unique. I also give this class a relationship
field to associate any surveys the user may create. On the other side of this equation I added a creator_id
foreign key to the Survey
class to link users to surveys they create.
I override the __init__(...)
method so that I can hash the password upon instantiating a new User
object. After that I give it the class method, authenticate
, to query a user by email and check that the supplied password hash matches the one stored in the database. If they match I return the authenticated user. Last but not least I tacked on a to_dict()
method to help with serializing user objects.
"""
models.py
- Data classes for the surveyapi application
"""
#
# omitting imports and what not
#
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(120), unique=True, nullable=False)
password = db.Column(db.String(255), nullable=False)
surveys = db.relationship('Survey', backref="creator", lazy=False)
def __init__(self, email, password):
self.email = email
self.password = generate_password_hash(password, method='sha256')
@classmethod
def authenticate(cls, **kwargs):
email = kwargs.get('email')
password = kwargs.get('password')
if not email or not password:
return None
user = cls.query.filter_by(email=email).first()
if not user or not check_password_hash(user.password, password):
return None
return user
def to_dict(self):
return dict(id=self.id, email=self.email)
class Survey(db.Model):
__tablename__ = 'surveys'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.Text)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
questions = db.relationship('Question', backref="survey", lazy=False)
creator_id = db.Column(db.Integer, db.ForeignKey('users.id'))
def to_dict(self):
return dict(id=self.id,
name=self.name,
created_at=self.created_at.strftime('%Y-%m-%d %H:%M:%S'),
questions=[question.to_dict() for question in self.questions])
Next up is to generate a new migration and update the database with it to pair the User
Python class with a users SQLite database table. To do this I run the following commands in the same directory as my manage.py
module.
(venv) $ python manage.py db migrate
(venv) $ python manage.py db upgrade
Ok, time to hop over to the api.py
module and implement the functionality to register and authenticate users along with verification functionality to protect the creation of new surveys. After all, I don't want any nefarious web bots or other bad actors polluting my awesome survey app.
To start I add the User
class to the list of imports from the models.py
module towards the top of the api.py
module. While I'm in there I will go ahead and add a couple of other imports I will be using later.
"""
api.py
- provides the API endpoints for consuming and producing
REST requests and responses
"""
from functools import wraps
from datetime import datetime, timedelta
from flask import Blueprint, jsonify, request, current_app
import jwt
from .models import db, Survey, Question, Choice, User
Now that I have all the tools I need imported I can implement a set of register and login view functions in the api.py
module.
I will begin with the register()
view function which expects an email and password to be sent along in JSON in the body of the POST request. The user is simply created with whatever is given for the email and password and I merrily return a JSON response (which isn't necessarily the best approach, but it will work for the moment).
"""
api.py
- provides the API endpoints for consuming and producing
REST requests and responses
"""
#
# omitting inputs and other view functions
#
@api.route('/register/', methods=('POST',))
def register():
data = request.get_json()
user = User(**data)
db.session.add(user)
db.session.commit()
return jsonify(user.to_dict()), 201
Cool. The backend is able to create new users eager to create gobs of surveys so, I better add some functionality to authenticate them and let them get on with creating their surveys.
The login function uses the User.authenticate(...)
class method to try to find and authenticate a user. If the user matching the given email and password is found then the login function progresses to create a JWT token, otherwise None
is returned, resulting in the login function to return a "failure to authenticate" message with the appropriate HTTP status code of 401.
I create the JWT token using PyJWT (as jwt
) by encoding a dictionary containing the following:
- sub - the subject of the
jwt
, which in this case is the user's email - iat - the time the
jwt
was issued at - exp - is the moment the
jwt
should expire, which is 30 minutes after issuing in this case
"""
api.py
- provides the API endpoints for consuming and producing
REST requests and responses
"""
#
# omitting inputs and other view functions
#
@api.route('/login/', methods=('POST',))
def login():
data = request.get_json()
user = User.authenticate(**data)
if not user:
return jsonify({ 'message': 'Invalid credentials', 'authenticated': False }), 401
token = jwt.encode({
'sub': user.email,
'iat':datetime.utcnow(),
'exp': datetime.utcnow() + timedelta(minutes=30)},
current_app.config['SECRET_KEY'])
return jsonify({ 'token': token.decode('UTF-8') })
The encoding process utilizes the value of the BaseConfig
class's SECRET_KEY
property defined in config.py
and held in the current_app
's config property once the Flask app is created.
Next up I would like to break up the GET and POST functionality that currently resides in a poorly named view function called fetch_survey(...)
shown below in its original state. Instead, I will let fetch_surveys(...)
be solely in charge of fetching all surveys when requesting "/api/surveys/" with a GET request. Survey creation, on the other hand, which happens when the same URL is hit with a POST request, will now reside in a new function called create_survey(...)
.
So this...
"""
api.py
- provides the API endpoints for consuming and producing
REST requests and responses
"""
#
# omitting inputs and other view functions
#
@api.route('/surveys/', methods=('GET', 'POST'))
def fetch_surveys():
if request.method == 'GET':
surveys = Survey.query.all()
return jsonify([s.to_dict() for s in surveys])
elif request.method == 'POST':
data = request.get_json()
survey = Survey(name=data['name'])
questions = []
for q in data['questions']:
question = Question(text=q['question'])
question.choices = [Choice(text=c) for c in q['choices']]
questions.append(question)
survey.questions = questions
db.session.add(survey)
db.session.commit()
return jsonify(survey.to_dict()), 201
becomes this...
"""
api.py
- provides the API endpoints for consuming and producing
REST requests and responses
"""
#
# omitting inputs and other view functions
#
@api.route('/surveys/', methods=('POST',))
def create_survey(current_user):
data = request.get_json()
survey = Survey(name=data['name'])
questions = []
for q in data['questions']:
question = Question(text=q['question'])
question.choices = [Choice(text=c) for c in q['choices']]
questions.append(question)
survey.questions = questions
survey.creator = current_user
db.session.add(survey)
db.session.commit()
return jsonify(survey.to_dict()), 201
@api.route('/surveys/', methods=('GET',))
def fetch_surveys():
surveys = Survey.query.all()
return jsonify([s.to_dict() for s in surveys])
The real key now is to protect the create_survey(...)
view function so that only authenticated users can create new surveys. Said another way, if a POST request is made against "/api/surveys" the application should check to make sure that it is being done by a valid and authenticated user.
In comes the handy Python decorator! I will use a decorator to wrap the create_survey(...)
view function which will check that the requester contains a valid JWT token in its header and turn away any requests that do not. I will call this decorator token_required
and implement it above all the other view functions in api.py
like so:
"""
api.py
- provides the API endpoints for consuming and producing
REST requests and responses
"""
#
# omitting inputs and other view functions
#
def token_required(f):
@wraps(f)
def _verify(*args, **kwargs):
auth_headers = request.headers.get('Authorization', '').split()
invalid_msg = {
'message': 'Invalid token. Registeration and / or authentication required',
'authenticated': False
}
expired_msg = {
'message': 'Expired token. Reauthentication required.',
'authenticated': False
}
if len(auth_headers) != 2:
return jsonify(invalid_msg), 401
try:
token = auth_headers[1]
data = jwt.decode(token, current_app.config['SECRET_KEY'])
user = User.query.filter_by(email=data['sub']).first()
if not user:
raise RuntimeError('User not found')
return f(user, *args, **kwargs)
except jwt.ExpiredSignatureError:
return jsonify(expired_msg), 401 # 401 is Unauthorized HTTP status code
except (jwt.InvalidTokenError, Exception) as e:
print(e)
return jsonify(invalid_msg), 401
return _verify
The primary logic of this decorator is to:
- Ensure it contains the "Authorization" header with a string that looks like a JWT token
- Validate that the JWT is not expired, which PyJWT takes care of for me by throwing a
ExpiredSignatureError
if it is no longer valid - Validate that the JWT is a valid token, which PyJWT also takes care of by throwing a
InvalidTokenError
if it is not valid - If all is valid then the associated user is queried from the database and returned to the function the decorator is wrapping
Now all that remains is to add the decorator to the create_survey(...)
method like so:
"""
api.py
- provides the API endpoints for consuming and producing
REST requests and responses
"""
#
# omitting inputs and other functions
#
@api.route('/surveys/', methods=('POST',))
@token_required
def create_survey(current_user):
data = request.get_json()
survey = Survey(name=data['name'])
questions = []
for q in data['questions']:
question = Question(text=q['question'])
question.choices = [Choice(text=c) for c in q['choices']]
questions.append(question)
survey.questions = questions
survey.creator = current_user
db.session.add(survey)
db.session.commit()
return jsonify(survey.to_dict()), 201
Implementing JWT Authentication in Vue.js SPA
With the back-end side of the authentication equation complete I now need to button up the client side by implementing JWT authentication in Vue.js. I start by creating a new module within the app called "utils'' within the src
directory and placing an index.js
file inside of the utils folder. This module will contain two things:
Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!
- An event bus which I can use to send messages around the application when certain things happen, like failed authentication in the event of a expired JWT
- A function to check a JWT to see if it is still valid or not
These two things are implemented like so:
// utils/index.js
import Vue from 'vue'
export const EventBus = new Vue()
export function isValidJwt (jwt) {
if (!jwt || jwt.split('.').length < 3) {
return false
}
const data = JSON.parse(atob(jwt.split('.')[1]))
const exp = new Date(data.exp * 1000) // JS deals with dates in milliseconds since epoch
const now = new Date()
return now < exp
}
The EventBus
variable is just an instance of the Vue object. I can utilize the fact that the Vue object has both an $emit
and a pair of $on
/ $off
methods, which are used to emit events as well as register and unregister to events.
The isValid(jwt)
function is what I will use to determine if a user is authenticated based on the information in the JWT. Recall from the earlier basic explanation of JWTs that a standard set of properties reside in an encoded JSON object of the form "[HEADER].[PAYLOAD].[SIGNATURE]". For example, say I have the following JWT:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJleGFtcGxlQG1haWwuY29tIiwiaWF0IjoxNTIyMzI2NzMyLCJleHAiOjE1MjIzMjg1MzJ9.1n9fx0vL9GumDGatwm2vfUqQl3yZ7Kl4t5NWMvW-pgw
I can decode the middle body section to inspect its contents using the following JavaScript:
const token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJleGFtcGxlQG1haWwuY29tIiwiaWF0IjoxNTIyMzI2NzMyLCJleHAiOjE1MjIzMjg1MzJ9.1n9fx0vL9GumDGatwm2vfUqQl3yZ7Kl4t5NWMvW-pgw'
const tokenParts = token.split('.')
const body = JSON.parse(atob(tokenParts[1]))
console.log(body) // {sub: "[email protected]", iat: 1522326732, exp: 1522328532}
Here the contents of the token body are sub
, representing the email of the subscriber, iat
, which is issued at timestamp in seconds, and exp
, which is the time in which the token will expire as seconds from epoch (the number of seconds that have elapsed since January 1, 1970 (midnight UTC/GMT), not counting leap seconds (in ISO 8601: 1970-01-01T00:00:00Z)). As you can see I am using the exp
value in the isValidJwt(jwt)
function to determine if the JWT is expired or not.
Next up is to add a couple of new AJAX functions to make calls to the Flask REST API to register new users and login existing ones, plus I will need to modify the postNewSurvey(...)
function to include a header containing a JWT.
// api/index.js
//
// omitting stuff ... skipping to the bottom of the file
//
export function postNewSurvey (survey, jwt) {
return axios.post(`${API_URL}/surveys/`, survey, { headers: { Authorization: `Bearer: ${jwt}` } })
}
export function authenticate (userData) {
return axios.post(`${API_URL}/login/`, userData)
}
export function register (userData) {
return axios.post(`${API_URL}/register/`, userData)
}
Ok, now I can put these things to use in the store to manage the state required to provide proper authentication functionality. To begin I import EventBus
and the isValidJwt(...)
function from the utils module as well as the two new AJAX functions from the api module. Then add a definition of a user
object and a jwt
token string in the store's state object like so:
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
// imports of AJAX functions will go here
import { fetchSurveys, fetchSurvey, saveSurveyResponse, postNewSurvey, authenticate, register } from '@/api'
import { isValidJwt, EventBus } from '@/utils'
Vue.use(Vuex)
const state = {
// single source of data
surveys: [],
currentSurvey: {},
user: {},
jwt: ''
}
//
// omitting all the other stuff below
//
Next, I need to add in a couple of action methods which will call either the register(...)
or authenticate(...)
AJAX functions we just defined. I name the one responsible for authenticating a user login(...)
, which calls the authenticate(...)
AJAX function and when it returns a successful response containing a new JWT, it commits a mutation I will name setJwtToken
, which needs to be added to the mutations object. In the event of an unsuccessful authentication request I chain a catch
method to the promise chain to catch the error and use the EventBus
to emit an event notifying any subscribers that authentication failed.
The register(...)
action method is quite similar to login(...)
, in fact, it actually utilizes login(...)
. I am also showing a small modification to the submitNewSurvey(...)
action method that passes the JWT token as an additional parameter to the postNewSurvey(...)
AJAX call.
const actions = {
// asynchronous operations
//
// omitting the other action methods...
//
login (context, userData) {
context.commit('setUserData', { userData })
return authenticate(userData)
.then(response => context.commit('setJwtToken', { jwt: response.data }))
.catch(error => {
console.log('Error Authenticating: ', error)
EventBus.$emit('failedAuthentication', error)
})
},
register (context, userData) {
context.commit('setUserData', { userData })
return register(userData)
.then(context.dispatch('login', userData))
.catch(error => {
console.log('Error Registering: ', error)
EventBus.$emit('failedRegistering: ', error)
})
},
submitNewSurvey (context, survey) {
return postNewSurvey(survey, context.state.jwt.token)
}
}
As mentioned previously, I need to add a new mutation that explicitly sets the JWT and the user data.
const mutations = {
// isolated data mutations
//
// omitting the other mutation methods...
//
setUserData (state, payload) {
console.log('setUserData payload = ', payload)
state.userData = payload.userData
},
setJwtToken (state, payload) {
console.log('setJwtToken payload = ', payload)
localStorage.token = payload.jwt.token
state.jwt = payload.jwt
}
}
The last thing that I would like to do in the store is to add a getter method that will be called in a couple of other places in the app which will indicate whether the current user is authenticated or not. I accomplish this by calling the isValidJwt(jwt)
function from the utils module within the getter like so:
const getters = {
// reusable data accessors
isAuthenticated (state) {
return isValidJwt(state.jwt.token)
}
}
Ok, I am getting close. I need to add a new Vue.js component for a login / register page in the application. I create a file called Login.vue
in the components directory. In the template section I give it two input fields, one for an email, which will serve as the username, and another for the password. Below them are two buttons, one for logging in if you are already a registered user and another for registering.
<!-- components/Login.vue -->
<template>
<div>
<section class="hero is-primary">
<div class="hero-body">
<div class="container has-text-centered">
<h2 class="title">Login or Register</h2>
<p class="subtitle error-msg">{{ errorMsg }}</p>
</div>
</div>
</section>
<section class="section">
<div class="container">
<div class="field">
<label class="label is-large" for="email">Email:</label>
<div class="control">
<input type="email" class="input is-large" id="email" v-model="email">
</div>
</div>
<div class="field">
<label class="label is-large" for="password">Password:</label>
<div class="control">
<input type="password" class="input is-large" id="password" v-model="password">
</div>
</div>
<div class="control">
<a class="button is-large is-primary" @click="authenticate">Login</a>
<a class="button is-large is-success" @click="register">Register</a>
</div>
</div>
</section>
</div>
</template>
Obviously this component will need some local state associated with a user as indicated by my use of v-model
in the input fields, so I add that in the component's data property next. I also add an errorMsg
data property which will hold any messages emitted by the EventBus
in the event of failed registration or authentication. To utilize the EventBus
I subscribe to the failedRegistering
and failedAuthentication
events in the mounted
Vue.js component lifecycle stage, and unregister them in the beforeDestroy
stage. Another thing to note is the usage of @click
event handlers being called upon clicking the Login and Register buttons. Those are to be implemented as component methods, authenticate()
and register()
.
<!-- components/Login.vue -->
<script>
export default {
data () {
return {
email: '',
password: '',
errorMsg: ''
}
},
methods: {
authenticate () {
this.$store.dispatch('login', { email: this.email, password: this.password })
.then(() => this.$router.push('/'))
},
register () {
this.$store.dispatch('register', { email: this.email, password: this.password })
.then(() => this.$router.push('/'))
}
},
mounted () {
EventBus.$on('failedRegistering', (msg) => {
this.errorMsg = msg
})
EventBus.$on('failedAuthentication', (msg) => {
this.errorMsg = msg
})
},
beforeDestroy () {
EventBus.$off('failedRegistering')
EventBus.$off('failedAuthentication')
}
}
</script>
Ok, now I just need to let the rest of the application know that the Login component exists. I do this by importing it in the router module and defining its route. While I'm in the router module I need to make an additional change to the NewSurvey
component's route to guard its access to only authenticated users as show below:
// router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import Home from '@/components/Home'
import Survey from '@/components/Survey'
import NewSurvey from '@/components/NewSurvey'
import Login from '@/components/Login'
import store from '@/store'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'Home',
component: Home
}, {
path: '/surveys/:id',
name: 'Survey',
component: Survey
}, {
path: '/surveys',
name: 'NewSurvey',
component: NewSurvey,
beforeEnter (to, from, next) {
if (!store.getters.isAuthenticated) {
next('/login')
} else {
next()
}
}
}, {
path: '/login',
name: 'Login',
component: Login
}
]
})
It's worth mentioning here that I am utilizing vue-router
's route guard beforeEnter
to check to see if the current user is authenticated via the isAuthenticated
getter from the store. If isAuthenticated
returns false then I redirect the application to the login page.
With the Login component coded up and its route defined I can provide access to it via a router-link component in the Header component within components/Header.vue
. I conditionally show either the link to the NewSurvey
component or the Login
component by utilizing the isAuthenticated
store getter once more within a computed property in the Header
component referenced by v-if
directives like so:
<!-- components/Header.vue -->
<template>
<nav class="navbar is-light" role="navigation" aria-label="main navigation">
<div class="navbar-menu">
<div class="navbar-start">
<router-link to="/" class="navbar-item">
Home
</router-link>
<router-link v-if="isAuthenticated" to="/surveys" class="navbar-item">
Create Survey
</router-link>
<router-link v-if="!isAuthenticated" to="/login" class="navbar-item">
Login / Register
</router-link>
</div>
</div>
</nav>
</template>
<script>
export default {
computed: {
isAuthenticated () {
return this.$store.getters.isAuthenticated
}
}
}
</script>
<style>
</style>
Excellent! Now I can finally fire up the dev servers for the Flask app and the Vue.js app and test to see if I can register and login a user.
I start the Flask dev server first.
(venv) $ python appserver.py
Then the webpack dev server to compile and serve the Vue.js app.
$ npm run dev
In my browser I visit http://localhost:8080
(or whatever port the webpack dev server indicates) and make sure the navbar now displays "Login / Register" in the place of "Create Survey" like shown below:
Next I click on the "Login / Register" link and fill out the inputs for an email and password then click register to make sure it functions as expected and I get redirected back to the home page and see the "Create Survey" link displayed instead of the "Login / Register" one that was there before registering.
Alright, my work is largely done. The only thing left to do is to add a little error handling to the submitSurvey(...)
Vue.js method of the NewSurvey
component to handle the event where a token happens to expire while the user is creating a new survey like so:
<script>
import NewQuestion from '@/components/NewQuestion'
export default {
components: { NewQuestion },
data () {
return {
step: 'name',
name: '',
questions: []
}
},
methods: {
//
// omitting other methods
//
submitSurvey () {
this.$store.dispatch('submitNewSurvey', {
name: this.name,
questions: this.questions
})
.then(() => this.$router.push('/'))
.catch((error) => {
console.log('Error creating survey', error)
this.$router.push('/')
})
}
}
}
</script>
Conclusion
In this post I demonstrated how to implement JWT authentication in the survey application using Vue.js and Flask. JWT is a popular and robust method for providing authentication within SPA applications, and I hope after reading this post you feel comfortable using these technologies to secure your applications. However, I do recommend visiting Scott's StackAbuse article for a deeper understanding of the how and why of JWT's work.
As always, thanks for reading and don't be shy about commenting or critiquing below.