Skip to content

Commit 7d538d3

Browse files
committed
php-sdk changes to support signed_request
1 parent 674dfe0 commit 7d538d3

File tree

2 files changed

+201
-22
lines changed

2 files changed

+201
-22
lines changed

src/facebook.php

Lines changed: 125 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ class Facebook
9797
*/
9898
protected static $DROP_QUERY_PARAMS = array(
9999
'session',
100+
'signed_request',
100101
);
101102

102103
/**
@@ -124,6 +125,11 @@ class Facebook
124125
*/
125126
protected $session;
126127

128+
/**
129+
* The data from the signed_request token.
130+
*/
131+
protected $signedRequest;
132+
127133
/**
128134
* Indicates that we already loaded the session as best as we could.
129135
*/
@@ -237,6 +243,21 @@ public function getBaseDomain() {
237243
return $this->baseDomain;
238244
}
239245

246+
/**
247+
* Get the data from a signed_request token
248+
*
249+
* @return String the base domain
250+
*/
251+
public function getSignedRequest() {
252+
if (!$this->signedRequest) {
253+
if (isset($_REQUEST['signed_request'])) {
254+
$this->signedRequest = $this->parseSignedRequest(
255+
$_REQUEST['signed_request']);
256+
}
257+
}
258+
return $this->signedRequest;
259+
}
260+
240261
/**
241262
* Set the Session.
242263
*
@@ -256,7 +277,7 @@ public function setSession($session=null, $write_cookie=true) {
256277

257278
/**
258279
* Get the session object. This will automatically look for a signed session
259-
* sent via the Cookie or Query Parameters if needed.
280+
* sent via the signed_request, Cookie or Query Parameters if needed.
260281
*
261282
* @return Array the session
262283
*/
@@ -265,8 +286,15 @@ public function getSession() {
265286
$session = null;
266287
$write_cookie = true;
267288

289+
// try loading session from signed_request in $_REQUEST
290+
$signedRequest = $this->getSignedRequest();
291+
if ($signedRequest) {
292+
// sig is good, use the signedRequest
293+
$session = $this->createSessionFromSignedRequest($signedRequest);
294+
}
295+
268296
// try loading session from $_REQUEST
269-
if (isset($_REQUEST['session'])) {
297+
if (!$session && isset($_REQUEST['session'])) {
270298
$session = json_decode(
271299
get_magic_quotes_gpc()
272300
? stripslashes($_REQUEST['session'])
@@ -309,6 +337,21 @@ public function getUser() {
309337
return $session ? $session['uid'] : null;
310338
}
311339

340+
/**
341+
* Gets a OAuth access token.
342+
*
343+
* @return String the access token
344+
*/
345+
public function getAccessToken() {
346+
$session = $this->getSession();
347+
// either user session signed, or app signed
348+
if ($session) {
349+
return $session['access_token'];
350+
} else {
351+
return $this->getAppId() .'|'. $this->getApiSecret();
352+
}
353+
}
354+
312355
/**
313356
* Get a Login URL for use with redirects. By default, full page redirect is
314357
* assumed. If you are using the generated URL with a window.open() call in
@@ -351,14 +394,12 @@ public function getLoginUrl($params=array()) {
351394
* @return String the URL for the logout flow
352395
*/
353396
public function getLogoutUrl($params=array()) {
354-
$session = $this->getSession();
355397
return $this->getUrl(
356398
'www',
357399
'logout.php',
358400
array_merge(array(
359-
'api_key' => $this->getAppId(),
360-
'next' => $this->getCurrentUrl(),
361-
'session_key' => $session['session_key'],
401+
'next' => $this->getCurrentUrl(),
402+
'access_token' => $this->getAccessToken(),
362403
), $params)
363404
);
364405
}
@@ -469,13 +510,7 @@ protected function _graph($path, $method='GET', $params=array()) {
469510
*/
470511
protected function _oauthRequest($url, $params) {
471512
if (!isset($params['access_token'])) {
472-
$session = $this->getSession();
473-
// either user session signed, or app signed
474-
if ($session) {
475-
$params['access_token'] = $session['access_token'];
476-
} else {
477-
$params['access_token'] = $this->getAppId() .'|'. $this->getApiSecret();
478-
}
513+
$params['access_token'] = $this->getAccessToken();
479514
}
480515

481516
// json_encode all params values that are not strings
@@ -576,7 +611,7 @@ protected function setCookieFromSession($session=null) {
576611
}
577612

578613
if (headers_sent()) {
579-
self::error_log('Could not set cookie. Headers already sent.');
614+
self::errorLog('Could not set cookie. Headers already sent.');
580615

581616
// ignore for code coverage as we will never be able to setcookie in a CLI
582617
// environment
@@ -597,8 +632,6 @@ protected function validateSessionObject($session) {
597632
// make sure some essential fields exist
598633
if (is_array($session) &&
599634
isset($session['uid']) &&
600-
isset($session['session_key']) &&
601-
isset($session['secret']) &&
602635
isset($session['access_token']) &&
603636
isset($session['sig'])) {
604637
// validate the signature
@@ -609,7 +642,7 @@ protected function validateSessionObject($session) {
609642
$this->getApiSecret()
610643
);
611644
if ($session['sig'] != $expected_sig) {
612-
self::error_log('Got invalid session signature in cookie.');
645+
self::errorLog('Got invalid session signature in cookie.');
613646
$session = null;
614647
}
615648
// check expiry time
@@ -619,6 +652,67 @@ protected function validateSessionObject($session) {
619652
return $session;
620653
}
621654

655+
/**
656+
* Returns something that looks like our JS session object from the
657+
* signed token's data
658+
*
659+
* TODO: Nuke this once the login flow uses OAuth2
660+
*
661+
* @param Array the output of getSignedRequest
662+
* @return Array Something that will work as a session
663+
*/
664+
protected function createSessionFromSignedRequest($data) {
665+
if (!isset($data['oauth_token'])) {
666+
return null;
667+
}
668+
669+
$session = array(
670+
'uid' => $data['user_id'],
671+
'access_token' => $data['oauth_token'],
672+
'expires' => $data['expires'],
673+
);
674+
675+
// put a real sig, so that validateSignature works
676+
$session['sig'] = self::generateSignature(
677+
$session,
678+
$this->getApiSecret()
679+
);
680+
681+
return $session;
682+
}
683+
684+
/**
685+
* Parses a signed_request and validates the signature.
686+
* Then saves it in $this->signed_data
687+
*
688+
* @param String A signed token
689+
* @param Boolean Should we remove the parts of the payload that
690+
* are used by the algorithm?
691+
* @return Array the payload inside it or null if the sig is wrong
692+
*/
693+
protected function parseSignedRequest($signed_request) {
694+
list($encoded_sig, $payload) = explode('.', $signed_request, 2);
695+
696+
// decode the data
697+
$sig = self::base64UrlDecode($encoded_sig);
698+
$data = json_decode(self::base64UrlDecode($payload), true);
699+
700+
if (strtoupper($data['algorithm']) !== 'HMAC-SHA256') {
701+
self::errorLog('Unknown algorithm. Expected HMAC-SHA256');
702+
return null;
703+
}
704+
705+
// check sig
706+
$expected_sig = hash_hmac('sha256', $payload,
707+
$this->getApiSecret(), $raw = true);
708+
if ($sig !== $expected_sig) {
709+
self::errorLog('Bad Signed JSON signature!');
710+
return null;
711+
}
712+
713+
return $data;
714+
}
715+
622716
/**
623717
* Build the URL for api given parameters.
624718
*
@@ -775,11 +869,11 @@ protected static function generateSignature($params, $secret) {
775869
}
776870

777871
/**
778-
* Prints to the error log if you aren't in command line mode.
872+
* Prints to the error log if you aren't in command line mode.
779873
*
780874
* @param String log message
781875
*/
782-
protected static function error_log($msg) {
876+
protected static function errorLog($msg) {
783877
// disable error log if we are running in a CLI environment
784878
// @codeCoverageIgnoreStart
785879
if (php_sapi_name() != 'cli') {
@@ -789,4 +883,16 @@ protected static function error_log($msg) {
789883
// print 'error_log: '.$msg."\n";
790884
// @codeCoverageIgnoreEnd
791885
}
886+
887+
/**
888+
* Base64 encoding that doesn't need to be urlencode()ed.
889+
* Exactly the same as base64_encode except it uses
890+
* - instead of +
891+
* _ instead of /
892+
*
893+
* @param String base64UrlEncodeded string
894+
*/
895+
protected static function base64UrlDecode($input) {
896+
return base64_decode(strtr($input, '-_', '+/'));
897+
}
792898
}

tests/tests.php

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ class FacebookTest extends PHPUnit_Framework_TestCase
1919
'uid' => '1677846385',
2020
);
2121

22+
private static $VALID_SIGNED_REQUEST = 'ZcZocIFknCpcTLhwsRwwH5nL6oq7OmKWJx41xRTi59E.eyJhbGdvcml0aG0iOiJITUFDLVNIQTI1NiIsImV4cGlyZXMiOiIxMjczMzU5NjAwIiwib2F1dGhfdG9rZW4iOiIyNTQ3NTIwNzMxNTJ8Mi5JX2VURmtjVEtTelg1bm8zakk0cjFRX18uMzYwMC4xMjczMzU5NjAwLTE2Nzc4NDYzODV8dUk3R3dybUJVZWQ4c2VaWjA1SmJkekdGVXBrLiIsInNlc3Npb25fa2V5IjoiMi5JX2VURmtjVEtTelg1bm8zakk0cjFRX18uMzYwMC4xMjczMzU5NjAwLTE2Nzc4NDYzODUiLCJ1c2VyX2lkIjoiMTY3Nzg0NjM4NSJ9';
23+
private static $NON_TOSSED_SIGNED_REQUEST = 'laEjO-az9kzgFOUldy1G7EyaP6tMQEsbFIDrB1RUamE.eyJhbGdvcml0aG0iOiJITUFDLVNIQTI1NiJ9';
24+
2225
public function testConstructor() {
2326
$facebook = new Facebook(array(
2427
'appId' => self::APP_ID,
@@ -111,15 +114,30 @@ public function testSetNullSession() {
111114
'Expect null session back.');
112115
}
113116

117+
public function testNonUserAccessToken() {
118+
$facebook = new Facebook(array(
119+
'appId' => self::APP_ID,
120+
'secret' => self::SECRET,
121+
'cookie' => true,
122+
));
123+
$this->assertTrue($facebook->getAccessToken() ==
124+
self::APP_ID.'|'.self::SECRET,
125+
'Expect appId|secret.');
126+
}
127+
114128
public function testSetSession() {
115129
$facebook = new Facebook(array(
116130
'appId' => self::APP_ID,
117131
'secret' => self::SECRET,
118132
'cookie' => true,
119133
));
120134
$facebook->setSession(self::$VALID_EXPIRED_SESSION);
121-
$this->assertTrue($facebook->getUser() == '1677846385',
135+
$this->assertTrue($facebook->getUser() ==
136+
self::$VALID_EXPIRED_SESSION['uid'],
122137
'Expect uid back.');
138+
$this->assertTrue($facebook->getAccessToken() ==
139+
self::$VALID_EXPIRED_SESSION['access_token'],
140+
'Expect access token back.');
123141
}
124142

125143
public function testGetSessionFromCookie() {
@@ -166,7 +184,8 @@ public function testSessionFromQueryString() {
166184
'secret' => self::SECRET,
167185
));
168186

169-
$this->assertEquals($facebook->getUser(), '1677846385',
187+
$this->assertEquals($facebook->getUser(),
188+
self::$VALID_EXPIRED_SESSION['uid'],
170189
'Expect uid back.');
171190
unset($_REQUEST['session']);
172191
}
@@ -466,7 +485,8 @@ public function testMagicQuotesQueryString() {
466485
'secret' => self::SECRET,
467486
));
468487

469-
$this->assertEquals($facebook->getUser(), '1677846385',
488+
$this->assertEquals($facebook->getUser(),
489+
self::$VALID_EXPIRED_SESSION['uid'],
470490
'Expect uid back.');
471491
unset($_REQUEST['session']);
472492
}
@@ -577,4 +597,57 @@ public function testAppSecretCall() {
577597
$this->assertTrue(count($response['data']) > 0,
578598
'Expect some data back.');
579599
}
600+
601+
public function testBase64UrlEncode() {
602+
$input = 'Facebook rocks';
603+
$output = 'RmFjZWJvb2sgcm9ja3M';
604+
605+
$this->assertEquals(FBPublic::publicBase64UrlDecode($output), $input);
606+
}
607+
608+
public function testSignedToken() {
609+
$facebook = new FBPublic(array(
610+
'appId' => self::APP_ID,
611+
'secret' => self::SECRET,
612+
));
613+
$payload = $facebook->publicParseSignedRequest(self::$VALID_SIGNED_REQUEST);
614+
$this->assertNotNull($payload, 'Expected token to parse');
615+
$session = $facebook->publicCreateSessionFromSignedRequest($payload);
616+
foreach (array('uid', 'access_token') as $key) {
617+
$this->assertEquals($session[$key], self::$VALID_EXPIRED_SESSION[$key]);
618+
}
619+
$this->assertEquals($facebook->getSignedRequest(), null);
620+
$_REQUEST['signed_request'] = self::$VALID_SIGNED_REQUEST;
621+
$this->assertEquals($facebook->getSignedRequest(), $payload);
622+
unset($_REQUEST['signed_request']);
623+
}
624+
625+
public function testNonTossedSignedtoken() {
626+
$facebook = new FBPublic(array(
627+
'appId' => self::APP_ID,
628+
'secret' => self::SECRET,
629+
));
630+
$payload = $facebook->publicParseSignedRequest(
631+
self::$NON_TOSSED_SIGNED_REQUEST);
632+
$this->assertNotNull($payload, 'Expected token to parse');
633+
$session = $facebook->publicCreateSessionFromSignedRequest($payload);
634+
$this->assertNull($session);
635+
$this->assertNull($facebook->getSignedRequest());
636+
$_REQUEST['signed_request'] = self::$NON_TOSSED_SIGNED_REQUEST;
637+
$this->assertEquals($facebook->getSignedRequest(),
638+
array('algorithm' => 'HMAC-SHA256'));
639+
unset($_REQUEST['signed_request']);
640+
}
641+
}
642+
643+
class FBPublic extends Facebook {
644+
public static function publicBase64UrlDecode($input) {
645+
return self::base64UrlDecode($input);
646+
}
647+
public function publicParseSignedRequest($intput) {
648+
return $this->parseSignedRequest($intput);
649+
}
650+
public function publicCreateSessionFromSignedRequest($payload) {
651+
return $this->createSessionFromSignedRequest($payload);
652+
}
580653
}

0 commit comments

Comments
 (0)