Mobile apps commonly use APIs to interact with backend services and information. In 2016, time spent in mobile apps grew an impressive 69% year to year, reinforcing most companies mobile-first strategies, while also providing fresh and attractive targets for cybercriminals. As an API provider, protecting your business assets against information scraping, malicious activity, and denial of service attacks is critical in maintaining a reputable brand and maximizing profits.
Properly used, API keys and tokens play an important role in application security, efficiency, and usage tracking. Though simple in concept, API keys and tokens have a fair number of gotchas to watch out for. In Part 1, we'll start off with a very simple example of API key usage and iteratively enhance its API protection. In Part 2, we will move from keys to JWT tokens within several OAuth2 scenarios, and in our final implementation, we will remove any user credentials and static secrets stored within the client and, even if a token is somehow compromised, we can minimize exposure to a single API call.
The simplest API key is just an application or developer ID string. To use an API, the developer registers his application with the API service and receives a unique ID to use when making API requests.
In the sequence diagram, the client is a mobile application. The resource owner is the application user, and a resource server is a backend server interacting with the client through API calls. We will use OAuth2 terminology as much as possible.
With each API call, the client passes the API key within the HTTP request. It is generally preferred to send the API key as part of the authorization header, for example:
authorization: key some-client-id
URLs are often logged, so if the API key is passed as a query parameter, it could show up in client logs and be easily observed, as demonstrated by this past Facebook vulnerability. This initial API key approach offers some basic protection. Any application making an API call will be rejected if the call does not contain a recognized ID. Different applications with different keys could also have different permission scopes associated with those keys; for example, one app could have read-only access while another may be granted administrative access to the same backend services.
Keys can be used to gather basic statistics about API usage such as call counting or traffic sourcing, perhaps rejecting calls from non-app user agents. Importantly, most API services use calling statistics to enforce rate limits per application to provide different tiers of service or reject suspiciously high frequency calling patterns. One obvious weakness with this simple approach is that the API call and key are passed in the clear. A man in the middle attack could successfully modify any API call or reverse engineer the API and use the observed API key to make its own malicious API calls. The compromised API key cannot be blacklisted without breaking existing application instances and requiring an upgrade of the entire installed base.
Transport Level Security (TLS) is a standard approach to securing an HTTP channel for confidentiality, integrity, and authentication. With mutual TLS, client and server exchange and verify each other’s public keys. With certificate pinning, the client and server know which public keys to expect, so they compare the actual exchanged keys with the expected ones, rather than verifying through a hierarchical chain of certificates The client and server must keep their own private keys secure. Once the keys are verified, the client and server negotiate a shared secret, a message authentication code (MAC) and encryption algorithms.
When running on an uncompromised mobile device, client traffic over TLS is reasonably safe from man in the middle attacks. Unfortunately, if an attacker can install your client application on a device he controls, he can use a packet sniffer to observe the public key exchange, and use that knowledge to decrypt the channel to observe the API key and reverse engineer your APIs. While he may not be able to observe traffic on other clients, he can now create his own malicious app, freely calling your API over a TLS-secure channel. So even when using TLS, you’ll need additional security to prevent APIs being called from unauthorized applications.
One of the first improvements we can make is to separate the API key into an ID and a shared secret. As before, the ID portion of the key is passed with each HTTP request, but the shared secret is used to sign and/or encrypt the information in transit.
To ensure message integrity, the client computes a message authentication code (MAC) for each request using the shared secret with an algorithm such as HMAC SHA-256. Using the same secret, the server computes the received message MAC and compares it with the MAC sent in the request.
Though the secret is known by both client and server, that secret is never present in the communication channel. An attacker might somehow see the ID, but without the secret, he cannot properly sign the request. As it stands, an attacker can still deny or replay the request, but he cannot alter it. Examples built around this scheme include the HAWK HTTP authentication specification or the Amazon S3 REST API signing and authorization scheme.
To further protect critical information from being observed, all or portions of a message can be encrypted before signing using key material derived from the shared secret.
We are starting to accumulate secrets on the client. We have the shared API secret and the client’s private TLS key.
In its basic form, the secrets will be static constants with the installed application package using developer-friendly names like SHARED_SECRET. It won’t take a junior hacker much time to extract that constant, and once he has it, your backend is compromised. As a first step, use code obfuscators, to make it harder to locate and extract the secret constant. To go a bit further, consider encoding a static secret in some computationally simple way, cut that encoding into small segments, and distribute them around the binary. Reassemble and decode the secret in memory as needed; never save it in persistent storage.
Though the public keys are not actually secrets, you want to obfuscate them as well. Their values can be observed, so if they are not obfuscated, they can be easily found and changed, making it easy to disable or spoof back-end traffic.
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, we have the same revocability issues as before; all app instances will be compromised until we upgrade the entire installed base with a new secret and a new technique to obscure it.
We will return to this challenge again in Part 2, where we will discuss approaches which remove the secret from the app altogether.
We have enhanced API security using Application keys, but we have not considered how to handle user credentials.
Starting simple, a client requests a user to provide user ID and password. Using basic access authentication, the client encodes and passes the credentials to the server which verifies them. If the credentials are valid, the server can start a user session and return a user session key. Multiple authentications using the same credentials should always return different key strings.
As we saw for application keys, we can use user keys to gather statistics and set authorization levels, but now we can do it with user-level granularity. Assuming we are using both app and user keys, the authorization levels for a user will be a function of both app and user; for example, a user may have administrative authorizations on one app while having only read permission on a different app, even though they are talking to the same backend server.
Similar to when using HTTP cookies, session state is typically maintained on the server. This may decrease server scalability, and if multiple servers can handle a user request, session data must be synchronized between them. We’ll address this with user tokens in part 2.
So far, our application keys are static and therefore have infinite lifetimes. By contrast, user keys are created on the server, and they can and should expire. When a user key expires, the user must reauthenticate to continue making API calls, and session state is lost. Users do not like to logon repeatedly, so a policy decision needs to be made on key lifetimes. The longer the lifetime, the more user convenience, but if a user key should be compromised, it could be used maliciously for a longer time as well.
If a key can last longer than an application instance, then it must be stored in persistent storage on the client between app invocations. This is inherently less secure than if the key only exists in memory. Use secure storage such as Keychain Services for IOS and consider SharedPreferences for Android.
Unlike an application key, a user key can be revoked without breaking installed applications.
We started off with a very simple example of API Key usage and iteratively enhanced its API protection to secure the communication channel and authorize both clients and users using API keys. In Part 2, we will move from keys to JWT tokens within several OAuth2 user authentication scenarios, and in our final implementation, we will remove any user credentials and static secrets stored within the client and, even if a token is somehow compromised, we can minimize exposure to a single API call.