Working with the programming APIs for cloud providers and SaaS vendors has taught me two things:
- There are very few truly RESTful programming APIs.
- Everyone feels the need to write a custom authentication protocol.
I've programmed against more web services interfaces than I can remember. In the last month alone, I've written to web services APIs for Aria, AWS, enStratus, GoGrid, the Rackspace Cloud, VMOps, Xero, and Zendesk.
Each one requires a different authentication mechanism. Two of them—Aria and AWS—defy all logic and require different authentication mechanisms for different parts of their respective APIs.
Let's end this here and now*. I'm tired of wasting brain cycles figuring out whether vendor A requires you to sign your query before or after you URL encode your parameters and I am fed up with vendors who insist on using interactive user credentials to authenticate API calls. Here's a set of standards that I think should be in place for any REST authentication scheme.
Here's the summary:
- All REST API calls must take place over HTTPS with a certificate signed by a trusted CA. All clients must validate the certificate before interacting with the server.
- All REST API calls should occur through dedicated API keys consisting of an identifying component and a shared, private secret. Systems must allow a given customer to have multiple active API keys and de-activate individual keys easily.
- All REST queries must be authenticated by signing the query parameters sorted in lower-case, alphabetical order using the private credential as the signing token. Signing should occur before URL encoding the query string.
All REST API calls must take place over HTTPS with a certificate signed by a trusted CA. All clients must validate the certificate before interacting with the server.
Encryption certainly adds overhead to every API call. Meaningful authentication, however, begins with an encrypted session. You can't securely authenticate with the server unless the authentication credentials are encrypted. Furthermore, a well-designed REST API should not work like local method calls—it should be capable of dealing with the overhead imposed by encryption.
Let's just be blunt: if you aren't encrypting your API calls, you aren't even pretending to be secure.
The second part may be a bit more controversial for the people who think that SSL is about encryption. Encryption is only part of SSL. Through the use of certificates signed by a trusted authority, SSL also protects you against "man-in-the-middle" attacks in which an agent inserts itself between client and server and sniffs the "encrypted" traffic.
If you are not validating the SSL certificate of the server, you don't know who is receiving your REST queries.
All REST API calls should occur through dedicated API keys consisting of an identifying component and a shared, private secret. Systems must allow a given customer to have multiple active API keys and de-activate individual keys easily.
The first important thing is that a system making a REST query is NOT an interactive user. You should therefore NEVER require or allow the user of interactive user credentials for REST authentication. The only exception is a client/server application in which the client represents an interactive user and the protocol between client and server is a RESTful API.
Well, as I mentioned above, your REST client is not an interactive user. Customers can fudge this by creating a dedicated account expressly for the purpose of REST authentication. Unfortunately, this enables attackers to go after the interactive account as an attack vector for compromising the overall system. Furthermore, because REST is authenticating a program and not person, it allows for stronger authentication than human user ID/password schemes allow.
If you don't use a dedicated user account, you open up all kinds of problems. Zendesk's REST authentication, for example, is a REST API that leverages one of your Zendesk admin accounts. If my Zendesk admin leaves the company or changes roles, I have to remember to alter my applications to reflect those changes. While I could use a dedicated account to get around this problem, Zendesk charges me for each admin account in addition to the extra money I am paying for API access.
The second part says that each REST server should support multiple API keys for each customer. This requirement makes it simpler to isolate potential compromises and address them when they happen.
Ten different applications by one customer talking to a given API is the same thing as ten different users of one customer interacting with a user interface. You would not have those ten users sharing one user ID/password, so why would you have the ten different applications sharing a single set of REST credentials? If one of those applications has been compromised, do you really want to go through the process of having to re-configure all of them for new keys?
When an application is compromised, you also need an elegant way to roll out replacement API keys. You therefore need the ability to have both the old and new keys active for a short period of time while you re-configure the application. Once the application is reconfigured, the server system should allow you to de-activate the compromised keys.
All REST queries must be authenticated by signing the query parameters sorted in lower-case, alphabetical order using the private credential as the signing token. Signing should occur before URL encoding the query string.
In other words, you don't pass the shared secret component of the API key as part of the query, but instead use it to sign the query. Your queries end up looking like this:
The string being signed is "/object?apikey=Qwerty2010×tamp=1261496500" and the signature is the HMAC-SHA256 hash of that string using the private component of the API key.
The main objection to this approach is that the private API key devolves into a kind of password for static calls. For example, if the query were instead:
The signature would be the same every time you made that specific query. However, you are using SSL, right? Furthermore, adding in a timestamp makes each query differ. For extra security, you can make the timestamp a more formal date-time value with time zone information and disallow queries outside of the query range.
The real controversy is whether signing should occur before or after URL encoding values. There is no "right" answer. I lean towards signing before encoding because most programming tools make it easier on the server side to get the unencoded values versus the encoded values. I'm sure good arguments can be made the other way. What I really care about is this: let's pick one and stick with it.
This is a battle I know I am going to lose. After all, people still can't settle on being truly RESTful (just look at the AWS EC2 monstrosity of an API). Authentication is almost certainly a secondary consideration. If you are reading this post and just don't want to listen to my suggestions, I plead with you to follow someone else's example and not roll your own authentication scheme.
*Yes, I know. I am being delusional here. Please humor me in my delusion.