Implementing REST Authentication

While there is not much written about REST authentication, there does seem to be a common theme among the few articles written about it that REST services should be authenticated by signing the query parameters using a private key and making the calls over HTTPS.  This posting will provide an example of the signing of query parameters using a simple Spring server. We’ll provide a small twist by putting the authentication information in headers.

Our authentication scheme will provide the API user with an API key and a private key. The client of our API will take the context path, service name, servlet path and path info and then sort the parameters in alphabetic order. A timestamp will be added to make each call unique. This new path will be used to create a signature that will be added into the query with the api key.

For example, the following query:

/rest-security/services/customers?limit=10

becomes:

/rest-security/services/customers?limit=10&apikey=RS0001&timestamp=2839489493&signature=abc3994859

Adding those those extra parameters to each call is not pretty, so I’m going to show an alternative where the values associated with authentication are included as header values instead. When we create a sorted URL for authentication, we’ll include those header values when making the signature.

We will use an HTTP Filter to validate the signature. I’m extending from Spring’s OncePerRequestFilter so it can be used in the Spring security chain if you are so inclined. The filter is pretty straight forward. We rebuild the URL to include the header values with the parameters and sort them. Then we get the API key and the signature from the header and validate them. If the validation fails, we return a 401 unauthorized to the caller.

public class RestSignatureFilter extends OncePerRequestFilter {
   @Override
   protected void doFilterInternal(HttpServletRequest request, 
         HttpServletResponse response, FilterChain filterChain) 
         throws ServletException, IOException {
      String url = SignatureHelper.createSortedUrl(request);
      String signature = request.getHeader(SignatureHelper.SIGNATURE_HEADER);
      String apiKey = request.getHeader(SignatureHelper.APIKEY_HEADER);
      try {
         if (!SignatureHelper.validateSignature(url, signature, apiKey)) {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, 
                "REST signature failed validation.");
            return;
         }
      } catch (Exception e) {
         response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, 
            "The REST Security Server experienced an internal error.");
         return;
      }
      filterChain.doFilter(request, response);
   }    
}

We create a sorted url by capturing all the parameters and the authentication headers in a tree map so the keys will be sorted. When then build the url with the context path, servlet path and path info followed by the parameters and headers sorted.

public static String createSortedUrl(HttpServletRequest request) {

   // use a TreeMap to sort the headers and parameters
   TreeMap<String, String> headersAndParams = new TreeMap<String, String>();    

   // load header values we care about
   Enumeration e = request.getHeaderNames();
   while (e.hasMoreElements()) {
      String key = (String) e.nextElement();
      if (SIGNATURE_KEYWORDS.contains(key)) {
         headersAndParams.put(key, request.getHeader(key));
      }
   }

   // load parameters
   for (Object key : request.getParameterMap().keySet()) {
      String[] o = (String[]) request.getParameterMap().get(key);
      headersAndParams.put((String) key, o[0]);
   }

   return createSortedUrl(
      request.getContextPath() + request.getServletPath() + request.getPathInfo(),
      headersAndParams);                
}

public static String createSortedUrl(String url, 
      TreeMap<String, String> headersAndParams) {
   // build the url with headers and parms sorted
   String params = "";
   for (String key : headersAndParams.keySet()) {
      if (params.length() > 0) {
         params += "@";
      }
      params += key + "=" + headersAndParams.get(key).toString();
   }
   if (!url.endsWith("?")) url += "?";
   return url + params; 
}

The validation of the signature looks up the public key using the API key and then uses java.security.Signature to validate the signature after it’s been decoded from it’s Base64 format.

public static boolean validateSignature(String url, String signatureString, 
      String apiKey) throws InvalidKeyException, Exception {

   String publicKey = SignatureHelper.getPublicKey(apiKey);
   if (publicKey == null) return false;

   Signature signature = Signature.getInstance(ALGORITHM);
   signature.initVerify(decodePublicKey(publicKey));
   signature.update(url.getBytes());
   try {
      return signature.verify(Base64.decodeBase64(signatureString));
   } catch (SignatureException e) {
      return false;
   }
}

The caller will need to add it’s API key and a timestamp to the headers. It then needs to create the same sorted URL as is created on the server side and create a signature against it using the private key it was provided when it received the API key. The signature is encoded in Base64 and added to the headers.

@Test
public void shouldReturnCustomers() throws Exception {
   final String urlApp = "/rest-security/services/customers?";
   final String fullUrl = "http://localhost:8080" + urlApp;

   CustomerList list = template.execute(fullUrl, HttpMethod.GET,
      new RequestCallback() {
         @Override
         public void doWithRequest(ClientHttpRequest request) 
               throws IOException {
            HttpHeaders headers = request.getHeaders();
            headers.add("Accept", "*/*");
            headers.add(SignatureHelper.APIKEY_HEADER, 
                        SignatureHelper.API_KEY);
            headers.add(SignatureHelper.TIMESTAMP_HEADER, 
                        "" + System.currentTimeMillis());
            try {
               headers.add(SignatureHelper.SIGNATURE_HEADER, 
               SignatureHelper.createSignature(headers, 
                     urlApp, SignatureHelper.PRIVATE_KEY));
            } catch (Exception e) {
               fail();
            }
         }
      }, responseExtractor);

   assertTrue(list.getCustomer().size() > 0);
}

public static String createSignature(HttpHeaders headers, String url, 
      String privateKey) throws Exception {

   TreeMap<String, String> sortedHeaders = new TreeMap<String, String>();
   for (String key : headers.keySet()) {
      if (SIGNATURE_KEYWORDS.contains(key)) {
         sortedHeaders.put(key, headers.get(key).get(0));
      }
   }

   String sortedUrl = createSortedUrl(url, sortedHeaders);

   KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);
   byte[] privateKeyBytes = Base64.decodeBase64(privateKey);
   EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(privateKeyBytes);

   Signature sig = Signature.getInstance(ALGORITHM);            
   sig.initSign(keyFactory.generatePrivate(privateKeySpec));
   sig.update(sortedUrl.getBytes());

   return Base64.encodeBase64URLSafeString(sig.sign());
}

The full code to this example can be found in the rest-security project on github.

One thought on “Implementing REST Authentication

  1. SShrestha says:

    Thank you.

  2. Robert says:

    Thank you , last peace of my service is the API key.

    cheers.

    1. arsh says:

      How did you you get the output..

      What will be the URL for signing request.. I used the above mentioned urls.. but i am getting a 500 Internal Server error.. “The REST Security Server experienced an internal error.” As written in the try and catch block of the code.
      Please help

  3. arsh says:

    What will be the URL for signing request.. I used the above mentioned urls.. but i am getting a 500 Internal Server error.. “The REST Security Server experienced an internal error.” As written in the try and catch block of the code.

    Please help

  4. Heaven says:

    I ran into some trouble:how to sign in C# and virify by JAVA?

Leave a Reply

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

*

*