×
By the end of this chapter, you should be able to:
Now that we have a better idea how to build APIs, we need to start thinking about how to secure them! We could use cookies and sessions, but this might become problematic if we want our API to interact with other APIs or different clients and mobile devices. A better alternative is to use token-based authentication; specifically, a technology called JSON Web Tokens (or JWT - pronounced JOT). A JSON Web token is a token that stores information and is comprised of three parts:
Header - A base64 encoded string which contains the type of token and the name of the algorithm used for the signature (see below)
Payload - A base64 encoded string which contains all of the keys and values for the token
Signature - A string which is the result of the HMAC SHA256 encrypted base64 encoded header, the base64 encoded payload and the secret key. This signature is what is used to verify the authenticity of the token when it is sent to the server.
HMAC SHA256 - Very difficult to hack/decrypt without a secret key (this is stored on the server)
Base64 - Very easy to decode, useful for easily converting into a 64 character length string.
To read more about JWTs, check out JWT.io or this blog post.
To get started using JWTs, we will use a module called Flask-JWT
. You can read more about it here. Let's start by creating a simple application with a User model.
It's important to realize that the header and the payload are not encrypted. They are simply encoded using a base64 encoding for ease of transmission across the Internet. In JavaScript, it's very easy to encode and decode base64 using the atob
and btoa
functions:
btoa('{"secretKey": "secret value"}') // "eyJzZWNyZXRLZXkiOiAic2VjcmV0IHZhbHVlIn0=" atob("eyJzZWNyZXRLZXkiOiAic2VjcmV0IHZhbHVlIn0=") // "{"secretKey": "secret value"}"
The security from a JWT comes from the signature, which is influenced by a secret key set on the server. Tampering with the payload isn't possible since there's no way for the client to also update the signature accordingly.
Having said that, you shouldn't put sensitive information in the payload: better to put a minimal amount of information (e.g. a user's id), so that if the server needs more information it can get it.
For more on base64 encodings, check out this article or this Stack Overflow question.
mkvirtualenv flask-jwt createdb flask-jwt pip install flask flask-jwt ipython flask-sqlalchemy psycopg2 flask-bcrypt flask-restful
Let's not worry about blueprints for now (we'll get to that in just a moment). Instead, let's just create some of the basic code we'll need in our app.py
. Note all of the things we're pulling from flask_jwt
(we'll get to them momentarily).
from flask import Flask, request from flask_sqlalchemy import SQLAlchemy from flask_bcrypt import Bcrypt from flask_restful import Api, Resource, fields, marshal_with from flask_jwt import JWT, jwt_required, current_identity app = Flask(__name__) bcrypt = Bcrypt(app) api = Api(app) app.config['SQLALCHEMY_DATABASE_URI'] = 'postgres://localhost/flask-jwt' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False app.config['SECRET_KEY'] = 'Ill never tell' db = SQLAlchemy(app) class User(db.Model): __tablename__ = 'users' id = db.Column(db.Integer, primary_key=True) username = db.Column(db.Text) password = db.Column(db.Text) def __init__(self, username, password): self.username = username self.password = bcrypt.generate_password_hash(password).decode('UTF-8') user_fields = { 'id': fields.Integer, 'username': fields.String } @api.resource('/users') class UserListAPI(Resource): @marshal_with(user_fields) def get(self): return User.query.all() @marshal_with(user_fields) def post(self): new_user = User(request.json['username'],request.json['password']) db.session.add(new_user) db.session.commit() return new_user if __name__ == '__main__': app.run(debug=True,port=3000)
(Note that in this example we've decorated the UserListAPI
class with @api.resource('/users')
. This has the same effect as what we did in the last chapter, when we defined the classes without decorators and then called something like api.add_resource(UserListAPI,'/users')
.)
Before checking out this API, let's hop into ipython
and add some data to our database:
from app import User, db db.create_all() matt = User("matt", "foo") elie = User("elie", "bar") tim = User("tim", "baz") db.session.add_all([matt,elie,tim]) db.session.commit()
If you start your application and curl http://localhost:3000/users
, you should get back JSON with each user's id and username. Passwords are intentionally missing: they aren't included in the user_fields
, because there's no reason for the client to get the hashed password, it should just be used on the server for authentication!
Posting should also work. Try curl -d '{"username": "janey", "password": "boom"}' http://localhost:3000/users --header "Content-Type: application/json"
.
So far, so good. We've can see all users in our application, and can create new users. But we don't have any authentication yet! We imported a bunch of stuff from flask_jwt
, so let's put it to use.
The first thing we'll need to do is write an authenticate
method which we can use to authenticate users. This method must be called authenticate
!
To authenticate a user using flask_jwt
, you must send a POST request to /auth
with the user's credentials. We'll authenticate by...
Here's how we can implement authenticate
:
def authenticate(username, password): user = User.query.filter(User.username == username).first() if bcrypt.check_password_hash(user.password, password): return user
To set up the authentication functionality, we also need to add the following line towards the bottom of our app.py
code (you can put it just before the if __name__ == '__main__'
check):
jwt = JWT(app, authenticate)
Now if you try to authorize with invalid credentials, you should get an error:
curl http://localhost:3000/auth -d '{"username": "matt", "password": "alwkjef"}' --header "Content-Type: application/json" { "description": "Invalid credentials", "error": "Bad Request", "status_code": 401 }
However, if you pass in the correct credentials, you should be just fine:
curl http://localhost:3000/auth -d '{"username": "matt", "password": "foo"}' --header "Content-Type: application/json" { "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZGVudGl0eSI6MSwibmJmIjoxNDc5MzQ0ODEwLCJpYXQiOjE0NzkzNDQ4MTAsImV4cCI6MTQ3OTM0NTExMH0.LOlkYNsgvUmCSVQHhchGLgJn-HSVTXfatulD8xqubhQ" }
If you decrypt the middle portion of the JWT, you should be able to see the payload explicitly; it should look something like this:
"{"identity":1,"nbf":1479344810,"iat":1479344810,"exp":1479345110}"
Identity
is the unique identifier for the user. "iat"
is a timestamp indicating when the JWT was issued, and "exp"
indicates when it expires (by default, five minutes from the "iat"
). "nbf"
stands for "not before," and indicates a time before which the JWT should not be accepted for processing. In this case, you see that the nbf
and the iat
have the same value.
(Curious about how to get a new token when the old one expires? We discuss that later in this chapter.)
We've got authentication working, but what about authorization? To build this out, we'll first need some routes we want to protect. Let's create a couple more routes, for getting and updating an individual user:
@api.resource('/users/<int:id>') class UserAPI(Resource): @marshal_with(user_fields) def get(self,id): return User.query.get(id) @marshal_with(user_fields) def patch(self,id): user = User.query.get(id) user.username = request.json['username'] user.password = bcrypt.generate_password_hash(request.json['password']).decode('UTF-8') db.session.add(user) db.session.commit() return user
Test that these routes work as expected using curl
.
Now, let's suppose we want to make these routes private, so that you can't access them unless you have authorized yourself as the correct user. We can do that by decorating them with @jwt_required()
. Inside of functions decorated in this way, we also have access to a current_identity
variable which refers to the current user:
@api.resource('/users/<int:id>') class UserAPI(Resource): @marshal_with(user_fields) @jwt_required() def get(self,id): return current_identity @marshal_with(user_fields) @jwt_required() def patch(self,id): current_identity.username = request.json['username'] current_identity.password = bcrypt.generate_password_hash(request.json['password']).decode('UTF-8') db.session.add(current_identity) db.session.commit() return current_identity
This won't work just yet, however, because we need to explicitly tell flask_jwt
how to define current_identity
. For that, we need to define an identity
function, and pass it into JWT
:
def identity(payload): user_id = payload['identity'] return User.query.get(user_id) jwt = JWT(app, authenticate, identity)
With this, these new routes should be protected! To access, them, you'll need to include a header of the form Authorization: JWT <INSERT_YOUR_JWT_HERE>
.
Now let's look at a more complex example with a 1:M with authentication
mkvirtualenv jwt-one-many pip install flask flask-jwt flask-restful flask-sqlalchemy flask-migrate psycopg2 ipython flask-bcrypt touch app.py createdb flask-jwt-example
from flask import Flask # current_identity is like current_user from flask_jwt import JWT, jwt_required, current_identity from flask_sqlalchemy import SQLAlchemy from sqlalchemy.exc import IntegrityError from flask_restful import Api, Resource, reqparse, marshal_with, fields from flask_bcrypt import Bcrypt from datetime import datetime app = Flask(__name__) bcrypt = Bcrypt(app) app.debug = True app.config['SECRET_KEY'] = 'super-secret' api = Api(app) db = SQLAlchemy(app) app.config['SQLALCHEMY_DATABASE_URI'] = "postgres://localhost/flask-jwt-example" app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False class User(db.Model): __tablename__ = 'users' id = db.Column(db.Integer, primary_key=True) username = db.Column(db.Text) password = db.Column(db.Text) posts = db.relationship('Post', backref='user', lazy='joined') created = db.Column(db.DateTime, default=datetime.utcnow) def __init__(self,username, password): self.username = username self.password = bcrypt.generate_password_hash(password).decode('UTF-8') class Post(db.Model): __tablename__ = 'posts' id = db.Column(db.Integer, primary_key=True) title = db.Column(db.Text) body = db.Column(db.Text) user_id = db.Column(db.Integer, db.ForeignKey('users.id')) created = db.Column(db.DateTime, default=datetime.utcnow) def __init__(self,title, body, user_id): self.title = title self.body = body self.user_id = user_id post_user_fields = { 'id': fields.Integer, 'username': fields.String, } post_fields= { 'id': fields.Integer, 'title': fields.String, 'body': fields.String, 'created': fields.DateTime(dt_format='rfc822'), 'user': fields.Nested(post_user_fields) } user_fields= { 'id': fields.Integer, 'username': fields.String, # don't want people seeing the password... 'posts': fields.Nested(post_fields), } @api.resource('/signup') class SignupAPI(Resource): @marshal_with(user_fields) def post(self): parser = reqparse.RequestParser() parser.add_argument('username', type=str, help='username') parser.add_argument('password', type=str, help='password') args = parser.parse_args() try: new_user = User(args['username'], args['password']) db.session.add(new_user) db.session.commit() except IntegrityError as e: return "Username already exists" return new_user @api.resource('/users/<int:id>') class UserAPI(Resource): @jwt_required() @marshal_with(user_fields) def get(self, id): return current_identity @marshal_with(user_fields) @jwt_required() def put(self, id): parser = reqparse.RequestParser() parser.add_argument('username', type=str, help='username') parser.add_argument('password', type=str, help='password') args = parser.parse_args() current_identity.name = args['username'] current_identity.password = bcrypt.generate_password_hash(args['password']).encode('utf-8') db.session.add(current_identity) db.session.commit() return found_user @jwt_required() def delete(self, id): db.session.delete(current_identity) db.session.commit() return None, 204 @api.resource('/users/<int:user_id>/posts') class PostsAPI(Resource): @marshal_with(post_fields) @jwt_required() def get(self, user_id): return current_identity.posts @marshal_with(post_fields) def post(self, user_id): parser = reqparse.RequestParser() parser.add_argument('name', type=str, help='Name') args = parser.parse_args() new_post = Post(args['name'], user_id) db.session.add(new_post) db.session.commit() return new_post @api.resource('/users/<int:user_id>/posts/<int:id>') class PostAPI(Resource): @jwt_required() @marshal_with(post_fields) def get(self, user_id, id): return Post.query.get_or_404(id) @marshal_with(post_fields) @jwt_required() def put(self, user_id, id): found_post = Post.query.get_or_404(id) parser = reqparse.RequestParser() parser.add_argument('name', type=str, help='Name') args = parser.parse_args() found_post.name = args['name'] db.session.add(found_post) db.session.commit() return found_post @jwt_required() def delete(self, user_id, id): post = Post.query.get_or_404(id) db.session.delete(post) db.session.commit() return None, 204 # get's run when a post is made to /auth with application/json as a content type def authenticate(username, password): user = User.query.filter(User.username == username).first() if bcrypt.check_password_hash(user.password, password): return user # get's run when jwt_required def identity(payload): user_id = payload['identity'] return User.query.get_or_404(user_id) jwt = JWT(app, authenticate, identity) if __name__ == '__main__': app.run()
Try to refactor this code to use blueprints and finish CRUD on the other resource!
There's much more you could do when it comes to working with tokens and building an API. Here are some topics worth considering if you'd like to push yourself:
Ideally, you will be deleting tokens on the client side since they will be stored in localStorage
. In order to invalidate generated tokens, we can set an expiration time on them by configuring the JWT_EXPIRATION_DELTA
value in Flask-JWT. Many OAuth2 providers will give access to something called a refresh token, which is a token used to generate new tokens that have a short expiration. You can read more about refresh tokens here or using other options like blacklisting tokens here. You can also look into Flask-JWT
extensions like this one, which streamline the process of creating refresh tokens.
When responding with data from an API call, it's very common that as your application grows it may be more expensive to fetch certain kinds of data. Very commonly, to speed up delivery, a cache is put in place so that if the data does not change, accessing it can be much faster after an initial request. You can learn more about caching requests here or through using a module called Flask-Cache or even with making your own here. In development it's fine to use a built in cache like SimpleCache. In production however, you will want to use a better tool like Memcached or Redis.
When you create a public API, you might want to limit the number of requests a user can make (before charging them money or just to keep traffic down). This is done through a process called rate limiting (where you return an expected response or a 429 error if there have been too many requests). You can learn more how to integrate that in your application using a module called Flask-Limiter.
When you're ready, move on to Testing Flask JSON APIs