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