View Javadoc
1   package de.juplo.facebook.token;
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
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 }