In this tutorial, we will cover how to make a website or api using the Flask microframework in python. We will leverage MongoDB as our database, with other useful features such as user authentication with the bcrypt hashing algorithm and full CRUD (Create, Read, Update, Delete) capabilities. In this tutorial, we will make a full website and a separate API for use in back end only situations.
Throughout this tutorial there may be a letter attached to the part number. ‘A’ represents you are making a full site with Flask only. ‘B’ represents you making an API for use with a separate front end such as Angular, React, or Vue.js.
TLDR: The code is available here
Required software: Python 3 and MongoDB server
Optional software: Postman or a similar REST client to test your endpoints and MongoDB Compass (comes with MongoDB server on Windows)
The first part of any python tutorial is to create our virtual environment. It is important that we create one in order to separate our machine’s packages from the packages required by our app. In the main folder we run
python -m venv VIRTUAL_ENVIRONMENT_NAME
You can name the virtual environment anything you’d like, but for most purposes, just name it “venv” so that the default .gitignore will ignore our environment (if you don’t know what this means, then just ignore it and name the environment “venv”). This will create a folder that we can just pretend isn’t there. Next we need to activate the environment. You will need to do the following steps whenever you want to do anything with the project, but don’t worry, it isn’t complicated. In the Windows command prompt run the following command
venv\Scripts\activate
And on Linux or Mac we run
. venv/bin/activate
or source venv/bin/activate
And now our terminal or command prompt has a little (venv) at the beginning of the line to indicate that we are using the virtual environment. For the rest of the tutorial, assume that the environment is active.
Next we need to install our required packages. We install packages with the pip install PACKAGE
command. The required packages are as follows:
Let’s explain the packages. Flask is the microframework that makes this possible. It is responsible for receiving requests and dispatching data. Flask-mongoengine is a document-object mapper that let’s us communicate with mongo in python and create python objects from documents. Flask-bcrypt is our password hashing algorithm. Password security is a little too time consuming for this tutorial, so look out for another blog post on it. Flask-jwt-extended let’s us us JSON web tokens to verify a user’s identity. The web token is stored in the browser, so we don’t need to store who is logged in on the server. Flask-restful-swagger-2 is a complex package that changes how to define our “routes” (we’ll get more into this later) and generates a SWAGGER (more on this later) for us to view our endpoints of the API. Flask-cors is an implementation of CORS (Cross-Origin-Resource-Sharing) that we’ll use to lock down our endpoints (or make them public if that’s your mojo).
Finally, let’s save our install config to a file with the following command:
pip freeze > requirements.txt
This will create a file called requirements.txt that has all of the packages we installed and their versions so that anyone can install the same packages with a single command ( pip install -r requirements.txt
)
Next we’ll create the main file and the driver. Start by making a file called app.py in the main folder, and we’ll import all of our packages
app.py
from flask import Flask
from flask_bcrypt import Bcrypt
from flask_jwt_extended import JWTManager
If you’re making an API, we also need to import the following
from flask_restful_swagger_2 import Api
from flask_cors import CORS
Next we need to create the app and it’s configs
app.py
app = Flask(__name__)
app.config['JWT_SECRET_KEY'] = 'aSuperSecretKeyThatNobodyShouldBeAbleToGuess'
app.config['MONGODB_SETTINGS'] = 'mongodb://localhost/YOUR-DB-NAME'
If you are making a website and not an API we need to tell flask_jwt_extended that the tokens are located in our cookies and not the headers
app.py
app.config['JWT_TOKEN_LOCATION'] = 'cookies'
And finally, let’s get our other packages running
app.py
bcrypt = Bcrypt(app)
jwt = JWTManager(app)
If you’re making an API, we also need to following
app.py
api = Api(app)
cors = CORS(app, resources={r'/*': { 'origins': '*' }}
Explain time! First we defined the app variable. This is the main Flask app. We also added two configs for app. ‘JWT_SECRET_KEY’ is used to sign our tokens so that we can encode and decode them securely. Make sure it’s not guessable as anyone can decode your sensitive tokens if it’s breached. ‘MONGODB_SETTINGS’ defines what database in mongo we are going to use.
Next are the other packages. These are mostly self-explanatory as they just initialize the package with app’s configs. The only one that needs explaining is cors. We defined it so that any route is accessible from any origin, indicated by the *.
Next we’ll make another file in the root of the project called wsgi.py. This file is called a driver, as it’s the file we run to start the project.
wsgi.py
from app import app
if __name__ == '__main__':
[tab]app.run(debug=True)
We’ll come back to app.py later, but if you run python wsgi.py
you will technically have a functional Flask app working!
Next, we’ll make a new folder called database that will house our database driver and it’s models. Models are how mongoengine relates mongo documents to python objects. We’ll start by making a file called db.py in the folder.
database/db.py
from mongoengine import connect
def initialize_db(app):
[tab]connect(host=app.config['MONGODB_SETTINGS'])
This file is our database driver. It is very simple. We create a function called initialize_db that will, well, initialize our database. To do this, in app.py, we import db.py and run the function.
app.py
from database.db import initialize_db
initialize_db(app) # Run the function after app and config init. End of the file works well.
Next we’ll make a models.py in the database folder. This file can get complicated, but as long as we keep it neat, it will be easy to traverse.
database/models.py
from mongoengine import Document, EmailField, StringField
from flask_bcrypt import generate_password_hash, check_password_hash
import string, random
class User(Document):
[tab]email = EmailField(required=True, unique=True)
[tab]password = StringField(required=True, min_length=6)
[tab]salt = StringField()
[tab]def hash_password(self):
[tab][tab]chars = string.ascii_letters + string.punctuation
[tab][tab]size = 12
[tab][tab]self.salt = ''.join(random.choice(chars) for x in range(size))
[tab][tab]self.password = generate_password_hash(self.password + self.salt).decode('utf8')
[tab]def check_password(self, password):
[tab][tab]return check_password_hash(self.password, password + self.salt)
We defined two fields for on the User document: an email and password field. Mongoengine has many fields (you can see all of them here). We’re using the EmailField and StringField (self explanatory). We then defined some functions we can use with User objects. hash_password will take the current password, which in our case will be the user’s actual password and create a salt and hash it. Again, password cryptography is complex and will take too much to explain here, so be on the look out for a new blog post with a full explanation. check_password will check the password hash against the provided password and the stored salt to see if they are the indeed the same password.
This is not required, but it makes serialization (the process of converting the object to a standard object) much easier. By default, if you try to send the user object to the client, it will fail because python doesn’t know how to convert the object. We will create a serialize function in the User object we made in part 3
database/models.py
def serialize(self):
[tab]return {
[tab][tab]'id': str(self.id),
[tab][tab]'email': self.email,
[tab]}
Simple. All this function does is convert (or serialize) the object into a python object that we can easily convert to JSON to send to the client.
Next we’ll tackle one of the most complex parts of the app: authentication. This section will cover routes for a website without the flask_restful_swagger_2 extension. We will make a folder called resources in the root of the project and create an auth.py file inside. Let’s start with the imports.
resources/auth.py
from flask import Blueprint, request, make_response, render_template, redirect, url_for
from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity, get_jwt, set_access_cookies, jwt_required
from database.models import User
import datetime
Don’t worry if you don’t understand the imports, especially the flask_jwt_extended imports, we will get more into them later. First, we need to make a Flask Blueprint. Blueprints allow us to register our routes at runtime, but for our purposes it’s to organize our code.
resources/auth.py
auth = Blueprint('auth', __name__, template_folder='templates')
Next we will create the SignupApi endpoint.
resources/auth.py
@auth.route('/signup', methods=['GET', 'POST']
def signup():
[tab]if request.method == 'POST':
[tab][tab]user = User(**request.form)
[tab][tab]user.hash_password()
[tab][tab]user.save()
[tab][tab]accessToken = create_access_token(identity=str(user.id), expires_delta=datetime.timedelta(days=1))
[tab][tab]resp = make_response(redirect(url_for('auth.dashboard'))) # Redirect to wherever after login
[tab][tab]resp.set_cookie('access_token_cookie', accessToken, max_age=60*60*24) # Expires in 1 day
[tab][tab]return resp
[tab]else:
[tab][tab]if request.cookies.get('access_token_cookie'):
[tab][tab][tab]return redirect(url_for('auth.dashboard'))
[tab][tab]return render_template('signup.html') # Our signup page
And that’s it. That’s our registration method. We allow a GET or POST request to pass through the function to both render the sign up page (GET) or to process the sign in and redirect somewhere else (POST). For the POST, we serialize the incoming form data to a User object, hash the password, save to the database, then redirect. Next, we’ll make our login function.
resources/auth.py
@auth.route('/login', methods=['GET', 'POST']
def login():
[tab]if request.method == 'POST':
[tab][tab]body = request.form
[tab][tab]user = User.objects.get(email=body['email'])
[tab][tab]authorized = user.check_password(body['password'])
[tab][tab]if not authorized:
[tab][tab][tab]return render_template('login.html', failed=True) # Our login page
[tab][tab]accessToken = create_access_token(identity=str(user.id), expires_delta=datetime.timedelta(days=1))
[tab][tab]resp = make_response(redirect(url_for('auth.dashboard'))) # Redirect to wherever after login
[tab][tab]resp.set_cookie('access_token_cookie', accessToken, max_age=60*60*24) # Expires in 1 day
[tab][tab]return resp
[tab]else:
[tab][tab]if request.cookies.get('access_token_cookie'):
[tab][tab][tab]return redirect(url_for('auth.dashboard'))
[tab][tab]return render_template('login.html') # Our login
page
Simple. First we convert the incoming form data to a python object, get the user from the provided email, check their password, and set the JWT tokens in the cookies if they are authorized or re render the page. The access token lasts for 1 day, so we need a way to refresh the tokens without having the user explicitly sign in every day.
resources/auth.py
@auth.after_app_request
@jwt_required(optional=True)
def refresh(response):
[tab]identity = get_jwt_identity()
[tab]if identity:
[tab][tab]expTimestamp = get_jwt()['exp']
[tab][tab]now = datetime.datetime.now()
[tab][tab]targetTimestamp = datetime.datetime.timestamp(now + datetime.timedelta(minutes=30))
[tab][tab]if targetTimestamp > expTimestamp:
[tab][tab][tab]accessToken = create_access_token(identity=identity, expires_delta=datetime.timedelta(days=1))
[tab][tab][tab]set_access_cookies(response, accessToken, max_age=60*60*24)
[tab]return response
It’s that easy. This function will refresh any access tokens that are within 30 minutes of expiration.
Finally, we need to make our dashboard route so we can redirect the user after a successful login
resources/auth.py
@auth.route('/dashboard', methods=['GET'])
def dashboard():
[tab]return render_template('dashboard.html')
This section will cover routes for an API with the flask_restful_swagger_2 extension. Like part 4A, we will make a resources folder in the root of the project and we will make an auth.py file. Let’s start with the imports.
resources/auth.py
from flask import request
from flask_restful_swagger_2 import Resource
from flask_jwt_extended import create_access_token, create_refresh_token, jwt_required, get_jwt_identity
from database.models import User
import datetime
Don’t worry if you don’t understand the imports, especially the flask_jwt_extended imports, we will get more into them later. Next we will create the SignupApi endpoint.
resources/auth.py
class SignupApi(Resource):
[tab]def post(self):
[tab][tab]user = User(**request.get_json())
[tab][tab]user.hash_password()
[tab][tab]user.save()
[tab][tab]return { 'id': str(user.id) }
And that’s it. That’s our registration endpoint. We define a post function in the class to signify that it only accepts POST requests. We then serialize the incoming JSON to a User object, hash the password, save to the database, then return their ID. The return is not very useful for the client, so any return will work. It is important that you return something so the client knows that the function executed successfully. Next, we’ll make our LoginApi endpoint.
resources/auth.py
class LoginApi(Resource):
[tab]def post(self):
[tab][tab]body = request.get_json()
[tab][tab]user = User.objects.get(email=body['email])
[tab][tab]authorized = user.check_password(body['password'])
[tab][tab]if not authorized:
[tab][tab][tab]return 'Unauthorized Error', 401
[tab][tab]accessToken = create_access_token(identity=str(user.id), expires_delta=datetime.timedelta(days=1))
[tab][tab]refreshToken = create_refresh_token(identity=str(user.id), expires_delta=datetime.timedelta(days=30))
[tab][tab]return { 'accessToken': accessToken, 'refreshToken': refreshToken }
Simple. First we convert the incoming JSON to a python object, get the user from the provided email, check their password, and send back JWT tokens if they are authorized or return an error if not. The access token lasts for 1 day and the refresh token lasts for 30 days, so we need a way to refresh the tokens without having the user explicitly sign in every day.
resources/auth.py
class RefreshApi(Resource):
[tab]@jwt_required(refresh=True)
[tab]def get(self):
[tab][tab]accessToken = create_access_token(identity=get_jwt_identity(), expires_delta=datetime.timedelta(days=1))
[tab][tab]refreshToken = create_refresh_token(identity=get_jwt_identity(), expires_delta=datetime.timedelta(days=30))
[tab][tab]return { 'accessToken': accessToken, 'refreshToken': refreshToken }
It’s that easy. This endpoint will check that the provided JWT refresh token is valid before allowing the inner code to run. The code just generates and returns a new access token and refresh token.
In this section we will register our functions so they are actually visible. We’ll start by making routes.py in the resources folder. First we import our auth blueprint.
resources/routes.py
from .auth import auth
Then we’ll create our route initialization function
resources/routes.py
def initialize_routes(app):
[tab]app.register_blueprint(auth)
Then we will go to the bottom of app.py to register our routes at start up. It is imperative that we place the import at the bottom to avoid a circular import error
app.py
from resources.routes import initialize_routes
initialize_routes(app)
And that’s it. We can run our wsgi.py driver and be able to sign up, log in, and refresh our tokens. Now you can take this knowledge to improve upon it, or look into using our SEPHIRA content management system to speed up your development time.
In this section we will register our routes so they are actually accessible. We’ll start by making routes.py in the resources folder. First we import our endpoints.
resources/routes.py
from .auth import SignupApi, LoginApi, RefreshApi
Then we’ll create our route initialization function.
resources/routes.py
def initialize_routes(api):
[tab]api.add_resource(SignupApi, '/auth/signup')
[tab]api.add_resource(LoginApi, '/auth/login')
[tab]api.add_resource(RefreshApi, 'auth/refesh')
The reason we are using the api variable instead of the app variable is because the api will automatically generate a SWAGGER for us based on the endpoints. SWAGGER is just a document that explains how each endpoint works. The flask_restful_swagger_2 package has a module called ‘swagger’ that we can use to thoroughly describe each endpoint, but that’s a little too much for this tutorial.
Then we will go to the bottom of app.py to register our routes at start up. It is imperative that we place the import at the bottom to avoid a circular import error
app.py
from resources.routes import initialize_routes
initialize_routes(api)
And that’s it. We can run our wsgi.py driver and be able to sign up, log in, and refresh our tokens. Now you can take this knowledge to improve upon it, or look into using our SEPHIRA content management system to speed up your development time.
The final file structure should look like this:
There’s still much more to do before this can be launched into production, most notably error handling, but with the basics done, you can move on to bigger and better things.