Hoot Back-End Starting with Auth Boilerplate
Learning objective: By the end of this lesson, students will be able to bootstrap Flask applications with their JWT authentication starter code.
Overview
Take a moment to review some of the starter code included in the Flask JWT Auth Template.
In this lesson, we’ll expand upon this code by adding modularity to accommodate additional functionality beyond user authentication.
Modularizing our Flask application
Now that we’re building a larger application with multiple resources and routes, let’s start the process of modularizing our code by splitting off the authentication functionality into a separate file.
We’ll leverage Flask’s blueprints pattern and create a Blueprint for authentication routes.
💡 Blueprints in Flask are a way to organize your application into smaller and more manageable components. They allow you to split your application into distinct modules, each with its own routes, templates, and static files. This makes it easier to maintain and scale your codebase, especially as your application grows. By using Blueprints, you can keep related functionality together and encapsulate routes and views in a clean and modular way.
Let’s create our auth_blueprint.py file.
Run the following command in your terminal:
touch auth_blueprint.py
Add the following to auth_blueprint.py:
from flask import Blueprint
authentication_blueprint = Blueprint('authentication_blueprint', __name__)
This provides us with a new blueprint object called authentication_blueprint. This object can be used much like the existing app object in app.py.
Remember, we use decorators to mount routes to our app object. This allows us to define what code should execute when specific URLs are accessed. For example, the decorator below tells Flask to map the root URL / to the function that follows it, which in this case is the index function:
@app.route('/')
def index():
return "Hello, world!"
Similarly, blueprint objects can be used to define routes in the same way.
Instead of using @app.route to define all routes in the application, we can utilize @authentication_blueprint.route. This allows us to keep authentication-related routes and logic encapsulated in a separate file.
🏆 This organization helps maintain a clean code structure and makes it easier to manage and scale our application as it grows.
To plug it into the main application, we need to register the blueprint.
To register the blueprint, first add the following import to app.py:
# Add to stack of imports
from auth_blueprint import authentication_blueprint
Next, add the following line below the initialization of app:
app = Flask(__name__)
# New line:
app.register_blueprint(authentication_blueprint)
🤯 With that, we have registered our blueprint, and any routes defined within
authentication_blueprintwill now be part of our Flask application.
Refactoring app.py
Now we need to move our existing auth routes out of app.py and into the new auth_blueprint.py (replacing all instances of @app with @authentication_blueprint).
We’ll begin by cleaning up app.py. Here we’ll want to remove the /sign-up, /sign-in, /users and user/<user_id> routes and our get_db_connection function.
Our app.py should look like the following:
from flask import Flask
from dotenv import load_dotenv
from auth_blueprint import authentication_blueprint
load_dotenv()
app = Flask(__name__)
app.register_blueprint(authentication_blueprint)
app.run()
Adding a database connection module
Next we’ll want to include our get_db_connection function in its own module. This approach will make sense for our application, as many pieces of our app will want this function.
First we’ll create a db_helpers.py file.
Run the following command in your terminal:
touch db_helpers.py
Add the following to db_helpers.py:
import os
import psycopg2
def get_db_connection():
connection = psycopg2.connect(
host='localhost',
database=os.getenv('POSTGRES_DATABASE'),
user=os.getenv('POSTGRES_USERNAME'),
password=os.getenv('POSTGRES_PASSWORD')
)
return connection
With the module built out, we should now be able to implement it in auth_blueprint.py.
Building the auth_blueprint.py blueprint
Add the following imports to the top of auth_blueprint.py,:
import os
import jwt
import bcrypt
import psycopg2, psycopg2.extras
from flask import Blueprint, jsonify, request
from db_helpers import get_db_connection
💡 Notice the inclusion of our
get_db_connection.
In auth_blueprint.py, find the initialization of the authentication_blueprint. We’ll add our routes directly below it.
Add the following to auth_blueprint.py:
authentication_blueprint = Blueprint('authentication_blueprint', __name__)
# Routes here:
@authentication_blueprint.route('/auth/sign-up', methods=['POST'])
def sign_up():
try:
new_user_data = request.get_json()
connection = get_db_connection()
cursor = connection.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
cursor.execute("SELECT * FROM users WHERE username = %s;", (new_user_data["username"],))
existing_user = cursor.fetchone()
if existing_user:
cursor.close()
return jsonify({"err": "Username already taken"}), 400
hashed_password = bcrypt.hashpw(bytes(new_user_data["password"], 'utf-8'), bcrypt.gensalt())
cursor.execute("INSERT INTO users (username, password) VALUES (%s, %s) RETURNING id, username", (new_user_data["username"], hashed_password.decode('utf-8')))
created_user = cursor.fetchone()
connection.commit()
connection.close()
payload = {"username": created_user["username"], "id": created_user["id"]}
token = jwt.encode({ "payload": payload }, os.getenv('JWT_SECRET'))
return jsonify({"token": token}), 201
except Exception as err:
return jsonify({"err": str(err)}), 401
@authentication_blueprint.route('/auth/sign-in', methods=["POST"])
def sign_in():
try:
sign_in_form_data = request.get_json()
connection = get_db_connection()
cursor = connection.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
cursor.execute("SELECT * FROM users WHERE username = %s;", (sign_in_form_data["username"],))
existing_user = cursor.fetchone()
if existing_user is None:
return jsonify({"err": "Invalid credentials."}), 401
password_is_valid = bcrypt.checkpw(bytes(sign_in_form_data["password"], 'utf-8'), bytes(existing_user["password"], 'utf-8'))
if not password_is_valid:
return jsonify({"err": "Invalid credentials."}), 401
payload = {"username": existing_user["username"], "id": existing_user["id"]}
token = jwt.encode({ "payload": payload }, os.getenv('JWT_SECRET'))
return jsonify({"token": token}), 201
except Exception as err:
return jsonify({"err": err.message}), 500
finally:
connection.close()
You should now be able to test this out in Postman and receive valid responses when making requests to http://127.0.0.1:5000/auth/sign-in and http://127.0.0.1:5000/auth/sign-up.
💡 You might have some errors to work through after this much refactoring, so keep at it until your Postman requests are error-free, and we’ll see you in the next step.