How to secure your Flask web app with HTTPS and Basic Authentication
In this article I take a look at securing your Python/Flask web applications with Basic Authentication and HTTPS. The application for this example is a somewhat a contrived one that serves static JSON files containing patient Covid-19 test data. I chose Glitch as the hosting provider.
I'm actually going to split this over two articles. In this one I'll look at authentication, and in the next I'll look at configuring Flask to use HTTPS for either local testing or running on your own infrastructure. If you are hosting your Python/Flask app on Glitch (or Heroku), you get HTTPS for free.
Covid-19 SNAFUs and other thoughts on how not to do things
You may have read about the recent Covid-19 testing SNAFU. Turns out patient test results were being loaded into an XLS file circa 1980s. I remember those days, and it was normal for a PC back then to have an 8-bit processor and a 16-bit memory map. That gave you 2^16 memory locations. Apps had a hard time dealing with more than that, without resorting to complex paging systems. Unsurprisingly for the time, the XLS file format is limited to around 65,000 lines (2^16). Each test result was taking up 16 lines, so when the 65,000 row limit was reached result data was just lost. I don't know about you, but the fact that each result took 16 lines shouts DATABASE RECORD at me. This is something I've written about years ago, but what do I know, and who reads this blog anyway? Anyway, this partly inspired me to play around and hack something up on Glitch. So without further ado...
Making your communications secure
When it comes to making your communications with a web app secure there are several aspects you need to consider:
- Authenticity of the server.
- Security of the data transmission itself.
- Does the client requesting data actually have permission to access that data.
I'll go into each of these in more detail in the following sections.
Authenticity of the server
With the first item you are asking does the server belong to who you think it belongs to? This is normally confirm by reference to a Digital Certificate installed on the server, and signed by what's known as a Certificate Authority (CA). The CA is a well-known entity that can verify the authenticity of the company or individual who put in the certificate request. They have to confirm you are who you say you are if you are going to be issued with a certificate. There are several types of certificate: Domain Verified (DV) and Extended Verification (EV) are two common ones. DV means a certificate is used for all apps on a domain. For Glitch, for example, a DV certificate covers *.glitch.com, so you essentially get a free certificate. This is quite common with hosting providers. The same happens on Neocities, where this humble website is hosted. If for example you checked using:
curl -v --head https://tonys-notebook.com
You would see the following:
* Server certificate:
* subject: CN=tonys-notebook.com
* start date: Aug 18 23:30:48 2020 GMT
* expire date: Nov 16 23:30:48 2020 GMT
* subjectAltName: host "tonys-notebook.com" matched cert's "tonys-notebook.com"
* issuer: C=US; O=Let's Encrypt; CN=Let's Encrypt Authority X3
* SSL certificate verify ok.
You can see that my site has a certificate, and the CA was Let's Encrypt, who kindly provide countless certificates for free. Thank you!
If you do the same for the web app hosted on Glitch (more on this later) you'll see the following:
* Server certificate:
* subject: CN=glitch.com
* start date: Feb 18 00:00:00 2020 GMT
* expire date: Mar 18 12:00:00 2021 GMT
* subjectAltName: host "capable-flying-cymbal.glitch.me" matched cert's "*.glitch.me"
* issuer: C=US; O=Amazon; OU=Server CA 1B; CN=Amazon
* SSL certificate verify ok.
You'll notice the matched cert's "*.glitch.me"
part.
What will happen is the client (in this case Curl) will always, when running over HTTPS, check the digital certificate of the server, to verify the server's authenticity. If this cannot be confirmed Curl will drop out with an error message, or if using a browser you will receive dire messages about connecting to an insecure server and basically "back away". If your browser evers does this follow its advice! It is checking your certificate against a database of known CAs. It won't like a CA it doesn't know about.
Security of transmission
The transmission between the client server, where the potentially sensitive data is to be found (think Covid-19 test results or credit card details), when running over HTTPS, is secured using encryption. There is a protocol, TLS, to ensure that keys are exchanged and data is encrypted correctly. I won't go into details other than to say this protects you from someone on a server on the transmission path from taking a sneaky peek at your private data. Again if you look at the header data you will see the protocol doing its magic dance:
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Client hello (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS change cipher, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256
Authentication - does the client have permission?
The next part is to ask while the client may be able to receive or send data in an encrypted format, does that client actually have the right to see someone's patient data for example? We have to find a way of making sure only specific clients have access to certain data and this is done through authentication.
There are various ways to implement this but this article looks at using Basic Authentication (BA). This is where the client uses a username and password in making a request of the server. The username and password is Base64 encoded, but is not encrypted. For that reason I would not use BA over HTTP. However, in this case username:password
is being sent over HTTPS so it's then encrypted and is more secure (although there are edge cases).
Authentication in Flask
So, how do you implement BA in Flask. There are several ways. I will look at two:
- Using
Flask-BasicAuth
. - Using authorization information in the headers.
Using Flask-BasicAuth
This is best illustrated by example:
app.config['BASIC_AUTH_USERNAME'] = username
app.config['BASIC_AUTH_PASSWORD'] = password
basic_auth = BasicAuth(app)
...
@app.route("/get-patient/<string:patient_id>", methods=['GET'])
@basic_auth.required
def get_file(patient_id):
print('Fetching data file')
filename = patient_id + '.json'
try:
f = open (filename, mode='r', encoding='utf-8')
data = f.read()
return (data)
except FileNotFoundError:
return('Patient not found\n', 404)
You will need to add Flask-BasicAuth
to your requirements.txt
file.
Using headers
This shows access to the headers:
@app.route("/check-auth")
def check_auth():
if (request.authorization['username'] == username and request.authorization['password'] == password):
return ("Auth good")
else:
return ('You need to be authenticated', 401)
Complete code
The complete example code is shown here:
from flask import Flask, request, jsonify
from flask_basicauth import BasicAuth
import os
from dotenv import load_dotenv
load_dotenv()
username = os.getenv("USERNAME")
password = os.getenv("PASSWORD")
app = Flask(__name__)
app.config['BASIC_AUTH_USERNAME'] = username
app.config['BASIC_AUTH_PASSWORD'] = password
basic_auth = BasicAuth(app)
@app.route("/")
def index():
return ('Covid-19 testing results')
# One way to do basic auth
@app.route("/check-auth")
def check_auth():
if (request.authorization['username'] == username and request.authorization['password'] == password):
return ("Auth good")
else:
return ('You need to be authenticated', 401)
# Another way to do basic auth
@app.route("/get-patient/<string:patient_id>", methods=['GET'])
@basic_auth.required
def get_file(patient_id):
print('Fetching data file')
filename = patient_id + '.json'
try:
f = open (filename, mode='r', encoding='utf-8')
data = f.read()
return (data)
except FileNotFoundError:
return('Patient not found\n', 404)
if __name__ == "__main__":
app.run()
Obviously this is quick and dirty code for experimentation purposes and not meant for production.
Testing
You can test out the app (which I will leave hosted on Glitch):
curl -umedical:secret https://capable-flying-cymbal.glitch.me/get-patient/1234-abcd
If you try:
curl -uhacker:guess https://capable-flying-cymbal.glitch.me/get-patient/1234-abcd
You should not be able to see any patient data.
Next steps
In a future article, I'll be taking a look at making your Flask web apps work with HTTPS when you are hosting on your own infrastructure and especially when you are testing locally.