Using the Decorator Pattern for Authentication

This is an entry in a larger series of blog posts about JWT authentication using Tornado and Neo4J. It’s not strictly necessary to read the previous post, but please check it out if you’re interested! Here’s the link.

Decorators are actually just a bit of syntactic sugar around the concept of higher order functions. A higher order function is one that takes other functions as arguments or returns another function. When you ‘decorate’ a function in python, you’re indicating that instead of calling your function directly, the python interpreter should instead pass your function to the decorator and call the function that the decorator outputs. . In our case, the decorator checks if the user is authenticated, and if so we allow the decorated function to execute.

In this example, jwt_auth is our decorator and handler_function is the function being decorated. We can also pass additional arguments to the decorated function so that it has access to authentication related data like the user’s email address.

"""JWT authenticator decorator for endpoints."""
import json
import jwt
from user_actions import user_exists


def jwt_auth(handler_function):
    """Authenticates JWT against User DB"""

    def _require_auth(self, *args, **kwargs):
        token = self.request.headers.get_list('Authorization').pop()
        if token:
            default_error_msg = "Invalid header authorization"
            parts = token.split()
            if parts[0].lower() != 'bearer':
                throw_authorization_error(self, default_error_msg)
            elif len(parts) == 1:
                throw_authorization_error(self, default_error_msg)
            elif len(parts) > 2:
                throw_authorization_error(self, default_error_msg)

            jwt_token = parts[1]
            try:
                jwt_secret = self.settings['jwt_secret']
                decoded_token = jwt.decode(jwt_token, jwt_secret)
                if user_exists(decoded_token['email']):
                    handler_function(self, *args, **kwargs)
                else:
                    throw_authorization_error(self, default_error_msg)

            except jwt.exceptions.DecodeError as error:
                error_msg = f'Invalid header authorization: {str(error)}'
                throw_authorization_error(self, error_msg)

        else:
            throw_authorization_error(self, default_error_msg)
    return _require_auth


def throw_authorization_error(handler, error_msg):
    """Responds to invalid request with error"""
    handler.set_status(401)
    handler.finish(json.dumps({
        'error': {
            'code': 401,
            'message': error_msg,
        }
    }))

We can then use our decorator as follows

    @jwt_auth
    def post(self):
        """Parses CSV to dataframe and stores decision tree to Neo4j."""
        csv_data = self.request.files["csv"][0]
        self.request.headers.get_list('Authorization').pop()
        data = pd.read_csv(
            StringIO(str(csv_data["body"], 'utf-8')))
        # pylint: disable=W0612
        train, test = train_test_split(data, test_size=.6)

        tree_model = create_tree_model(train)
        graphviz = sklearn.tree.export_graphviz(tree_model)
        graphviz_lines = graphviz.split('\n')
        java_types = map_java_datatypes(train)
        create_graph(graphviz_lines, java_types)

For the decorated function to successfully run, jwt_auth must return successfully. If not, it will never be called

As can be seen, authentication is a great use case for the decorator pattern as it allows us to no repeat ourselves whenever we need to ensure an endpoint has authentication. Rather than repeating our code over and over again, we can simply import our authentication and use the neat syntax of @jwt_auth which is syntactic sugar for transforming our method using the jwt_auth function.

If you have any questions, queries, quandaries or concerns feel free to email me at [email protected]. Moreover, if you think any of the code can be improved also feel free to email me, as I am always on a mission to improve my code.

Cheers!