1<?php 2 3 4/** 5 * This is a helper class for saving and loading state information. 6 * 7 * The state must be an associative array. This class will add additional keys to this 8 * array. These keys will always start with 'SimpleSAML_Auth_State.'. 9 * 10 * It is also possible to add a restart URL to the state. If state information is lost, for 11 * example because it timed out, or the user loaded a bookmarked page, the loadState function 12 * will redirect to this URL. To use this, set $state[SimpleSAML_Auth_State::RESTART] to this 13 * URL. 14 * 15 * Both the saveState and the loadState function takes in a $stage parameter. This parameter is 16 * a security feature, and is used to prevent the user from taking a state saved one place and 17 * using it as input a different place. 18 * 19 * The $stage parameter must be a unique string. To maintain uniqueness, it must be on the form 20 * "<classname>.<identifier>" or "<module>:<identifier>". 21 * 22 * There is also support for passing exceptions through the state. 23 * By defining an exception handler when creating the state array, users of the state 24 * array can call throwException with the state and the exception. This exception will 25 * be passed to the handler defined by the EXCEPTION_HANDLER_URL or EXCEPTION_HANDLER_FUNC 26 * elements of the state array. 27 * 28 * @author Olav Morken, UNINETT AS. 29 * @package SimpleSAMLphp 30 */ 31class SimpleSAML_Auth_State 32{ 33 34 35 /** 36 * The index in the state array which contains the identifier. 37 */ 38 const ID = 'SimpleSAML_Auth_State.id'; 39 40 41 /** 42 * The index in the cloned state array which contains the identifier of the 43 * original state. 44 */ 45 const CLONE_ORIGINAL_ID = 'SimpleSAML_Auth_State.cloneOriginalId'; 46 47 48 /** 49 * The index in the state array which contains the current stage. 50 */ 51 const STAGE = 'SimpleSAML_Auth_State.stage'; 52 53 54 /** 55 * The index in the state array which contains the restart URL. 56 */ 57 const RESTART = 'SimpleSAML_Auth_State.restartURL'; 58 59 60 /** 61 * The index in the state array which contains the exception handler URL. 62 */ 63 const EXCEPTION_HANDLER_URL = 'SimpleSAML_Auth_State.exceptionURL'; 64 65 66 /** 67 * The index in the state array which contains the exception handler function. 68 */ 69 const EXCEPTION_HANDLER_FUNC = 'SimpleSAML_Auth_State.exceptionFunc'; 70 71 72 /** 73 * The index in the state array which contains the exception data. 74 */ 75 const EXCEPTION_DATA = 'SimpleSAML_Auth_State.exceptionData'; 76 77 78 /** 79 * The stage of a state with an exception. 80 */ 81 const EXCEPTION_STAGE = 'SimpleSAML_Auth_State.exceptionStage'; 82 83 84 /** 85 * The URL parameter which contains the exception state id. 86 */ 87 const EXCEPTION_PARAM = 'SimpleSAML_Auth_State_exceptionId'; 88 89 90 /** 91 * State timeout. 92 */ 93 private static $stateTimeout = null; 94 95 96 /** 97 * Get the persistent authentication state from the state array. 98 * 99 * @param array $state The state array to analyze. 100 * 101 * @return array The persistent authentication state. 102 */ 103 public static function getPersistentAuthData(array $state) 104 { 105 // save persistent authentication data 106 $persistent = array(); 107 108 if (array_key_exists('PersistentAuthData', $state)) { 109 foreach ($state['PersistentAuthData'] as $key) { 110 if (isset($state[$key])) { 111 $persistent[$key] = $state[$key]; 112 } 113 } 114 } 115 116 // add those that should always be included 117 $mandatory = array( 118 'Attributes', 119 'Expire', 120 'LogoutState', 121 'AuthInstant', 122 'RememberMe', 123 'saml:sp:NameID' 124 ); 125 foreach ($mandatory as $key) { 126 if (isset($state[$key])) { 127 $persistent[$key] = $state[$key]; 128 } 129 } 130 131 return $persistent; 132 } 133 134 135 /** 136 * Retrieve the ID of a state array. 137 * 138 * Note that this function will not save the state. 139 * 140 * @param array &$state The state array. 141 * @param bool $rawId Return a raw ID, without a restart URL. Defaults to FALSE. 142 * 143 * @return string Identifier which can be used to retrieve the state later. 144 */ 145 public static function getStateId(&$state, $rawId = false) 146 { 147 assert(is_array($state)); 148 assert(is_bool($rawId)); 149 150 if (!array_key_exists(self::ID, $state)) { 151 $state[self::ID] = SimpleSAML\Utils\Random::generateID(); 152 } 153 154 $id = $state[self::ID]; 155 156 if ($rawId || !array_key_exists(self::RESTART, $state)) { 157 // Either raw ID or no restart URL. In any case, return the raw ID. 158 return $id; 159 } 160 161 // We have a restart URL. Return the ID with that URL. 162 return $id.':'.$state[self::RESTART]; 163 } 164 165 166 /** 167 * Retrieve state timeout. 168 * 169 * @return integer State timeout. 170 */ 171 private static function getStateTimeout() 172 { 173 if (self::$stateTimeout === null) { 174 $globalConfig = SimpleSAML_Configuration::getInstance(); 175 self::$stateTimeout = $globalConfig->getInteger('session.state.timeout', 60 * 60); 176 } 177 178 return self::$stateTimeout; 179 } 180 181 182 /** 183 * Save the state. 184 * 185 * This function saves the state, and returns an id which can be used to 186 * retrieve it later. It will also update the $state array with the identifier. 187 * 188 * @param array &$state The login request state. 189 * @param string $stage The current stage in the login process. 190 * @param bool $rawId Return a raw ID, without a restart URL. 191 * 192 * @return string Identifier which can be used to retrieve the state later. 193 */ 194 public static function saveState(&$state, $stage, $rawId = false) 195 { 196 assert(is_array($state)); 197 assert(is_string($stage)); 198 assert(is_bool($rawId)); 199 200 $return = self::getStateId($state, $rawId); 201 $id = $state[self::ID]; 202 203 // Save stage 204 $state[self::STAGE] = $stage; 205 206 // Save state 207 $serializedState = serialize($state); 208 $session = SimpleSAML_Session::getSessionFromRequest(); 209 $session->setData('SimpleSAML_Auth_State', $id, $serializedState, self::getStateTimeout()); 210 211 SimpleSAML\Logger::debug('Saved state: '.var_export($return, true)); 212 213 return $return; 214 } 215 216 217 /** 218 * Clone the state. 219 * 220 * This function clones and returns the new cloned state. 221 * 222 * @param array $state The original request state. 223 * 224 * @return array Cloned state data. 225 */ 226 public static function cloneState(array $state) 227 { 228 $clonedState = $state; 229 230 if (array_key_exists(self::ID, $state)) { 231 $clonedState[self::CLONE_ORIGINAL_ID] = $state[self::ID]; 232 unset($clonedState[self::ID]); 233 234 SimpleSAML\Logger::debug('Cloned state: '.var_export($state[self::ID], true)); 235 } else { 236 SimpleSAML\Logger::debug('Cloned state with undefined id.'); 237 } 238 239 return $clonedState; 240 } 241 242 243 /** 244 * Retrieve saved state. 245 * 246 * This function retrieves saved state information. If the state information has been lost, 247 * it will attempt to restart the request by calling the restart URL which is embedded in the 248 * state information. If there is no restart information available, an exception will be thrown. 249 * 250 * @param string $id State identifier (with embedded restart information). 251 * @param string $stage The stage the state should have been saved in. 252 * @param bool $allowMissing Whether to allow the state to be missing. 253 * 254 * @throws SimpleSAML_Error_NoState If we couldn't find the state and there's no URL defined to redirect to. 255 * @throws Exception If the stage of the state is invalid and there's no URL defined to redirect to. 256 * 257 * @return array|NULL State information, or null if the state is missing and $allowMissing is true. 258 */ 259 public static function loadState($id, $stage, $allowMissing = false) 260 { 261 assert(is_string($id)); 262 assert(is_string($stage)); 263 assert(is_bool($allowMissing)); 264 SimpleSAML\Logger::debug('Loading state: '.var_export($id, true)); 265 266 $sid = self::parseStateID($id); 267 268 $session = SimpleSAML_Session::getSessionFromRequest(); 269 $state = $session->getData('SimpleSAML_Auth_State', $sid['id']); 270 271 if ($state === null) { 272 // Could not find saved data 273 if ($allowMissing) { 274 return null; 275 } 276 277 if ($sid['url'] === null) { 278 throw new SimpleSAML_Error_NoState(); 279 } 280 281 \SimpleSAML\Utils\HTTP::redirectUntrustedURL($sid['url']); 282 } 283 284 $state = unserialize($state); 285 assert(is_array($state)); 286 assert(array_key_exists(self::ID, $state)); 287 assert(array_key_exists(self::STAGE, $state)); 288 289 // Verify stage 290 if ($state[self::STAGE] !== $stage) { 291 /* This could be a user trying to bypass security, but most likely it is just 292 * someone using the back-button in the browser. We try to restart the 293 * request if that is possible. If not, show an error. 294 */ 295 296 $msg = 'Wrong stage in state. Was \''.$state[self::STAGE]. 297 '\', should be \''.$stage.'\'.'; 298 299 SimpleSAML\Logger::warning($msg); 300 301 if ($sid['url'] === null) { 302 throw new Exception($msg); 303 } 304 305 \SimpleSAML\Utils\HTTP::redirectUntrustedURL($sid['url']); 306 } 307 308 return $state; 309 } 310 311 312 /** 313 * Delete state. 314 * 315 * This function deletes the given state to prevent the user from reusing it later. 316 * 317 * @param array &$state The state which should be deleted. 318 */ 319 public static function deleteState(&$state) 320 { 321 assert(is_array($state)); 322 323 if (!array_key_exists(self::ID, $state)) { 324 // This state hasn't been saved 325 return; 326 } 327 328 SimpleSAML\Logger::debug('Deleting state: '.var_export($state[self::ID], true)); 329 330 $session = SimpleSAML_Session::getSessionFromRequest(); 331 $session->deleteData('SimpleSAML_Auth_State', $state[self::ID]); 332 } 333 334 335 /** 336 * Throw exception to the state exception handler. 337 * 338 * @param array $state The state array. 339 * @param SimpleSAML_Error_Exception $exception The exception. 340 * 341 * @throws SimpleSAML_Error_Exception If there is no exception handler defined, it will just throw the $exception. 342 */ 343 public static function throwException($state, SimpleSAML_Error_Exception $exception) 344 { 345 assert(is_array($state)); 346 347 if (array_key_exists(self::EXCEPTION_HANDLER_URL, $state)) { 348 // Save the exception 349 $state[self::EXCEPTION_DATA] = $exception; 350 $id = self::saveState($state, self::EXCEPTION_STAGE); 351 352 // Redirect to the exception handler 353 \SimpleSAML\Utils\HTTP::redirectTrustedURL( 354 $state[self::EXCEPTION_HANDLER_URL], 355 array(self::EXCEPTION_PARAM => $id) 356 ); 357 } elseif (array_key_exists(self::EXCEPTION_HANDLER_FUNC, $state)) { 358 // Call the exception handler 359 $func = $state[self::EXCEPTION_HANDLER_FUNC]; 360 assert(is_callable($func)); 361 362 call_user_func($func, $exception, $state); 363 assert(false); 364 } else { 365 /* 366 * No exception handler is defined for the current state. 367 */ 368 throw $exception; 369 } 370 } 371 372 373 /** 374 * Retrieve an exception state. 375 * 376 * @param string|NULL $id The exception id. Can be NULL, in which case it will be retrieved from the request. 377 * 378 * @return array|NULL The state array with the exception, or NULL if no exception was thrown. 379 */ 380 public static function loadExceptionState($id = null) 381 { 382 assert(is_string($id) || $id === null); 383 384 if ($id === null) { 385 if (!array_key_exists(self::EXCEPTION_PARAM, $_REQUEST)) { 386 // No exception 387 return null; 388 } 389 $id = $_REQUEST[self::EXCEPTION_PARAM]; 390 } 391 392 $state = self::loadState($id, self::EXCEPTION_STAGE); 393 assert(array_key_exists(self::EXCEPTION_DATA, $state)); 394 395 return $state; 396 } 397 398 399 /** 400 * Get the ID and (optionally) a URL embedded in a StateID, in the form 'id:url'. 401 * 402 * @param string $stateId The state ID to use. 403 * 404 * @return array A hashed array with the ID and the URL (if any), in the 'id' and 'url' keys, respectively. If 405 * there's no URL in the input parameter, NULL will be returned as the value for the 'url' key. 406 * 407 * @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no> 408 * @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no> 409 */ 410 public static function parseStateID($stateId) 411 { 412 $tmp = explode(':', $stateId, 2); 413 $id = $tmp[0]; 414 $url = null; 415 if (count($tmp) === 2) { 416 $url = $tmp[1]; 417 } 418 return array('id' => $id, 'url' => $url); 419 } 420} 421