
Authentication in Single Page Application. Part 2
In the first part, we were talking about Authorization header and that it’s a common way to implement authentication in SPA. It has its pros and cons. 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=strict
The browser remembers the cookie and automatically sends it in every subsequent request:
Cookie: access_token=ce073b61
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 Part 1 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: XMLHttpRequest
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 Part 1 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
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>
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
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
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.
Let's meet Svitla
We look forward to sharing our expertise, consulting you about your product idea, or helping you find the right solution for an existing project.
Your message is received. Svitla's sales manager of your region will contact you to discuss how we could be helpful.