Articles and Libraries

General notes

Cross-origin requests, are resource-scoped. In REST terms, the server can decide whether to allow cross-origin requests to a particular API or not. That capability is naturally exposed by the Go middleware above - and you can wrap any handlers (which correspond to paths) you have for which you want to enable CORS. In python frameworks, a @cors(...) decorator is more conventional.

Once you’re familiar with the idea of CORS being resource scoped, enabling CORS “globally” or “fixing CORS issue” will strike you as anti-patterns. For example you might not want to enable cross-origin requests to your graphql playground path, obviously. Or any API not intended for your browser app to access.

Sending and receiving cookies with CORS

While developing a web-app, I want to have the app served from the dist/ directory and all API calls will be driven by a dedicated server written in Go. In local, the server listens at localhost:8080 and the Vite app is served from localhost:5173 by Vite’s builtin server. Doesn’t matter what bundle/build tool you use, of course.

The login API at http://localhost:8080/login will receive form data like email and password and create an auth-token 1 and respond with status 200 along with Set-Cookie:... response header. The cookie needs to have its SameSite attribute set to None and Secure attribute set to true - which makes the browser accept the cookie only if the cross-origin request (to localhost in this case, to the /login API) happens over HTTPS, or if the domain is localhost.

Also, this request should be done with credentials: "include" in the fetch options. This actually confused me: my understanding initially was that credentials: "include" in a fetch call only makes it send the cookies that are already present, but it turns out you also need it to receive and store the cookie via Set-Cookie response header. In fact, the documentation states exactly that - and of course I didn’t read it before wasting my time debugging :) 2

Make sure to do the request via fetch instead of action attribute of your <form> element - otherwise the browser location will end up on the auth server’s address (http://localhost:8080/login in my case) - probably don’t want that :).

The above will make the browser store your cookie for the domain localhost:5173 itself even though it was set by a different origin. Subsequent requests to CORS-enabled APIs should be done with credentials: "include" - which makes sense, since by default the browser does not include credentials (in our case, cookies) in a cross-origin request.

The Domain attribute of a cookie is a domain name indepdent of port. So if you set localhost - the cookie will be sent as part of requests from the client app to any localhost:XXX address (provided you send credentials of course). In fact, you can set the domain to something like my-org.com and the cookie will be sent to any subdomain address also like auth.my-org.com and app.my-org.com.

In short

Server side:

  • Enable CORS for your /login API by allowing your web-app’s origin(s)
  • Generate your auth-token and set the following attributes on your auth-token cookie SameSite: None, Domain: my-org.com, Secure: true and send the cookie in Set-Cookie response header.

Client side:

  • Always set {credentials: "include"} to your fetch API calls to ‘/login’ and any authenticated API thereafter.

  1. AES-GCM is a good “authenticated encryption” method nowadays it seems. ↩︎

  2. “Tells browsers to include credentials in both same- and cross-origin requests, and always use any credentials sent back in responses.” ↩︎