Implementing JWT Authentication in Python APIs

Introduction

JSON Web Tokens (JWT) have become a popular way to handle authentication and authorization in modern web applications. In this article, we will walk through how to implement JWT authentication in a Python Flask API, step by step.

What is JWT?

JWT (pronounced "jot") is a compact way to securely transmit information between parties as a JSON object. Think of it as a digital ID card that your API can check to verify who's making a request.

A JWT consists of three parts.

  1. Header: type of token and signing algorithm
  2. Payload: contains claims like the user data
  3. Signature: verifies the token hasn't been tampered with using the encoded header, encoded payload, and a secret key.

Setting Up Our Flask API

Let's start setting up our flask API.

Step 1. Initial Setup

Let's import the basic packages required for our project.

from flask import Flask, request, jsonify, make_response, render_template
import jwt
from datetime import datetime, timedelta
from functools import wraps

Here, we have imported our basic packages.

Now, we can set up our Flask app and add a secret key to generate jwt token. This secret is crucial - it's used to sign our JWTs so we can verify them later.

from flask import Flask
app = Flask(__name__)
app.config["SECRET_KEY"] = "a322444343443434"

I am storing this here for demo purposes; it's better to store them separately in an env file to keep it safe. Also, we should always use a strong secret key to make our process stronger.

Step 2. Store data in memory

Since we are currently not using any external database, we are going to store the data in our project like below.

users_db = {
    "admin": {
        "username": "admin",
        "password": "admin123",
        "role": "admin"
    }
}

Here, we're using a simple dictionary to store users. In a real application, you'd use a proper database.

Step 3. Token Verification

Now, let's create logic for token verification.

from functools import wraps
from flask import request, jsonify
import jwt

def token_required(func):
    @wraps(func)
    def decorated(*args, **kwargs):
        token = None

        if 'Authorization' in request.headers:
            token = request.headers['Authorization'].split(" ")[1]

        if not token:
            return jsonify({'message': "Token is missing!"}), 401  # Unauthorized
        try:
            payload = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])
        except jwt.ExpiredSignatureError:
            return jsonify({'message': "Token has expired"}), 403  # Forbidden
        except jwt.InvalidTokenError:
            return jsonify({'message': "Invalid token"}), 403
        return func(*args, **kwargs) 
    return decorated

Here, the token_required decorator is a middleware that validates JWT tokens for protected routes. It first checks if a token exists in the Authorization header (in the format Bearer <token>). If no token is found, it returns a 401 Unauthorized error. If a token exists, it attempts to decode and verify it using the app's secret key. If the token is expired, it returns a 403 Forbidden error, and if the token is invalid for any other reason (e.g., tampered with), it also returns a 403. If the token is valid, the request proceeds to the original route function. This ensures only authenticated users with valid tokens can access protected endpoints.

We can use this decorator on any route that requires authentication.

Step 4. Our API Routes

Let's create a public route, and to access this, no authentication is needed.

@app.route('/public')
def public():
    return jsonify({"message": "For everyone"}), 200

Here, this is a simple endpoint, and anyone can access it - no token is required.

Now, let's create a protected route that requires a valid JWT token.

@app.route('/auth')
@token_required
def auth():
    return jsonify({"message" : 'JWT authentication successful.'}), 200

Here, we created an auth route and used the @token_required decorator, that's our JWT guard in action, and it will make sure users have the proper JWT token to access this route.

Now, let's create a registration route to let users register.

@app.route('/register', methods=['POST'])
def register():
    if not request.is_json:
        return jsonify({"message": "Request must be JSON"}), 400

    data = request.get_json()
    if not data.get('username') or not data.get('password'):
        return jsonify({'message': "Username and password are required"}), 400

    username = data['username']
    if username in users_db:
        return jsonify({"message": "User already exists"}), 409

    users_db[username] = {
        "username": username,
        "password": data['password'],
        "role": 'user'
    }
    return jsonify({"message": "Registration successful"}), 201

Here, this endpoint lets new users register and checks some basic conditions, like whether the request is in the proper format, whether the user already exists, etc. If everything is okay, we will let the user register. In a real app, you'd want to hash passwords before storing them, but here we are not doing that.

Now, let's let our user log in and get them their JWT token.

@app.route('/login', methods=['POST'])
def login():
    if not request.is_json:
        return jsonify({"message": "Request must be JSON"}), 400

    data = request.get_json()
    username = data.get("username")
    password = data.get("password")

    if not username or not password:
        return jsonify({'message': 'Username and password required'}), 400

    if username not in users_db:
        return jsonify({"message": "User not found"}), 404

    if not users_db[username]['password'] == password:
        return jsonify({"message": "Invalid password"}), 401

    token = jwt.encode(
        {
            'user': username,
            'exp': datetime.now() + timedelta(minutes=1)  # Token expires in 1 minute
        },
        app.config['SECRET_KEY'],
        algorithm='HS256'
    )
    return jsonify({'token': token})

Here, our users get their JWT after providing valid credentials. Notice that we set the token to expire in 1 minute; that means the token is only valid for 1 minute.

Step 5. Test API

Now, let's test our API in Postman.

Let's register a new user.

New user

Here, we registered a user and got a success message and 201 status code denoting that the resource is created.

Before logging in, let's access a route that doesn't require us to be logged in.

Logged in

Here, we successfully accessed this route even without login since this was the public route.

Now, let's log in the user so we can access authenticated route.

Send

Here, we logged in a user and in response got 200 status denoting Ok. This returns a token you'll use for authenticated requests.

Response

Here, while making the request, we passed the JWT token in the Authorization header to access the auth route since it was decorated with the token required, and we successfully accessed this and got the success message.

With this, we successfully used JWT to authenticate our Python API.

Security Considerations

While JWTs are powerful, we should keep these points in mind while using them.

  • Always use HTTPS to prevent token interception
  • Store your secret key securely (not in code)
  • Set reasonable expiration times
  • Never store sensitive data in the JWT payload
  • Consider refresh tokens for better security

Conclusion

Here, we completed this basic authentication implementation using JWT in Python API. Although this will authorize the API but remember that authentication is a complex topic - always use well-tested libraries and follow security best practices in production applications.

Also, we can make this project better by adding password hashing, implementing refresh tokens, adding more claims to the JWT payload, integrating with a real database, and more.

Up Next
    Ebook Download
    View all
    Learn
    View all