Posted on December 3, 2021 at 04:53 AM
When designing systems that enable secure authentication and authorization for API access, you must consider how your applications and users should authenticate themselves. In this article, we’ll compare three different ways to achieve this: API Keys, HTTP Basic Authentication, and OAuth. We’ll also highlight what the benefits and drawbacks are for each method.
Using API keys is a way to authenticate an application accessing the API, without referencing an actual user. The app adds the key to each API request, and the API can use the key to identify the application and authorize the request. The key can then be used to perform things like rate limiting, statistics, and similar actions.
How the key is sent differs between APIs. Some APIs use query parameters, some use the Authorize header, some use the body parameters, and so on. For instance, Google Cloud accepts the API key with a query parameter like this:
curl -X POST https://language.googleapis.com/v1/documents:analyzeEntities?key=API_KEY
Cloudflare requires the API key to be sent in a custom header:
curl https://api.cloudflare.com/client/v4/zones/cd7d0123e301230df9514d \
-H “Content-Type:application/json” \
-H “X-Auth-Key:1234567893feefc5f0q5000bfo0c38d90bbeb” \
It’s relatively easy for clients to use API keys. Even though most providers use different methods, adding a key to the API request is quite simple.
The API key only identifies the application, not the user of the application. It’s often difficult to keep the key a secret. For server-to-server communication, it’s possible to hide the key using TLS and restrict the access to only be used in backend scenarios. However, since many other types of clients will consume the APIs, the keys are likely to leak.
HTTP Basic Auth is a simple method that creates a username and password style authentication for HTTP requests. This technique uses a header called Authorization, with a base64 encoded representation of the username and password. Depending on the use case, HTTP Basic Auth can authenticate the user of the application, or the app itself.
A request using basic authentication for the user daniel with the password looks like this:
Authorization: Basic ZGFuaWVsOnBhc3N3b3Jk
When using basic authentication for an API, this header is usually sent in every request. The credentials become more or less an API key when used as authentication for the application. Even if it represents a username and password, it’s still just a static string.
In theory, the password could be changed once in a while, but that’s usually not the case. As with the API keys, these credentials could leak to third parties. Granted, since credentials are sent in a header, they are less likely to end up in a log somewhere than using a query or path parameter, as the API key might do.
Using basic authentication for authenticating users is usually not recommended since sending the user credentials for every request would be considered bad practice. If HTTP Basic Auth is only used for a single request, it still requires the application to collect user credentials. The user has no means of knowing what the app will use them for, and the only way to revoke the access is to change the password.
HTTP Basic Auth is a standardized way to send credentials. The header always looks the same, and the components are easy to implement. It’s easy to use and might be a decent authentication for applications in server-to-server environments.
When a user is authenticated, the application is required to collect the password. From the user’s perspective, it’s not possible to know what the app does with the password. The application will gain full access to the account, and there’s no other way for the user to revoke the access than to change the password. Passwords are long-lived tokens, and if an attacker would get a hold of a password, it will likely go unnoticed. When used to authenticate the user, multi-factor authentication is not possible.
A token-based architecture relies on the fact that all services receive a token as proof that the application is allowed to call the service. The token is issued by a third party that can be trusted by both the application and the service. Currently, the most popular protocol for obtaining these tokens is OAuth 2.0, specified in RFC 6749.
OAuth specifies mechanisms where an application can ask a user for access to services on behalf of the user, and receive a token as proof that the user agreed. To demonstrate how OAuth works, let’s consider the following use case.
A user Alice has an account with a service where she can report the current indoor temperature of her home. Alice also wants to give third-party applications access to read the temperature data, to be able to plot the temperatures on a graph, and cross-reference with data from other services.
The temperature service exposes an API with the temperature data, so the third-party app should be able to access the data quite easily. But how do we make only Alice’s data available to the application?
Using Basic authentication, the application can collect Alice’s username and password for the temperature service and use those to request the service’s data. The temperature service can then verify the username and password, and return the requested data.
However, as we noted, there are a few problems with this approach:
The user has to trust the application with the credentials. The user has no means of knowing what the credentials are used for.
The only way for the user to revoke the access is to change the password.
The application is not authenticated
The scope of access can not be controlled. The user has given away full access to the account.
Two-factor authentication cannot be used
Historically, this has created a need for services to develop “application-specific passwords,” i.e., additional passwords for your account to be used by applications. This removes the need to give away the actual password, but it usually means giving away full access to the account. On the service provider side, you could build logic around combining application-specific passwords with API keys, which could limit access as well, but they would be entirely custom implementations.
Let’s look at how we could solve this problem using an OAuth 2.0 strategy. To allow for better authentication, the temperature service must publish an Authorization Server (AS) in charge of issuing the tokens. This AS allows third-party applications to register, and receive credentials for their application to be able to request access on behalf of users.
To request access, the application can then point the user’s browser to the AS with parameters like:
This request will take the user to the AS of the temperature service, where the AS can authenticate Alice with whatever method is available. Since this happens in the browser, multiple factors are possible, and the only one seeing the data is the temperature service and the owner of the account.
Once Alice has authenticated, the AS can ask if it’s ok to allow access for the third party. In this case, the read_temperature scope was asked for, so the AS can prompt a specific question.
When Alice accepts, the client can authenticate itself. A token is issued as proof that Alice accepted the delegated access, and it is sent back to the third-party application.
Now, the third-party application can call the API using the received token. The token is sent along with the request by adding it to the Authorization header with the Bearer keyword as follows:
GET /temperature HTTP/1.1
Upon receiving the request, the service can validate the token, and see that Alice allowed the application to read the temperature listings from her account, and return the data to the application.
The issued token can be returned in two ways, either by returning a reference to the token data or returning the value of the token directly. For the reference token, the service will have to send a request to the AS to validate the token and return the data associated with it. This process is called introspection, and a sample response looks like this:
In this response, we can see that the user Alice has granted the application third_party_graphs access to her account, with the scope of read_temperatures. Based on this information, the service can decide if it should allow or deny the request. The client_id can also be used for statistics and rate-limiting of the application. Note that we only got the username of the account in the example, but since the AS does the authentication, it can also return additional claims in this response (things like account type, address, shoe size, etc.) Claims can be anything that can allow the service to make a well-informed authorization decision.
For returning the value, a token format like JSON Web Token (JWT) is usually used. This token can be signed or encrypted so that the service can verify the token by simply using the public key of the trusted AS.
We can see a clear difference here:
Alice only gave her credentials to the trusted site.
Multi-factor authentication can be used.
Alice can revoke access to the app, by asking the temperature site to withdraw her consent, without changing her password
Alice can allow the third-party app to access only certain information from her account.
Claims about the user can be delivered to the service directly through the request. No additional lookups are required.
The flow is entirely standardized.
Since OAuth 2.0 was developed in the time of a growing API market, most of the use cases for API keys and Basic Authentication have already been considered within the protocol. It’s safe to say that it beats the competition on all accounts. For small, specific use cases, it might be ok to use API keys or Basic Authentication, but anyone building systems that plan to grow should be looking into a token-based architecture such as the Neo Security Architecture.
In the use case above, I only described the user flow, but OAuth, of course, specifies alternative flows for obtaining tokens in server-to-server environments. You can read more on those in my earlier post that explores eight types of OAuth flows and powers.
We could talk tech all day. But we’d like to do things too,
like everything we’ve been promising out here.