Tutorial

JWT Authentication in Python APIs: A Practical Guide

Learn how to secure your Python APIs with JSON Web Tokens (JWT) using PyJWT and Flask. This guide covers token creation, endpoint protection, common mistakes, and production-ready patterns for FastAPI as well.

August 2026 8 min read 12 views 0 hearts

Locking Down Your Python APIs: A Practical Guide to JWT Authentication

If you've been building APIs with Python, you've probably realized that letting anyone access your endpoints is a recipe for disaster. That's where JSON Web Tokens (JWT) come in. They're not just some fancy tech buzzword—they're one of the most practical ways to secure your APIs without overcomplicating things.

When I first started working with APIs at PythonSkillset, I spent weeks trying to understand authentication methods. JWT turned out to be the sweet spot between security and simplicity. Let me show you how it actually works.

What Makes JWT Different?

Think of JWT as a digital ID card. When a user logs into your API, you create a token that contains their identity information. Instead of checking their password every single time they make a request, they just show you this token. You verify it's genuine, and that's it.

The beauty is in the structure. A JWT has three parts: - Header: Says what kind of token it is and how it's encrypted - Payload: The actual data (user ID, expiration time, etc.) - Signature: Digital proof that nobody tampered with the token

All these parts are base64-encoded and separated by dots, making something that looks like eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxMjN9.xxxxxxxx

Getting Started with PyJWT

First, you need to install the library. I recommend PyJWT because it's actively maintained and works well with Flask and FastAPI:

pip install pyjwt

Now here's a common scenario from my work at PythonSkillset. We needed to protect a user dashboard API. Here's how we set up the token creation:

import jwt
import datetime

def create_jwt_token(user_id):
    payload = {
        'user_id': user_id,
        'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1),
        'iat': datetime.datetime.utcnow()
    }

    token = jwt.encode(payload, 'your-secret-key', algorithm='HS256')
    return token

The exp claim is crucial—it sets an expiration time. Never create tokens that last forever. One hour works well for most applications.

Protecting Your Endpoints

Here's where things get interesting. You need to verify tokens on every protected route. Let me show you how we do this at PythonSkillset with Flask:

from functools import wraps
from flask import request, jsonify

def token_required(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        token = request.headers.get('Authorization')

        if not token:
            return jsonify({'message': 'Token is missing'}), 401

        try:
            # Remove 'Bearer ' prefix
            token = token.split(' ')[1] if 'Bearer' in token else token
            data = jwt.decode(token, 'your-secret-key', algorithms=['HS256'])
            current_user = data['user_id']
        except jwt.ExpiredSignatureError:
            return jsonify({'message': 'Token has expired'}), 401
        except jwt.InvalidTokenError:
            return jsonify({'message': 'Invalid token'}), 401

        return f(current_user, *args, **kwargs)

    return decorated

@app.route('/api/dashboard')
@token_required
def dashboard(current_user):
    return jsonify({'user_id': current_user, 'data': 'sensitive info'})

Notice how we handle the "Bearer" prefix? Many developers forget this and wonder why their tokens don't work. The Authorization header usually comes as Bearer eyJhbGci..., and you need to strip that prefix before decoding.

Common Mistakes I've Seen

At PythonSkillset, we review a lot of API security implementations. Here are the three most common mistakes:

1. Storing secrets in code Never hardcode your JWT secret. Use environment variables or a secrets manager:

import os
SECRET_KEY = os.environ.get('JWT_SECRET_KEY')

2. Ignoring token storage Where users store their tokens matters. Local storage in browsers is vulnerable to XSS. HTTP-only cookies are safer for web apps.

3. Not refreshing tokens Your tokens expire in an hour, but what if the user is actively using the app? Implement refresh tokens—short-lived access tokens paired with longer-lived refresh tokens.

Making It Production-Ready

For real-world applications, consider these additions:

  • Use RS256 instead of HS256 for distributed systems. HS256 requires sharing a secret, while RS256 uses public/private key pairs.
  • Add rate limiting to token endpoints. Otherwise, attackers can brute force your login.
  • Log failed token verifications. You'll spot attack patterns early.

Here's a more complete example using FastAPI that I wish I had when I started:

from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import jwt

app = FastAPI()
security = HTTPBearer()

async def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
    token = credentials.credentials
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
        return payload
    except:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid or expired token"
        )

@app.get("/api/protected")
async def protected_route(user = Depends(verify_token)):
    return {"message": "Access granted", "user": user}

The Depends(verify_token) pattern makes it incredibly easy to protect multiple routes without repeating code.

Testing Your Implementation

Don't skip this step. Use a tool like Postman or write simple tests:

import requests

# Get a token
login_response = requests.post('http://localhost:5000/login', json={
    'username': 'testuser',
    'password': 'testpass'
})
token = login_response.json()['token']

# Use the token
headers = {'Authorization': f'Bearer {token}'}
response = requests.get('http://localhost:5000/api/dashboard', headers=headers)
print(response.status_code)  # Should be 200

The Bottom Line

JWT authentication doesn't have to be complicated. Start with the basics—create tokens, verify them, handle errors gracefully. As you get comfortable, add layers like refresh tokens and role-based access.

Remember, security is a journey, not a destination. The APIs I build today at PythonSkillset are far more secure than what I wrote six months ago, and that's exactly how it should be. Keep learning, keep testing, and your users' data will stay safe.

Comments

Questions, corrections, and tips stay visible for everyone reading this page.

0 in thread

Join the discussion

Shown next to your comment.

Up to 4,000 characters

No comments yet

Be the first to leave a note — it helps the next reader.