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
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:
- Header — says what algorithm was used to sign it (e.g., HS256).
- Payload — contains the claims: who the user is (
sub), when the token expires (exp), and any custom data. - 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:
- User logs in — The client sends credentials (usually username and password) to a login endpoint, over HTTPS.
- 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).
- 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.
- 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.
- Client sends the token — For every protected request, the client sends the token in the
Authorizationheader:Authorization: Bearer <token>. - 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 samealgorithmfor 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 capitalBinBearer— 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
leewayparameter 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
expon 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
localStoragein a browser — this exposes them to XSS attacks. Prefer HttpOnly cookies or memory storage.
Variations
- 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. - 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.
- Leverage a library like
python-joseorauthlibinstead ofPyJWTfor 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
expclaim and validate it on every request to prevent token replay after expiry. - Transmit JWTs in the
Authorizationheader asBearer <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.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.