« Back to Index

[Python Tornado Basic Auth Middleware]

View original Gist on GitHub

Tags: #auth #authentication #basicauth #python #tornado #POST

Python Tornado Basic Auth Middleware.py

"""
Explanation:
    tornado.web.RequestHandler calls an internal `_execute` method before
    calling any other method such as `get` or `post` etc.

    we wrap the internal `_execute` method so that it either returns False if
    no basic authentication provided or it will return the result of calling
    the actual `_execute` method.

Tutorial:
    http://kevinsayscode.tumblr.com/post/7362319243/easy-basic-http-authentication-with-tornado
"""

import base64

import tornado.ioloop
import tornado.web


def require_basic_auth(handler_class):
    def wrap_execute(handler_execute):
        # returns True is basic auth provided, otherwise it sends back a 401
        # status code and requests user input their credentials.
        #
        # todo: write logic or pass in a function so that it can determine
        # whether the authentication is accepted (e.g. you find the credentials
        # within an external database).
        def check_basic_auth(handler, kwargs):
            def render_inputs(handler):
                # If the browser didn't send us authorization headers,
                # send back a response letting it know that we'd like
                # a username and password (the "Basic" authentication
                # method).  Without this, even if your visitor puts a
                # username and password in the URL, the browser won't
                # send it.  The "realm" option in the header is the
                # name that appears in the dialog that pops up in your
                # browser.
                handler.set_status(401)
                handler.set_header('WWW-Authenticate', 'Basic realm=Restricted')  # noqa
                handler._transforms = []
                handler.finish()

            auth_header = handler.request.headers.get('Authorization')
            if auth_header is None or not auth_header.startswith('Basic '):
                render_inputs(handler)
                return False

            # The information that the browser sends us is
            # base64-encoded, and in the format "username:password".
            # Keep in mind that either username or password could
            # still be unset, and that you should check to make sure
            # they reflect valid credentials!
            auth_decoded = base64.decodebytes(bytes(auth_header[6:], 'utf-8'))
            username, password = auth_decoded.decode('utf-8').split(':', 2)
            kwargs['basicauth_user'], kwargs['basicauth_pass'] = username, password  # noqa

            # foo and bar should be pulled from somewhere, not hardcoded!
            if username != "foo" and password != "bar":
                render_inputs(handler)
                return False

            return True

        # Since we're going to attach this to a RequestHandler class,
        # the first argument will wind up being a reference to an
        # instance of that class.
        def _execute(self, transforms, *args, **kwargs):
            if not check_basic_auth(self, kwargs):
                return False
            return handler_execute(self, transforms, *args, **kwargs)

        # return our new function, which either returns False if basic auth
        # wasn't provided, otherwise it returns the result of calling the
        # original _execute function.
        return _execute

    # wrap tornado's internal `_execute` method, which is called first before
    # any other method in the RequestHandler class
    handler_class._execute = wrap_execute(handler_class._execute)

    # return the modified class
    return handler_class


@require_basic_auth
class MainHandler(tornado.web.RequestHandler):
    # use !s inside format syntax to control string conversion
    # https://docs.python.org/3.6/library/string.html#formatstrings
    def get(self, basicauth_user, basicauth_pass):
        msg = 'Hi there, {0!s}!  Your password is {1}.'
        body = msg.format(basicauth_user, basicauth_pass)
        self.write(body)

    def post(self, **kwargs):
        basicauth_user = kwargs['basicauth_user']
        basicauth_pass = kwargs['basicauth_pass']
        msg = 'Hi there, {0!s}!  Your password is {1}.'
        body = msg.format(basicauth_user, basicauth_pass)
        self.write(body)


def make_app():
    return tornado.web.Application([
        (r"/", MainHandler),
    ])


if __name__ == "__main__":
    app = make_app()
    app.listen(9000)
    tornado.ioloop.IOLoop.current().start()