1 package de.juplo.facebook;
4 import java.io.IOException;
5 import java.io.UnsupportedEncodingException;
6 import java.security.InvalidKeyException;
7 import java.security.NoSuchAlgorithmException;
9 import java.util.HashMap;
11 import java.util.regex.Matcher;
12 import java.util.regex.Pattern;
13 import javax.crypto.Mac;
14 import javax.crypto.spec.SecretKeySpec;
15 import org.apache.commons.codec.binary.Base64;
16 import org.codehaus.jackson.JsonNode;
17 import org.codehaus.jackson.map.ObjectMapper;
18 import org.slf4j.Logger;
19 import org.slf4j.LoggerFactory;
20 import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
21 import org.springframework.security.oauth2.client.resource.UserRedirectRequiredException;
22 import org.springframework.security.oauth2.client.token.AccessTokenRequest;
23 import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeAccessTokenProvider;
24 import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
25 import org.springframework.security.oauth2.common.OAuth2AccessToken;
29 * This class extends {@link AuthorizationCodeAccessTokenProvider} and adds
30 * support for signed requests, which are issued by Facebook, if the Canvas-
31 * or Tab-Page of a Facebook-App is accessed for the first time.
33 * @author Kai Moritz <kai@juplo.de>
35 public class SignedRequestAwareAuthorizationCodeAccessTokenProvider
36 extends AuthorizationCodeAccessTokenProvider
38 private final Logger log =
39 LoggerFactory.getLogger(SignedRequestAwareAuthorizationCodeAccessTokenProvider.class);
40 private final static Pattern pattern =
41 Pattern.compile("([a-zA-Z0-9_-]+)\\.([a-zA-Z0-9_-]+)");
43 public final static String PARAM_SIGNED_REQUEST = "signed_request";
46 private String secret;
47 private ObjectMapper objectMapper;
51 public OAuth2AccessToken obtainAccessToken(
52 OAuth2ProtectedResourceDetails details,
53 AccessTokenRequest parameters
58 return super.obtainAccessToken(details, parameters);
60 catch (UserRedirectRequiredException redirect)
62 log.debug("no valid access-token available: checking for signed request");
64 if (!parameters.containsKey(PARAM_SIGNED_REQUEST))
67 "parameter " + PARAM_SIGNED_REQUEST + " is not present"
72 String signed_request = parameters.get(PARAM_SIGNED_REQUEST).get(0);
74 Matcher matcher = pattern.matcher(signed_request);
75 if (!matcher.matches())
77 log.error("invalid signed_request: {}", signed_request);
81 String signature = matcher.group(1);
82 String rawdata = matcher.group(2);
87 data = new String(Base64.decodeBase64(rawdata), "UTF-8");
88 log.debug("JSON-data: {}", data);
90 catch (UnsupportedEncodingException e)
92 log.error("error while decoding data: {}", e.getMessage());
99 json = objectMapper.readTree(data);
101 catch (IOException e)
103 log.error("error \"{}\" while parsing JSON-data: {}", e, data);
107 String algorithm = "";
110 algorithm = json.get("algorithm").asText();
112 catch (NullPointerException e) {}
113 if (algorithm.isEmpty())
115 log.error("field \"algorithm\" is missing: {}", data);
118 algorithm = algorithm.replaceAll("-", "");
123 SecretKeySpec key = new SecretKeySpec(secret.getBytes("UTF-8"), algorithm);
124 Mac mac = Mac.getInstance(algorithm);
126 byte[] hmacData = mac.doFinal(rawdata.getBytes("UTF-8"));
127 check = new String(Base64.encodeBase64URLSafe(hmacData), "UTF-8");
130 UnsupportedEncodingException |
131 NoSuchAlgorithmException |
132 InvalidKeyException |
133 IllegalStateException e
136 log.error("signature check failed!", e);
139 if (!check.equals(signature))
141 log.error("signature does not match!");
146 * Extract additional information and store it in the token
148 * https://developers.facebook.com/docs/reference/login/signed-request/
152 Map<String,Object> additionalInformation = new HashMap<>();
155 additionalInformation.put(
157 new Date(json.get("issued_at").getLongValue()*1000L)
159 Map<String,Object> user = new HashMap<>();
162 json.get("user").get("country").asText()
166 json.get("user").get("locale").asText()
170 json.get("user").get("age").get("min").getNumberValue()
172 if (json.get("user") != null && json.get("user").get("max") != null)
175 json.get("user").get("age").get("max").getNumberValue()
177 additionalInformation.put("user", user);
178 if (json.get("app_data") != null)
179 additionalInformation.put("app_data", json.get("app_data").asText());
180 if (json.get("page") != null)
182 Map<String,Object> page = new HashMap<>();
183 page.put("id", json.get("page").get("id").asText());
184 page.put("liked", json.get("page").get("liked").asBoolean());
185 page.put("admin", json.get("page").get("admin").asBoolean());
186 additionalInformation.put("page", page);
189 catch (NullPointerException e)
191 log.warn("expected additional data is missing: {}", data);
194 DefaultOAuth2AccessToken token = null;
197 String value = json.get("oauth_token").asText();
200 log.error("field \"oauth_token\" is missing: {}", data);
203 token = new DefaultOAuth2AccessToken(value);
204 token.setExpiration(new Date(json.get("expires").getLongValue()*1000L));
206 additionalInformation.put(
208 json.get("user_id").asText()
211 token.setAdditionalInformation(additionalInformation);
213 catch (NullPointerException e)
217 log.error("field \"oauth_token\" is missing: {}", data);
221 log.warn("expected additional data is missing: {}", data);
229 public String getSecret()
234 public void setSecret(String secret)
236 this.secret = secret;
239 public ObjectMapper getObjectMapper()
244 public void setObjectMapper(ObjectMapper objectMapper)
246 this.objectMapper = objectMapper;