5df98b719486a0198c9f95b2bc7b066179e983f9
[facebook-utils] / src / main / java / de / juplo / facebook / SignedRequestAwareAuthorizationCodeAccessTokenProvider.java
1 package de.juplo.facebook;
2
3
4 import java.io.IOException;
5 import java.io.UnsupportedEncodingException;
6 import java.security.InvalidKeyException;
7 import java.security.NoSuchAlgorithmException;
8 import java.util.Date;
9 import java.util.HashMap;
10 import java.util.Map;
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;
26
27
28 /**
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.
32  *
33  * @author Kai Moritz <kai@juplo.de>
34  */
35 public class SignedRequestAwareAuthorizationCodeAccessTokenProvider
36     extends AuthorizationCodeAccessTokenProvider
37 {
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_-]+)");
42
43   public final static String PARAM_SIGNED_REQUEST = "signed_request";
44
45
46   private String secret;
47   private ObjectMapper objectMapper;
48
49
50   @Override
51   public OAuth2AccessToken obtainAccessToken(
52       OAuth2ProtectedResourceDetails details,
53       AccessTokenRequest parameters
54       )
55   {
56     try
57     {
58       return super.obtainAccessToken(details, parameters);
59     }
60     catch (UserRedirectRequiredException redirect)
61     {
62       log.debug("no valid access-token available: checking for signed request");
63
64       if (!parameters.containsKey(PARAM_SIGNED_REQUEST))
65       {
66         log.info(
67             "parameter " + PARAM_SIGNED_REQUEST + " is not present"
68             );
69         throw redirect;
70       }
71
72       String signed_request = parameters.get(PARAM_SIGNED_REQUEST).get(0);
73
74       Matcher matcher = pattern.matcher(signed_request);
75       if (!matcher.matches())
76       {
77         log.error("invalid signed_request: {}", signed_request);
78         throw redirect;
79       }
80
81       String signature = matcher.group(1);
82       String rawdata = matcher.group(2);
83
84       String data;
85       try
86       {
87         data = new String(Base64.decodeBase64(rawdata), "UTF-8");
88         log.debug("JSON-data: {}", data);
89       }
90       catch (UnsupportedEncodingException e)
91       {
92         log.error("error while decoding data: {}", e.getMessage());
93         throw redirect;
94       }
95
96       JsonNode json;
97       try
98       {
99         json = objectMapper.readTree(data);
100       }
101       catch (IOException e)
102       {
103         log.error("error \"{}\" while parsing JSON-data: {}", e, data);
104         throw redirect;
105       }
106
107       String algorithm = "";
108       try
109       {
110         algorithm = json.get("algorithm").asText();
111       }
112       catch (NullPointerException e) {}
113       if (algorithm.isEmpty())
114       {
115         log.error("field \"algorithm\" is missing: {}", data);
116         throw redirect;
117       }
118       algorithm = algorithm.replaceAll("-", "");
119
120       String check;
121       try
122       {
123         SecretKeySpec key = new SecretKeySpec(secret.getBytes("UTF-8"), algorithm);
124         Mac mac = Mac.getInstance(algorithm);
125         mac.init(key);
126         byte[] hmacData = mac.doFinal(rawdata.getBytes("UTF-8"));
127         check = new String(Base64.encodeBase64URLSafe(hmacData), "UTF-8");
128       }
129       catch (
130           UnsupportedEncodingException |
131           NoSuchAlgorithmException |
132           InvalidKeyException |
133           IllegalStateException e
134           )
135       {
136         log.error("signature check failed!", e);
137         throw redirect;
138       }
139       if (!check.equals(signature))
140       {
141         log.error("signature does not match!");
142         throw redirect;
143       }
144
145       /**
146        * Extract additional information and store it in the token
147        * See:
148        * https://developers.facebook.com/docs/reference/login/signed-request/
149        * TODO:
150        * - Attribute "code"
151        */
152       Map<String,Object> additionalInformation = new HashMap<>();
153       try
154       {
155         additionalInformation.put(
156             "issued_at",
157             new Date(json.get("issued_at").getLongValue()*1000L)
158             );
159         Map<String,Object> user = new HashMap<>();
160         user.put(
161             "country",
162             json.get("user").get("country").asText()
163             );
164         user.put(
165             "locale",
166             json.get("user").get("locale").asText()
167             );
168         user.put(
169             "age_min",
170             json.get("user").get("age").get("min").getNumberValue()
171             );
172         if (json.get("user") != null && json.get("user").get("max") != null)
173           user.put(
174               "age_max",
175               json.get("user").get("age").get("max").getNumberValue()
176               );
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)
181         {
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);
187         }
188       }
189       catch (NullPointerException e)
190       {
191         log.warn("expected additional data is missing: {}", data);
192       }
193
194       DefaultOAuth2AccessToken token = null;
195       try
196       {
197         String value = json.get("oauth_token").asText();
198         if (value.isEmpty())
199         {
200           log.error("field \"oauth_token\" is missing: {}", data);
201           throw redirect;
202         }
203         token = new DefaultOAuth2AccessToken(value);
204         token.setExpiration(new Date(json.get("expires").getLongValue()*1000L));
205
206         additionalInformation.put(
207             "user_id",
208             json.get("user_id").asText()
209             );
210
211         token.setAdditionalInformation(additionalInformation);
212       }
213       catch (NullPointerException e)
214       {
215         if (token == null)
216         {
217           log.error("field \"oauth_token\" is missing: {}", data);
218           throw redirect;
219         }
220         else
221           log.warn("expected additional data is missing: {}", data);
222       }
223
224       return token;
225     }
226   }
227
228
229   public String getSecret()
230   {
231     return secret;
232   }
233
234   public void setSecret(String secret)
235   {
236     this.secret = secret;
237   }
238
239   public ObjectMapper getObjectMapper()
240   {
241     return objectMapper;
242   }
243
244   public void setObjectMapper(ObjectMapper objectMapper)
245   {
246     this.objectMapper = objectMapper;
247   }
248 }