Authentication
Last modified on Fri 24 Jun 2022

Authentication is the process of verifying someone's identity. There are many ways to verify identities over HTTP APIs, the most popular of which will be described below.

When we talk about API authentication, it's important to distinguish its two components:

  1. the mechanism — the medium utilized by the authentication protocol
  2. the method — the protocol for authenticating the user

These two work in tandem and sometimes they're indistinguishable, but we'll explain each one separately and show how they work together.

Mechanisms

As mentioned above, a mechanism is a medium utilized by the authentication protocol. The architecture of HTTP allows for several authentication mechanisms.

Cookies

Cookies are pieces of data exchanged between the client and the server through headers. The server sends one or more cookies via a Set-Cookie response header and then the browser saves them and includes them in subsequent requests to the server in a Cookie header. The browser can also send cookies to the server without receiving any first. The goal here is to somehow hold the state between server and client, meaning that the server will have a way to maintain a session with the stateless client.

When setting cookies, the server can apply one or more restrictions, like expiration date and domains where they're supposed to be used. You can learn more about how cookies work on MDN.

There are two important cookie attributes you should know about:

Note: If you have a cookie based authentication using the HttpOnly flag is among the safest option for storing sensitive information in a browser, so if you have a JS-based frontend application that will consume your APIs, and you need to keep the secrets really secret, consider using this technique.

Authorization header

HTTP supports sending a standard Authorization header in requests. The header consists of a type (indicating what kind of a credential it is) and a value separated by whitespace. An example header can look like this: Basic aHR0cHM6Ly95b3V0dS5iZS9kUXc0dzlXZ1hjUQ==.

MDN has a nice list of types. The one most commonly seen in APIs is the so-called Bearer token. As explained in the Bearer Token Usage RFC, a bearer token is:

A security token with the property that any party in possession of the token (a "bearer") can use the token in any way that any other party in possession of it can. When the server generates a token, it should make it opaque (meaning they can't be interpreted by anyone other than the server).

The Authorization header is the preferred mechanism if your API is to be used by platforms other than the browser (browsers using it are susceptible to XSS attacks). It's easier to control than cookies outside of browsers, and it doesn't come with security concerns around using query strings (described below). If your API will be consumed by both browsers and mobile platforms, you should provide for both cookies and the Authorization header as cookies can complicate the implementation on mobile platforms, and JS clients can't store/exchange authorization information securely with the server, other than through a cookie.

Basic auth

Basic auth is the simplest authentication method. It's a type of Authorization header authentication, where the "type" value is Basic.

Query string

The query string is the part of the URL used to provide additional parameters. The query string starts after the question mark character (?) and delimits multiple parameters with ampersands (&). Each parameter consists of a name and a string value separated by an equal sign (=).

For example, the URL example.com?api_key=123&token=abc has two query params: api_key with the value 123, and token with the value abc.

The query string can be used to transmit keys and tokens in requests to the server, similar to cookies and the Authorization header. At the first glance, this might seem insecure, but if it's sent over the https protocol, the query string is encrypted with the rest of the request and it's visible only to the client and the server.

However, there are other concerns when using query strings to transmit sensitive data:

Consider using query strings for authentication only when other methods are unavailable.

Methods

API keys

API keys are unique strings used to identify clients. Think of them like normal passwords, and you should also treat them as such.

API keys in most cases aren't used to authenticate the users themselves, but rather external access to user's resources. In those scenarios, users authenticate through other means (like a server-side session) and then they request one or more keys for API access.

Regarding the format of API keys, they usually consist of alphanumeric characters and have a minimum length to prevent brute-force attacks.

If your system allows access via API keys, it should also provide a way to revoke them, in case they're stolen.

Remind users of security measures they can take to decrease the likelihood of a malicious party accessing their keys and resources: - never store an API key in a public place (private repos on Github count as public too) and never embed it directly in code (unless the key is meant to be public) - regenerate API keys periodically - delete a key if you're not using it anymore

One option for generating API keys is SecureRandom, a Ruby standard library.

Access and refresh tokens

Access tokens and refresh tokens are artifacts used in the OAuth 2.0 authorization protocol. This protocol enables a user to grant permissions for accessing resources from a 3rd party application. It is most widely used for features like Single Sign On (SSO) via a variety of social media platforms (eg: Facebook, Instagram, LinkedIn).

Once the user logs into the authorization server and grants access to their personal data, an access token is returned to the client server. This token can be used for subsequent communication with the server on behalf of the user. For that reason, it's critical to have security strategies that minimize the risk of compromising access tokens, for example creating access tokens with a short lifespan. The downside of short-lived access tokens is the fact that the client application must prompt the user to log in again, which makes for bad UX. A better solution would be to use refresh tokens.

Refresh tokens are artifacts that let the client application refresh an access token without asking the user to log in. They usually livie longer than access tokens, but that also means that if the refresh token is stolen, the malicious user has the power to create new access tokens, so additional security techniques need to be in place, like refresh token rotation or automatic reuse detection.

JWT

The JSON Web Token standard is a method for creating the mentioned access tokens or any other artifact used for transmitting information between 2 servers. You can read more about it on jwt.io.

Server-side sessions

Believe it or not, cookies aren't the answer sometimes. Here are some usual arguments against them:

When you can't store your session data inside a cookie, Rails has other options that are easily configurable:

config/initializers/session_store.rb

Rails.application.config.session_store :cookie_store

Two of the most widely used alternatives for storing session data are:

Both solutions come out of the box with their corresponding dependencies: redis-rails & activerecord-session_store.

On reinventing authentication

For whatever reasons, you may feel like building your own authentication solution. Pursue this motivation, but not with the intention to use it in a production environment.

Authentication is a complex beast encompassing many aspects, most of which you won't be aware of until you start building your own solution, and some of which you'll only learn about when someone bypasses its security.

There are megapopular public libraries available for all common authentication methods, which come with the assurance:

If you think your custom solution will live up to those expectations in the time you have to implement it, then feel free to build it. However, there are better and more productive ways to use your time: solving problems which haven't been solved already.