loading...
Cover image for A small guide to authentication and security for SPA

A small guide to authentication and security for SPA

stereobooster profile image stereobooster ・3 min read

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

Posted on by:

stereobooster profile

stereobooster

@stereobooster

Hello, I'm a full stack web developer. Follow me on Twitter!

Discussion

markdown guide
 

All the basic HTTP headers that your server and endpoints must have, regardless of the language or framework are explained here
pentest-tools.com/blog/essential-h...

If you do not have time to learn about them just use them, is still better than not having them 😀

 

Neat, thanks for sharing. Do you have any suggestions for creating tenants and different user privileges in a web app?

 

Depends on what you mean. Last time I looked into authorisation thing, I guess, it was Rails app and we used cancancan for it (if I remember it right)

 

Open ID scopes should handle that. The authorization server can decide which user has access to what action:resource.