Background
I have been working on enhancing a legacy project which was based on monolith architecture (https://en.wikipedia.org/wiki/Monolithic_application), let’s call that project ABC hosted at www.abc.com for convenience.
This is how the legacy system worked:
whenever a request is made to the system(www.abc.com) and it doesn’t match the defined api routes(/api), it responds to that request with HTML (client/frontend files).
When the user signs in, the backend responds with Set-Cookie which contains a JWT and that cookie was being sent on all the further calls to establish authentication. This was working fine as all the calls were being made from abc.com itself and the required JWT containing cookie was automatically being sent to the backend.
As a part of an enhancement to the architecture of this project, the frontend and backend were separated and now the backend was hosted at abc-api.com and the frontend was hosted at abc.com and this started breaking the login system.
After some debugging, I realized that this is breaking due to the cookie being set on another domain and the SameSite attribute-related changes that have happened across browsers.
Understanding the ‘SameSite’ Attribute in cookies
The SameSite attribute of the Set-Cookie HTTP response header allows you to declare if your cookie should be restricted to a first-party or same-site context.
- MDN Web Docs
When the cookie is considered as first-party (or same-site context)?
A cookie is associated with a particular domain and scheme (such as http or https), and may also be associated with subdomains if the Set-Cookie Domain attribute is set. If the cookie domain and scheme match the current page, the cookie is considered to be from the same site as the page and is referred to as a first-party cookie.
Recently there have been changes regarding the SameSite attribute standards, Which are :
Earlier if no SameSite attribute were specified, the cookies were being sent for all requests, and now if the SameSite attribute is not specified, it would have defaulted to Lax.
Lax — Cookies are not sent on normal cross-site subrequests (for example to load images or frames into a third party site), but are sent when a user is navigating to the origin site (i.e., when following a link)Developers must use a new cookie setting, SameSite=None, to designate cookies for cross-site access. Cookies with SameSite=None must now also specify the ‘secure’ attribute so cross-site cookies can only be accessed over HTTPS connections.
Cookies from the same domain are no longer considered to be from the same site if sent using a different scheme i.e, http: / https:/
Getting back to the issue
So, as mentioned above, after the frontend and backend were separated, frontend being at abc.com and backend being at abc-api.com, when the user was logging in, abc-api.com was responding cookie containing JWT but any further communication was failing because all the further communication was being made from abc.com and those calls did not contain the cookies by default so authentication was failing.
Solution
An immediate solution to make sure that the login works similar to how it worked in the legacy monolith app, was to make sure that cookies can be sent from the browser to abc-api.com whenever the frontend at abc.com calls any APIs.
To enable this:
The cookie should have SameSite=None so that it can be sent by default in third party contexts (which was the default behavior before SameSite was introduced)
the cookie has to be set as secure, to comply with guidelines for SameSite=None
The frontend and backend both have to be on https, so that the secure cookie can be transmitted
This fixed the login issue, and the cookie behavior is now more or less similar to the earlier setup.
Plans:
The current solution with SameSite=None gets us back to the original working implementation, which works fine, but still has all the drawbacks (csrf, privacy) which are supposed to be solved by the introduction of SameSite cookies.
We still need to have csrf protection enabled
We still need to set this cookie as httpOnly so that it can’t be leaked through Javascript