From 2d7ccaa2139e89e77b57120274f38b146dd45c0f Mon Sep 17 00:00:00 2001 From: Kai Moritz Date: Sat, 22 Mar 2014 11:52:56 +0100 Subject: [PATCH] Extended AuthorizationCodeAccessTokenProvider to support signed requests --- ...eAuthorizationCodeAccessTokenProvider.java | 239 ++++++++++++++++++ 1 file changed, 239 insertions(+) create mode 100644 src/main/java/de/juplo/facebook/SignedRequestAwareAuthorizationCodeAccessTokenProvider.java diff --git a/src/main/java/de/juplo/facebook/SignedRequestAwareAuthorizationCodeAccessTokenProvider.java b/src/main/java/de/juplo/facebook/SignedRequestAwareAuthorizationCodeAccessTokenProvider.java new file mode 100644 index 0000000..8d4c7ee --- /dev/null +++ b/src/main/java/de/juplo/facebook/SignedRequestAwareAuthorizationCodeAccessTokenProvider.java @@ -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 + */ +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 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 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 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; + } +} -- 2.20.1