Extended AuthorizationCodeAccessTokenProvider to support signed requests
authorKai Moritz <kai@juplo.de>
Sat, 22 Mar 2014 10:52:56 +0000 (11:52 +0100)
committerKai Moritz <kai@juplo.de>
Tue, 6 Oct 2015 06:22:20 +0000 (08:22 +0200)
src/main/java/de/juplo/facebook/SignedRequestAwareAuthorizationCodeAccessTokenProvider.java [new file with mode: 0644]

diff --git a/src/main/java/de/juplo/facebook/SignedRequestAwareAuthorizationCodeAccessTokenProvider.java b/src/main/java/de/juplo/facebook/SignedRequestAwareAuthorizationCodeAccessTokenProvider.java
new file mode 100644 (file)
index 0000000..8d4c7ee
--- /dev/null
@@ -0,0 +1,239 @@
+package de.juplo.facebook;
+
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import org.apache.commons.codec.binary.Base64;
+import org.codehaus.jackson.JsonNode;
+import org.codehaus.jackson.map.ObjectMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
+import org.springframework.security.oauth2.client.resource.UserRedirectRequiredException;
+import org.springframework.security.oauth2.client.token.AccessTokenRequest;
+import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeAccessTokenProvider;
+import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
+import org.springframework.security.oauth2.common.OAuth2AccessToken;
+
+
+/**
+ * This class extends {@link AuthorizationCodeAccessTokenProvider} and adds
+ * support for signed requests, which are issued by Facebook, if the Canvas-
+ * or Tab-Page of a Facebook-App is accessed for the first time.
+ *
+ * @author Kai Moritz <kai@juplo.de>
+ */
+public class SignedRequestAwareAuthorizationCodeAccessTokenProvider
+    extends AuthorizationCodeAccessTokenProvider
+{
+  private final Logger log =
+      LoggerFactory.getLogger(SignedRequestAwareAuthorizationCodeAccessTokenProvider.class);
+  private final static Pattern pattern =
+      Pattern.compile("([a-zA-Z0-9_-]+)\\.([a-zA-Z0-9_-]+)");
+
+  public final static String PARAM_SIGNED_REQUEST = "signed_request";
+
+
+  private String secret;
+  private ObjectMapper objectMapper;
+
+
+  @Override
+  public OAuth2AccessToken obtainAccessToken(
+      OAuth2ProtectedResourceDetails details,
+      AccessTokenRequest parameters
+      )
+  {
+    try
+    {
+      return super.obtainAccessToken(details, parameters);
+    }
+    catch (UserRedirectRequiredException redirect)
+    {
+      log.debug("no valid access-token available: checking for signed request");
+
+      if (!parameters.containsKey(PARAM_SIGNED_REQUEST))
+      {
+        log.info(
+            "parameter " + PARAM_SIGNED_REQUEST + " is not present"
+            );
+        throw redirect;
+      }
+
+      String signed_request = parameters.get(PARAM_SIGNED_REQUEST).get(0);
+
+      Matcher matcher = pattern.matcher(signed_request);
+      if (!matcher.matches())
+      {
+        log.error("invalid signed_request: {}", signed_request);
+        throw redirect;
+      }
+
+      String signature = matcher.group(1);
+      String rawdata = matcher.group(2);
+
+      String data;
+      try
+      {
+        data = new String(Base64.decodeBase64(rawdata), "UTF-8");
+        log.debug("JSON-data: {}", data);
+      }
+      catch (UnsupportedEncodingException e)
+      {
+        log.error("error while decoding data: {}", e.getMessage());
+        throw redirect;
+      }
+
+      JsonNode json;
+      try
+      {
+        json = objectMapper.readTree(data);
+      }
+      catch (IOException e)
+      {
+        log.error("error \"{}\" while parsing JSON-data: {}", e, data);
+        throw redirect;
+      }
+
+      String algorithm = "";
+      try
+      {
+        algorithm = json.get("algorithm").asText();
+      }
+      catch (NullPointerException e) {}
+      if (algorithm.isEmpty())
+      {
+        log.error("field \"algorithm\" is missing: {}", data);
+        throw redirect;
+      }
+      algorithm = algorithm.replaceAll("-", "");
+
+      String check;
+      try
+      {
+        SecretKeySpec key = new SecretKeySpec(secret.getBytes("UTF-8"), algorithm);
+        Mac mac = Mac.getInstance(algorithm);
+        mac.init(key);
+        byte[] hmacData = mac.doFinal(rawdata.getBytes("UTF-8"));
+        check = new String(Base64.encodeBase64URLSafe(hmacData), "UTF-8");
+      }
+      catch (
+          UnsupportedEncodingException |
+          NoSuchAlgorithmException |
+          InvalidKeyException |
+          IllegalStateException e
+          )
+      {
+        log.error("signature check failed!", e);
+        throw redirect;
+      }
+      if (!check.equals(signature))
+      {
+        log.error("signature does not match!");
+        throw redirect;
+      }
+
+      DefaultOAuth2AccessToken token = null;
+      try
+      {
+        String value = json.get("oauth_token").asText();
+        if (value.isEmpty())
+        {
+          log.error("field \"oauth_token\" is missing: {}", data);
+          throw redirect;
+        }
+        token = new DefaultOAuth2AccessToken(value);
+        token.setExpiration(new Date(json.get("expires").getLongValue()*1000L));
+
+        /**
+         * Extract additional information and store it in the token
+         * See:
+         * https://developers.facebook.com/docs/reference/login/signed-request/
+         * TODO:
+         * - Attribute "code"
+         */
+        Map<String,Object> additionalInformation = new HashMap<>();
+        additionalInformation.put(
+            "issued_at",
+            new Date(json.get("issued_at").getLongValue()*1000L)
+            );
+        additionalInformation.put(
+            "user_id",
+            json.get("user_id").asText()
+            );
+        Map<String,Object> user = new HashMap<>();
+        user.put(
+            "country",
+            json.get("user").get("country").asText()
+            );
+        user.put(
+            "locale",
+            json.get("user").get("locale").asText()
+            );
+        user.put(
+            "age_min",
+            json.get("user").get("age").get("min").getNumberValue()
+            );
+        if (json.get("user") != null && json.get("user").get("max") != null)
+          user.put(
+              "age_max",
+              json.get("user").get("age").get("max").getNumberValue()
+              );
+        additionalInformation.put("user", user);
+        if (json.get("app_data") != null)
+          additionalInformation.put("app_data", json.get("app_data").asText());
+        if (json.get("page") != null)
+        {
+          Map<String,Object> page = new HashMap<>();
+          page.put("id", json.get("page").get("id").asText());
+          page.put("liked", json.get("page").get("liked").asBoolean());
+          page.put("admin", json.get("page").get("admin").asBoolean());
+          additionalInformation.put("page", page);
+        }
+        token.setAdditionalInformation(additionalInformation);
+      }
+      catch (NullPointerException e)
+      {
+        if (token == null)
+        {
+          log.error("field \"oauth_token\" is missing: {}", data);
+          throw redirect;
+        }
+        else
+          log.warn("expected additional data is missing: {}", data);
+      }
+
+      return token;
+    }
+  }
+
+
+  public String getSecret()
+  {
+    return secret;
+  }
+
+  public void setSecret(String secret)
+  {
+    this.secret = secret;
+  }
+
+  public ObjectMapper getObjectMapper()
+  {
+    return objectMapper;
+  }
+
+  public void setObjectMapper(ObjectMapper objectMapper)
+  {
+    this.objectMapper = objectMapper;
+  }
+}