Let's Go Further Authentication › Authentication options
Previous · Contents · Next
Chapter 15.1.

Authentication options

Before we get started on writing any code, let’s talk about how we’re going to authenticate requests to our API and find out which user a request is coming from.

Choosing a high-level approach to API authentication can be tricky — there are many different options, and it’s not always immediately clear which one is the right fit for your project. So in this chapter we’ll discuss some of the most common approaches at a high level, talk through their relative pros and cons, and finish with some general guidelines about when they’re appropriate to use.

Specifically, the five approaches that we’ll compare are:

HTTP basic authentication

Perhaps the simplest way to determine who is making a request to your API is to use HTTP basic authentication.

With this method, the client includes an Authorization header with every request containing their credentials. The credentials need to be in the format username:password and base64 encoded. So, for example, to authenticate as alice@example.com:pa55word the client would send the following header:

Authorization: Basic YWxpY2VAZXhhbXBsZS5jb206cGE1NXdvcmQ=

In your API, you can then extract the credentials from this header using Go’s Request.BasicAuth() method, and verify that they’re correct before continuing to process the request.

A big plus of HTTP basic authentication is how simple it is for clients. They can just send the same header with every request — and HTTP basic authentication is supported out-of-the-box by most programming languages, web browsers, and tools such as curl and wget.

It’s often useful in the scenario where your API doesn’t have ‘real’ user accounts, but you want a quick and easy way to restrict access to it or protect it from prying eyes.

For APIs with ‘real’ user accounts and — in particular — hashed passwords, it’s not such a great fit. Comparing the password provided by a client against a (slow) hashed password is a deliberately costly operation, and when using HTTP basic authentication you need to do that check for every request. That will create a lot of extra work for your API server and add significant latency to responses.

But even then, basic authentication can still be a good choice if traffic to your API is very low and response speed is not important to you.

Token authentication

The high-level idea behind token authentication (also sometimes known as bearer token authentication) works like this:

  1. The client sends a request to your API containing their credentials (typically username or email address, and password).

  2. The API verifies that the credentials are correct, generates a bearer token which represents the user, and sends it back to the user. The token expires after a set period of time, after which the user will need to resubmit their credentials again to get a new token.

  3. For subsequent requests to the API, the client includes the token in an Authorization header like this:

    Authorization: Bearer <token>
  4. When your API receives this request, it checks that the token hasn’t expired and examines the token value to determine who the user is.

For APIs where user passwords are hashed (like ours), this approach is better than basic authentication because it means that the slow password check only has to be done periodically — either when creating a token for the first time or after a token has expired.

The downside is that managing tokens can be complicated for clients — they will need to implement the necessary logic for caching tokens, monitoring and managing token expiry, and periodically generating new tokens.

We can break down token authentication further into two sub-types: stateful and stateless token authentication. They are quite different in terms of their pros and cons, so let’s discuss them both separately.

Stateful token authentication

In a stateful token approach, the value of the token is a high-entropy cryptographically-secure random string. This token — or a fast hash of it — is stored server-side in a database, alongside the user ID and an expiry time for the token.

When the client sends back the token in subsequent requests, your application can look up the token in the database, check that it hasn’t expired, and retrieve the corresponding user ID to find out who the request is coming from.

The big advantage of this is that your API maintains control over the tokens — it’s straightforward to revoke tokens on a per-token or per-user basis by deleting them from the database or marking them as expired.

Conceptually it’s also simple and robust — the security is provided by the token being ‘unguessable’, which is why it’s important to use a high-entropy cryptographically-secure random value for the token.

So, what are the downsides?

Apart from the awkwardness for clients in having to manage tokens, it’s difficult to find much to criticize about this approach. Perhaps the fact that it requires a database lookup is a negative — but in most cases you will need to make a database lookup to check the user’s activation status or retrieve additional information about them anyway.

Stateless token authentication

In contrast, stateless tokens encode the user ID and expiry time in the token itself. The token is cryptographically signed to prevent tampering and (in some cases) encrypted to prevent the contents being read.

There are a few different technologies that you can use to create stateless tokens. Encoding the information in a JWT (JSON Web Token) is probably the most well-known approach, but PASETO, Branca and nacl/secretbox are viable alternatives too. Although the implementation details of these technologies are different, the overarching pros and cons in terms of authentication are similar.

The main selling point of using stateless tokens for authentication is that the work to encode and decode the token can be done in memory, and all the information required to identify the user is contained within the token itself. There’s no need to perform a database lookup to find out who a request is coming from.

The primary downside of stateless tokens is that they can’t easily be revoked once they are issued.

In an emergency, you could effectively revoke all tokens by changing the secret used for signing your tokens (forcing all users to reauthenticate), or another workaround is to maintain a blocklist of revoked tokens in a database (although that defeats the ‘stateless’ aspect of having stateless tokens).

Finally, with JWTs in particular, the fact that they’re highly configurable means that there are lots of things you can get wrong. The Critical vulnerabilities in JSON Web Token libraries and JWT Security Best Practices articles provide a good introduction to the type of things you need to be careful of here.

Because of these downsides, stateless tokens — and JWTs in particular — are generally not the best choice for managing authentication in most API applications.

But they can be very useful in a scenario where you need delegated authentication — where the application creating the authentication token is different from the application consuming it, and those applications don’t share any state (which means that using stateful tokens isn’t an option). For instance, if you’re building a system which has a microservice-style architecture behind the scenes, then a stateless token created by an ‘authentication’ service can subsequently be passed to other services to identify the user.

API-key authentication

The idea behind API-key authentication is that a user has a non-expiring secret ‘key’ associated with their account. This key should be a high-entropy cryptographically-secure random string, and a fast hash of the key (SHA256 or SHA512) should be stored alongside the corresponding user ID in your database.

The user then passes their key with each request to your API in a header like this:

Authorization: Key <key>

On receiving it, your API can regenerate the fast hash of the key and use it to look up the corresponding user ID from your database.

Conceptually, this isn’t a million miles away from the stateful token approach — the main difference is that the keys are permanent keys, rather than temporary tokens.

On one hand, this is nice for the client as they can use the same key for every request and they don’t need to write code to manage tokens or expiry. On the other hand, the user now has two long-lived secrets to manage which can potentially compromise their account: their password, and their API key.

Supporting API keys also adds additional complexity to your API application — you’ll need a way for users to regenerate their API key if they lose it or the key is compromised, and you may also wish to support multiple API keys for the same user, so they can use different keys for different purposes.

It’s also important to note that API keys themselves should only ever be communicated to users over a secure channel, and you should treat them with the same level of care that you would a user’s password.

OAuth 2.0 / OpenID Connect

Another option is to leverage OAuth 2.0 for authentication. With this approach, information about your users (and their passwords) is stored by a third-party identity provider like Google or Facebook rather than yourself.

The first thing to mention here is that OAuth 2.0 is not an authentication protocol, and you shouldn’t really use it for authenticating users. The oauth.net website has a great article explaining this, and I highly recommend reading it.

If you want to implement authentication checks against a third-party identity provider, you should use OpenID Connect (which is built directly on top of OAuth 2.0).

There’s a comprehensive overview of OpenID Connect here, but at a very, very, high level it works like this:

Like all the other options we’ve looked at, there are pros and cons to using OpenID Connect. The big plus is that you don’t need to persistently store user information or passwords yourself. The big downside is that it’s quite complex — although there are some helper packages like coreos/go-oidc which do a good job of masking that complexity and providing a simple interface for the OpenID Connect workflow that you can hook in to.

It’s also important to point out that using OpenID Connect requires all your users to have an account with the identity provider, and the ‘authentication and consent’ step requires human interaction via a web browser — which is probably fine if your API is the back-end for a website, but not ideal if it is a ‘standalone’ API with other computer programs as clients.

What authentication approach should I use?

It’s difficult to give blanket guidance on what authentication approach is best to use for your API. As with most things in programming, different tools are appropriate for different jobs.

But as simple, rough, rules-of-thumb:

In the rest of this book, we’re going to implement authentication using the stateful authentication token pattern. In our case we’ve already built a lot of the necessary logic for this as part of our activation tokens work.