7
votes

I'm trying to parse Facebook signed_request inside Java Servlet's doPost. And I decode the signed request using commons-codec-1.3's Base64. Here is the code which I used to do it inside servlet's doPost

String signedRequest = (String) req.getParameter("signed_request");
String payload = signedRequest.split("[.]", 2)[1];
payload = payload.replace("-", "+").replace("_", "/").trim();
String jsonString = new String(Base64.decodeBase64(payload.getBytes()));

when I System.out the jsonString it's malformed. Sometime's it misses the ending } of JSON sometime it misses "} in the end of the string.

How can I get the proper JSON response from Facebook?

5

5 Answers

7
votes

facebook is using Base64 for URLs and you are probably trying to decode the text using the standard Base64 algorithm. among other things, the URL variant doesn't required padding with "=".

  1. you could add the required characters in code (padding, etc)
  2. you can use commons-codec 1.5 ( new Base64(true)), where they added support for this encoding.
4
votes

The Facebook is sending you "unpadded" Base64 values (the URL "standard") and this is problematic for Java decoders that don't expect it. You can tell you have the problem when the Base64 encoded data that you want to decode has a length that is not a multiple of 4.

I used this function to fix the values:

public static String padBase64(String b64) {
    String padding = "";
    // If you are a java developer, *this* is the critical bit.. FB expects
    // the base64 decode to do this padding for you (as the PHP one
    // apparently
    // does...
    switch (b64.length() % 4) {
    case 0:
        break;
    case 1:
        padding = "===";
        break;
    case 2:
        padding = "==";
        break;
    default:
        padding = "=";
    }
    return b64 + padding;

}
1
votes

I have never done this in Java so I don't have a full answer, but the fact that you are sometimes losing one and sometimes two characters from the end of the string suggests it may be an issue with Base64 padding. You might want to output the value of payload and see if when it ends with '=' then jsonString is missing '}' and when payload ends with '==' then jsonString is missing '"}'. If that seems to be the case then something is going wrong with the interpretation of the equals signs at the end of payload which are supposed to represent empty bits.

Edit: On further reflection I believe this is because Facebook is using Base64 URL encoding (which does not add = as pad chars) instead of regular Base64, whereas your decoding function is expecting regular Base64 with the trailing = chars.

1
votes

I've upgraded to common-codec-1.5 using code very similar to this and am not experiencing this issue. Have you confirmed that payload really is malformed by using an online decoder?

0
votes

Hello in the year 2021.

The other answers are obsolete, because with Java 8 and newer you can decode the base64url scheme by using the new Base64.getUrlDecoder() (instead of getDecoder).

The base64url scheme is a URL and filename safe dialect of the main base64 scheme and uses "-" instead of "+" and "_" instead of "/" (because the plus and slash chars have special meanings in URLs). Also it does not use "=" chars for the padding (0 to 4 chars) at the end of string.

Here is how you can parse the Facebook signed_request parameter in Java into a Map object:

public static Map<String, String> parseSignedRequest(HttpServletRequest httpReq, String facebookSecret) throws ServletException {
    String signedRequest = httpReq.getParameter("signed_request");
    String splitArray[]  = signedRequest.split("\\.", 2);
    String sigBase64     = splitArray[0];
    String payloadBase64 = splitArray[1];
    String payload       = new String(Base64.getUrlDecoder().decode(payloadBase64));

    try {
        Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
        SecretKeySpec secretKey = new SecretKeySpec(facebookSecret.getBytes(), "HmacSHA256");
        sha256_HMAC.init(secretKey);

        String sigExpected = Base64.getUrlEncoder().withoutPadding().encodeToString(sha256_HMAC.doFinal(payloadBase64.getBytes()));
        if (!sigBase64.equals(sigExpected)) {
            LOG.warn("sigBase64 = {}", sigBase64);
            LOG.warn("sigExpected = {}", sigExpected);
            throw new ServletException("Invalid sig = " + sigBase64);
        }
    } catch (IllegalStateException | InvalidKeyException | NoSuchAlgorithmException ex) {
        throw new ServletException("parseSignedRequest", ex);
    }

    // use Jetty JSON parsing or some other library
    return (Map<String, String>) JSON.parse(payload);
}

I have used the Jetty JSON parser:

<dependency>
    <groupId>org.eclipse.jetty</groupId>
    <artifactId>jetty-util</artifactId>
    <version>9.4.43.v20210629</version>
</dependency>

but there are more libraries available in Java for parsing JSON.