Add JWT Authentication

Add authentication with JWT tokens in this Secure development tutorial. Learn core concepts, hands-on implementation, and troubleshooting for secure JWT-based auth.

Focus: add authentication with jwt tokens

Sponsored

You’ve built your API, your database queries are clean, and your endpoints are live. But right now, anyone can call them. No login, no identity, no way to know who’s making the request — and that’s a disaster waiting to happen. Every unauthenticated endpoint is an open door to your data and your infrastructure. Adding authentication with JWT tokens is the key to locking that door, giving you a stateless, scalable way to verify users without storing sessions on the server. In this lesson, you’ll learn exactly how to implement JWT authentication in a Python web app, step by step, so you can protect your endpoints and know exactly who’s knocking.

The problem this lesson solves

Think about a typical REST API. It exposes endpoints like /api/users, /api/orders, or /api/admin. Without authentication, these endpoints are like a bank vault with the door left ajar — anyone who finds the URL can walk in and grab whatever they want. You need a way to:

  • Verify that a user is who they claim to be (authentication).
  • Authorize that user to access certain resources (authorization).
  • Maintain that state across multiple requests without overwhelming the server.

Traditional session-based authentication stores session data on the server (in memory or a database). That works, but it creates problems at scale — every request must hit the session store, and load-balanced servers need shared session storage. JWT (JSON Web Token) flips this around: the token itself contains all the information needed to authenticate the user. The server just verifies the token’s signature; it doesn’t need to check a database or a session cache. That’s a huge win for performance and scalability, and it’s the standard way to add authentication in modern microservices and SPAs.

By the end of this lesson, you’ll be able to add authentication with JWT tokens to your own Python application, protecting your endpoints from unauthorized access.

Core concept / mental model

Imagine JWT as a digitally signed ID card. The card has three parts:

  1. Header — says what algorithm was used to sign it (e.g., HS256).
  2. Payload — contains the claims: who the user is (sub), when the token expires (exp), and any custom data.
  3. Signature — a cryptographic hash of the header and payload, created with a secret key (or a private key).

Anyone can read the payload (it’s base64-encoded, not encrypted), but only someone who knows the secret can create a valid signature. So if an attacker tries to change the payload (say, from "user": "alice" to "user": "admin"), the signature will no longer match, and the server will reject the token.

This is stateless authentication: the server doesn’t store any session data. It just issues tokens on login and verifies them on each request. The token is the session.

Here’s a simple mental diagram:

Client                    Server
  |  POST /login            |
  | (username, password)    |
  |------------------------>|  Verify credentials
  |                         |  Create JWT
  |  <---------------------|
  |  JWT token             |
  |                         |
  |  GET /protected         |
  |  Authorization: Bearer <jwt> |
  |------------------------>|  Verify signature & expiry
  |                         |  Return protected data
  |  <---------------------|

How it works step by step

Let’s break down the flow of adding JWT authentication into concrete steps:

  1. User logs in — The client sends credentials (usually username and password) to a login endpoint, over HTTPS.
  2. Server verifies credentials — The server checks the username and password against its user store (e.g., a database). Never store plaintext passwords; use hashed passwords (e.g., bcrypt).
  3. Server creates a JWT — The server generates a token containing the user’s ID, expiration time, and any other claims. It signs it with a secret key.
  4. Client stores the token — The client (browser, mobile app) stores the token securely (e.g., in memory or an HttpOnly cookie) and includes it in subsequent requests.
  5. Client sends the token — For every protected request, the client sends the token in the Authorization header: Authorization: Bearer <token>.
  6. Server verifies the token — The server decodes the token, checks the signature, and validates the expiration. If valid, it extracts the user identity and proceeds. If invalid or expired, it returns a 401 Unauthorized.

That’s the whole dance. The key thing is that the server never has to look up a session — the token itself carries the identity.

Hands-on walkthrough

Let’s implement this in Python. We’ll use a minimal web framework (like Flask) and the PyJWT library. First, install dependencies:

pip install Flask PyJWT

Step 1: Create the Flask app and a secret key

Create a file app.py:

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

app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key-here'  # In production, use env var!

def create_token(user_id):
    payload = {
        'sub': str(user_id),      # subject: the user’s ID
        'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1),  # expiry
        'iat': datetime.datetime.utcnow()  # issued at
    }
    return jwt.encode(payload, app.config['SECRET_KEY'], algorithm='HS256')

def decode_token(token):
    return jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])

Step 2: Login endpoint that issues a JWT

In a real app, you’d verify the password against a database. Here we simulate with a hardcoded user.

@app.route('/login', methods=['POST'])
def login():
    data = request.get_json()
    username = data.get('username')
    password = data.get('password')

    # Dummy check — replace with real password verification!
    if username == 'admin' and password == '1234':
        token = create_token(1)
        return jsonify({'token': token})
    else:
        return jsonify({'error': 'Invalid credentials'}), 401

Test it:

curl -X POST http://localhost:5000/login -H "Content-Type: application/json" -d '{"username":"admin","password":"1234"}'

Expected output:

{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwiZXhwIjoxNj..."}

Step 3: Protect an endpoint with a decorator

Now we create a decorator that checks the token on protected routes.

def token_required(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        token = None
        auth_header = request.headers.get('Authorization')
        if auth_header and auth_header.startswith('Bearer '):
            token = auth_header.split(' ')[1]  # get the token after "Bearer"
        if not token:
            return jsonify({'error': 'Token is missing!'}), 401
        try:
            payload = decode_token(token)
            request.user_id = payload['sub']  # store user id for later use
        except jwt.ExpiredSignatureError:
            return jsonify({'error': 'Token has expired!'}), 401
        except jwt.InvalidTokenError:
            return jsonify({'error': 'Invalid token!'}), 401
        return f(*args, **kwargs)
    return decorated

@app.route('/protected')
@token_required
def protected():
    return jsonify({'message': f'Hello user {request.user_id}! This is protected data.'})

Now test with the token:

curl http://localhost:5000/protected -H "Authorization: Bearer <your-token>"

Expected output:

{"message":"Hello user 1! This is protected data."}

If you omit the header or use an invalid token, you get a 401 error.

Step 4: Handle token expiration gracefully

Our token expires after one hour. If you try to use it after that, you’ll get:

{"error":"Token has expired!"}

And that’s it — you’ve successfully added authentication with JWT tokens to your Flask app!

Compare options / when to choose what

JWT isn’t the only way to authenticate. Here’s a quick comparison:

Approach State Best for Pros Cons
Session-Cookie Server-side Traditional web apps Simple to revoke, no client logic Requires session store, scales poorly
JWT Stateless APIs, microservices Scalable, works with CDNs, frontend frameworks Hard to revoke, token size, secret management
OAuth2 Delegated Third-party access Standard, supports scopes Complex, multiple flows
API Keys None Service-to-service Simple Not tied to a user, low security

When to choose JWT:

  • You’re building a REST API that will be consumed by multiple clients (web, mobile).
  • You need to scale horizontally without shared session storage.
  • You want to keep the auth logic simple and self-contained.

When to avoid JWT:

  • You need immediate revocation on logout (e.g., financial apps). JWTs can’t be invalidated before expiry unless you add a blacklist.
  • You’re building a traditional server-rendered web app where session cookies are simpler.

Troubleshooting & edge cases

1. InvalidSignatureError or DecodeError

You get this when the token is malformed or the signing key is wrong.

  • Cause: The token was created with a different secret, or it’s been tampered with.
  • Fix: Check that app.config['SECRET_KEY'] is consistent across all requests and that you’re using the same algorithm for encoding and decoding.

2. ExpiredSignatureError

Token has passed its exp claim.

  • Fix: Either issue a new token via a refresh token flow, or ask the user to log in again. Never ignore expiry checks.

3. Token is missing in the request

If the Authorization header is absent or doesn’t start with Bearer, you’ll get a 401.

  • Fix: Make sure the client sends the header exactly as: Authorization: Bearer <token>. Watch out for capital B in Bearer — it’s case-sensitive per the HTTP spec.

4. Token is too long for some servers

Some web servers have limits on header size (e.g., 8KB). JWT tokens can get large if you put too much data in them.

  • Fix: Keep the payload minimal. Store only identifiers like user ID, not full user objects.

5. Clock skew

The exp and iat claims rely on server time. If your servers have different system times, tokens might expire prematurely.

  • Fix: Add a small leeway parameter when decoding: jwt.decode(token, key, leeway=30) to allow 30 seconds of drift.

What you learned & what's next

You now know how to add authentication with JWT tokens to a Python app. You’ve learned:

  • The core idea behind JWTs — a signed token that carries identity.
  • How to issue tokens on login and verify them on protected endpoints.
  • Common pitfalls like token expiry, header syntax, and secret management.

You’ve completed the hands-on exercise, so you can now protect APIs in your own projects.

Next in the Secure development track, you’ll dive into securing your secrets — managing environment variables, what to do when a secret leaks, and how to rotate signing keys. That’s crucial because your JWT is only as secure as your secret key. If an attacker gets your secret, they can mint their own tokens with any user ID — a total disaster. So, stay tuned!

Practice recap

Try extending the Flask app: add a /me endpoint that returns the logged-in user’s ID from the token, and implement a simple /logout that blacklists the token in memory. Then, experiment with different expiration times and test what happens when you send an expired token. This hands-on practice will solidify your understanding of JWT authentication and prepare you for the next lesson on secret management.

Common mistakes

  • Hardcoding the secret key in source code — if it’s in your repo, it’s compromised. Use environment variables or a secret manager.
  • Not checking token expiration properly — you must validate exp on every request, not just at login.
  • Forgesting to use HTTPS in production — JWTs are sent in headers, and over HTTP they can be sniffed by anyone on the network.
  • Storing tokens in localStorage in a browser — this exposes them to XSS attacks. Prefer HttpOnly cookies or memory storage.

Variations

  1. Use asymmetric signing (RS256) with a private/public key pair — the public key can be given to other services to verify tokens without sharing the signing secret.
  2. Implement a refresh token flow: issue short-lived access tokens and long-lived refresh tokens, so users don’t have to log in as often.
  3. Leverage a library like python-jose or authlib instead of PyJWT for more features (e.g., JWK support, OAuth2 integration).

Real-world use cases

  • A REST API for a mobile app where users log in once and the app stores the JWT to authenticate all subsequent requests.
  • Microservices architecture where internal services verify JWTs signed by an auth service to authorize requests between services.
  • A single-page application (SPA) that calls multiple backend APIs, each verifying the same JWT without shared session state.

Key takeaways

  • JWT is a stateless authentication mechanism — the server verifies the token’s signature instead of looking up a session.
  • A JWT consists of header, payload, and signature; the payload is readable but tamper-proof thanks to the signature.
  • Always include an exp claim and validate it on every request to prevent token replay after expiry.
  • Transmit JWTs in the Authorization header as Bearer <token> and use HTTPS to prevent interception.
  • Never store secrets in code; use environment variables and consider rotating keys regularly.
  • For immediate token revocation, JWTs require extra mechanisms like blacklists or short expirations.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.