Effortless REST with Flask, Part 2

In Part 1, we created the base scaffolding for a production-ready Flask API. But what’s an API without data, and if we’re serving sensitive data, how do we go about securing it? Effortless REST with Flask, Part 2 is all about data and security. Follow along step-by-step as we connect our API to a live database and secure our endpoints. As a reminder, You can access the code from the git repo here.

Let’s get started!

Connecting a Database

1. Sign up For Heroku / Log in to your Heroku account

2. Create a new app

3. Once created, Configure/Add the “Heroku Postgres” add on with the Hobby Dev Plan (free)

4. Add the Heroku CLI to your terminal

Once everything is configured, getting your Heroku database connection string is as easy as

heroku config:get DATABASE_URL -a {your_heroku_app_name}

The output of this command will be what we give our Flask SqlAlchemy plugin as our connection string. To make this easy, add the above command to the tasks.py script so you’ll always fetch the DATABASE_URL before starting your Flask application. You’ll also need to add your HEROKU_APP_NAME at the top of tasks.py.

Connecting to the Database

We define a db variable, and then within the factory function, we initialize the database with the application. Because we've already set up our config to include an "SQLALCHEMY_DATABASE_URI" variable, we can use this simple command to connect our app to the database.

Defining Models

"User Model"
# pylint: disable=no-member
from app import db
from sqlalchemy.sql import func

# A generic user model that might be used by an app powered by flask-praetorian
class User(db.Model):
__tablename__ = "users"

user_id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(255), unique=True)
password = db.Column(db.String(255))
roles = db.Column(db.Text)
is_active = db.Column(db.Boolean, default=True, server_default="true")
created_datetime = db.Column(
db.DateTime(), nullable=False, server_default=func.now()
)

@property
def rolenames(self):
try:
return self.roles.split(",")
except Exception:
return []

@classmethod
def lookup(cls, username: str):
return cls.query.filter_by(username=username).one_or_none()

@classmethod
def identify(cls, user_id: int):
return cls.query.get(user_id)

@property
def identity(self):
return self.user_id

def is_valid(self):
return self.is_active

def __repr__(self):
return f"<User {self.user_id} - {self.username}>"

What makes SqlAlchemy so powerful is that you can define the database model right here in Python code. You can define the table name, properties, and even other SQL native elements such as defaults and uniqueness. This allows you to interact with the database models and entities with ease in Python. If you don’t define a method here on the model, you can still import it and build queries elsewhere in your code.

Seeding the Database

invoke init-db
invoke seed-db

These two commands are defined in the tasks.py file and create the tables in our database and seed them with two users.

Using SQLAlchemy Models to Query the Database

Notice how we can import our User model at the beginning of our create_app function and then later use it within our route to construct a query that fetches all users. NOTE: If you're unfamiliar with the List[user] syntax, this is Python typing syntax with mypy.

Now that we’ve got a route defined and a model being queried let’s run a GET request by going to ‘http://127.0.0.1:5000/uhoh'.

You should see a response that says: TypeError: Object of type user is not JSON serializable

The TypeError tells us that our SQLAlchemy model response is not JSON serializable. In other words, the user variable can’t be transformed into JSON so easily. Instead, we need to deserialize the user and convert it into JSON. We can do this by using the Marshmallow package. Marshmallow is a foundational package when doing the input validation, deserialization into app-level objects, and serialization of app-level objects into Python types. We highly recommend that you get familiar with this package as it can offer you “superpowers” for app-objects coming in and out of your API.

Below is an example route that deserializes the SQLAlchemy model into JSON. It uses a defined schema to know how to deserialize the SQLAlchemy model.

When calling the route with http://127.0.0.1:5000/marsh, we see that the SQLAlchemy models correctly get transformed into JSON and the web browser displays the JSON.

Congratulations! You’ve now connected to a database and have an API serving data from it!

Authentication and Authorization

In thechap-3/app/config.py file, add the JWT_ACCESS_LIFESPAN, and JWT_REFRESH_LIFESPAN config variable.

Then in the chap-3/app/__init__.py file, we can initialize the Praetorian library, add the /login route, and protect the routes with special decorator @auth_required.

"Main Flask App"
...
# Application Factory
# https://flask.palletsprojects.com/en/1.1.x/patterns/appfactories/
def create_app(config_name: str) -> Flask:
"""Create the Flask application
Args:
config_name (str): Config name mapping to Config Class
Returns:
[Flask]: Flask Application
"""
from app.config import config_by_name
from app.models import User
# Create the app
app = Flask(__name__)
# Log the current config name being used and setup app with the config
app.logger.debug(f"CONFIG NAME: {config_name}")
config = config_by_name[config_name]
app.config.from_object(config)
# Initialize the database
db.init_app(app)
# Initialize the flask-praetorian instance for the app
guard.init_app(app, User)

@app.route("/")
def hello_world() -> str: # pylint: disable=unused-variable
return "Hello World!"

@app.route("/login", methods=["POST"])
def login():
# Ignore the mimetype and always try to parse JSON.
req = request.get_json(force=True)
username = req.get("username", None)
password = req.get("password", None)
user = guard.authenticate(username, password)
ret = {"access_token": guard.encode_jwt_token(user)}
return (jsonify(ret), 200)

@app.route("/users", methods=["GET"])
@auth_required
def users():
users = User.query.all()
return jsonify(UserSchema(many=True).dump(users))

@app.route("/users/admin-only", methods=["GET"])
@roles_required("admin")
def users_admin():
users = User.query.all()
return jsonify(UserSchemaWithPassword(many=True).dump(users))

return app

We initialize the guard variable with our User SQLAlchemy model. This is the model that Flask Praetorian will use to authenticate users when doing guard.authenticate() in the /login route. When the user is authenticated, we can encode a JWT token from the user and send it back to the client. The client can then use that token when making requests to the backend, such as in /users route. The @auth_required decorated will check the request headers for an Authentication Bearer token. If the token is present, it’ll parse it and validate the user against the database. If the user is authenticated, then the route can proceed. When building production-ready applications, you should always consider what data will feed through the app. When we set up applications, we treat all data as if it’s sensitive, and users need to be authenticated. This is a great best practice to adopt.

Flask Praetorian also gives us authorization with the @roles_required decorator, as is demonstrated in the /users/admin-only route. This decorator authenticates the user and then verifies that the proper roles are present on the user before allowing the request to continue.

To see the above actions yourself, use the file called effortless_rest_with_flask_postman.json that allows you to import Postman requests to work with your local API quickly.

Securing your application can be incredibly challenging, but Flask Praetorian makes it simple. With a few endpoint configurations, you can easily add authentication and authorization. You now have an API that’s connected to a database along with routes that have general authentication and optional authorization. It would be perfectly reasonable to start building your business logic into your application now, but we still have something missing. How will our API consumers know about our API interfaces and inputs? What documentation can they rely on to be in concordance with our API? In Part 3 of this series, we’ll answer those questions with auto-generated API documentation within our Flask app. Until next time!

Who we are as people is who we are as a company. We share the same values in the way we work with each other, our partners and our customers. We are ATD.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store