arrow Articles

CORS in FastAPI

date July 22, 2021

date 7 min read

Want to know how to configure CORS headers correctly in FastAPI? We show you how!
Tim Armstrong Tim Armstrong, Founder, Consultant Engineer, Expert in Performance & Security
CORS in FastAPI

SHARE

linkedin share twitter share reddit share

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?

A diagram showing the handshake done by a browser when a cross-origin request is made

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. 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 correct. 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 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:

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 large APIaaS platforms (“large” meaning hundreds of thousands of applications in this case). Starlette could improve this performance by utilising a set or dictionary.

To get basic CORS support working we’re going to add the following code below below the app = FastAPI() definition.

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

The allow_origins 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 argument allows for a closer facsimile of our Nginx example, but still suffers from the same risks.

allow_credentials

The allow_credentials argument simply adds the Access-Control-Allow-Credentials: true header to the HTTP response. Allowing 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

The allow_headers 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, 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:

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 utilising python’s LRU Cache could help here if TTFB performance is a key concern.

Using an in-memory DB, or even a regular hashtable, as opposed to the list that the middleware uses by default would also 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 customising 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.

arrow Articles overview