Using OAuth2 with Flask-REST-JSONAPI

This is hopefully the first in a series of posts about adding oauth2 support to a basic web project using Flask and Ember. In the next few weeks there should be follow up articles on adding oauth2 support to Ember to talk to our backend server, as well as how to add Google and Facebook authentiction.

Authentication is confusing at the best of times, there’s so many terms and definitions that you tend to learn and use once while configuring authentication then you never think of them again. This post will help go over some of the basics of setting up a base Authentication program for your application and use a few common libraries to do so.

Authlib is a flexible OAuth library for Python. It is a very flexible framework that can be adapted to work in a variety of situations. I’ve been using Flask-REST-JSONAPI lately as I tend to use EmberJS for my frontend work and it’s support for the JSONAPI standard works seamlessly with Ember-data.

OAuth2 enables a third-party application to obtain limited access to a http service. The basic workflow for OAuth2 is as follows: 

  • The Client sends login identifiers to an authorization server
  • The authorization Server verifies the clients identity and returns a token to the client
  • The client sends the token to the resources server when requesting a resource
  • The resource server verifies the validity of the token, and if valid returns the resource to the client

Authorization servers may support several grant types. A grant type defines a way of how the authorization server will verify the request and issue the token.

Common Grant types are:

  • Authorization Code Grant – The client has a code that they can exchange for a token
  • Password Grant – The client has a username / password they can exchange for a token
  • Refresh Token Grant – The Client has a token that can be exchanged for an auth token

Authorization servers can require that Clients verify themselves before they can request a token on behalf of a user. A Client must provide it’s client information to obtain an access token.

Methods to do this are the following:

  • None – public client and no client secret
  • Client Secret – a code given to a client used to verify its self

For this example, I relied heavily on this documentation https://docs.authlib.org/en/latest/flask/2/index.html#flask-oauth2-server

First of all we need to create the models needed for our Authentication server, Authlib provides several base classes that simplifies the work needed to create tables and models for your server

import time
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

from authlib.integrations.sqla_oauth2 import (
   OAuth2ClientMixin,
   OAuth2AuthorizationCodeMixin,
   OAuth2TokenMixin,
)

class OAuth2Client(db.Model, OAuth2ClientMixin):
   __tablename__ = 'oauth2_client'

   id = db.Column(db.Integer, primary_key=True)
   user_id = db.Column(
       db.Integer, db.ForeignKey('user.id', ondelete='CASCADE'))
   user = db.relationship('User')

This represents a client in our system. This will create a table to store the client secret issued to your clients, as well as metadata specific to the client.

The metadata is stored as a JSON Field that stores the following information

  • Client_name
  • Client URI
  • Grant types
  • Redirect uris
  • Response types
  • Scope
  • token_endpoint_auth_method
class OAuth2Client(db.Model, OAuth2ClientMixin):
   __tablename__ = 'oauth2_client'

   id = db.Column(db.Integer, primary_key=True)
   user_id = db.Column(
       db.Integer, db.ForeignKey('user.id', ondelete='CASCADE'))
   user = db.relationship('User')

This will store the tokens issued to the users in your system. It stores the client they authenticated with, the token itself, scopes, issued and expiry dates, and a reference to the user it’s associated with.

class OAuth2Token(db.Model, OAuth2TokenMixin):
   __tablename__ = 'oauth2_token'

   id = db.Column(db.Integer, primary_key=True)
   user_id = db.Column(
       db.Integer, db.ForeignKey('user.id', ondelete='CASCADE'))
   user = db.relationship('User')

   def is_refresh_token_active(self):
       if self.revoked:
           return False
       expires_at = self.issued_at + self.expires_in * 2
       return expires_at >= time.time()

Our user class we will keep really simple, but for this authentication we are going to use email as the username instead of a custom username field. You will also notice we’re using passlib to verify a sha256 password hash on the user’s password

from passlib.hash import sha256_crypt

class User(db.Model):
   id = db.Column(db.Integer, primary_key=True)
   first_name = db.Column(db.String)
   last_name = db.Column(db.String)
   email = db.Column(db.String)
   password = db.Column(db.String)

   def get_user_id(self):
       return self.id

   def check_password(self, password):
      return sha256_crypt.verify(self.password, password)

Next we need to add support for how the authentication server will validate a simple password grant from a client.

from authlib.oauth2.rfc6749 import grants

class PasswordGrant(grants.ResourceOwnerPasswordCredentialsGrant):
   TOKEN_ENDPOINT_AUTH_METHODS = [
       'none', 'client_secret_basic', 'client_secret_post'
   ]
   def authenticate_user(self, email, password):
       user = User.query.filter_by(email=email).first()
       if user is not None and user.check_password(password):
           return user
from authlib.integrations.flask_oauth2 import AuthorizationServer, ResourceProtector

from authlib.oauth2 import OAuth2Error


query_client = create_query_client_func(db.session, OAuth2Client)
save_token = create_save_token_func(db.session, OAuth2Token)
authorization = AuthorizationServer(
   query_client=query_client,
   save_token=save_token,
)

def config_oauth(app):

   authorization.init_app(app)
   authorization.register_grant(PasswordGrant)
   
   # support revocation
   revocation_cls = create_revocation_endpoint(db.session, OAuth2Token)
   authorization.register_endpoint(revocation_cls)
  bearer_cls = create_bearer_token_validator(db.session, OAuth2Token)
  



@oauth_bp.route('/oauth/authorize', methods=['GET', 'POST'])
def authorize():
   user = current_user()
   if request.method == 'GET':
       try:
           grant = authorization.validate_consent_request(end_user=user)
       except OAuth2Error as error:
           return error.error
       return render_template('authorize.html', user=user, grant=grant)
   if not user and 'username' in request.form:
       email = request.form.get('username')
       user = User.query.filter_by(email=email).first()
   return authorization.create_authorization_response(grant_user=user)

@oauth_bp.route('/oauth/token', methods=['POST'])
def issue_token():
   return authorization.create_token_response(request)


@oauth_bp.route('/oauth/revoke', methods=['POST'])
def revoke_token():
   return authorization.create_endpoint_response('revocation')

@oauth_bp.route('/api/me')
@require_oauth(‘ALL’)
def api_me(**kwargs):
   user = kwargs['current_user']
   return jsonify(id=user.id, email=user.email)

The TOKEN_ENDPOINT_AUTH_METHODS define how it will verify your client before doing the username / password check.

The authenticate_user function is called when the Authentication server has a username password combination to look up. These are passed by the client in form post data to be verified.

Here we do a simple query in our database to find the user that corresponds to an email, and verifies their password is correct. We then return the user object back to the auth server request.

Next we need to initialize the auth server and configure it’s routes:

from authlib.integrations.flask_oauth2 import AuthorizationServer, ResourceProtector

from authlib.oauth2 import OAuth2Error


query_client = create_query_client_func(db.session, OAuth2Client)
save_token = create_save_token_func(db.session, OAuth2Token)
authorization = AuthorizationServer(
   query_client=query_client,
   save_token=save_token,
)

def config_oauth(app):

   authorization.init_app(app)
   authorization.register_grant(PasswordGrant)
   
   # support revocation
   revocation_cls = create_revocation_endpoint(db.session, OAuth2Token)
   authorization.register_endpoint(revocation_cls)
  bearer_cls = create_bearer_token_validator(db.session, OAuth2Token)
  



@oauth_bp.route('/oauth/authorize', methods=['GET', 'POST'])
def authorize():
   user = current_user()
   if request.method == 'GET':
       try:
           grant = authorization.validate_consent_request(end_user=user)
       except OAuth2Error as error:
           return error.error
       return render_template('authorize.html', user=user, grant=grant)
   if not user and 'username' in request.form:
       email = request.form.get('username')
       user = User.query.filter_by(email=email).first()
   return authorization.create_authorization_response(grant_user=user)

@oauth_bp.route('/oauth/token', methods=['POST'])
def issue_token():
   return authorization.create_token_response(request)


@oauth_bp.route('/oauth/revoke', methods=['POST'])
def revoke_token():
   return authorization.create_endpoint_response('revocation')

@oauth_bp.route('/api/me')
@login_required
def api_me(**kwargs):
  """ And finally here is a route to test that auth is working"""
   user = kwargs['current_user']
   return jsonify(id=user.id, email=user.email)

So now if you make a request to /oauth/authorize with the user login details in a form post

Then the endpoint will return you a valid token. You can then set this token in your header to request resources on your server (using the @login_required )

The login_required is a simple wrapper that does the following to verify your token, it simply looks for the provided token in the database and if valid allows the endpoint to execute:

import json
from functools import wraps

from flask import request, make_response

from flask_rest_jsonapi.errors import jsonapi_errors
from flask_rest_jsonapi.utils import JSONEncoder


def login_required(func):
   """Check that the user is logged in and has access
   :param callable func: the function to decorate
   :return callable: the wrapped function
   """
   @wraps(func)
   def wrapper(*args, **kwargs):
       if not 'Authorization' in request.headers:
           error = json.dumps(jsonapi_errors([{'source': '',
                                               'detail': 'A user must be logged in to view this resource',
                                               'title': 'No Authorization Header',
                                               'status': '403'}]), cls=JSONEncoder)
           return make_response(error, 403, {'Content-Type': 'application/vnd.api+json'})
       token_string = request.headers['Authorization'][7:]
       token = OAuth2Token.query.filter_by(access_token=token_string).first()
       if not token or token.revoked:
           error = json.dumps(jsonapi_errors([{'source': '',
                                               'detail': 'A user must be logged in to view this resource',
                                               'title': 'Invalid Authorization Token',
                                               'status': '403'}]), cls=JSONEncoder)
           return make_response(error, 403, {'Content-Type': 'application/vnd.api+json'})

       return func(*args, **dict(kwargs, current_user=token.user))
   return wrapper

Then in your request header you would add:

Authorization : Bearer TOKEN_GOES_HERE

And when you make the request to /api/me , It will return you your current users id and email.

One Reply to “Using OAuth2 with Flask-REST-JSONAPI”

Leave a Reply

Your email address will not be published. Required fields are marked *