As a developer, API Keys are typically issued to you to identify the project you are working on and to enforce rate and access limits on proper API usage. These API keys are typically just static secrets baked into your app or web page, and they are pretty easy to steal but painful to replace. You can do better.
When securing API calls to a backend service, it’s important to understand both who the user is and what the app or user’s agent is which is making the call. A user authenticates his identity using some form of credentials including an id and password, hardware token, biometrics, and/or two-factor authentication. If user authentication passes, you don’t know whether those credentials are being used by a good app or a bad bot.
Authenticating the app’s identity is typically done using a user-agent string, an API key, and/or a captcha test. Most of these credentials are pretty weak - a user agent string is easily spoofed, a static API key is easily stolen, and although a captcha verifies that a human and not a bot is likely involved, it doesn’t prove that it is the expected app which is actually making the call.
Authorizing an API call to access and/or modify a back-end resource requires that both user and app authentication be as strong as practical. App authentication could be considered even more important than user authentication, since, if you don't first authenticate the app, then an unchallenged, malicious bot could test and abuse user credentials by attempting to sign-in through the open login API.
App authentication is arguably the weakest link in API security. Of the popular approaches, only API keys are issued by the backend service; the other approaches are not unique to the service, so it makes sense to focus your security improvements on creating stronger API keys.
An API key is issued by a back-end service provider to identify the the developer and/or app accessing it. The API key may be split into ID and secret strings, or both the ID may be opaquely encoded into a single string. The secret or encoded key may also contain additional information such as an expiration date.
To use an API, a developer registers his application with the API service and receives a unique key to use when making API requests.
This API key is typically used to identify the calling app, impose rate and quota limits on the calling app, restrict the app to a subset of the API’s capabilities, and/or bill the developer for services used.
The API key is sent as part of each API call. That key is easily observed or tampered with over an HTTP request, so using HTTPS (TLS) is imperative. However, even using HTTPS, devices are vulnerable to man-in-the-middle attacks, so additional techniques, such as certificate pinning [REF] are recommended but too often unadopted because of implementation and maintenance complexities.
One way to improve security is to keep the API key out of the channel. Instead of adding the plaintext API key to a request, we will use the API key to sign each request. This is typically done using a hash-based message authentication code (HMAC).
The API call is still sent in plain text over HTTPS, so the message could be observed if HTTPS is defeated. However, the HMAC signature ensures that this message has not been tampered with, and we have improved API security because the API key itself is no longer visible in the API call.
Since the same message will result in the same HMAC signature, it is important to consider what is in a message. Signing just the API endpoint URI, for example, will enable an attacker to reuse that signed API call with different headers or body values. Including additional components in the message string will limit replay. For example, adding the user authorization header will prevent an attacker from making that API call with a different user or after this user’s authorization has expired.
We now have the API key out of the calling channel, but it is still inside the installed app. At its most vulnerable, the key will be a static constant easily found within the application package. As a first step, use code obfuscators to make it difficult to locate and extract the secret, and consider encoding a static secret in some computationally simple way, perhaps cutting up and distributing the encoded secret across multiple locations within the binary. Reassemble and decode the secret in memory as needed, and never store the decoded and reassembled API key on the device. Do what you can to make reverse engineering the app as difficult as possible without exceeding performance and power budgets.
Regardless of your efforts, it is not a matter of if a secret will be stolen, but if the time and effort to steal it is worth the return. Make it as difficult as you can afford. If an API secret is stolen from one device, all app instances will be compromised until we upgrade the entire installed base with a new secret and a new technique to obscure it.
The best API key is one that is never present in the channel, persistent storage, nor the application code. So far we’ve hidden the key inside the code, but can we really remove it from the application code altogether?
For starters, consider providing the API key remotely on an as needed basis. When your app is ready to make an API call, it can request the API key from this remote service.
Similar to the original API calling channel, we don’t want to return the actual API key in this channel. Since there is no message to sign, instead we’ll use a short-lived JSON Web Token (JWT) signed with the API key. Now this API key is known only to the API’s back-end service and the API key service, but not the device nor any of its communication channels.
As it stands, anyone, valid app or malicious bot, can successfully request an API access token. We need a method to verify that the agent requesting the API access token is and only is the authentic app. One way to do this is to register the app with the API key service. Before the app is ready to make an API call, it will request that it’s authenticity be verified. Through a cryptographically complete and unreplayable challenge-response protocol, the API key service will decide whether the app is authentic, uncompromised, and running in a trustworthy environment. If satisfied, it will provide a short-lived API access token properly signed by the API key or, if unsatisfied, an improperly signed token. The app adds the API key token to the API call, and the back end service will validate the API key token, ensuring the app’s identity, before further evaluating the API call.
Because user authentication protocols are API-driven, we suggested earlier that you can’t have strong user identity without strong app identity. Popular authorization protocols such as OAuth2 do rely on an app ID-secret pair equivalent to a typical API key, and this API key can just as easily be abused by a fake app to steal and use valid user credentials to attack back-end services. For an example of hardening the OAuth2 protocol to prevent this type of attack, see Strengthening OAuth2 for Mobile.
For a commercial implementation of this approach for mobile apps, see the Approov service. For a more general overview of mobile API security, start with Mobile API Security Techniques.