We are keen on security – recently we have published the Node.js Security Checklist. As a sequel, let’s dive deep into the world of cookies, tokens and other web authentication methods.

We are going to start with the most basic one, the HTTP Basic authentication, continue withcookies and tokens, and finish up with signatures and one-time passwords.

HTTP Basic authentication

HTTP Basic authentication is a method for the client to provide a username and a password when making a request.

This is the simplest possible way to enforce access control as it doesn’t require cookies, sessions or anything else. To use this, the client has to send the Authorization header along with every request it makes. The username and password are not encrypted, but constructed this way:

  • username and password are concatenated into a single string: username:password
  • this string is encoded with Base64
  • the Basic keyword is put before this encoded value

Example for a user named john with password secret:

curl --header "Authorization: Basic am9objpzZWNyZXQ=" my-website.com

The same can be observed in Chrome as well:

Google Chrome basic web authentication method

Implementing it is pretty easy in Node.js as well – the following snippet shows how you can do an Express middleware to do so.

import basicAuth from basic-auth;
function unauthorized(res) {
res.set(WWW-Authenticate, Basic realm=Authorization Required);
return res.send(401);
};
export default function auth(req, res, next) {
const {name, pass} = basicAuth(req) || {};
if (!name || !pass) {
return unauthorized(res);
};
if (name === john && pass === secret) {
return next();
}
return unauthorized(res);
};
view rawbasicAuth.es6 hosted with ❤ by GitHub

Of course, you can do it on a higher level, like in nginx.

Looks simple, right? So what are the drawbacks of using HTTP Basic authentication?

The cons:

  • the username and password are sent with every request, potentially exposing them – even if sent via a secure connection
  • connected to SSL/TLS, if a website uses weak encryption, or an attacker can break it, the usernames and passwords will be exposed immediately
  • there is no way to log out the user using Basic auth
  • expiration of credentials is not trivial – you have to ask the user to change password to do so

Cookies

When a server receives an HTTP request in the response, it can send a Set-Cookie header. The browser puts it into a cookie jar, and the cookie will be sent along with every request made to the same origin in the Cookie HTTP header.

To use cookies for authentication purposes, there are a few key principles that one must follow.

Always use HttpOnly cookies

To mitigate the possibility of XSS attacks always use the HttpOnly flag when setting cookies. This way they won’t show up in document.cookies.

Always use signed cookies

With signed cookies, a server can tell if a cookie was modified by the client.

This can be observed in Chrome as well – first let’s take a look at how a server set cookies:

illustration of Chrome cookie set for web authentication purposes

Later on all the requests use the cookies set for the given domain:

web authentication method illustration Chrome cookie usage


The cons:

  • Need to make extra effort to mitigate CSRF attacks
  • Incompatibility with REST – as it introduces a state into a stateless protocol

Tokens

Nowadays JWT (JSON Web Token) is everywhere – still it is worth taking a look on potential security issues.

First let’s see what JWT is!

JWT consists of three parts:

  • Header, containing the type of the token and the hashing algorithm
  • Payload, containing the claims
  • Signature, which can be calculated as follows if you chose HMAC SHA256: HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

Adding JWT to Koa applications is only a couple of lines of code:

var koa = require(koa);
var jwt = require(koa-jwt);
var app = koa();
app.use(jwt({
secret: very-secret
}));
// Protected middleware
app.use(function *(){
// content of the token will be available on this.state.user
this.body = {
secret: 42
};
});
view rawserver.js hosted with ❤ by GitHub

Example usage – (to check out the validity/content of the token, you can use jwt.io):

curl --header "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ" my-website.com

As the previous ones, the tokens can be observed in Chrome as well:

Google Chrome JSON Web Token as a web authentication method

If you are writing APIs for native mobile applications or SPAs, JWT can be a good fit. One thing to keep in mind: to use JWT in the browser you have to store it in either LocalStorage or SessionStorage, which can lead to XSS attacks.

The cons:

Signatures

Either using cookies or tokens, if the transport layer for whatever reason gets exposed your credentials are easy to access – and with a token or cookie the attacker can act like the real user.

A possible way to solve this – at least when we are talking about APIs and not the browser is to sign each request. How does that work?

When a consumer of an API makes a request it has to sign it, meaning it has to create a hash from the entire request using a private key. For that hash calculation you may use:

  • HTTP method
  • Path of the request
  • HTTP headers
  • Checksum of the HTTP payload
  • and a private key to create the hash

To make it work, both the consumer of the API and the provider have to have the same private key. Once you have the signature, you have to add it to the request, either in query strings or HTTP headers. Also, a date should be added as well, so you can define an expiration date.

AWS Request Signing: Flow of a Web Authentication MethodAWS Request Signing Flow – source

Why go through all these steps? Because even if the transport layer gets compromised, an attacker can only read your traffic, won’t be able to act as a user, as the attacker will not be able to sign requests – as the private key is not in his/her possession. Most AWS services are using this kind of authentication.

node-http-signature deals with HTTP Request Signing and worth checking out.

The cons:

  • cannot use in the browser / client, only between APIs

One-Time Passwords

One-Time passwords algorithms generate a one-time password with a shared secret and either the current time or a counter:

  • Time-based One-time Password Algorithm, based on the current time,
  • HMAC-based One-time Password Algorithm, based on a counter.

These methods are used in applications that leverage two-factor authentication: a user enters the username and password then both the server and the client generates a one-time password.

In Node.js, implementing this using notp is relatively easy.

Cons:

  • with the shared-secret (if stolen) user tokens can be emulated
  • because clients can be stolen / go wrong every real-time application have methods to bypass this, like an email reset that adds additional attack vectors to the application

Which web authentication method to pick when?

If you have to support a web application only, either cookies or tokens are fine – for cookies think about XSRF, for JWT take care of XSS.

If you have to support both a web application and a mobile client, go with an API that supports token-based authentication.

If you are building APIs that communicate with each other, go with request signing.

You have additional thoughts or insights on the topic? Share it in the comments.