JWT Authentication and Authorization for Python Flask REST APIs
A practical guide to implement auth resources in your Flask REST API using JWTJSON 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 functioninit
will be called in ourapp.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.