Search code examples
javajsonplayframeworkplayframework-2.0instagram

How to verify Instagram real-time API x-hub-signature in Java?


I'm using Play framework to develop consumer for Instagram real-time API. But still could not perform x-hub-signature verification properly. So, how can we perform Instagram x-hub-signature verification using Java and Play framework?

Here is my current code:

  1. From the Play framework, I obtain the JSON payload using this method:

    public static Result receiveInstaData(){
        JsonNode json = request().body().asJson();
    
        //obtain the x-hub-signature from the header
        //obtain the corresponding client secret
    
        VerificationResult verificationResult =  
            SubscriptionUtil.verifySubscriptionPostSignature(
                clientSecret, json.toString(), xHubSignature);
    
        if(verificationResult.isSuccess()){
        //do something
        }
     }
    
  2. Then inside the SubscriptionUtil, I perform verification using this following code:

    public static VerificationResult verifySubscriptionPostSignature(String clientSecret, String rawJsonData, String xHubSignature) {
        SecretKeySpec keySpec;
        keySpec = new SecretKeySpec(clientSecret.getBytes("UTF-8"), HMAC_SHA1);
    
        Mac mac;
        mac = Mac.getInstance(HMAC_SHA1);
        mac.init(keySpec);
    
        byte[] result;
        result = mac.doFinal(rawJsonData.getBytes("UTF-8"));
        String encodedResult = Hex.encodeHexString(result);
    
        return new VerificationResult(encodedResult.equals(xHubSignature), encodedResult);
       }
    

I created a standalone Python script that copies the instagram-python implementation and both of them produce the same results for the same clientSecret and jsonString. Maybe I should provide with raw binary data instead of String.

If let's say we need a raw binary data for JSON request, then I need to create my custom BodyParser to parse the JSON request to raw binary data[5]

References:

[1-4]http://pastebin.com/g4uuDwzn (SO doesn't allow me to post more than 2 links, so I put all the references here. The links contain the signature verification in Ruby, Python and PHP)

[5]https://groups.google.com/forum/#!msg/play-framework/YMQb6yeDH5o/jU8FD--yVPYJ

[6]My standalone python script: #! /usr/bin/env python

import sys
import hmac
import hashlib

hc_client_secret = "myclientsecret"
hc_raw_response = "[{\"subscription_id\":\"1\",\"object\":\"user\",\"object_id\":\"1234\",\"changed_aspect\":\"media\",\"time\":1297286541},{\"subscription_id\":\"2\",\"object\":\"tag\",\"object_id\":\"nofilter\",\"changed_aspect\":\"media\",\"time\":1297286541}]"

client_secret = hc_client_secret
raw_response = hc_raw_response

if len(sys.argv) != 3:
    print 'Usage verify_signature <client_secret> <raw_response>.\nSince the inputs are invalid, use the hardcoded value instead!'
else:
    client_secret = sys.argv[1]
    raw_response = sys.argv[2]  

print "client_secret = " + client_secret
print "raw_response = " + raw_response

digest = hmac.new(client_secret.encode('utf-8'), msg=raw_response.encode('utf-8'), digestmod=hashlib.sha1).hexdigest()
print digest

Solution

  • Finally I managed to find the solution. For the Controller in Play Framework, we need to use BodyParser.Raw so the we can extract the payload request as raw data, i.e. array of bytes.

    Here's the code for the controller in Play Framework:

    @BodyParser.Of(BodyParser.Raw.class)
    public static Result receiveRawInstaData(){
        Map<String, String[]> headers = request().headers();
        RawBuffer jsonRaw = request().body().asRaw();
    
        if(jsonRaw == null){
            logger.warn("jsonRaw is null. Something is wrong with the payload");
            return badRequest("Expecting serializable raw data");
        }
    
        String[] xHubSignature = headers.get(InstaSubscriptionUtils.HTTP_HEADER_X_HUB_SIGNATURE);
        if(xHubSignature == null){
            logger.error("Invalid POST. It does not contain {} in its header", InstaSubscriptionUtils.HTTP_HEADER_X_HUB_SIGNATURE);
            return badRequest("You are not Instagram!\n");
        }
    
        String json;
        byte[] jsonRawBytes;
    
        jsonRawBytes = jsonRaw.asBytes();
        json = new String(jsonRawBytes, StandardCharsets.UTF_8);
    
        try {
            String clientSecret = InstaSubscriptionUtils.getClientSecret(1);
            VerificationResult verificationResult = SubscriptionUtil.verifySubscriptionPostRequestSignature
                    (clientSecret,jsonRawBytes, xHubSignature[0]);
            if(verificationResult.isSuccess()){
                logger.debug("Signature matches!. Received signature: {}, calculated signature: {}", xHubSignature[0], verificationResult.getCalculatedSignature());
            }else{
                logger.error("Signature doesn't match. Received signature: {}, calculated signature: {}", xHubSignature[0], verificationResult.getCalculatedSignature());
                return badRequest("Signature does not match!\n");
            }
        } catch (InstagramException e) {
            logger.error("Instagram exception.", e);
            return internalServerError("Internal server error. We will attend to this problem ASAP!");
        }
    
        logger.debug("Received xHubSignature: {}", xHubSignature[0]);
        logger.info("Sucessfully received json data: {}", json);
    
        return ok("OK!");
    }
    

    And for the code for method verifySubscriptionPostRequestSignature in SubscriptionUtil

    public static VerificationResult verifySubscriptionPostRequestSignature(String clientSecret, byte[] rawJsonData, String xHubSignature) throws InstagramException{
        SecretKeySpec keySpec;
        keySpec = new SecretKeySpec(clientSecret.getBytes(StandardCharsets.UTF_8), HMAC_SHA1);
        Mac mac;
    
        try {
            mac = Mac.getInstance(HMAC_SHA1);
            mac.init(keySpec);
            byte[] result = mac.doFinal(rawJsonData);
            String encodedResult = Hex.encodeHexString(result);
    
            return new VerificationResult(encodedResult.equals(xHubSignature), encodedResult);
        } catch (NoSuchAlgorithmException e) {
            throw new InstagramException("Invalid algorithm name!", e);
        } catch (InvalidKeyException e){
            throw new InstagramException("Invalid key: " + clientSecret, e);
        }
    }
    

    I implemented this solution in jInstagram, here is the link to the source code: SubscriptionUtil