1<?php
2/**
3 * Copyright 2017 Facebook, Inc.
4 *
5 * You are hereby granted a non-exclusive, worldwide, royalty-free license to
6 * use, copy, modify, and distribute this software in source code or binary
7 * form for use in connection with the web services and APIs provided by
8 * Facebook.
9 *
10 * As with any software that integrates with the Facebook platform, your use
11 * of this software is subject to the Facebook Developer Principles and
12 * Policies [http://developers.facebook.com/policy/]. This copyright notice
13 * shall be included in all copies or substantial portions of the software.
14 *
15 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
18 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
21 * DEALINGS IN THE SOFTWARE.
22 *
23 */
24namespace Facebook;
25
26use Facebook\Exceptions\FacebookSDKException;
27
28/**
29 * Class SignedRequest
30 *
31 * @package Facebook
32 */
33class SignedRequest
34{
35    /**
36     * @var FacebookApp The FacebookApp entity.
37     */
38    protected $app;
39
40    /**
41     * @var string The raw encrypted signed request.
42     */
43    protected $rawSignedRequest;
44
45    /**
46     * @var array The payload from the decrypted signed request.
47     */
48    protected $payload;
49
50    /**
51     * Instantiate a new SignedRequest entity.
52     *
53     * @param FacebookApp $facebookApp      The FacebookApp entity.
54     * @param string|null $rawSignedRequest The raw signed request.
55     */
56    public function __construct(FacebookApp $facebookApp, $rawSignedRequest = null)
57    {
58        $this->app = $facebookApp;
59
60        if (!$rawSignedRequest) {
61            return;
62        }
63
64        $this->rawSignedRequest = $rawSignedRequest;
65
66        $this->parse();
67    }
68
69    /**
70     * Returns the raw signed request data.
71     *
72     * @return string|null
73     */
74    public function getRawSignedRequest()
75    {
76        return $this->rawSignedRequest;
77    }
78
79    /**
80     * Returns the parsed signed request data.
81     *
82     * @return array|null
83     */
84    public function getPayload()
85    {
86        return $this->payload;
87    }
88
89    /**
90     * Returns a property from the signed request data if available.
91     *
92     * @param string     $key
93     * @param mixed|null $default
94     *
95     * @return mixed|null
96     */
97    public function get($key, $default = null)
98    {
99        if (isset($this->payload[$key])) {
100            return $this->payload[$key];
101        }
102
103        return $default;
104    }
105
106    /**
107     * Returns user_id from signed request data if available.
108     *
109     * @return string|null
110     */
111    public function getUserId()
112    {
113        return $this->get('user_id');
114    }
115
116    /**
117     * Checks for OAuth data in the payload.
118     *
119     * @return boolean
120     */
121    public function hasOAuthData()
122    {
123        return $this->get('oauth_token') || $this->get('code');
124    }
125
126    /**
127     * Creates a signed request from an array of data.
128     *
129     * @param array $payload
130     *
131     * @return string
132     */
133    public function make(array $payload)
134    {
135        $payload['algorithm'] = isset($payload['algorithm']) ? $payload['algorithm'] : 'HMAC-SHA256';
136        $payload['issued_at'] = isset($payload['issued_at']) ? $payload['issued_at'] : time();
137        $encodedPayload = $this->base64UrlEncode(json_encode($payload));
138
139        $hashedSig = $this->hashSignature($encodedPayload);
140        $encodedSig = $this->base64UrlEncode($hashedSig);
141
142        return $encodedSig . '.' . $encodedPayload;
143    }
144
145    /**
146     * Validates and decodes a signed request and saves
147     * the payload to an array.
148     */
149    protected function parse()
150    {
151        list($encodedSig, $encodedPayload) = $this->split();
152
153        // Signature validation
154        $sig = $this->decodeSignature($encodedSig);
155        $hashedSig = $this->hashSignature($encodedPayload);
156        $this->validateSignature($hashedSig, $sig);
157
158        $this->payload = $this->decodePayload($encodedPayload);
159
160        // Payload validation
161        $this->validateAlgorithm();
162    }
163
164    /**
165     * Splits a raw signed request into signature and payload.
166     *
167     * @return array
168     *
169     * @throws FacebookSDKException
170     */
171    protected function split()
172    {
173        if (strpos($this->rawSignedRequest, '.') === false) {
174            throw new FacebookSDKException('Malformed signed request.', 606);
175        }
176
177        return explode('.', $this->rawSignedRequest, 2);
178    }
179
180    /**
181     * Decodes the raw signature from a signed request.
182     *
183     * @param string $encodedSig
184     *
185     * @return string
186     *
187     * @throws FacebookSDKException
188     */
189    protected function decodeSignature($encodedSig)
190    {
191        $sig = $this->base64UrlDecode($encodedSig);
192
193        if (!$sig) {
194            throw new FacebookSDKException('Signed request has malformed encoded signature data.', 607);
195        }
196
197        return $sig;
198    }
199
200    /**
201     * Decodes the raw payload from a signed request.
202     *
203     * @param string $encodedPayload
204     *
205     * @return array
206     *
207     * @throws FacebookSDKException
208     */
209    protected function decodePayload($encodedPayload)
210    {
211        $payload = $this->base64UrlDecode($encodedPayload);
212
213        if ($payload) {
214            $payload = json_decode($payload, true);
215        }
216
217        if (!is_array($payload)) {
218            throw new FacebookSDKException('Signed request has malformed encoded payload data.', 607);
219        }
220
221        return $payload;
222    }
223
224    /**
225     * Validates the algorithm used in a signed request.
226     *
227     * @throws FacebookSDKException
228     */
229    protected function validateAlgorithm()
230    {
231        if ($this->get('algorithm') !== 'HMAC-SHA256') {
232            throw new FacebookSDKException('Signed request is using the wrong algorithm.', 605);
233        }
234    }
235
236    /**
237     * Hashes the signature used in a signed request.
238     *
239     * @param string $encodedData
240     *
241     * @return string
242     *
243     * @throws FacebookSDKException
244     */
245    protected function hashSignature($encodedData)
246    {
247        $hashedSig = hash_hmac(
248            'sha256',
249            $encodedData,
250            $this->app->getSecret(),
251            $raw_output = true
252        );
253
254        if (!$hashedSig) {
255            throw new FacebookSDKException('Unable to hash signature from encoded payload data.', 602);
256        }
257
258        return $hashedSig;
259    }
260
261    /**
262     * Validates the signature used in a signed request.
263     *
264     * @param string $hashedSig
265     * @param string $sig
266     *
267     * @throws FacebookSDKException
268     */
269    protected function validateSignature($hashedSig, $sig)
270    {
271        if (\hash_equals($hashedSig, $sig)) {
272            return;
273        }
274
275        throw new FacebookSDKException('Signed request has an invalid signature.', 602);
276    }
277
278    /**
279     * Base64 decoding which replaces characters:
280     *   + instead of -
281     *   / instead of _
282     *
283     * @link http://en.wikipedia.org/wiki/Base64#URL_applications
284     *
285     * @param string $input base64 url encoded input
286     *
287     * @return string decoded string
288     */
289    public function base64UrlDecode($input)
290    {
291        $urlDecodedBase64 = strtr($input, '-_', '+/');
292        $this->validateBase64($urlDecodedBase64);
293
294        return base64_decode($urlDecodedBase64);
295    }
296
297    /**
298     * Base64 encoding which replaces characters:
299     *   + instead of -
300     *   / instead of _
301     *
302     * @link http://en.wikipedia.org/wiki/Base64#URL_applications
303     *
304     * @param string $input string to encode
305     *
306     * @return string base64 url encoded input
307     */
308    public function base64UrlEncode($input)
309    {
310        return strtr(base64_encode($input), '+/', '-_');
311    }
312
313    /**
314     * Validates a base64 string.
315     *
316     * @param string $input base64 value to validate
317     *
318     * @throws FacebookSDKException
319     */
320    protected function validateBase64($input)
321    {
322        if (!preg_match('/^[a-zA-Z0-9\/\r\n+]*={0,2}$/', $input)) {
323            throw new FacebookSDKException('Signed request contains malformed base64 encoding.', 608);
324        }
325    }
326}
327