Using JWT Flask JWT Authentication- A Quick Guide

This tutorial helps you build a simple Flask API and demonstrates how to secure it using JWT. In the end, you can test your API authentication using a sample schema.
profile
Babatunde KoikiFirst published: 2021-12-09Last updated: 2025-06-25
guest-post,securing-flask-api-with-jwt
Table of Contents

What is Authentication?

Authentication is an essential part of any web application. But unfortunately, it is not always easy to implement.

What is Authentication?

Authentication is a process of verifying that an entity is who they claim to be. For example, a user might authenticate by providing a username and password. If the username and password are valid, the system will check if the user can access the resource. After the system checks the user's details against its database and if the details are valid, the user is thus authenticated and can access available resources.

Authentication Factors

The following factors are used to authenticate a user.

Single-factor Authentication

This authentication is used when a user provides a username/email/phone number and a password. This is the most common and weakest authentication factor. The user simply inputs the email and password, and the system checks if the data is valid; if valid, the user gets authenticated and can access the resource. What happens if another person who is not a legitimate user tries to access the resource? The system denies access to the resource.

Multi-factor Authentication

This authentication uses more than one factor to authenticate a user. For example, the user tries to log in with, say, email and password; if the data is correct, a code is sent to the user's phone number, and the user is asked to input the code. If the user enters the code, the user gets logged in; otherwise, the user is not authenticated. Some applications even go a step further by not using two factors but using three factors.

Types of Authentication

There are three types of authentication, as follows:

  1. Knowledge Authentication: The user is asked something that only they can provide or know -- e.g., password. This is the most common type and also the easiest.
  2. Property Authentication: The user is asked for something they own or possess. For example, they can use a hardware authentication device like YubiKey or an authenticator app on their phone. The idea is that users will be asked to set an authentication factor that verifies the identity more securely. This isn’t always used alone; it’s used alongside another authentication type, say, Knowledge authentication.
  3. Biological Authentication: The user is asked to verify their identity using something biologically unique to them -- e.g., a fingerprint or iris scan.

In most applications, knowledge and property authentication are used as an extra layer of authentication.

Authentication vs. Authorization

The following are the differences between authentication and authorization:

  1. Authentication verifies identity (usually through credential validation)) while authorization grants or denies permissions to a user.

  2. Authentication is used to verify that users are who they say they are. Authorization is used to verify that a user has permission to do something.

Starter Application

In this tutorial, you'll work on authentication in flask middleware for an existing API built with Flask and PyMongo. The API is a book library API using which users can create books and upload cover images for the books and relevant data. PyMongo is used to connect to the mongo database. You'll use the PyJWT library to generate and verify JWT tokens for auth in flask.

You can learn more about JSON Web Tokens (JWT) here.

To get started, clone the repository and set up the application by running the following commands:

1git clone https://github.com/LoginRadius/engineering-blog-samples.git # Clone the repository 2cd /Flask/loginRadius-flask-auth # change directory 3python3 -m venv env # create virtual environment; if you're using Windows, `py -m venv env` 4source env/bin/activate # activate virtual environment, if you're using windows, env/Scripts/activate 5pip install -r requirements.txt # install dependencies 6# https://packaging.python.org/en/latest/guides/installing-using-pip-and-virtual-environments/

The application is now set up and ready to run. You can run the app using the command flask run in the project directory. You can test that all the endpoints are working by testing the app in an API testing tool, like Postman.

Authentication Middleware

As you've noticed, anybody can access the API; you need to restrict access to the API. Create new book data if they have the correct data, then add, delete, and update book data, but you don't want that. To do this, you need to implement an authentication middleware.

When we talk about authentication with flask, middlewares are created in Flask by creating a decorator; a function can have multiple middlewares, and the order matters a lot.

To create your auth middleware, you need to install PyJWT -- the library you'll use to generate tokens. You’ll also use Pillow to alter image data before saving them to disk. Run the following command to install the packages:

1pip install pyjwt pillow

You need to add a secret key to your application; this is what you should pass to JWT.

Add the following to your app.py file below the app declaration.

1# app = Flask(__name__) 2SECRET_KEY = os.environ.get('SECRET_KEY') or 'this is a secret' 3print(SECRET_KEY) 4app.config['SECRET_KEY'] = SECRET_KEY

Let's create a file called auth_middleware.py in the root of your application and place the following inside this file:

1from functools import wraps 2import jwt 3from flask import request, abort 4from flask import current_app 5import models 6def token_required(f): 7@wraps(f) 8def decorated(*args, **kwargs): 9token = None 10if "Authorization" in request.headers: 11token = request.headers["Authorization"].split(" ")[1] 12if not token: 13return { 14"message": "Authentication Token is missing!", 15"data": None, 16"error": "Unauthorized" 17}, 401 18try: 19data=jwt.decode(token, current_app.config["SECRET_KEY"], algorithms=["HS256"]) 20current_user=models.User().get_by_id(data["user_id"]) 21if current_user is None: 22return { 23"message": "Invalid Authentication token!", 24"data": None, 25"error": "Unauthorized" 26}, 401 27if not current_user["active"]: 28abort(403) 29except Exception as e: 30return { 31"message": "Something went wrong", 32"data": None, 33"error": str(e) 34}, 500 35 return f(current_user, *args, **kwargs) 36 37return decorated

The function above is simply a decorator function. Inside this function, you check if there is an Authorization field in the headers part of the request; if this is missing, you return an authorization error.

Next, you check if it exists but is not valid; if it is not valid, you also return an authorization error.

If everything goes fine, then the view function is called. As you can see, you return f(current_user, *args, **kwargs), where f is the next decorator or function that's being called after this decorator -- in your case, the view function, which means that the first argument of any view function that uses this decorator must be current_user.

Auth Routes

You currently have a route to creating a new user, but you don't have one to log in. From what you have above, you're checking if the token passed as the header is valid, but now the question is -- how do you get to know the token. Basically, the login route fetches the token and sends it to the client.

Add the following function below the add_user function:

1@app.route("/users/login", methods=["POST"]) 2def login(): 3 try: 4 data = request.json 5 if not data: 6 return { 7 "message": "Please provide user details", 8 "data": None, 9 "error": "Bad request" 10 }, 400 11 # validate input 12 is_validated = validate_email_and_password(data.get('email'), data.get('password')) 13 if is_validated is not True: 14 return dict(message='Invalid data', data=None, error=is_validated), 400 15 user = User().login( 16 data["email"], 17 data["password"] 18 ) 19 if user: 20 try: 21 # token should expire after 24 hrs 22 user["token"] = jwt.encode( 23 {"user_id": user["_id"]}, 24 app.config["SECRET_KEY"], 25 algorithm="HS256" 26 ) 27 return { 28 "message": "Successfully fetched auth token", 29 "data": user 30 } 31 except Exception as e: 32 return { 33 "error": "Something went wrong", 34 "message": str(e) 35 }, 500 36 return { 37 "message": "Error fetching auth token!, invalid email or password", 38 "data": None, 39 "error": "Unauthorized" 40 }, 404 41 except Exception as e: 42 return { 43 "message": "Something went wrong!", 44 "error": str(e), 45 "data": None 46 }, 500

Protecting API Routes in Flask

So far, you've been able to create your auth middleware, but you need to use this middleware to protect routes. All you need to do is to pass this middleware immediately after the app.route middleware, then make current_user the first argument of the view function, as follows:

1@app.route('/') 2@token_required 3def user(current_user): 4 return jsonify(current_user) 5@app.route('/<pdt_id>') 6@token_required 7def product(current_user, pdt_id): 8return jsonify(Product.find({'user_id': pdt_id}))

Add this middleware (@token_required) to every function you only want authenticated users to access. In the end, your whole app.py file should look as follows.

1import jwt, os 2from dotenv import load_dotenv 3from flask import Flask, request, jsonify 4from save_image import save_pic 5from validate import validate_book, validate_email_and_password, validate_user 6load_dotenv() 7app = Flask(name) 8SECRET_KEY = os.environ.get('SECRET_KEY') or 'this is a secret' 9print(SECRET_KEY) 10app.config['SECRET_KEY'] = SECRET_KEY 11from models import Books, User 12from auth_middleware import token_required 13@app.route("/") 14def hello(): 15return "Hello World!" 16@app.route("/users/", methods=["POST"]) 17def add_user(): 18try: 19user = request.json 20if not user: 21return { 22"message": "Please provide user details", 23"data": None, 24"error": "Bad request" 25}, 400 26is_validated = validate_user(**user) 27if is_validated is not True: 28return dict(message='Invalid data', data=None, error=is_validated), 400 29user = User().create(**user) 30if not user: 31return { 32"message": "User already exists", 33"error": "Conflict", 34"data": None 35}, 409 36return { 37"message": "Successfully created new user", 38"data": user 39}, 201 40except Exception as e: 41return { 42"message": "Something went wrong", 43"error": str(e), 44"data": None 45}, 500 46@app.route("/users/login", methods=["POST"]) 47def login(): 48try: 49data = request.json 50if not data: 51return { 52"message": "Please provide user details", 53"data": None, 54"error": "Bad request" 55}, 400 56# validate input 57is_validated = validate_email_and_password(data.get('email'), data.get('password')) 58if is_validated is not True: 59return dict(message='Invalid data', data=None, error=is_validated), 400 60user = User().login( 61data["email"], 62data["password"] 63) 64if user: 65try: 66# token should expire after 24 hrs 67user["token"] = jwt.encode( 68{"user_id": user["_id"]}, 69app.config["SECRET_KEY"], 70algorithm="HS256" 71) 72return { 73"message": "Successfully fetched auth token", 74"data": user 75} 76except Exception as e: 77return { 78"error": "Something went wrong", 79"message": str(e) 80}, 500 81return { 82"message": "Error fetching auth token!, invalid email or password", 83"data": None, 84"error": "Unauthorized" 85}, 404 86except Exception as e: 87return { 88"message": "Something went wrong!", 89"error": str(e), 90"data": None 91}, 500 92@app.route("/users/", methods=["GET"]) 93@token_required 94def get_current_user(current_user): 95return jsonify({ 96"message": "successfully retrieved user profile", 97"data": current_user 98}) 99@app.route("/users/", methods=["PUT"]) 100@token_required 101def update_user(current_user): 102try: 103user = request.json 104if user.get("name"): 105user = User().update(current_user["_id"], user["name"]) 106return jsonify({ 107"message": "successfully updated account", 108"data": user 109}), 201 110return { 111"message": "Invalid data, you can only update your account name!", 112"data": None, 113"error": "Bad Request" 114}, 400 115except Exception as e: 116return jsonify({ 117"message": "failed to update account", 118"error": str(e), 119"data": None 120}), 400 121@app.route("/users/", methods=["DELETE"]) 122@token_required 123def disable_user(current_user): 124try: 125User().disable_account(current_user["_id"]) 126return jsonify({ 127"message": "successfully disabled acount", 128"data": None 129}), 204 130except Exception as e: 131return jsonify({ 132"message": "failed to disable account", 133"error": str(e), 134"data": None 135}), 400 136@app.route("/books/", methods=["POST"]) 137@token_required 138def add_book(current_user): 139try: 140book = dict(request.form) 141if not book: 142return { 143"message": "Invalid data, you need to give the book title, cover image, author id,", 144"data": None, 145"error": "Bad Request" 146}, 400 147if not request.files["cover_image"]: 148return { 149"message": "cover image is required", 150"data": None 151}, 400 152 book["image_url"] = request.host_url+"static/books/"+save_pic(request.files["cover_image"]) 153 book["user_id"] = current_user["_id"] 154 is_validated = validate_book(**book) 155 if is_validated is not True: 156 return { 157 "message": "Invalid data", 158 "data": None, 159 "error": is_validated 160 }, 400 161 book = Books().create(**book) 162 if not book: 163 return { 164 "message": "The book has been created by user", 165 "data": None, 166 "error": "Conflict" 167 }, 400 168 return jsonify({ 169 "message": "successfully created a new book", 170 "data": book 171 }), 201 172except Exception as e: 173 return jsonify({ 174 "message": "failed to create a new book", 175 "error": str(e), 176 "data": None 177 }), 500 178 179@app.route("/books/", methods=["GET"]) 180@token_required 181def get_books(current_user): 182try: 183books = Books().get_by_user_id(current_user["_id"]) 184return jsonify({ 185"message": "successfully retrieved all books", 186"data": books 187}) 188except Exception as e: 189return jsonify({ 190"message": "failed to retrieve all books", 191"error": str(e), 192"data": None 193}), 500 194@app.route("/books/<book_id>", methods=["GET"]) 195@token_required 196def get_book(current_user, book_id): 197try: 198book = Books().get_by_id(book_id) 199if not book: 200return { 201"message": "Book not found", 202"data": None, 203"error": "Not Found" 204}, 404 205return jsonify({ 206"message": "successfully retrieved a book", 207"data": book 208}) 209except Exception as e: 210return jsonify({ 211"message": "Something went wrong", 212"error": str(e), 213"data": None 214}), 500 215@app.route("/books/<book_id>", methods=["PUT"]) 216@token_required 217def update_book(current_user, book_id): 218try: 219book = Books().get_by_id(book_id) 220if not book or book["user_id"] != current_user["_id"]: 221return { 222"message": "Book not found for user", 223"data": None, 224"error": "Not found" 225}, 404 226book = request.form 227if book.get('cover_image'): 228book["image_url"] = request.host_url+"static/books/"+save_pic(request.files["cover_image"]) 229book = Books().update(book_id, **book) 230return jsonify({ 231"message": "successfully updated a book", 232"data": book 233}), 201 234except Exception as e: 235return jsonify({ 236"message": "failed to update a book", 237"error": str(e), 238"data": None 239}), 400 240@app.route("/books/<book_id>", methods=["DELETE"]) 241@token_required 242def delete_book(current_user, book_id): 243try: 244book = Books().get_by_id(book_id) 245if not book or book["user_id"] != current_user["_id"]: 246return { 247"message": "Book not found for user", 248"data": None, 249"error": "Not found" 250}, 404 251Books().delete(book_id) 252return jsonify({ 253"message": "successfully deleted a book", 254"data": None 255}), 204 256except Exception as e: 257return jsonify({ 258"message": "failed to delete a book", 259"error": str(e), 260"data": None 261}), 400 262@app.errorhandler(403) 263def forbidden(e): 264return jsonify({ 265"message": "Forbidden", 266"error": str(e), 267"data": None 268}), 403 269@app.errorhandler(404) 270def forbidden(e): 271return jsonify({ 272"message": "Endpoint Not Found", 273"error": str(e), 274"data": None 275}), 404 276if name == "main": 277app.run(debug=True)

Before running the application, let's look at the save_pic function inside the save_image.py file. This is the function responsible for saving uploaded pictures.

1from PIL import Image 2import secrets, os 3from flask import current_app as app 4def save_pic(picture): 5file_name = secrets.token_hex(8) +os.path.splitext(picture.filename)[1] 6if not os.path.isdir(os.path.join(app.root_path, 'static')): 7os.mkdir(os.path.join(app.root_path,"static")) 8os.mkdir(os.path.join(app.root_path,"static/images")) 9os.mkdir(os.path.join(app.root_path,"static/images/books")) 10if not os.path.isdir(os.path.join(app.root_path, 'static/images')): 11os.mkdir(os.path.join(app.root_path,"static/images")) 12os.mkdir(os.path.join(app.root_path,"static/images/books")) 13if not os.path.isdir(os.path.join(app.root_path, 'static/images/books')): 14os.mkdir(os.path.join(app.root_path,"static/images/books")) 15file_path = os.path.join(app.root_path, "static/images/books", file_name) 16picture = Image.open(picture) 17picture.thumbnail((150, 150)) 18picture.save(file_path) 19return file_name

You should also add the following functions as helper methods of the User model class.

1def disable_account(self, user_id): 2 user = db.users.update_one( 3 {"_id": bson.ObjectId(user_id)}, 4 {"$set": {"active": False}} 5 ) 6 user = self.get_by_id(user_id) 7 return user 8def encrypt_password(self, password): 9return generate_password_hash(password) 10def login(self, email, password): 11"""Login a user""" 12user = self.get_by_email(email) 13if not user or not check_password_hash(user["password"], password): 14return 15user.pop("password") 16return user

Your models.py file should look as follows:

1"""Application Models""" 2import bson, os 3from dotenv import load_dotenv 4from pymongo import MongoClient 5from werkzeug.security import generate_password_hash, check_password_hash 6load_dotenv() 7DATABASE_URL=os.environ.get('DATABASE_URL') or 'mongodb://localhost:27017/myDatabase' 8print(DATABASE_URL) 9client = MongoClient(DATABASE_URL) 10db = client.myDatabase 11class Books: 12"""Books Model""" 13def init(self): 14return 15def create(self, title="", description="", image_url="", category="", user_id=""): 16 """Create a new book""" 17 book = self.get_by_user_id_and_title(user_id, title) 18 if book: 19 return 20 new_book = db.books.insert_one( 21 { 22 "title": title, 23 "description": description, 24 "image_url": image_url, 25 "category": category, 26 "user_id": user_id 27 } 28 ) 29 return self.get_by_id(new_book.inserted_id) 30 31def get_all(self): 32 """Get all books""" 33 books = db.books.find() 34 return [{**book, "_id": str(book["_id"])} for book in books] 35 36def get_by_id(self, book_id): 37 """Get a book by id""" 38 book = db.books.find_one({"_id": bson.ObjectId(book_id)}) 39 if not book: 40 return 41 book["_id"] = str(book["_id"]) 42 return book 43 44def get_by_user_id(self, user_id): 45 """Get all books created by a user""" 46 books = db.books.find({"user_id": user_id}) 47 return [{**book, "_id": str(book["_id"])} for book in books] 48 49def get_by_category(self, category): 50 """Get all books by category""" 51 books = db.books.find({"category": category}) 52 return [book for book in books] 53 54def get_by_user_id_and_category(self, user_id, category): 55 """Get all books by category for a particular user""" 56 books = db.books.find({"user_id": user_id, "category": category}) 57 return [{**book, "_id": str(book["_id"])} for book in books] 58 59def get_by_user_id_and_title(self, user_id, title): 60 """Get a book given its title and author""" 61 book = db.books.find_one({"user_id": user_id, "title": title}) 62 if not book: 63 return 64 book["_id"] = str(book["_id"]) 65 return book 66 67def update(self, book_id, title="", description="", image_url="", category="", user_id=""): 68 """Update a book""" 69 data={} 70 if title: data["title"]=title 71 if description: data["description"]=description 72 if image_url: data["image_url"]=image_url 73 if category: data["category"]=category 74 75 book = db.books.update_one( 76 {"_id": bson.ObjectId(book_id)}, 77 { 78 "$set": data 79 } 80 ) 81 book = self.get_by_id(book_id) 82 return book 83 84def delete(self, book_id): 85 """Delete a book""" 86 book = db.books.delete_one({"_id": bson.ObjectId(book_id)}) 87 return book 88 89def delete_by_user_id(self, user_id): 90 """Delete all books created by a user""" 91 book = db.books.delete_many({"user_id": bson.ObjectId(user_id)}) 92 return book 93 94class User: 95"""User Model""" 96def init(self): 97return 98def create(self, name="", email="", password=""): 99 """Create a new user""" 100 user = self.get_by_email(email) 101 if user: 102 return 103 new_user = db.users.insert_one( 104 { 105 "name": name, 106 "email": email, 107 "password": self.encrypt_password(password), 108 "active": True 109 } 110 ) 111 return self.get_by_id(new_user.inserted_id) 112 113def get_all(self): 114 """Get all users""" 115 users = db.users.find({"active": True}) 116 return [{**user, "_id": str(user["_id"])} for user in users] 117 118def get_by_id(self, user_id): 119 """Get a user by id""" 120 user = db.users.find_one({"_id": bson.ObjectId(user_id), "active": True}) 121 if not user: 122 return 123 user["_id"] = str(user["_id"]) 124 user.pop("password") 125 return user 126 127def get_by_email(self, email): 128 """Get a user by email""" 129 user = db.users.find_one({"email": email, "active": True}) 130 if not user: 131 return 132 user["_id"] = str(user["_id"]) 133 return user 134 135def update(self, user_id, name=""): 136 """Update a user""" 137 data = {} 138 if name: 139 data["name"] = name 140 user = db.users.update_one( 141 {"_id": bson.ObjectId(user_id)}, 142 { 143 "$set": data 144 } 145 ) 146 user = self.get_by_id(user_id) 147 return user 148 149def delete(self, user_id): 150 """Delete a user""" 151 Books().delete_by_user_id(user_id) 152 user = db.users.delete_one({"_id": bson.ObjectId(user_id)}) 153 user = self.get_by_id(user_id) 154 return user 155 156def disable_account(self, user_id): 157 """Disable a user account""" 158 user = db.users.update_one( 159 {"_id": bson.ObjectId(user_id)}, 160 {"$set": {"active": False}} 161 ) 162 user = self.get_by_id(user_id) 163 return user 164 165def encrypt_password(self, password): 166 """Encrypt password""" 167 return generate_password_hash(password) 168 169def login(self, email, password): 170 """Login a user""" 171 user = self.get_by_email(email) 172 if not user or not check_password_hash(user["password"], password): 173 return 174 user.pop("password") 175 return user

Here's an example of the user request:

1{ 2 "name" : "abc xyz", 3 "email" : "xyz@gmail.com", 4 "password" : "Abc@123" 5}

Here, the name should have two words, and the password should have at least an uppercase later, a lower case letter, a digit, and a special character.

And an example of the book request:

1{ 2 "title":"name of book", 3 "cover_image": "path to image file locally", 4 "category": "['romance', 'peotry', 'politics', 'picture book', 'science', 'fantasy', 'horror', 'thriller'], 5 "description":"description", 6 "user_id":"user_id" 7}

While passing a book request, pass it via the form-data tab in Postman.

Conclusion

This article has explained flask JWT authentication .

In some cases, handling flask authentication yourself may not be good enough or efficient -- to overcome this, you can simply use third-party authentication providers like LoginRadius. You can check out this tutorial to learn how to add LoginRadius to your Flask application.

You can find the complete code for this article on Github. You can reach out to me on Twitter if you've any questions.

Babatunde Koiki
By Babatunde KoikiHe is an experienced software engineer with great passion for building reliable, scalable, and maintainable products. And he loves great documentation. When he is not writing code, he hangs out with friends or watches movies.
Featured Posts

How to Implement JWT Authentication for CRUD APIs in Deno

Multi-Factor Authentication (MFA) with Redis Cache and OTP

Introduction to SolidJS

Build a Modern Login/Signup Form with Tailwind CSS and React

Implement HTTP Streaming with Node.js and Fetch API

NestJS: How to Implement Session-Based User Authentication

NestJS User Authentication with LoginRadius API

How to Authenticate Svelte Apps

Flutter Authentication: Implementing User Signup and Login

How to Secure Your LoopBack REST API with JWT Authentication

Node.js User Authentication Guide

Your Ultimate Guide to Next.js Authentication

Local Storage vs. Session Storage vs. Cookies

How to Secure a PHP API Using JWT

Using JWT Flask JWT Authentication- A Quick Guide

Build Your First Smart Contract with Ethereum & Solidity

What are JWT, JWS, JWE, JWK, and JWA?

How to Build an OpenCV Web App with Streamlit

32 React Best Practices That Every Programmer Should Follow

How to Build a Progressive Web App (PWA) with React

Bootstrap 4 vs. Bootstrap 5: What is the Difference?

JWT Authentication — Best Practices and When to Use

What Are Refresh Tokens? When & How to Use Them

How to Upgrade Your Vim Skills

How to Implement Role-Based Authentication with React Apps

How to Authenticate Users: JWT vs. Session

How to Use Azure Key Vault With an Azure Web App in C#

How to Implement Registration and Authentication in Django?

11 Tips for Managing Remote Software Engineering Teams

Implementing User Authentication in a Python Application

Add Authentication to Play Framework With OIDC and LoginRadius

Share On:
Share on TwitterShare on LinkedIn