This is by no means is an exhaustive guide, just for you get started.
Setup: let's assume we want to build new SPA deployed to m.example.com
, also we have an old application, for example, Ruby on Rails, deployed to www.example.com
. The new application will be a static website, e.g. we will only have assets (JS, HTML, CSS, images) deployed there (it could be an application with backend and SSR, but let's omit this for simplicity). Also, we will have api.example.com
as API endpoint for our SPA application.
Shared sessions
We want to share sessions across new and old applications. To do this we need to use cookies at the root domain - HTTP headers for cookies can look like this:
set-cookie: SID=...; Domain=.example.com
Pay attention to the dot at the beginning of the domain. This way browser will send cookies to all of our subdomains e.g. m.example.com
, www.example.com
, api.example.com
. Once the user authenticates in one of our services their will be authenticated everywhere.
Security for cookies
All of those considerations are for api.example.com
and www.example.com
.
HttpOnly
HttpOnly
directive disallows access to cookies for JavaScript to prevent hijacking of the session through XSS.
set-cookie: SID=...; HttpOnly
Secure
Secure
directive instructs the browser to send cookies only through HTTPSto prevent hijacking of the session through man in the middle attack. (Attack still possible if the attacker will be able to fake certificate)
set-cookie: SID=...; Secure
SameSite
SameSite
directive prevents CSRF attacks. I choose to use a more relaxed version of this directive (Lax
) it should be enough in most cases (read about instruction and see yourself if it is enough for you or not).
set-cookie: SID=...; SameSite=Lax
Security for assets
All of those HTTP headers are for m.example.com
and www.example.com
.
Strict-Transport-Security
Strict-Transport-Security: max-age=86400
X-Content-Type-Options
X-Content-Type-Options: nosniff
X-Frame-Options
X-Frame-Options: DENY
X-XSS-Protection
X-XSS-Protection: 1; mode=block
Content-Security-Policy
I don't use Content-Security-Policy
in this post, but I strongly recommend you to use it. (Maybe I will write a separate post about it)
Security for API
CORS
Use CORS. Specify what methods are allowed, and for how long to cache preflight request
access-control-allow-methods: GET,HEAD,PUT,PATCH,POST,DELETE
access-control-max-age: 86400
Specify from which domain is allowed to access API
access-control-allow-origin: https://m.example.com
Specify allow-credentials
otherwise, cookies won't work. Take into account that you can't use the star (*
) with credentials directive.
access-control-allow-credentials: true
JSON API
For all requests, except maybe endpoints accessible without authentication, require Content-Type
, this will trigger a check of CORS (via preflight request):
Content-Type: application/json; charset=utf-8
JS client
Now we have all the basics, it's time to actually make a call from our frontend to API. Let's use fetch
API for this.
Anonymous requests
For endpoints which allow access from anonymous users use "plain" fetch. Don't use Content-Type
, otherwise, it will become slower without any benefit for the user.
fetch(url)
Authenticated requests
For other requests use credentials: "include"
to enable cookies (this is the default option in the latest Fetch specification, but not all browsers implemented it). Use headers: { "Content-Type": "application/json; charset=utf-8"}
to trigger CORS check and actually pass check of the backend (which we "implemented" earlier).
For GET
requests:
fetch(url, {
credentials: "include",
headers: { "Content-Type": "application/json; charset=utf-8"}
})
For POST
requests:
fetch(url, {
credentials: "include",
headers: { "Content-Type": "application/json; charset=utf-8"},
method: "POST",
body: JSON.stringify(params)
})
Photo by Tianshu Liu on Unsplash