Introduction
JSON Web Tokens (or JWTs) provide a means of transmitting information from the client to the server in a stateless, secure way.
1 Setup a Password Hash
The solution is to use a Password Hash. Let us see what a hash is, so go to the python shell in the terminal and run the command
We will get a long random string as shown below:
Hence even if the hacker gets access to them, he won’t be able to decrypt. Also, we have another function to compare the Hash with a password, called check_password_hash.
It works as below:
Now hit enter, it will return True if matched and False if unmatched.
2 Adding Hashed Passwords to Your Database
Also if you don’t have FlaskSQLAlchemy, simply install it using the pip command:
Okay, now that SQLAlchemy is in place, create a file models.py and add the code:
Here:
3. Setting the Flask_login Extension
Also, we need to create and initialize the Flask_login extension. We do it using the code:
4. Complete Code
That’s it with the models.py part. Let us just look at the whole code once:
Do check out our SQLAlchemy Article if you are unfamiliar with Flask SQLAlchemy.
Coding our main Flask application file
Now let’s code our main Flask Application File.
1 Linking Database to our Flask File
Okay now we need to link our SQLite database with SQLALchemy. So for that add the code:
Just replace <db_name> with whatever name you want. Also, we need to link our SQLAlchemy DB instance (present in models.py file) with the main app. For that, add:
3 Coding a Simple View
Hence add a simple view:
Note how we have used the @login_required decorator. The blog.html template would be:
Do checkout our Flask Templates article to know more about Templates.
Basic application structure
For this application, we’ll have a virtual environment in its own directory, as well as a folder containing the main application files. Here’s an overview of the app’s structure:
.├── auth-app│ ├── app.py │ ├── database.db │ ├── forms.py │ ├── manage.py │ ├── migrations│ ├── models.py │ ├── requirements.txt │ ├── routes.py │ ├── run│ ├── static│ └── templates│ ├── auth.html │ ├── base.html │ └── index.html └── venv
Code smell
Finally, take a look at test_auth.py. Notice the duplicate code? For example:
There are eight occurrences of this. To fix, add the following helper at the top of the file:
Configuring for flask-login
I wouldn’t be a gentleman unless I revealed my config.py file. Again, this should all be straightforward if you’ve been following the rest of our series:
Creating log-in and sign-up forms
Hopefully you’ve become somewhat acquainted with Flask-WTF or WTForms in the past. We create two form classes in forms.py which cover our most essential needs: a sign-up form, and a log-in form:
Creating our login routes
Let’s get things started by setting up a Blueprint for our authentication-related routes:
Database setup
Let’s set up Postgres.
NOTE: If you’re on a Mac, check out Postgres app.
Once the local Postgres server is running, create two new databases from psql that share the same name as your project name:
NOTE: There may be some variation on the above commands, for creating a database, based upon your version of Postgres. Check for the correct command in the Postgres documentation.
Before applying the database migrations we need to update the config file found in project/server/config.py. Simply update the database_name:
Set the environment variables in the terminal:
Update the following tests in project/tests/test__config.py:
Run them to ensure they still pass:
You should see:
Forms.py
a). Registration form
Getting started
Enough theory, let’s start implementing some code!
Project setup
Start by cloning the project boilerplate and then create a new branch:
Create and activate a virtualenv and install the dependencies:
This is optional, but it’s a good idea to create a new Github repository and update the remote:
Database setup
Let’s set up Postgres.
NOTE: If you’re on a Mac, check out Postgres app.
Once the local Postgres server is running, create two new databases from psql
that share the same name as your project name:
NOTE: There may be some variation on the above commands, for creating a database, based upon your version of Postgres. Check for the correct command in the Postgres documentation.
Before applying the database migrations we need to update the config file found in project/server/config.py. Simply update the database_name
:
Set the environment variables in the terminal:
Update the following tests in project/tests/test__config.py:
Run them to ensure they still pass:
You should see:
Important: login helpers
Before your app can work like the above, we need to finish auth.py by providing a couple more routes:
Jwt setup
The auth workflow works as follows:
This cycle repeats until the token expires or is revoked. In the latter case, the server issues a new token.
The tokens themselves are divided into three parts:
We’ll dive a bit deeper into the payload, but if you’re curious, you can read more about each part from the Introduction to JSON Web Tokens article.
To work with JSON Web Tokens in our app, install the PyJWT package:
Logout route tests
Tests valid logout:
deftest_valid_logout(self):""" Test for logout before token expires """withself.client:# user registrationresp_register=self.client.post('/auth/register',data=json.dumps(dict(email='[email protected]',password='123456')),content_type='application/json',)data_register=json.loads(resp_register.data.decode())self.assertTrue(data_register['status']=='success')self.assertTrue(data_register['message']=='Successfully registered.')self.assertTrue(data_register['auth_token'])self.assertTrue(resp_register.content_type=='application/json')self.assertEqual(resp_register.status_code,201)# user loginresp_login=self.client.post('/auth/login',data=json.dumps(dict(email='[email protected]',password='123456')),content_type='application/json')data_login=json.loads(resp_login.data.decode())self.assertTrue(data_login['status']=='success')self.assertTrue(data_login['message']=='Successfully logged in.')self.assertTrue(data_login['auth_token'])self.assertTrue(resp_login.content_type=='application/json')self.assertEqual(resp_login.status_code,200)# valid token logoutresponse=self.client.post('/auth/logout',headers=dict(Authorization='Bearer ' json.loads(resp_login.data.decode())['auth_token']))data=json.loads(response.data.decode())self.assertTrue(data['status']=='success')self.assertTrue(data['message']=='Successfully logged out.')self.assertEqual(response.status_code,200)
Migrations
Add a models.py file to the “server” directory:
# project/server/models.pyimportdatetimefromproject.serverimportapp,db,bcryptclassUser(db.Model):""" User Model for storing user related details """__tablename__="users"id=db.Column(db.Integer,primary_key=True,autoincrement=True)email=db.Column(db.String(255),unique=True,nullable=False)password=db.Column(db.String(255),nullable=False)registered_on=db.Column(db.DateTime,nullable=False)admin=db.Column(db.Boolean,nullable=False,default=False)def__init__(self,email,password,admin=False):self.email=emailself.password=bcrypt.generate_password_hash(password,app.config.get('BCRYPT_LOG_ROUNDS')).decode()self.registered_on=datetime.datetime.now()self.admin=admin
Objectives
By the end of this tutorial, you will be able to…
Project setup
Start by cloning the project boilerplate and then create a new branch:
Create and activate a virtualenv and install the dependencies:
This is optional, but it’s a good idea to create a new Github repository and update the remote:
Refactor
For the PyBites Challenge, let’s refactor some code to correct an issue added to the GitHub repo. Start by adding the following test to test_auth.py:
Register route
Start with a test:
Make sure to add the import:
Run the tests. You should see the following error:
Now, let’s write the code to get the test to pass. Add the following to project/server/auth/views.py:
classRegisterAPI(MethodView):""" User Registration Resource """defpost(self):# get the post datapost_data=request.get_json()# check if user already existsuser=User.query.filter_by(email=post_data.get('email')).first()ifnotuser:try:user=User(email=post_data.get('email'),password=post_data.get('password'))# insert the userdb.session.add(user)db.session.commit()# generate the auth tokenauth_token=user.encode_auth_token(user.id)responseObject={'status':'success','message':'Successfully registered.','auth_token':auth_token.decode()}returnmake_response(jsonify(responseObject)),201exceptExceptionase:responseObject={'status':'fail','message':'Some error occurred. Please try again.'}returnmake_response(jsonify(responseObject)),401else:responseObject={'status':'fail','message':'User already exists. Please Log in.',}returnmake_response(jsonify(responseObject)),202# define the API resourcesregistration_view=RegisterAPI.as_view('register_api')# add Rules for API Endpointsauth_blueprint.add_url_rule('/auth/register',view_func=registration_view,methods=['POST'])
Sanity check
Did it work?
There you have it
If you’ve made it this far, I commend you for your courage. To reward your accomplishments, I’ve published the source code for this tutorial on Github for your reference. Godspeed, brave adventurer.
Conclusion
In this tutorial, we went through the process of adding authentication to a Flask app with JSON Web Tokens. Turn back to the objectives from the beginning of this tutorial. Can you put each one into action? What did you learn?
What’s next? How about the client-side. Check out Token-Based Authentication With Angular for adding Angular into the mix.
To see how to build a complete web app from scratch using Flask, check out our video series:
Free Bonus:Click here to get access to a free Flask Python video tutorial that shows you how to build Flask web app, step-by-step.
Feel free to share your comments, questions, or tips in the comments below. The full code can be found in the flask-jwt-auth repository.
Cheers!
Initializing flask-login
Setting up Flask-Login via the application factory pattern is no different from using any other Flask plugin (or whatever they’re called now). This makes setting up easy: all we need to do is make sure Flask-Login is initialized in __init__.py along with the rest of our plugins, as we do with Flask-SQLAlchemy:
This is the minimum we need to set up Flask-Login properly.