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.
- Header: type of token and signing algorithm
- Payload: contains claims like the user data
- 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.