In previous articles, we've covered what CORS is, the reverse proxy methods to fixing "no 'access-control-allow-origin' header present," and a how to configure it for various languages and libraries. This tutorial covers how to get CORS setup in one of the first python ASGI (Asynchronous Server Gateway Interface) API Frameworks: FastAPI.
Whether you are a fan of using Python's Async for handling web requests, or not, FastAPI brings more to the table than just that. Pulling together features like Flask's routing interface, Django-Rest-Framework's automatic OpenAPI generation, Pydantic's input validation, Starlette's HTTP (along with WebSocket and GraphQL) handling & middleware all into one package makes FastAPI worth considering even if you don't plan on using Async at all.
However before we dive into how to set CORS up in FastAPI using Starlette's CORS middleware, let's have a brief recap on some CORS fundamentals.
What is CORS?
Cross-Origin Resource Sharing (CORS) is a protocol for relaxing the Same-Origin policy to allow scripts from one [sub]domain (Origin) to access resources at another. It does this via a preflight exchange of headers with the target resource.
When a script makes a request to a different [sub]domain than it originated from the browser first sends an `OPTIONS`
request to that resource to validate that the resource is expecting requests from external code.
The OPTIONS request carries the `Origin`
header, along with some other information about the request (check out the CORS explainer for more detail). The target resource then validates these details and (if valid) responds with its own set of headers describing what is permissible and how long to cache the preflight response for.
In our fixing "no 'access-control-allow-origin' header present" article we did this validation and response generation with an Nginx Reverse-proxy and some RegEx. Which, while a good DevOps solution to the problem, lacks a degree of flexibility and relies heavily on our RegEx being safe. This Reverse-Proxy approach we covered in that article is a very good stopgap solution as it is easy to set up and requires no code changes. It does however have some significant shortcomings.
The biggest of which being the RegEx at the centre of that approach.
Risks of RegEx
A large number of CORS vulnerabilities are caused by misconfigured RegEx search strings in such reverse-proxy configurations.
For example, the RegEx string `^https\:\/\/.*example\.com$`
might at first glance look like a valid solution to allowing us to have scripts from any subdomain of example.com contact our API over HTTPS. It certainly does work for such cases, as both www.example.com and blog.example.com would satisfy this RegEx.
However, what a lot of people miss is that it also accepts literally anything that starts with `https://`
ends in `example.com`
. So, for example, even `www.evilexample.com`
is a perfectly valid origin then according to the RegEx search string.
Obviously then, we want a solution that is similarly flexible (if not more flexible) while carrying a lower risk of misconfiguration.
Code-Based Solutions
Our next port of call then is to start looking at implementing this with as minimal a code change as possible. Fortunately, every production-ready web server framework has some level of CORS support. In earlier articles, we covered Django (Python), a GorillaMux (Golang), Laravel (PHP), Spring(Java), and Rails(Ruby) approaches to this, so if those are your preferred libraries (and Languages) then those articles are definitely worth a read!
In this tutorial, we're looking at how to set this up in FastAPI.
The Pedagogical Resource
For the rest of this tutorial we'll be using the following server stub:
```python
from fastapi import FastAPI
from models import Document, Session, select
from validation_models import NewDocument
app = FastAPI()
@app.get("/document/{item_id}")
async def read_document(item_id: int):
async with Session() as session:
result = await session.execute(select(Document).filter(Document.id == item_id))
document: Document = result.one()[0]
return document.as_dict()
@app.post("/document/")
async def write_document(new_document: NewDocument):
async with Session() as session:
async with session.begin():
document = Document(name=new_document.name, bodytext=new_document.body, tags=",".join(new_document.tags))
session.add(document)
return document.as_dict()
```
The `models.py`
file contains a fairly standard SQLAlchemy Async configuration and table definition (`class Document(Base):...`
), and the `validation_models.py`
contains a run-of-the-mill Pydantic model definition (`class NewDocument(BaseModel):...`
).
As this isn't a "FastAPI with SQLAlchemy Quickstart" guide, we're going to assume you already have your own models and don't need code samples for that.
Adding Basic CORS Support
So for convenience, presumably at least, FastAPI provides Starlette's CORS middleware at `fastapi.middleware.cors`
, you can of course also use it directly from Starlette at `starlette.middleware.cors`
it's exactly the same either way.
Out-of-the-box this middleware supports both a static list of origins and origin regex, which allows for quite some flexibility. However this static list is searched with plaintext comparisons in O(n) time, so could be a limitation for APIaaS platforms.
To get basic CORS support working we're going to add the following code below below the `app = FastAPI()`
definition.
```python
origins = [
"https://www.example.com",
"https://example.com",
"https://api.example.com"
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["POST", "GET"],
allow_headers=["*"],
max_age=3600,
)
```
The names here, should be self-explanatory if you've read our "What is CORS" explainer, but for convenience, here's a brief refresher.
allow_origins
This argument is a list (or tuple) of acceptable origins that the middleware will search and string compare each time a request comes in.
allow_origin_regex (Alternative to `allow_origins`
)
The `allow_origin_regex`
option allows for a closer facsimile of our Nginx example, but still suffers from the same risks.
allow_credentials
This is probably the simplest option as it simply adds the `Access-Control-Allow-Credentials: true`
header to the HTTP response. It allows the browser to send any Authorization cookies that it has that match the API's domain.
allow_methods
The `allow_methods`
argument provides the list of acceptable methods that code from the remote origin is allowed to use.
allow_headers
This argument supplies the list of acceptable header keys in addition to the ones accepted by default (I.E. `Accept`
, `Accept-Language`
, `Content-Language`
and `Content-Type`
).
max_age
The final argument that we've added `max_age`
sets the TTL for the browser to use when caching our responses.
Going Further
So far we've replicated a similar result to our Nginx Reverse-Proxy solution, which is fine, but we're working in code for a reason. We wanted to be able to dynamically add new Origins to our list of supported `Access-Control-Allow-Origin`
responses (using a database perhaps).
So How Do We Make it Dynamic?
The easiest way to make this dynamic is to subclass the `CORSMiddleware`
class from Starlette and override the `is_allowed_origin`
method with our own. An example of this might look like this:
```python
class DynamicCORSMiddleware(CORSMiddleware):
def is_allowed_origin(self, origin: str) -> bool:
with Session() as session:
result = session.query(Origin).filter_by(value=origin)
return result.count() > 0
```
Obviously using a SQL database for a preflight request like this might increase the TTFB (Time to First Byte) of the actual request before it's appropriately cached in the browser, so utilizing python's LRU Cache or an in-memory database like LMDB is probably desirable to maintain a responsive user experience.
Using an in-memory DB, or even a regular hashtable, as opposed to the list that the middleware uses by default can greatly improve the performance for APIaaS and CDN platforms bringing the search time down from O(n) to O(log n). Where at the moment a lot of the services default to using the arguably evil `*`
wildcard flag.
Caveats and Summary
The biggest caveat in using FastAPI's / Starlette's CORSMiddleware is that the options are all fail-safe. Meaning that if you forget to define one appropriately then it will reject any request that carries that option in the preflight request. This is a good thing as it means that you're not accidentally opening yourself up to unwanted traffic, but it can be a little obtuse when you're trying to debug your initial set-up.
The other point to consider here is that (as with most implementations of CORS middleware) This version does not allow per-origin rules without overriding a significant portion of the middleware yourself. As a result, you need to ensure that you're only exposing methods and headers that you trust all of the origins to use safely.
In summary, FastAPI's support for Starlette's middleware library makes configuring CORS correctly incredibly easy and allows for clean and easy to maintain code. Thanks to the simplicity of that middleware library extending and customizing the middleware to add more flexibility is incredibly simple.
Further Reading
If you hunger for more CORS knowledge check out our "What is CORS?" explainer article, or our Fixing "no 'access-control-allow-origin' header present" article.
This post was written by Tim Armstrong. Tim has worn many hats over the years, from "Dark Lord of Network Operations" at Nerdalize to "Lead Software Engineer" at De Beers. These days, he's going by "Consultant Engineer & Technical Writer." You can find him on Twitter as @omatachyru, and at plaintextnerds.com.