MishkaCmsCore.Runtime.Plugs.ApiCorsPlug (Mishka CMS Core v0.0.1)

Copy Markdown View Source

CORS (Cross-Origin Resource Sharing) plug for API endpoints.

This plug handles CORS headers for API requests, comparing the request's Origin with the origins the current site allows. It supports both preflight (OPTIONS) requests and regular API requests, automatically setting the appropriate CORS headers based on the site configuration.

What an origin is

An origin is a scheme, a host and a port, and all three have to be the same: https://site.com does not allow http://site.com or https://site.com:9999, because a cookie is not kept apart by scheme or port and the answer carries access-control-allow-credentials: true. The request's Origin is read by MishkaCmsCore.Runtime.OriginValidator.request_origin/1 and must be nothing but those three.

What a site allows

Worked out by MishkaCmsCore.Runtime.OriginValidator.site_origins/1 when the site is saved, and looked up in MishkaCmsCore.Runtime.States.SiteState (allows_origin?/2) for each request, so a site created or edited while the server runs is answered for what it allows now. A loopback host is localhost, a name under .localhost, 127.0.0.1 or ::1.

The site hasAllowedNot allowed
host: "site.com"https://site.comhttp://site.com, https://site.com:8443, https://sub.site.com
host: "site-a.localhost" (loopback)https://site-a.localhost, http://site-a.localhosthttp://site-a.localhost:3000
frontend_domain: "app.example.com"https://app.example.comhttp://app.example.com, https://app.example.com:3000
frontend_domain: "app.example.com:3000"https://app.example.com:3000http://app.example.com:3000, https://app.example.com
frontend_domain: "localhost:5173" (loopback)https://localhost:5173, http://localhost:5173http://localhost:5174
frontend_domain: "https://app.example.com:8443"https://app.example.com:8443http://app.example.com:8443, https://app.example.com
allowed_origins: ["https://partner.com"]https://partner.com, https://partner.com:443http://partner.com, https://partner.com:8443
allowed_origins: ["http://other.com:8080/"]http://other.com:8080https://other.com:8080, http://other.com

Never allowed: a missing site, a phoenix_only site, more than one Origin header, null, and an Origin with a path, a query, a fragment, credentials or a port that is not a number.

Development

Compiled only where the app's config sets :env_dev?, a loopback origin on any port, over http or https, is allowed for any site the rules above let answer at all — so a frontend on http://localhost:5173 reads the API of a site the database knows only by its public address. A release has no such clause, and neither do the tests, which set :env_test? alone. Not Mix.env/0: a dependency is compiled in :prod whatever the app runs in.

The answer

An allowed origin gets access-control-allow-origin with the origin as a browser writes it, never the request's own text, and access-control-allow-credentials: true; a preflight from it gets the methods, headers and max age too and is answered 204. A preflight from any other origin is answered 204 with none of them. Every response that comes through here varies by Origin: Origin is merged into the vary header already set or set later, once, whatever its case.

Usage

pipeline :api do
  plug :accepts, ["json"]
  plug MishkaCmsCore.Runtime.Plugs.ApiTenantLookupPlug
  plug MishkaCmsCore.Runtime.Plugs.ApiCorsPlug
end

Options

  • :site — the site to answer for, in place of conn.assigns[:current_site]. MishkaCmsCore.Runtime.Plugs.ApiTenantLookupPlug passes the switched-off site it refuses, so that site's preflight is answered and its 503 carries the headers an allowed origin needs to read it.

Summary

Functions

call(conn, opts)

init(opts)