Create a light REST API web application using Bottle framework Vuejs and MongoDB Part4

JWT_Python_Bottle framework_PyMogo_VueJS_Rest API_API_MongoDB_Linux_

In this last part of this tutorial, we will be finalizing the application by implementing an authorization layer using tokens generated with JWT .

Currently anyone can call the APIs we've created so far even though they are not authenticated, to remediate to that we will make it a necessity  to provide a valid token along with the user id for each API request, to authorize the user to manipulate only his notes and not someone else's .

These are the steps we will be implementing :

  • First we will generate a token with an expiration time for every user that authenticates successfully, and we store that token in the database in the field token we've created in the users document .
  • On every request to an API we will verify if the token and the user id provided are valid, if it's the case then we allow the request .
  • On user logout we delete the token from the database for that user .

Installing python JWT:

Let's start by installing  python JWT  In the same python virtual environment we used in previous parts of the tutorial :

pip install PyJWT

If you get the following error :

ModuleNotFoundError: No module named 'setuptools_rust'

Try updating your pip :

pip3 install --upgrade pip
pip install PyJWT

Generating tokens:
Let's update the authentication function to generate a tokens :

...
import jwt
SECRET_KEY = "hkBxrbZ9Td4QEwgRewV6gZSVH4q78vBia4GBYuqd09SsiMsIjH"
...
@route('/authentication', method='POST')
def authentication():
username = request.json['username']
password = request.json['password']

user=db.users.find_one({"username":username})
if user :
if verif_password_hash(password,user['password']) :
timeLimit= datetime.datetime.utcnow() + datetime.timedelta(minutes=30)

payload = {"user_id": username,"exp":timeLimit}
token = jwt.encode(payload,SECRET_KEY)


response.set_cookie("username",user['username'],domain="localhost",expires=timeLimit)
response.set_cookie("user_id",str(user['_id']),domain="localhost",expires=timeLimit)
response.set_cookie("token",token,domain="localhost",expires=timeLimit)


db.users.update_one(user,{"$set" : {"token":token}})
return_data = {
"error": "0",
"message": "Success",
"token": token,
"expire_time": f"{timeLimit}"
}

else :
return_data = {"error": "1", "message": "Fail", "token": "","expire_time": ""}
else :
return_data = {"error": "2", "message": "User doesn't exist","token": "","expire_time": ""}
return json.dumps(return_data)
...

The jwt.encode will generate a token for us if we provide it with some JSON payload and a SECRET_KEY, the payload will be encoded in the token, and can be extracted back if we have the token and the SECRET_KEY.

In our case we will encode the user_id and exp which stands for expiration, it is a reserved key word for JWT so it can implements the expiration time feature .

Now regarding the SECRET_KEY, it's a random string of 50 characters, we need to generate one manually and must be kept secret as the name implies, but to simplify things we will store it in a variable at the beginning of the server.py file .

You can generate a SECRET_KEY using some online string generators, or using these 3 lines of code that you can run in a python interpreter prompt, and then copy past it into the variable :

>>> import os
>>> from base64 import b64encode
>>> b64encode(os.urandom(50)).decode('utf-8')
'essCgUxsRV4vzx87zbm5YfvLfCc6b13U62DVkHNa4+u/Caf6Xxp2ONHzO7LCS+nTrmw='

Once that's set and the token is generated we store it the database for that specific user and in a cookie variable. I went ahead also in include it in the returned JSON .

Now we need a way to verify the token on each API request, luckily we can take advantage of the python decorators so we don't have to  make any changes to our existing API functions:

def token_required(func):
def wrapper():
return func()
return wrapper

...

@route('/api/listnotes',method="GET")
@token_required
def apiListNotes():
user_id = request.get_cookie("user_id")
notes=db.notes.find({'user_id':ObjectId(user_id)})
#list_notes = list(notes)
list_notes = [ {'_id':str(ll['_id']),'title':ll['title'],'description':ll['description']} for ll in notes ]
return json.dumps({"notes":list_notes})

I've created a function called token_required that we can use as a decorator on any function we have. In the example above anytime there is a call to the function  apiListNotes, the token_required will be executed first .

So now we simply need to implement our logic for the token verification in token_required before deciding to allow or decline calling the target function.
Validate Tokens:

We will be passing the TOKEN and the USERID as URL argument on each API request like this : '/api/listnotes?TOKEN={{ token }}&USERID={{user_id}}', it's definitely not a secure way but we will go along with it :

def token_required(func):
def wrapper():
try:
if "TOKEN" in request.query and "USERID" in request.query :
token = request.query['TOKEN']
userid = request.query['USERID']

try:
data = jwt.decode(token,SECRET_KEY, algorithms=['HS256'])
user=db.users.find_one({"token":token,"_id":ObjectId(userid)})

if user :
return func()
else :
return_data = {"error": "1","message": "Invalid Token for the user" }
response.content_type="application/json"
response.status=401
except jwt.exceptions.ExpiredSignatureError:
return_data = { "error": "1","message": "Token has expired" }
response.content_type="application/json"
response.status=401
except:
return_data = {"error": "2", "message": "Invalid Token" }
response.content_type="application/json"
response.status=401
else:
return_data = {"error" : "3", "message" : "TOKEN and USERID are required"}
response.content_type="application/json"
response.status=401
except Exception as e:
return_data = { "error" : "4","message" : "An error occured" }
response.content_type="application/json"
response.status=500

return json.dumps(return_data)
return wrapper

Let's break this down, First there is an initial try except block which will catch any exception that we didn't specify, if it's the case we prepare a return_data variable that contains an error id and message and we update also the content_type and status of the response so that t browser gets a correct HTTP response code. We do this for the inside exceptions as well .

Next thing I did is to make sure that the TOKEN and USERID were provided as URL argument, if it's the case we can proceed to the token verification and that by using the jwt.decode function which requires the token , SECRET_KEY  and a list of algorithms .

The jwt.decode will throw an exception if the token is invalid or expired, so we set proper details in the return_data variable before return it to the browser .

At this point it should be okay to return to the original function the decorator was applied to, but I want to add one last verification and that to fetch the user from the database with both the user id and the token, and only then if we have a match we proceed to the original function.

We can now add the @token_required decorator to the rest of the APIs :

@route('/api/addnote' , method="POST")
@token_required
def apiAddNote():
...

@route('/api/updatenote' , method="POST")
@token_required
def apiUpdateNote():
...

@route('/api/deletenote' , method="POST")
@token_required
def apiDeleteNote():
...

If we try to go to the dashboard in the browser now we will see that everything is broken, if you open the browser's dev tools, the reason becomes clear:


We need to provide the TOKEN and USERID , we do have them stored in the cookies so we can grab them directly from the dashboard.html using some Javascript or JQuery, but i prefer to just pass them as template parameters :

@route('/')
def home():
user_id = request.get_cookie("user_id")
username = request.get_cookie("username")
token = request.get_cookie("token")
if user_id and username and token :
user_details = {'token':token,'username':username, 'user_id':user_id }
return template('dashboard.html',user_details)
else :
redirect("/login")

And in dashboard.html we update the URLs :

axios.get('/api/listnotes?TOKEN={{ token }}&USERID={{user_id}}')
...
axios.post('/api/addnote?TOKEN={{ token }}&USERID={{user_id}}', data, {
...
axios.post('/api/updatenote?TOKEN={{ token }}&USERID={{user_id}}', data, {
...
axios.post('/api/deletenote?TOKEN={{ token }}&USERID={{user_id}}', data, {

Let's update the navigation bar as well to show the username currently logged-in :

...
<nav class="navbar navbar-inverse">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="/dashboard">
<p> <i class="fa fa-1x fa-user" ></i> {{ username }}</p>
</a>
</div>
<a href="/logout" class="btn btn-default navbar-btn pull-right">Logout</a>
</div>
</nav>
...

Now everything should be working again :


One last thing to do is to delete the token from the database and cookies when the user logs out :

@route('/logout')
def logout():
user_id = request.get_cookie("user_id")
user=db.users.find_one({"_id":ObjectId(user_id)})
db.user.update_one(user,{"$set" : {"token":""}})


response.delete_cookie("username")
response.delete_cookie("user_id")
response.set_cookie("token","")
redirect("/login")


Conclusion:

As i said in the beginning of this tutorial series, the application will compact a lot of interesting technologies, it goes to show that a lot can be happening in the background .

Even though Bottle framework is very easy to start with, most of the features must be implemented manually, it's a good thing if you want to control exactly what you have in your application to keep it very light , but sometimes it's it not because you will have to implement some of the features that are already out of the box in  other frameworks. There are plugins though that can be added to Bottle framework such as one to manage sessions.

I did highlight as well that I don't recommend using this in a production environment, for security reasons, first the tokens are sent through URL arguments, it could have been better if we sent them using a POST request as well, but they still can be sniffed easily, a solution probably is to add HTTPS which will require to use a web server such as Apache or Nginix.

Bottle framework does not handle CSRF by default, so it is not protected against attacks that forces the end user to execute unwanted actions on the web application, there is a plugging for that, but i didn't use it to not add to the complexity to the project.

Despite all that it's a pretty interesting combo of technologies, and I will definitely use it as a dashboard for my future IoT projects for example.

Comments :