Make your API unhackable, like the Titanic

This post needs no introduction, so it doesn’t have one. If you want to write an API and need to know how to make it secure, and have the requests authenticate against a server or a client, look no further! Well, do look a bit further, because I’m going to tell you how to do all these things in this post.

Use cases

As with most other things, your API authentication method will depend on your use case. I will detail a few common ones, along with the best authentication scheme for each one:

Local library

This can hardly be considered an API at all, but it is a use case. If you’re writing a client library that is closed-source, such as a component or an SDK that other libraries or programs will link against, and want to ensure that people won’t be able to use functionality without you giving them a license, the most common method is to provide them with the license details in some signed blob.

For example, if I have a library that will be used on iOS apps and want to ensure that only the “io.stochastic.tungsten” app can use it, I could generate the following license data, sign it with my key, and send it to the customer. An example, in executable pseudocode, aka Python:

>>> LICENSE_DETAILS = {
    "app_name": "io.stochastic.tungsten",
    "license_type": "superawesome",
    "signature": "G9XASry2VGLcAvtFfAQ3YLjH8c8f7Nc4wGgX3i+etc",
}

To generate the signature above, you need a public/private key pair, generated, for example, with TweetNaCl (highly recommended). You will generate this only once, keep the secret key secret so only you will know it, and include the public key in the SDK.

# Make sure you store this generated key and keep it secret.
>>> secret_key = nacl.signing.SigningKey.generate()

# Go ahead and sign the concatenated information with it.
>>> text = "io.stochastic.tungsten|superawesome"
>>> sig = secret_key.sign(text, encoder=nacl.encoding.Base64Encoder).signature
>>> sig
"G9XASry2VGLcAvtFfAQ3YLjH8c8f7Nc4wGgX3i+etc"

The client then will just verify the signature, and it will know that the app is authorized to use the SDK functions if the signature matches:

# The SDK will have this public key, so it can verify the signatures. This key
# is not secret/sensitive data.
>>> public_key = secret_key.verify_key
>>> public_key.encode(nacl.encoding.Base64Encoder)
'cI7Yl1BHqi2KfQRnj1u9B/LPF5JOaaZwXIYQFA8EcVY='

>>> sig_data = "io.stochastic.tungsten|superawesome"

>>> public_key.verify(sig, encoder=nacl.encoding.Base64Encoder) == sig_data
True

The manner in which the details are concatenated doesn’t matter, as long as it’s consistent. For example, you could just sort the keys alphabetically and concatenate them in that order.

This isn’t even API design, it’s just licensing, so I’m not sure why I included it here, but now it’s here and I’m not going to go back and delete it, so just don’t read it if you don’t want to.

Identification-only

If the API you are designing only needs a way to identify users who sign up, the easiest way to do it is to generate a long, random key on signup and give it to the application:

>>> import shortuuid
>>> shortuuid.ShortUUID().random()
'piexCFpafoVJ4oJHAotxLm'

Store this key in your database (keep it secret), and, whenever the application sends it to you, you will know that it has completed the signup process, and you will be able to identify it by this unique key. Simple!

If you’re using HTTP, I prefer to pass the API key either in the query string of the request (always over TLS, like every request should have!) or, even better, in the “username” field of HTTP Basic auth.

Client-mediated authentication

This is probably the most involved method of the three. If your users are services that need to pass requests to you through insecure channels (such as their customers’ browsers), there’s an easy way to ensure that that information came from your customers themselves.

Suppose that your user’s service’s backend needs to send you a request to say that the customer that contacts you needs to have their amount of gold coins stored in your bank increased by 10. Their backend can’t contact your backend, because there’s a big wall in the middle, so the only way they can talk to you is to have their customer relay that request.

What they should do is sign the request so you can verify they’re the ones who sent it, and not some lying user. You don’t need to use full-blown asymmetric cryptography here, there are easier primitives that will do pretty much the same thing.

Initially, you need to give your user an API key (it doesn’t have to be high-entropy, it just has to be unique) and an API secret (this one does have to be high-entropy):

API_KEY = "jdoe"
API_SECRET = "c22b5f9178342609428d6f51b2c5af4c0bde6a42"

What this will be used for is signing the requests that will be relayed through the third party. They will be something like:

{
    "api_key": "jdoe",
    "currency": "goldcoins",
    "amount": 10,
    "signature": "73e2affb987dd2cd24504864d9a35df1",
}

The signature is produced by HMACing the values, as above:

>>> hmac.new("c22b5f9178342609428d6f51b2c5af4c0bde6a42", "goldcoins|10").hexdigest()
"73e2affb987dd2cd24504864d9a35df1"

This ensures that the server can identify the user by the API key above, and can use the secret to regenerate and verify the MAC itself. Just make sure the concatenated values can never contain the separator, otherwise you open yourself up to attacks.

IMPORTANT: You should also include a timestamp or a serial number in the signed payload, so that replay attacks won’t work. This means that, if someone gives you the same request twice, you will know that it’s the same request and only ever give them 10 gold coins for it, rather than keep giving them coins every time you receive the same (old) payload.

Epilogue

Those are the most common methods of API authentication.

If I have just saved you one hundred hours of searching around for information, consider sending me your love on Twitter. OK TTYL!