--- /dev/null
+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;
+ }
+}