JWT Authentication and Authorization for Python Flask REST APIs

A practical guide to implement auth resources in your Flask REST API using JWT
Bruno Tatsuya June 24, 2022

JSON Web Token (JWT) is a well-known authentication pattern for protecting web applications routes and pages. It doesn't demand user session management in databases or server-side caching. In this article, let's explore a practical guide to implement authentication and authorization for Flask REST APIs made simple.

The folder structure

For this practical guide, we will be using this simple folder structure:

/ ├───.vscode │ └───launch.json ├───api │ │ auth.py │ └───routes.py ├───services │ │ auth_guard.py │ │ auth_provider.py │ └───jwt_handler.py └─── app.py

Install the PyJWT package

PyJWT is a great package by Auth0 that implements JWT resources and provides simple high-level functions to work with it. Let's start by installing it into our project:

pip install PyJWT

Remember to add it later to your requirements.txt!

Generate your secret key

In order to sign your JWT, you must use a private key (secret key) which only your application (and anything else) have access to. In this guide, we are going to use the HS256 algorithm, which means that we are going to use a symmetric key for both signing and checking parts. In this scenario, is recommended that your secret be strong enough to make brute-forcing techniques computationally infeasible.

I do like to generate my strong keys randomly at RandomKeygen, a free website for this purpose. At this site, the CodeIgniter Encryption Keys section provides good keys we can use as our secret. Let's use, for this guide, the following key: yXzB0lEGphwTYfKenExHmNqMmpFJJRjI.

Important: This key is really secret, so please do not use public or known strings. The key above is only for example purposes and you must generate a different one for your application.

Put your secret key in a safe place

You should not hardcode your key at any .py file of your project. You can put it into a settings file or recover it from environment variables (which is the safest option). If you are using an IDE like Visual Studio Code, you can input environment variables in the env tag of launch.json, like below:

// .vscode/launch.json { "configurations": [ { "name": "Python: Flask", "type": "python", "request": "launch", "module": "flask", "env": { "FLASK_APP": "app.py", "FLASK_ENV": "development", "SECRET_KEY": "yXzB0lEGphwTYfKenExHmNqMmpFJJRjI" // <--- There is our secret! }, "args": [ "run", "--no-debugger", "--host=0.0.0.0" ], "jinja": true, "subProcess": true } ] }

Create a JWT handler service

We can create a service for handling JWT operations, like generation and verification/recovery of information from inside of it.

# services/jwt_handler.py import os import jwt from datetime import datetime, timedelta def generate_jwt(payload, lifetime=None): # Generates a new JWT token, wrapping information provided by payload (dict) # Lifetime describes (in minutes) how much time the token will be valid if lifetime: payload['exp'] = (datetime.now() + timedelta(minutes=lifetime)).timestamp() return jwt.encode(payload, os.environ.get('SECRET_KEY'), algorithm="HS256") def decode_jwt(token): # Tries to retrieve payload information inside of a existent JWT token (string) # Will throw an error if the token is invalid (expired or inconsistent) return jwt.decode(token, os.environ.get('SECRET_KEY'), algorithms=["HS256"])

As you notice, we pass os.environ.get('SECRET_KEY') as our secret key, configured in launch.json.

Create an authentication provider service

We must create a resource that will be responsible for providing methods for credentials validation. This is the voice of the truth in the auth ecosystem and in real life, it can query a database, integrate to external services, etc to check if the informed credentials are valid. Here, we will only create a symbolic provider with the method authenticate, which receives email and password and returns a dictionary with user information, if valid.

# services/auth_provider.py def authenticate(email, password): if email == 'admin@admin.com' and password == 'bad_password': return { 'username': 'admin', 'email': 'admin@admin.com', 'roles': ['admin', 'user'] } else: return False

Create an authentication endpoint

Now that we've created an auth provider service, we can build a public endpoint to enable users for getting a valid JWT:

# api/auth.py from flask import request, jsonify from services.auth_provider import authenticate from services.jwt_handler import generate_jwt def init(app): @app.route('/api/auth', methods=['POST']) def auth(): email = request.json.get('email') password = request.json.get('password') if not email or not password: return jsonify({"message": "Email or password missing", "status": 400}), 400 user_data = authenticate(email, password) if not user_data: return jsonify({"message": "Invalid credentials", "status": 400}), 400 token = generate_jwt(payload=user_data, lifetime=60) # <--- generates a JWT with valid within 1 hour by now return jsonify({"data": token, "status": 200}), 200

Note: We used the factory pattern to prevent importing app into the routes modules. Thus, the function init will be called in our app.py later.

Create an auth guard service

Ok, so all we need to do now is create an auth guard service to protect our routes! This will be responsible to prevent unauthorized access to specific routes, given a request. We are also going to implement authorization by looking up the roles key in the JWT payload.

Let's start by creating a simple function called check_jwt that checks if the received request has a valid JWT. We will assume that the JWT will be present as a Bearer token in the Authorization header key.

# services/auth_guard.py from flask import request from services.jwt_handler import decode_jwt def check_jwt(): # Gets token from request header and tries to get it's payload # Will raise errors if token is missing, invalid or expired token = request.headers.get('Authorization') if not token: raise Exception('Missing access token') jwt = token.split('Bearer ')[1] try: return decode_jwt(jwt) except Exception as e: raise Exception(f'Invalid access token: {e}')

And now, we can construct a decorator called auth_guard to implement authentication and authorization gates, like this:

# services/auth_guard.py from flask import request, jsonify from services.jwt_handler import decode_jwt def check_jwt(): # Gets token from request header and tries to get it's payload # Will raise errors if token is missing, invalid or expired token = request.headers.get('Authorization') if not token: raise Exception('Missing access token') jwt = token.split('Bearer ')[1] try: return decode_jwt(jwt) except Exception as e: raise Exception(f'Invalid access token: {e}') def auth_guard(role=None): def wrapper(route_function): def decorated_function(*args, **kwargs): # Authentication gate try: user_data = check_jwt() except Exception as e: return jsonify({"message": f'{e}', "status": 401}), 401 # Authorization gate if role and role not in user_data['roles']: return jsonify({"message": 'Authorization required.', "status": 403}), 403 # Proceed to original route function return route_function(*args, **kwargs) decorated_function.__name__ = route_function.__name__ return decorated_function return wrapper

Note that this is a special decorator, since we need a intermediate function only to assign route_function.__name__ to decorated_function.__name__. This is because of Flask's routing parser: we need to assign the name of the route function as the name of the returned decorated function, otherwise, more than one route will try to be mapped to the same function name, throwing the classic AssertionError: View function mapping is overwriting an existing endpoint function error.

Protecting the routes

With our friendly auth_guard decorator, we can now protect any route just by decorating route functions with it.

Say we want to restrict a route to authenticated users only, but without any specific role required. Then we do:

# api/routes.py from flask import jsonify from services.auth_guard import auth_guard def init(app): @app.route('/api/protected_route', methods=['GET']) @auth_guard() # <--- Requires the authentication, but do not restricts authorization by roles def protected_route(): return jsonify({"message": 'You have accessed a protected route.', "status": 200}), 200

If we want to enable role restriction, we decorate like the examples below:

# api/routes.py from flask import jsonify from services.auth_guard import auth_guard def init(app): @app.route('/api/protected_route', methods=['GET']) @auth_guard() # <--- Requires the authentication, but do not restricts authorization by roles def protected_route(): return jsonify({"message": 'You have accessed a protected route.', "status": 200}), 200 @app.route('/api/protected_route_user', methods=['GET']) @auth_guard('user') # <--- Requires the authentication AND authorization by 'user' role def protected_route_user(): return jsonify({"message": 'You have accessed a user protected route.', "status": 200}), 200 @app.route('/api/protected_route_admin', methods=['GET']) @auth_guard('admin') # <--- Requires the authentication AND authorization by 'admin' role def protected_route_admin(): return jsonify({"message": 'You have accessed a admin protected route.', "status": 200}), 200 @app.route('/api/protected_route_super_admin', methods=['GET']) @auth_guard('super_admin') # <--- Requires the authentication AND authorization by 'super_admin' role def protected_route_super_admin(): return jsonify({"message": 'You are a SUPER ADMIN!', "status": 200}), 200

Pretty neat, isn't it?

Don't forget your app.py!

In order to all of this works, we will need to write the app.py importing and calling the routes init factories. We can do it like:

# app.py from flask import Flask from api.auth import init as init_auth_routes from api.routes import init as init_routes def create_app(): app = Flask(__name__) init_auth_routes(app) init_routes(app) return app if __name__ == '__main__': app = create_app() app.run()

You can also struct your project with a fancy code architecture made with a simple decorator approach. Read about in A Simple Decorator Approach for Building Web Applications with Python Flask!


Need the code template? Get it in this repo at GitHub!

Regards,
Bruno Tatsuya.