Using API keys to identify the service making the call, authenticating the service making the call, and checking that data received has not been changed.

So, you’ve followed all the talk about microservices and created one, complete with its very own REST API to be invoked by various client applications or, indeed, other services. At this point, however, you start to wonder about security – surely you don’t just want anyone calling your service…?

Using API keys to authorise and validate calls

Like a lot of the microservices landscape, there are a number of techniques and practices that can be applied to a given scenario; security is no exception. This article is going to look at how we can use API keys to authorise and validate calls that a service receives. These keys allow us to answer the following important questions:

  1. Which service is making the call?
  2. Do I trust the service that is making the call?
  3. Have any of the parameters, or the data, been altered?

Because API keys allow you to make sure that data hasn’t been altered, they can also be used to protect your service from other problems such as replay attacks whereby the same request is sent multiple times.

One important thing to note is that API keys will not help you with authentication; that is, the identity of the user and what they are allowed to do. That is typically handled by other security options such as Oauth and OpenID Connect.

What makes up an API key then?

An API key is composed of a number of component parts, each of which represents some data that needs to be sent to your service. Here’s an example key:

[Parameter data] + [service ID] + [time stamp]

where:

  • Parameter data is the data being sent to your API.
  • Service ID is an identifier that your API will use to recognise the service (remember that this won’t say who the service is acting on behalf of).
  • Time stamp corresponds to when the request was made.

So, this answers question 1 above, we now know which service is making the API call. However, at the moment, any of these items could just be invented by a malicious client and sent to your service. We still need to answer questions 2 and 3 above: do we trust the caller, and has anything been altered?

Using a hash to secure the microservice

The way to answer questions 2 and 3 is using something called a Hash-based Message Authentication Code or HMAC. An HMAC takes some data, creates a hash of it, and then encrypts that hash. What the hash does is ensure that the data used to generate the hash has not been changed; even changing a single byte of the data will cause a different hash to be generated. (This is why you see websites publish a hash alongside a download link to a mirror site as it allows you to check whether or not a change has been made to the file since the creator published it.)

Once we have the hash, it is then encrypted by what is known as a shared secret which is where the encryption and decryption are performed using something that was previously shared between both parties, such as a password.

Now we know how an HMAC works, how does it help us secure our microservice? What we do is generate an HMAC using the values in our API key and then transmit it along with everything else. The receiving service is then able to use the HMAC to authorise the call and validate the parameters. Going back to our questions, both are now answered with the use of a HMAC as follows:

  • Do I trust the service that is making the call? The answer is yes because the shared secret is only known by the sender and receiver, no one else knows it, so it must have come from the trusted service.
  • Have any of the parameters, or the data, been altered? No, none have been altered if the hash matches the API key, i.e the data + service ID + time stamp in this example.

Things to consider…

  • How to share the secret. This is typically done via a second channel. So take GitHub, for example. When you want to authorise an application to access GitHub services, as part of that process you are given a shared secret to use which is specific to your app. Public/private keys obviously solve this problem but require a public key infrastructure (PKI) to be setup, which means that the shared secret approach offers a lower configuration and maintenance overhead. Having said that, if both the client and server are part of the same PKI then you can use that.
  • How to read the shared secret. Hard-coding the secret into your application needs to be avoided, otherwise it won’t be stateless, easily deployed into cloud environments, easy to change, or will just be plain visible to someone looking at your code!
    Typically the secret is read from environment variables, which has the advantage of working in pretty much all languages (remember that a microservice architecture can be a ployglot one), but needs to be configured to only be visible from the process running the microservice, rather than all proceses on the server. You can always use more language-specific features to read the shared secret, such a system properties in a JVM, but the important thing is no hard-coding.
  • Transmitting the HMAC over the wire. When an HMAC is generated you are typically left with a series of meaningless bytes. You need to think about if they are going to need to be encoded prior to transmission. Take a HTTP REST call, for example. You’re going to need to take some extra steps such as base 64 and URL-encoding before invoking the REST end-point. If, however, you are making a call over something that supports binary formats, such as a messaging system, then it may not be necessary to encode the HMAC at all.

Finally, you can add whatever additional data you want to your API; there isn’t a specification to follow, you just need to consider the three security questions posed at the start of this article: who is calling me, do I trust them, and has anything changed in transit?

An example API key implementation

In order to illustrate the techniques talked about in this article there is a WAS Liberty sample available. The diagram below shows the overall architecture of the sample, which consists of two Liberty servers, each hosting their own JAX-RS microservice. The configuration is stored in various Liberty server files such as server.xml. The numbers indicate the process flow starting with a client accessing the REST API of microservice 1 and ending with the invocation inside microservice 2. Finally, shared components have the same colour:


SecureMS-APIKeys

Let’s now walk through the flow and see what is happening at each stage, together with some code snippets.

(1) Client makes a request

A client makes a request to the REST API of the first microservice; this may be by the actual user or another microservice. Inside the message-handling code, we create a new JAX-RS client with which we are going to invoke the second (remote) microservice. Ideally we want to create this client every time so that we can be stateless, pick up any configuration changes etc.

@GET
@Produces(MediaType.TEXT_PLAIN)
public String getMessage() {
	Client client = ClientBuilder.newClient();

(2) Creating the JAX-RS client

The client needs to know where the remote service is located (remember, no hard-coding) and so retrieves the value from the environment variable which was configured in the Liberty server.env file. The other thing that the client does is configure a JAX-RS client interceptor. This is the APIKey class and its constructor takes a service ID and the name of the environment variable storing the shared secret. This class is going to be responsible for creating the API key that will be used during the call. Splitting this out into a separate class allows us to easily add or remove the use of API keys without having to change the business logic.

//register the API key generator for use with the client call
APIKey apikey = new APIKey(SERVICE_ID, SYSPROP_SECRET);
client.register(apikey);

(3) Client generates the API key

The API key class reads the shared secret from the environment variable specified in the constructor (which has its value set in the Liberty server.env file). It then creates the API key by feeding the query string parameters, service ID, and timestamp into an HMAC generator. The service ID, timestamp, and HMAC are then appended to the URL to be invoked by the JAX-RS client.

/*
* Entry point for the client that wants to make a request to a second
* service. It takes the original URI supplied and adds additional query string
* parameters. These are
*
* 1. The service ID supplied by the client
* 2. A timestamp of when the request was made
* 3. A generated API key for this invocation.
*
* @see javax.ws.rs.client.ClientRequestFilter#filter(javax.ws.rs.client.ClientRequestContext)
*/
@Override
public void filter(ClientRequestContext ctx) throws IOException {
	String idparams = Params.serviceID.toString() + serviceID + Params.stamp.toString() + Long.toString(System.currentTimeMillis());
	String apikey = ctx.getUri().getRawQuery() + idparams;
	String hmac = URLEncoder.encode(digest(apikey), CHAR_SET);
	URI uri = URI.create(ctx.getUri().toString() + idparams + Params.apikey.toString() + hmac);
	System.setProperty(SYSPROP_LOGGING, "Outgoing request url : " + uri.toString());
ctx.setUri(uri);
}

(4) Invoke the API

The JAX-RS client now calls the specified service location using the interceptor modified URL which contains the API key and HMAC.

//make the request
String log = "Target set in JAXRS client : " + svcurl + "?id=1&full=truen";
WebTarget target = client.target(svcurl + "?id=1&full=true");
Invocation.Builder builder = target.request(MediaType.APPLICATION_JSON);
Response response = builder.build("GET").invoke();
String resp = response.readEntity(String.class);
response.close();

(5) Authorising filter

The same class that was used to alter the outgoing client request can also be deployed as a filter. Obviously it’s not always possible to use the same class for both services. If you can, however, it makes life easier in terms of controlling what authorisation steps are involved and ensuring that all parties use the same algorithms.

The following code snippet goes through a number of stages as it authorises the request. It starts by checking that we’ve actually received what we expected in terms of data items, then goes through validating the HMAC, and finally making additional checks using the timestamp. Similar to the client interceptor in step 3, using a filter allows us to secure our service (or change that security) without changing the business logic inside the service.

while(!state.equals(AuthenticationState.PASSED)) {
switch(state) {
case hasQueryString : //check that there is a query string which will contain the service ID and api key
	queryString = ((HttpServletRequest) request).getQueryString(); //this is the raw version
	state = (queryString == null) ? AuthenticationState.ACCESS_DENIED : AuthenticationState.hasAPIKeyParam;
break;
case hasAPIKeyParam : //check there is an apikey parameter
	pos = queryString.lastIndexOf(Params.apikey.toString());
	state = (pos == -1) ? AuthenticationState.ACCESS_DENIED : AuthenticationState.isAPIKeyValid;
break;
case isAPIKeyValid : //validate API key against all parameters (except the API key itself)
	queryString = queryString.substring(0, pos); //remove API key from end of query string
	String hmac = request.getParameter(Params.apikey.name());
	apikey = digest(queryString);
	state = !apikey.equals(hmac) ? AuthenticationState.ACCESS_DENIED : AuthenticationState.hasKeyExpired;
break;
case hasKeyExpired : //check that key has not timed out
	time = Long.parseLong(request.getParameter(Params.stamp.name()));
	state = (System.currentTimeMillis() - time) > timeoutMS ? AuthenticationState.ACCESS_DENIED : AuthenticationState.checkReplay;
break;
case checkReplay : //simple replay check - only allows the one time use of API keys, storing time allows expired keys to be purged
	Long value = usedKeys.putIfAbsent(apikey, time);
	state = value != null ? AuthenticationState.ACCESS_DENIED : AuthenticationState.PASSED;
break;
case ACCESS_DENIED :
default :
	((HttpServletResponse)response).sendError(HttpServletResponse.SC_FORBIDDEN);
return;
}
}

(6) Service process the request

Finally, the service is now invoked (assuming that it has passed the checks in the filter). Depending on your requirements, you can receive the service ID as part of the JAX-RS invocation which then allows you to do additional checking such as validating that the service is entitled to perform the requested operation.

@GET
public String getMessage(final @QueryParam("serviceID") String svcid, final @QueryParam("id") String id) {
	String msg = "Received params : ntserviceID = " + svcid + "ntid = " + id + "nResponse to client : nt";
/*
* Optionally at this point you would check that the service
* is entitled to make this call
*/

Extending the API key

The sample API key shows how you can trust and validate incoming requests from other microservices. This is an extensible model in that you can add as many additional data elements to the API key as fits your organisational needs. Alternatively, you can also add in extra security validation steps.

Summary

This article has explained the principles behind allowing your microservice to be invoked by other microservices. It has described some of the important security questions that need to be answered and how API keys can help you accomplish this.

However, API keys are only one component of your security toolbox, and generally cannot be used in isolation. Additional steps and techniques need to be employed to establish the identity of the person under which the service is being invoked. Typically, your service is not going to be invoked directly by the user, but by another service acting on behalf of a user. Furthermore, once that has been done you still need to perform validation on the actual input data, such as checking that numbers are really numbers and are within acceptable ranges.

1 comment on"Using API keys to secure your microservice"

  1. […] Using API keys to secure your microservice is Adam’s sample. He explains what API keys are, where they come from, and how they’re used. […]

Join The Discussion

Your email address will not be published. Required fields are marked *