Introduction
Let's imagine authentication as the bouncer at your favorite club, checking IDs and ensuring only the right people get in. When developing APIs, we use tokens, similar to a wristband given at the club, proving that you've been checked and can party freely!
One of the most popular types of these tokens is the JSON Web Token, or short JWT("jot"). Just like a wristband, a JWT carries information about the user, verifying who they are and what they're allowed to do on our web application. It's like a VIP pass granting you access to the coolest areas of the club or denying access if you're not on the list.
This topic covers everything from the basic structure and properties of JWTs to points you should consider when deciding whether to use them in your Flask application, along with detailed instructions for implementation.
Session vs JWT
While there are many approaches to authentication, the two most common ones are session-based authentication and token authentication. Sessions are a common method for authentication and authorization in web applications. They involve storing a unique identifier on the client side, usually in the form of a cookie. The server uses this identifier to retrieve relevant user information from its database when needed. However, relying solely on sessions has several limitations. Firstly, as the number of concurrent users grows, the server must manage increasing amounts of session data, potentially leading to performance issues. Secondly, sessions are primarily designed for browser-based interactions, limiting their usefulness in scenarios involving mobile apps or other APIs.
In contrast, tokens, specifically JWTs, operate differently from traditional sessions because they do not require a server-stored session ID in a cookie. Instead, JWTs include all required user information within the token itself. During the login process, the server generates a JWT containing the user's details and returns it to the client, such as a browser or mobile app. The client then saves the JWT for future requests. On each new request, the client sends the JWT to the server, which verifies and decodes it to obtain the user's information. Valid tokens permit the user to access the desired resource. Compared to sessions, JWTs provide improved scalability and flexibility due to their self-contained design.
A closer look inside
A JWT is a compact, self-contained token that securely transmits information between parties as a JSON object. This information can be verified because it's digitally signed. Let's take a look at a sample token
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiIsImxhbmciOiJlbiJ9.Z0EWN-mh27_8vIRcyl1ik5tc3aoLneAee67IOEw6Lhc
This token might appear gibberish at first glance, but it actually contains the following parts, each separated by .:
- Header: The header contains information about the token, such as the algorithm used to sign it and the token type. In this case, the header is
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. You can easily decode this with Python's builtin base64 package to {"alg": "HS256","typ": "JWT"}. - Payload: The payload contains the user's information, such as their name, email address, and expiration date. In this case, the payload is
eyJuYW1lIjoiSm9obiIsImxhbmciOiJlbiJ9. which again is decoded to{"name": "John","lang": "en"}. - Signature: The signature is a cryptographic hash of the header and payload. In this case, the signature is
Z0EWN-mh27_8vIRcyl1ik5tc3aoLneAee67IOEw6Lhc.
The signature acts as a digital fingerprint of the entire JWT. When a client receives a JWT from a server, it can verify the authenticity of the token by checking if the signature matches the expected value. If the signature doesn't match, the client knows the token has been tampered with or is invalid.
The signature is computed using a cryptographic algorithm like HMAC SHA-256 or RSA encryption. Using these strong algorithms makes the likelihood of generating identical signatures for unrelated messages extremely low, making it difficult for attackers to create fake tokens that would pass verification checks.
Since the signature is calculated based on the entire content of the JWT, including the header and payload, any modification to either of these sections would cause the signature to fail validation. This means that even if an attacker were able to intercept a JWT at any point, they wouldn't be able to modify its contents without triggering detection.
It's important to note that the payload contains claims/data which are merely encoded, not encrypted. This means anyone with the token can easily read the data, so sensitive information such as passwords should never be stored in a JWT.
Flask-jwt-extended
That is enough theory. Let's now dive into the implementation of JWT in Flask. Although various libraries exist for managing JWTs in Flask, you will use Flask-JWT-Extended in this topic due to its comprehensive functionality and ease of use. Not only does Flask-JWT-Extended support JWT creation and verification, but it also allows you to handle token expiration, revocation, blacklisting, and much more.
Let's get started.
- Installation:
pip install flask-jwt-extended. - Configuration: configuring Flask-JWT-Extended with a Flask application, similar to other Flask extensions, is straightforward. Simply let the
JWTManagertake overfrom flask import Flask from flask_jwt_extended import JWTManager app = Flask(__name__) app.config['JWT_SECRET_KEY'] = 'your-secret-key' # Replace 'your-secret-key' with your actual secret key jwt = JWTManager(app)And voila, Flask-JWT-Extended is now configured with your Flask application and ready to use.
Additionally, the secret key helps prevent unauthorized parties from tampering with the token during transmission between the server and clients.
JWT_SECRET_KEY configuration variable to secure JSON Web Tokens (JWT) through encryption and authentication. During token generation, the library generates a unique digital signature using the secret key, ensuring the integrity and authenticity of the token. Upon receiving an incoming JWT, the library checks whether the signature matches the original one created during issuance; if they don't match, the token is considered tampered with and rejected, preventing unauthorized access to protected resources.
Without a secure JWT_SECRET_KEY, your application would be vulnerable to attacks like token hijacking or impersonation. Therefore, it's essential to keep your JWT_SECRET_KEY confidential and store it securely, such as in an environment variable.
While it's possible to define the secret key at various points in the code, it's most common to do it right after creating the Flask instance object, as done in the above code block.
For a clearer understanding of JWTs implementation, working through a practical example is best. Therefore, you are going to build a simple login API that leverages JWTs for user authentication. This will be helpful to understand both the concepts behind JWT as well as the flask-jwt-extended library.
In this example, instead of a database model, you will use a simple dictionary to represent your user data. This approach helps to simplify the concepts and focus on the core mechanics of JWT authentication. However, you would likely use a database model to store and retrieve user data in a real-world application.
from flask import Flask, request, jsonify
from flask_jwt_extended import JWTManager, create_access_token
app = Flask(__name__)
app["JWT_SECRET_KEY"] = "secret-key"
jwt = JWTManager(app)
user = {"username":"Nate", "password":"you-will-never-guess"} #mock user
@app.route("/login", methods = ["POST"]
def login():
username = request.form.get("userame")
password = request.form.get("password")
if not username:
return jsonify({"msg": "Missing username parameter"}), 400
if not password:
return jsonify({"msg": "Missing password parameter"}), 400
if username == user['username'] and password == user['password']:
access_token = create_access_token(identity=username) #create the jwt token
return jsonify(access_token=access_token), 200
return jsonify({"msg": "Bad username or password"}), 401
In the login view function above, you get the username and password from the user using forms. After checking their value against the stored ones, you create a JWT and return it as JSON.
The main focus here is the create_access_token function, which takes as input identity a string identifying the user associated with the token. In most cases, this would be the user's username or email address.
But now you may ask, what if I want to store more information and not just the identity of the user? You can pass a dictionary of extra data via the additional_claims parameter of the create_access_token() method. This will allow you to store additional metadata alongside the standard username or email claim.
After this, you can create routes that are only available to users with valid tokens in their Authorization header, as described above, by adding the decorator @jwt_required() after Flask's @app.route().
#...
from flask_jwt_extended import JWTManager, create_access_token, jwt_required, get_jwt()
#...
@app.route("/protected")
@jwt_required() #protects the route
def protected():
current_user = get_jwt()["sub"]
return {"message": f"Hello, {current_user}! This is a protected route."}
get_jwt() is used to get a dictionary containing data you inputted to create_access_token() function beforehand, as well as other predefined data. The major ones are:
- jti: which unique identifier for the JSON token.
- sub: the value passed as identity in
create_access_token(). which is, in our case, the username. - iat , exp : time of creation and the default expiration time in Unix timestamp in seconds, respectively.
- all the other key/value data you have added through
additional_claimsin thecreate_access_token()function
Just like that, you can use JWT to authenticate users during logins. And as long as the user has the tokens, they will stay logged in. You might want to revoke/disable the tokens at some point, but again flask-jwt-extended has got you covered.
JWT Blacklisting
JWT revocation enables you to invalidate specific JWTs without affecting other valid tokens. It involves keeping track of revoked tokens on the server side and checking against this list whenever a client tries to use a token. Revoked tokens are rejected, forcing clients to obtain fresh ones. Database tables are common ways to keep track of the revoked tokens, but here, you will simply create a list containing the jti unique identifier for the token.
All you need to do to revoke tokens from a certain collection is define a callback function using token_in_blocklist_loader()decorator from your JWTManager instance. The callback function is called every time you visit any protected route. It must receive the JWT header and JWT payload as arguments and return True if the JWT has been revoked.
#...
jwt = JWTManager(app)
#...
@jwt.token_in_blocklist_loader
def check_if_token_is_revoked(jwt_header, jwt_payload):
jti = jwt_payload["jti"] #1. get the id of the JWT tokenv from the jwt_payload
#2. check using the jti if this token belongs to your blacklist-list/database-table.
#3. return True if it does indeed exist in the blacklist collection, otherwises False.
In the following example, you want to revoke the token as soon as the user visits the "/logout." to do this, I simply register the JWT token with its ID jti to a dictionary and use this to revoke the token access to visit the "/protected" address from the above code
#.....
blacklist = []
@app.route("/logout", methods=["DELETE"])
@jwt_required()
def logout():
jti = get_jwt()["jti"]
blacklist.append(jti)
return jsonify(msg="Access token revoked")
@jwt.token_in_blocklist_loader
def check_if_token_is_revoked(jwt_header, jwt_payload):
jti = jwt_payload["jti"]
return jti in blacklist #True if in the list, otherwise False
After this, the revoked token can't be used to get resources from any of @jwt_required()routes after logging out.
You can read more about other ways to manipulate your tokens using flask-jwt-extended from their official documentation, which includes a very gentle introduction to topics such as refreshing tokens, token expiration, and the different options on where to store the jwt tokens.
Client-Side
Client-side usage of JWT involves adding the access token to every outgoing HTTP request as part of the Authorization header. The format of the header looks like this:
Authorization: Bearer <access_token>
only request containing this can be accepted if you want to access routes made with @jwt_required .
Conclusion
- JWT offers stateless authentication, allowing servers to verify and authorize requests without requiring session data stored on the server. This will come in handy when scaling up your system or deploying multiple instances of your application.
- Flask-JWT-Extended simplifies implementing JWT support in Flask applications by providing functions for generating, verifying, and blacklisting tokens.