Basic authentication with Python Flask

For a simple web application in a home automation scenario, basic authentication can be a sufficient solution. Setting up a REST API and a web app with Flask is very easy, and adding basic authentication requires just a few more steps that can be reused between different applications.

Let’s say we have a Flask application that defines routes for a web page (index.html) and a REST endpoint that returns a secret message in JSON-format:


@app.route('/api/secret')
def api_secret():
return jsonify({'message':get_secret_message()})
@app.route('/')
def index():
return render_template('index.html', message=get_secret_message())

To protect these resources with basic authentication we want to check if the client is already authorized and in that case return the normal responses with status code 200. If the client is not authorized, a response with code 401 (unauthorized) that states that basic authentication is needed should be returned. When a browser receives this information, it will bring up a login dialog. Logging in will create a new request with an Authentication header containing the user name and password.

To achieve this with Python and Flask, we can use the wrap decorator in the functools library to create an authorization decorator that can be used on any function. I’ve take this idea from Armin Ronacher’s snippet http://flask.pocoo.org/snippets/8/


def ok_user_and_password(username, password):
return username == app.config['USERNAME'] and password == app.config['PASSWORD']
def authenticate():
message = {'message': "Authenticate."}
resp = jsonify(message)
resp.status_code = 401
resp.headers['WWW-Authenticate'] = 'Basic realm="Main"'
return resp
def requires_authorization(f):
@functools.wraps(f)
def decorated(*args, **kwargs):
auth = request.authorization
if not auth or not ok_user_and_password(auth.username, auth.password):
return authenticate()
return f(*args, **kwargs)
return decorated

If we decorate a function with @requires_authorization, the authorization check will run first, and if already authorized, the function will be executed. If not authorized, the authentication will be issued. In my example we would have:


@app.route('/api/secret')
@requires_authorization
def api_secret():
return jsonify({'message':get_secret_message()})
@app.route('/')
@requires_authorization
def index():
return render_template('index.html', message=get_secret_message())

Note that the @requires_authorization decorator must be placed after Flask’s @app.route decorator.

Adding SSL

As the user name and password is sent in clear text with every request when using basic authentication it is possible for a hacker to sniff the credentials. To add one degree of protection we can use encryption with https/SSL. To do this, we first need to create an ssl key and a certificate. OpenSSL can be used for this:


openssl genrsa 1024 > ssl.key
openssl req -new -x509 -nodes -sha1 -days 365 -key ssl.key > ssl.cert

Then, instead of starting the flask app with just app.run(host=’0.0.0.0′) we provide an extra argument that is a tuple with the paths to the key and cert files.


context = ('ssl.cert', 'ssl.key')
app.run(host='0.0.0.0', port=80, ssl_context=context)

Parsing arguments

When the application is started, I want to provide a user and and password for the one and only user that is allowed to access the system. I use the getopt module for this and store the data in Flask’s config dictionary for later use:


import sys, getopt
from basic_auth import app
def main(argv):
user = ''
password = ''
try:
opts, args = getopt.getopt(argv,"u:p:")
except getopt.GetoptError:
printHelp()
sys.exit(2)
for opt, arg in opts:
if opt in ("-u"):
user = arg
elif opt in ("-p"):
password = arg
if len(user) == 0 or len (password) == 0:
printHelp()
sys.exit(0)
print('User name is:', user)
print('Password is:', password)
app.config['USERNAME']=user
app.config['PASSWORD']=password
def printHelp():
print('Usage: ',__file__,'-u <user name> -p <password>')
if __name__ == '__main__':
main(sys.argv[1:])
context = ('ssl.cert', 'ssl.key')
app.run(host='0.0.0.0', port=80, ssl_context=context)

Now I can start the Flask application with a specified user and password that must be provided for accessing the REST API and the web page.

Structuring the application

A complete example can be fetched from GitHub:
https://github.com/LarsBergqvist/python_flask_authentication

It is setup as a structured Flask application where a runserver.py file is used for parsing the input and starting the program. __init__.py in a subfolder contains the creation of the Flask app object and the routes and helper function are put in separate files/modules in the same subfolder. If you want to try this example, make sure to put the generated ssl key and cert in the same folder as the runserver.py script. The application can be started with e.g.:

sudo python3 runserver.py -u JeffLynne -p ELO

and you should then be able to browse to https://localhost:80 and login with your specified user to see the message. You can also browse to https://localhost:80/api/secret to see the secret in JSON format via the api.

 

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s