Skip to content
This repository was archived by the owner on Jan 13, 2022. It is now read-only.

Commit 52dd953

Browse files
author
Fosco Marotto
committed
Merge pull request #114 from SammyK/decouple-signed-request-handling
Decoupled signed request handling
2 parents c9d0671 + 25604b6 commit 52dd953

12 files changed

+939
-396
lines changed
Lines changed: 386 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,386 @@
1+
<?php
2+
/**
3+
* Copyright 2014 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+
*/
24+
namespace Facebook\Entities;
25+
26+
use Facebook\FacebookSDKException;
27+
use Facebook\FacebookSession;
28+
29+
/**
30+
* Class SignedRequest
31+
* @package Facebook
32+
*/
33+
class SignedRequest
34+
{
35+
36+
/**
37+
* @var string
38+
*/
39+
public $rawSignedRequest;
40+
41+
/**
42+
* @var array
43+
*/
44+
public $payload;
45+
46+
/**
47+
* Instantiate a new SignedRequest entity.
48+
*
49+
* @param string|null $rawSignedRequest The raw signed request.
50+
* @param string|null $state random string to prevent CSRF.
51+
* @param string|null $appSecret
52+
*/
53+
public function __construct($rawSignedRequest = null, $state = null, $appSecret = null)
54+
{
55+
if (!$rawSignedRequest) {
56+
return;
57+
}
58+
59+
$this->rawSignedRequest = $rawSignedRequest;
60+
$this->payload = static::parse($rawSignedRequest, $state, $appSecret);
61+
}
62+
63+
/**
64+
* Returns the raw signed request data.
65+
*
66+
* @return string|null
67+
*/
68+
public function getRawSignedRequest()
69+
{
70+
return $this->rawSignedRequest;
71+
}
72+
73+
/**
74+
* Returns the parsed signed request data.
75+
*
76+
* @return array|null
77+
*/
78+
public function getPayload()
79+
{
80+
return $this->payload;
81+
}
82+
83+
/**
84+
* Returns a property from the signed request data if available.
85+
*
86+
* @param string $key
87+
* @param mixed|null $default
88+
*
89+
* @return mixed|null
90+
*/
91+
public function get($key, $default = null)
92+
{
93+
if (isset($this->payload[$key])) {
94+
return $this->payload[$key];
95+
}
96+
return $default;
97+
}
98+
99+
/**
100+
* Returns user_id from signed request data if available.
101+
*
102+
* @return string|null
103+
*/
104+
public function getUserId()
105+
{
106+
return $this->get('user_id');
107+
}
108+
109+
/**
110+
* Checks for OAuth data in the payload.
111+
*
112+
* @return boolean
113+
*/
114+
public function hasOAuthData()
115+
{
116+
return isset($this->payload['oauth_token']) || isset($this->payload['code']);
117+
}
118+
119+
/**
120+
* Creates a signed request from an array of data.
121+
*
122+
* @param array $payload
123+
* @param string|null $appSecret
124+
*
125+
* @return string
126+
*/
127+
public static function make(array $payload, $appSecret = null)
128+
{
129+
$payload['algorithm'] = 'HMAC-SHA256';
130+
$payload['issued_at'] = time();
131+
$encodedPayload = static::base64UrlEncode(json_encode($payload));
132+
133+
$hashedSig = static::hashSignature($encodedPayload, $appSecret);
134+
$encodedSig = static::base64UrlEncode($hashedSig);
135+
136+
return $encodedSig.'.'.$encodedPayload;
137+
}
138+
139+
/**
140+
* Validates and decodes a signed request and returns
141+
* the payload as an array.
142+
*
143+
* @param string $signedRequest
144+
* @param string|null $state
145+
* @param string|null $appSecret
146+
*
147+
* @return array
148+
*/
149+
public static function parse($signedRequest, $state = null, $appSecret = null)
150+
{
151+
list($encodedSig, $encodedPayload) = static::split($signedRequest);
152+
153+
// Signature validation
154+
$sig = static::decodeSignature($encodedSig);
155+
$hashedSig = static::hashSignature($encodedPayload, $appSecret);
156+
static::validateSignature($hashedSig, $sig);
157+
158+
// Payload validation
159+
$data = static::decodePayload($encodedPayload);
160+
static::validateAlgorithm($data);
161+
if ($state) {
162+
static::validateCsrf($data, $state);
163+
}
164+
165+
return $data;
166+
}
167+
168+
/**
169+
* Validates the format of a signed request.
170+
*
171+
* @param string $signedRequest
172+
*
173+
* @throws FacebookSDKException
174+
*/
175+
public static function validateFormat($signedRequest)
176+
{
177+
if (strpos($signedRequest, '.') !== false) {
178+
return;
179+
}
180+
181+
throw new FacebookSDKException(
182+
'Malformed signed request.', 606
183+
);
184+
}
185+
186+
/**
187+
* Decodes a raw valid signed request.
188+
*
189+
* @param string $signedRequest
190+
*
191+
* @returns array
192+
*/
193+
public static function split($signedRequest)
194+
{
195+
static::validateFormat($signedRequest);
196+
197+
return explode('.', $signedRequest, 2);
198+
}
199+
200+
/**
201+
* Decodes the raw signature from a signed request.
202+
*
203+
* @param string $encodedSig
204+
*
205+
* @returns string
206+
*
207+
* @throws FacebookSDKException
208+
*/
209+
public static function decodeSignature($encodedSig)
210+
{
211+
$sig = static::base64UrlDecode($encodedSig);
212+
213+
if ($sig) {
214+
return $sig;
215+
}
216+
217+
throw new FacebookSDKException(
218+
'Signed request has malformed encoded signature data.', 607
219+
);
220+
}
221+
222+
/**
223+
* Decodes the raw payload from a signed request.
224+
*
225+
* @param string $encodedPayload
226+
*
227+
* @returns array
228+
*
229+
* @throws FacebookSDKException
230+
*/
231+
public static function decodePayload($encodedPayload)
232+
{
233+
$payload = static::base64UrlDecode($encodedPayload);
234+
235+
if ($payload) {
236+
$payload = json_decode($payload, true);
237+
}
238+
239+
if (is_array($payload)) {
240+
return $payload;
241+
}
242+
243+
throw new FacebookSDKException(
244+
'Signed request has malformed encoded payload data.', 607
245+
);
246+
}
247+
248+
/**
249+
* Validates the algorithm used in a signed request.
250+
*
251+
* @param array $data
252+
*
253+
* @throws FacebookSDKException
254+
*/
255+
public static function validateAlgorithm(array $data)
256+
{
257+
if (isset($data['algorithm']) && $data['algorithm'] === 'HMAC-SHA256') {
258+
return;
259+
}
260+
261+
throw new FacebookSDKException(
262+
'Signed request is using the wrong algorithm.', 605
263+
);
264+
}
265+
266+
/**
267+
* Hashes the signature used in a signed request.
268+
*
269+
* @param string $encodedData
270+
* @param string|null $appSecret
271+
*
272+
* @return string
273+
*
274+
* @throws FacebookSDKException
275+
*/
276+
public static function hashSignature($encodedData, $appSecret = null)
277+
{
278+
$hashedSig = hash_hmac(
279+
'sha256', $encodedData, FacebookSession::_getTargetAppSecret($appSecret), $raw_output = true
280+
);
281+
282+
if ($hashedSig) {
283+
return $hashedSig;
284+
}
285+
286+
throw new FacebookSDKException(
287+
'Unable to hash signature from encoded payload data.', 602
288+
);
289+
}
290+
291+
/**
292+
* Validates the signature used in a signed request.
293+
*
294+
* @param string $hashedSig
295+
* @param string $sig
296+
*
297+
* @throws FacebookSDKException
298+
*/
299+
public static function validateSignature($hashedSig, $sig)
300+
{
301+
if (mb_strlen($hashedSig) === mb_strlen($sig)) {
302+
$validate = 0;
303+
for ($i = 0; $i < mb_strlen($sig); $i++) {
304+
$validate |= ord($hashedSig[$i]) ^ ord($sig[$i]);
305+
}
306+
if ($validate === 0) {
307+
return;
308+
}
309+
}
310+
311+
throw new FacebookSDKException(
312+
'Signed request has an invalid signature.', 602
313+
);
314+
}
315+
316+
/**
317+
* Validates a signed request against CSRF.
318+
*
319+
* @param array $data
320+
* @param string $state
321+
*
322+
* @throws FacebookSDKException
323+
*/
324+
public static function validateCsrf(array $data, $state)
325+
{
326+
if (isset($data['state']) && $data['state'] === $state) {
327+
return;
328+
}
329+
330+
throw new FacebookSDKException(
331+
'Signed request did not pass CSRF validation.', 604
332+
);
333+
}
334+
335+
/**
336+
* Base64 decoding which replaces characters:
337+
* + instead of -
338+
* / instead of _
339+
* @link http://en.wikipedia.org/wiki/Base64#URL_applications
340+
*
341+
* @param string $input base64 url encoded input
342+
*
343+
* @return string decoded string
344+
*/
345+
public static function base64UrlDecode($input)
346+
{
347+
$urlDecodedBase64 = strtr($input, '-_', '+/');
348+
static::validateBase64($urlDecodedBase64);
349+
return base64_decode($urlDecodedBase64);
350+
}
351+
352+
/**
353+
* Base64 encoding which replaces characters:
354+
* + instead of -
355+
* / instead of _
356+
* @link http://en.wikipedia.org/wiki/Base64#URL_applications
357+
*
358+
* @param string $input string to encode
359+
*
360+
* @return string base64 url encoded input
361+
*/
362+
public static function base64UrlEncode($input)
363+
{
364+
return strtr(base64_encode($input), '+/', '-_');
365+
}
366+
367+
/**
368+
* Validates a base64 string.
369+
*
370+
* @param string $input base64 value to validate
371+
*
372+
* @throws FacebookSDKException
373+
*/
374+
public static function validateBase64($input)
375+
{
376+
$pattern = '/^[a-zA-Z0-9\/\r\n+]*={0,2}$/';
377+
if (preg_match($pattern, $input)) {
378+
return;
379+
}
380+
381+
throw new FacebookSDKException(
382+
'Signed request contains malformed base64 encoding.', 608
383+
);
384+
}
385+
386+
}

0 commit comments

Comments
 (0)