In this installment of our Python FastAPI learning series, we explore the implementation of SSL/HTTPS for localhost and also the enabling of Cross-Origin Resource Sharing, or CORS.

🐍 Index of the Complete Series.

117-feature-image.png
Python FastAPI: Implementing SSL/HTTPS and CORS

The code requires Python 3.12.4. Please refer to the following discussion on how to upgrade to Python 3.12.4.

🚀 Please note, complete code for this post can be downloaded from GitHub with:

git clone -b v0.6.0 https://github.com/behai-nguyen/fastapi_learning.git

❶ I have previously implemented both of these using Rust. Please refer to the following two posts below:

  1. Rust: actix-web get SSL/HTTPS for localhost. The sections on OpenSSL Toolkit and Generate a Self-Signed Encrypted Private Key and a Certificate are relevant to this post. If you’re not yet familiar with self-signed certificates, please refer to these sections. They discuss concepts that aren’t specific to Rust.
  2. Rust: actix-web CORS, Cookies and AJAX calls. The general discussion on CORS and AJAX calls in this post should complement the official FastAPI tutorial CORS (Cross-Origin Resource Sharing).

❷ The changes to the code are quite minimal. The updated structure of the project is outlined below. Since there are no changes to any files in the ./src and ./tests directories, we have omitted these two from the illustration below.

– Please note, those marked with are updated, and those marked with are new.

/home/behai/fastapi_learning/
.
├── cert ☆
│ ├── cert.pem
│ └── key.pem
├── .env ☆
├── logger_config.yaml
├── main.py ★
├── pyproject.toml ★
├── pytest.ini
├── README.md ★
└── ...

❸ The implementation of SSL/HTTPS for localhost.

FastAPI makes the implementation of SSL/HTTPS for localhost fairly easy. My principle reference is this post Adding HTTPS to FastAPI. It is more than a year old: the ssl paramater in this call uvicorn.run("main:app", host="0.0.0.0", port=8000, ssl=ssl_context) is outdated, and so we don’t actually need the cryptography package either.

The implementation is straightfoward. First, we need to generate the self-signed certificate files. Then, we need to get application to load and use these files.

⓵ To generate the self-signed certificate, on both Windows 10 and Ubuntu 22.10 run the command:

openssl req -x509 -newkey rsa:4096 -nodes -out cert.pem -keyout key.pem -days 365

Be prepared, you will need to fill in the following prompts:

Country Name (2 letter code) [AU]: AU
State or Province Name (full name) [Some-State]: Victoria
Locality Name (eg, city) []: Melbourne
Organization Name (eg, company) [Internet Widgits Pty Ltd]: Personal
Organizational Unit Name (eg, section) []: Development
Common Name (e.g. server FQDN or YOUR name) []: <Windows: Full computer name> <Linux: hostname --fqdn>
Email Address []: behai_nguyen@hotmail.com

For Common Name (e.g. server FQDN or YOUR name), on Windows 10, it is the full computer name, in Linux it is the hostname. For more information, please refer to the Generate a Self-Signed Encrypted Private Key and a Certificate section mentioned previously.

The self-signed encrypted private key and the certificate files, key.pem and cert.pem, are stored in the ./cert sub-directory.

⓶ The code changes occur only in the main.py module:

82
83
84
85
86
87
88
89
90
91
92
93
#
# Remove the code block below to use the command:
#
#    (venv) <venv path> uvicorn main:app --host 0.0.0.0 --port 5000 --ssl-keyfile ./cert/key.pem --ssl-certfile ./cert/cert.pem
#
# The command to run with the below code block:
#
#    (venv) <venv path> python main.py
#
if __name__ == "__main__":
    uvicorn.run("main:app", host="0.0.0.0", port=5000, \
                ssl_keyfile="./cert/key.pem", ssl_certfile="./cert/cert.pem")

FastAPI certainly makes it a lot easier.

The available routes should now be accessed via https:

  1. GET, https://0.0.0.0:port/admin/me: Returns the currently logged-in user’s information in either JSON or HTML format. This route is accessible only to authenticated sessions.
  2. GET, https://0.0.0.0:port/auth/login: Returns the application login page in HTML format.
  3. POST, https://0.0.0.0:port/auth/token: Authenticates users. The response can be in either JSON or HTML format.
  4. POST, https://0.0.0.0:port/auth/logout: Logs out the currently logged-in or authenticated user. Currently, this redirects to the application’s HTML login page.
  5. GET, https://0.0.0.0:port/: This is the same as https://0.0.0.0:port/auth/login.
  6. GET, https://0.0.0.0:port/auth/home: Returns the application home page in HTML format after a user has successfully logged in. This route is accessible only to authenticated sessions.
  7. GET, https://0.0.0.0:port/api/me: This is a duplicate of https://0.0.0.0:port/admin/me, but this route returns the currently logged-in user’s information in JSON only.
  8. POST, https://0.0.0.0:port/api/login: This is a duplicate of https://0.0.0.0:port/auth/token, but the response is in JSON only.

❹ Enabling Cross-Origin Resource Sharing, or CORS.

We can just follow the official tutorial page CORS (Cross-Origin Resource Sharing) to enable CORS. Instead of hard-coding the CORS information, we can load them from the .env file.

We will need the python-dotenv package. Please re-run the editable install command:

(venv) <venv path> pip install -e .

The code changes in the main.py module should be self-explanatory.

❺ CORS, AJAX and cross domain in action.

In this final section, we will illustrate how CORS behaves. We use the HTML page ajax_test.html which calls the function runAjaxCrossDomain(…) to send AJAX requests to the application. You can just copy this ajax_test.html page to your localhost and run it locally.

💥 Please note, about HTTPS and localhost: I have observed that some browsers, such as DuckDuckGo, will not allow access to self-signed certificate endpoints like https://localhost/... or https://0.0.0.0/... The error displayed is NET::ERR_CERT_AUTHORITY_INVALID.

FireFox and other Chromium-based browsers warn about security risk, just accept and proceed. You still might have problems accessing https://0.0.0.0/... endpoints via AJAX. I have found that, first access a GET endpoint, such as https://192.168.0.16:5000/auth/login, accept the risk and proceed, and you should get a valid response. Then, using the same browser to run the ajax_test.html page. This will get around the AJAX accessing problem.

⓵ The ALLOW_ORIGINS for CORS is set to http://localhost. We will illustrate the following requests: logging in, retrieving the logged-in user information, and finally, logging out.

● The login request:

URL: https://192.168.0.16:5000/auth/token
Method: POST
Content Type: application/x-www-form-urlencoded; charset=UTF-8
Body: username=behai_nguyen@hotmail.com&password=password
Header: {"x-expected-format": "application/json"}

We should receive a response as illustrated in the screenshot below:


● The request to retrieve the information of the logged-in user:

URL: https://192.168.0.16:5000/admin/me
Method: GET
Content Type: application/x-www-form-urlencoded; charset=UTF-8
Body: 
Header: {"Authorization":"Bearer behai_nguyen@hotmail.com", "x-expected-format":"application/json"}

We don’t actually need to include the “Content Type” and the “Body” fields.”

We should receive a response as illustrated in the screenshot below:


● The logout request:

URL: https://192.168.0.16:5000/auth/logout
Method: GET
Content Type: application/x-www-form-urlencoded; charset=UTF-8
Body: 
Header: {"Authorization":"Bearer behai_nguyen@hotmail.com"}

We don’t actually need to include the “Content Type” and the “Body” fields.”

We should receive a response as illustrated in the screenshot below:


⓶ The ALLOW_ORIGINS for CORS is set to https://google.com. This means that the application expects AJAX requests to come from https://google.com. Since we are sending AJAX requests from http://localhost, they should be rejected. And indeed, they are, as illustrated in the screenshots below:


❻ Writing this post has made me appreciate FastAPI even more 😂… Implementing SSL/HTTPS for localhost is straightforward; I didn’t expect it to be this easy.

Thank you for reading. I hope you find the information in this post useful. Stay safe, as always.

✿✿✿

Feature image source:

🐍 Index of the Complete Series.