Authentication with JWTs
At the end of the introductory course, we introduced a popular extension for Flask: Flask-JWT-Extended (link).
While this e-book cannot explain everything about the extension, we will aim to cover everything used in this introductory API—of course any questions head to the Slack channel and ask away!
What is a JWT?
A JWT is a JSON Web Token, it is a bunch of encoded data into a long string. The encoded data contains things like:
- An
iss
field, which is generally used to identify uniquely the user that generated this JWT; - A
jti
field, which is a unique identifier for this JWT (not for the user!); - An
alg
field, which defines which algorithm was used to encode this JWT; - An
exp
field, which contains a timestamp of the expiration date of this JWT; - Arbitrary data we want to include in it (called "claims").
For more information on JWTs, please read this OpenID RFC definition.
Decorators
Flask-JWT-Extended provides some decorators for our resource methods.
@jwt_required
Decorating a method with this decorator means before executing the function body, Flask-JWT-Extended will check that the request headers contain a valid Authorization
header in this format:
Authorization: Bearer {{access_token}}
Where access_token
is a valid encoded JWT. For example, it might look like this:
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1NDEwOTQ1NzEsIm5iZiI6MTU0MTA5NDU3MSwianRpIjoiNjQ0NzFjYjQtOTRkMC00MmQyLTk5ZWQtZDFmYWFhZWQzYTBlIiwiZXhwIjoxNTQxMDk1NDcxLCJpZGVudGl0eSI6MSwiZnJlc2giOnRydWUsInR5cGUiOiJhY2Nlc3MifQ.ebE_7IdNXlBlxstkaAmdDM0D6Cj52eqjYuJ6CWswr-8
If the access token is included and is valid, then Flask-JWT-Extended keeps track of the decoded data as long as we're responding to the request that contained it.
Then we can use the functions (explained below) to get more information about the JWT—such as get_jwt_identity()
to get the value of the iss
field in the JWT. In our application, this field will contain the id
field of the user that generated the JWT.
@jwt_optional
Similar to the decorator above, but there is no hard requirement that the access token be included in the Authorization header. If it is not included, then we proceed as if the user was not logged in (which let's face it, if they didn't include their access token, they are not).
Users are only logged in if they include a JWT in their request. Otherwise our API has no way of telling who is making a request and whether they are logged in or not. It is the access token that tells us a user is logged in!
@fresh_jwt_required
Similar to the decorators above, but the JWT included must be marked as "fresh". In our API, we're marking tokens as "fresh" when they are generated immediately following a user log in request.
If they are generated using token refresh (read more about it in the Token Refresh feature page), they are marked as "non-fresh".
We use fresh tokens when we want to make really sure the user is who they say they are. Because they need to log in in order to gain a fresh token, we can be almost certain they are genuinely them (and not, for example, have forgotten to log out in a public computer!).
Some endpoints we may want to ensure token freshness for:
- Users changing their password
- Users deleting important data from their account
- Users interacting with the administration panel of a product
- Users exporting important data
Note these endpoints do not exist in our application, they are just some examples.
Functions
get_jwt_identity()
This function from Flask-JWT-Extended is used to retrieve the value of the identity (iss
field) in the JWT. Our application is saving the user's id
field into this iss
field, so this retrieves a user id
.
We can then use it to retrieve user details from the database.
Example usage:
class ItemList(Resource):
@jwt_optional
def get(self):
user_id = get_jwt_identity() # Getting JWT identity
items = [item.json() for item in ItemModel.find_all()]
if user_id: # If identity is present, we return all item details
return {"items": items}, 200
return ( # Otherwise we return only item names
{
"items": [item["name"] for item in items],
"message": "More data available if you log in.",
},
200,
)
get_jwt_claims()
This function is used to retrieve all other claims from the JWT. For example, our application is saving a claim field is_admin
, which should be True
if the user is an administrator of our application.
Example usage:
@jwt_required
def delete(self, name):
# Using claims to check user is an admin before deleting an item
claims = get_jwt_claims()
if not claims["is_admin"]:
return {"message": "Admin privilege required."}, 401
item = ItemModel.find_by_name(name)
if item:
item.delete_from_db()
return {"message": "Item deleted."}, 200
return {"message": "Item not found."}, 404
get_raw_jwt()
This function allows us to get all JWT fields, so we can access the more seldom used ones such as jti
(unique JWT identifier).
We use this in our application to add individual JWT IDs to a blacklist so they cannot be re-used (this is how you implement logout—more information in the Logout feature page!)
Example usage:
class UserLogout(Resource):
@jwt_required
def post(self):
jti = get_raw_jwt()["jti"]
user_id = get_jwt_identity()
BLACKLIST.add(jti)
return {"message": "User <id={}> successfully logged out.".format(user_id)}, 200
Token freshness
This is a larger topic, so we've written its own page for it. Check it out at the Token Refresh feature page!