Skip to content

Cross-Origin Resource Sharing (CORS)¤

Tip

This article explains the key concept of CORS very well.

The Cross-Origin Resource Sharing standard works by adding HTTP headers that let servers describe which origins are permitted to read a response from a web browser. For HTTP methods that can cause side-effects (in particular methods other than GET, or POST with certain MIME types), the specification mandates that browsers "preflight" the request with OPTIONS, and only then send the actual request. Servers can also tell clients whether "credentials" (cookies and HTTP authentication) should be sent with requests.

ASAB applies the same origin, methods, headers, and credentials policy to preflight (OPTIONS) and to actual responses. CORS is installed only on paths listed in cors_preflight_paths (or passed to enable_cors()). Auth and tenant wrappers skip OPTIONS so preflight is not blocked by authentication.

If the request has no Origin header, or the origin is not allowed, CORS headers are omitted. Untrusted origins are never echoed. A preflight request still receives 204 No Content.

Configuration¤

CORS starts automatically when [web] cors is non-empty. Leave it empty if the application will call WebContainer.enable_cors() from code.

[web]
cors=*
cors_preflight_paths=/*
cors_allow_headers=Authorization, Content-Type, X-App, X-Request-Id
cors_allow_methods=GET, POST, PUT, PATCH, DELETE, OPTIONS
cors_allow_credentials=no
Option Meaning
cors Origin policy. Empty (the default) does not start CORS. * allows every origin. Otherwise a comma- and/or whitespace-separated allowlist of origins, normalized to comma-separated values with no extra spaces. If the list contains *, it is treated as *.
cors_preflight_paths Path prefixes (/foo/*) and exact paths that receive CORS headers, including OPTIONS preflight. Values must start with "/". The default /* covers the whole application.
cors_allow_headers Value of Access-Control-Allow-Headers.
cors_allow_methods Value of Access-Control-Allow-Methods.
cors_allow_credentials When yes, responses include Access-Control-Allow-Credentials: true.

Credentials and *¤

Browsers reject Access-Control-Allow-Origin: * together with Access-Control-Allow-Credentials: true. When cors is * (or allow_origin="*") and credentials are enabled, ASAB echoes the request Origin instead of sending *. That allows any origin to make a credentialed request. Turn credentials off if you want a true wildcard (Access-Control-Allow-Origin: *).

Preflight paths¤

Preflight requests use the OPTIONS method on the same path as the actual request. Use cors_preflight_paths (or the preflight_paths argument of enable_cors()) to list those paths, separated by comma and/or whitespace.

A trailing glob is a prefix: /foo/* matches /foo and everything under /foo/. Other entries are exact paths. The glob is converted to an aiohttp route (/foo/{tail:.*}) only when registering OPTIONS, not by replacing * in the whole config string.

[web]
cors=*
cors_preflight_paths=/api/*, /.well-known/openid-configuration

Enabling CORS from code¤

Applications that decide allowed origins at runtime (for example SeaCat Auth checking registered clients) should leave [web] cors empty and call enable_cors():

container.enable_cors(
    allow_origin=client_svc.is_origin_allowed,
    preflight_paths=[
        "/openidconnect/*",
        "/.well-known/openid-configuration",
        "/.well-known/oauth-authorization-server",
        "/.well-known/jwks.json",
        "/.well-known/oauth-protected-resource",
        "/.well-known/oauth-protected-resource/*",
    ],
    allow_headers=["Authorization", "Content-Type", "X-App", "X-Request-Id"],
    allow_credentials=True,
)

allow_origin is a required argument. It may be "*", a string or iterable of origins (parsed like [web] cors), or a synchronous callable origin: str -> bool.

If enable_cors() is called again (for example after config started CORS with *, then the application installs a validator), the origin policy is replaced and any new preflight paths are added. The same OPTIONS route is not registered twice.

Reference¤

asab.web.WebContainer.enable_cors(allow_origin, preflight_paths=None, allow_headers=None, allow_methods=None, allow_credentials=None) ¤

Enable Cross-Origin Resource Sharing on this web container.

If [web] cors is non-empty, this method is called automatically during container construction. Applications such as SeaCat Auth can call it from code instead (leave cors empty and pass a callable origin check).

Calling this method again replaces the origin policy and adds any new preflight paths. OPTIONS routes that are already registered are not added twice.

Parameters:

Name Type Description Default
allow_origin Union[str, Iterable[str], Callable[[str], bool]]

"*" to allow every origin, a string or iterable of allowed origins, or a callable origin: str -> bool for a dynamic allowlist.

required
preflight_paths Union[str, Iterable[str], None]

Path prefixes (/foo/*) and exact paths that receive CORS headers, including OPTIONS preflight. Defaults to [web] cors_preflight_paths.

None
allow_headers Union[str, Iterable[str], None]

Allowed request headers. Defaults to [web] cors_allow_headers.

None
allow_methods Union[str, Iterable[str], None]

Allowed HTTP methods. Defaults to [web] cors_allow_methods.

None
allow_credentials Optional[bool]

Whether browsers may send cookies and Authorization. Defaults to [web] cors_allow_credentials. When this is true, the response echoes the request Origin even if allow_origin is "*"; Access-Control-Allow-Origin: * is never combined with credentials.

None
Source code in asab/web/container.py
def enable_cors(
	self,
	allow_origin: typing.Union[str, typing.Iterable[str], typing.Callable[[str], bool]],
	preflight_paths: typing.Union[str, typing.Iterable[str], None] = None,
	allow_headers: typing.Union[str, typing.Iterable[str], None] = None,
	allow_methods: typing.Union[str, typing.Iterable[str], None] = None,
	allow_credentials: typing.Optional[bool] = None,
):
	"""
	Enable Cross-Origin Resource Sharing on this web container.

	If `[web] cors` is non-empty, this method is called automatically during container
	construction. Applications such as SeaCat Auth can call it from code instead
	(leave `cors` empty and pass a callable origin check).

	Calling this method again replaces the origin policy and adds any new preflight
	paths. OPTIONS routes that are already registered are not added twice.

	Args:
		allow_origin: `"*"` to allow every origin, a string or iterable of allowed
			origins, or a callable `origin: str -> bool` for a dynamic allowlist.
		preflight_paths: Path prefixes (`/foo/*`) and exact paths that receive CORS
			headers, including OPTIONS preflight. Defaults to `[web] cors_preflight_paths`.
		allow_headers: Allowed request headers. Defaults to `[web] cors_allow_headers`.
		allow_methods: Allowed HTTP methods. Defaults to `[web] cors_allow_methods`.
		allow_credentials: Whether browsers may send cookies and Authorization.
			Defaults to `[web] cors_allow_credentials`. When this is true, the
			response echoes the request `Origin` even if `allow_origin` is `"*"`;
			`Access-Control-Allow-Origin: *` is never combined with credentials.
	"""
	if preflight_paths is None:
		preflight_paths = self.Config.get("cors_preflight_paths")
	if allow_headers is None:
		allow_headers = self.Config.get("cors_allow_headers")
	if allow_methods is None:
		allow_methods = self.Config.get("cors_allow_methods")
	if allow_credentials is None:
		allow_credentials = self.Config.getboolean("cors_allow_credentials")

	# Normalize paths before touching any state so a bad value cannot leave
	# the handler half-updated.
	preflight_paths = cors.normalize_path_list(preflight_paths)

	if self.CORSHandler is None:
		self.CORSHandler = cors.CORSHandler(
			allow_origin=allow_origin,
			paths=preflight_paths,
			allow_headers=allow_headers,
			allow_methods=allow_methods,
			allow_credentials=allow_credentials,
		)
	else:
		self.CORSHandler.set_policy(
			allow_origin,
			allow_headers,
			allow_methods,
			allow_credentials,
		)
		self.CORSHandler.add_paths(preflight_paths)

	self._register_preflight_routes()

asab.web.WebContainer.add_preflight_handlers(preflight_paths) ¤

Add OPTIONS handlers and CORS path patterns to already-enabled CORS.

Use enable_cors() to start CORS. This method only extends the set of paths.

Parameters:

Name Type Description Default
preflight_paths Iterable[str]

Path prefixes (/foo/*) and exact paths that should receive CORS, including OPTIONS preflight.

required
Source code in asab/web/container.py
def add_preflight_handlers(self, preflight_paths: typing.Iterable[str]):
	"""
	Add OPTIONS handlers and CORS path patterns to already-enabled CORS.

	Use `enable_cors()` to start CORS. This method only extends the set of paths.

	Args:
		preflight_paths: Path prefixes (`/foo/*`) and exact paths that should
			receive CORS, including OPTIONS preflight.
	"""
	if self.CORSHandler is None:
		raise RuntimeError("CORS is not enabled; call enable_cors() first.")
	self.CORSHandler.add_paths(preflight_paths)
	self._register_preflight_routes()