diff --git a/lib/SimpleSAML/Auth/State.php b/lib/SimpleSAML/Auth/State.php index b08b5a11c387f0a2562ad5bee63000069b83f787..447f9619ebb29921ea9dbef183a49f754a3094ae 100644 --- a/lib/SimpleSAML/Auth/State.php +++ b/lib/SimpleSAML/Auth/State.php @@ -1,5 +1,6 @@ <?php + /** * This is a helper class for saving and loading state information. * @@ -27,374 +28,394 @@ * @author Olav Morken, UNINETT AS. * @package SimpleSAMLphp */ -class SimpleSAML_Auth_State { - - - /** - * The index in the state array which contains the identifier. - */ - const ID = 'SimpleSAML_Auth_State.id'; - - - /** - * The index in the cloned state array which contains the identifier of the - * original state. - */ - const CLONE_ORIGINAL_ID = 'SimpleSAML_Auth_State.cloneOriginalId'; - - - /** - * The index in the state array which contains the current stage. - */ - const STAGE = 'SimpleSAML_Auth_State.stage'; - - - /** - * The index in the state array which contains the restart URL. - */ - const RESTART = 'SimpleSAML_Auth_State.restartURL'; +class SimpleSAML_Auth_State +{ - /** - * The index in the state array which contains the exception handler URL. - */ - const EXCEPTION_HANDLER_URL = 'SimpleSAML_Auth_State.exceptionURL'; - - - /** - * The index in the state array which contains the exception handler function. - */ - const EXCEPTION_HANDLER_FUNC = 'SimpleSAML_Auth_State.exceptionFunc'; - - - /** - * The index in the state array which contains the exception data. - */ - const EXCEPTION_DATA = 'SimpleSAML_Auth_State.exceptionData'; + /** + * The index in the state array which contains the identifier. + */ + const ID = 'SimpleSAML_Auth_State.id'; - /** - * The stage of a state with an exception. - */ - const EXCEPTION_STAGE = 'SimpleSAML_Auth_State.exceptionStage'; - - - /** - * The URL parameter which contains the exception state id. - */ - const EXCEPTION_PARAM = 'SimpleSAML_Auth_State_exceptionId'; - - - /** - * State timeout. - */ - private static $stateTimeout = NULL; - - - /** - * Get the persistent authentication state from the state array. - * - * @param array $state The state array to analyze. - * @return array The persistent authentication state. - */ - public static function getPersistentAuthData(array $state) - { - // save persistent authentication data - $persistent = array(); - - if (array_key_exists('PersistentAuthData', $state)) { - foreach ($state['PersistentAuthData'] as $key) { - if (isset($state[$key])) { - $persistent[$key] = $state[$key]; - } - } - } - - // add those that should always be included - $mandatory = array( - 'Attributes', - 'Expire', - 'LogoutState', - 'AuthInstant', - 'RememberMe', - 'saml:sp:NameID' - ); - foreach ($mandatory as $key) { - if (isset($state[$key])) { - $persistent[$key] = $state[$key]; - } - } - - return $persistent; - } - - - /** - * Retrieve the ID of a state array. - * - * Note that this function will not save the state. - * - * @param array &$state The state array. - * @param bool $rawId Return a raw ID, without a restart URL. Defaults to FALSE. - * @return string Identifier which can be used to retrieve the state later. - */ - public static function getStateId(&$state, $rawId = FALSE) { - assert('is_array($state)'); - assert('is_bool($rawId)'); - - if (!array_key_exists(self::ID, $state)) { - $state[self::ID] = SimpleSAML\Utils\Random::generateID(); - } - - $id = $state[self::ID]; - - if ($rawId || !array_key_exists(self::RESTART, $state)) { - // Either raw ID or no restart URL. In any case, return the raw ID. - return $id; - } - - // We have a restart URL. Return the ID with that URL. - return $id . ':' . $state[self::RESTART]; - } - - - /** - * Retrieve state timeout. - * - * @return integer State timeout. - */ - private static function getStateTimeout() { - if (self::$stateTimeout === NULL) { - $globalConfig = SimpleSAML_Configuration::getInstance(); - self::$stateTimeout = $globalConfig->getInteger('session.state.timeout', 60*60); - } - - return self::$stateTimeout; - } - - - /** - * Save the state. - * - * This function saves the state, and returns an id which can be used to - * retrieve it later. It will also update the $state array with the identifier. - * - * @param array &$state The login request state. - * @param string $stage The current stage in the login process. - * @param bool $rawId Return a raw ID, without a restart URL. - * @return string Identifier which can be used to retrieve the state later. - */ - public static function saveState(&$state, $stage, $rawId = FALSE) { - assert('is_array($state)'); - assert('is_string($stage)'); - assert('is_bool($rawId)'); - - $return = self::getStateId($state, $rawId); - $id = $state[self::ID]; - - // Save stage - $state[self::STAGE] = $stage; - - // Save state - $serializedState = serialize($state); - $session = SimpleSAML_Session::getSessionFromRequest(); - $session->setData('SimpleSAML_Auth_State', $id, $serializedState, self::getStateTimeout()); - - SimpleSAML\Logger::debug('Saved state: ' . var_export($return, TRUE)); - - return $return; - } - - - /** - * Clone the state. - * - * This function clones and returns the new cloned state. - * - * @param array $state The original request state. - * @return array Cloned state data. - */ - public static function cloneState(array $state) { - $clonedState = $state; - - if (array_key_exists(self::ID, $state)) { - $clonedState[self::CLONE_ORIGINAL_ID] = $state[self::ID]; - unset($clonedState[self::ID]); - - SimpleSAML\Logger::debug('Cloned state: ' . var_export($state[self::ID], TRUE)); - } else { - SimpleSAML\Logger::debug('Cloned state with undefined id.'); - } - - return $clonedState; - } - - - /** - * Retrieve saved state. - * - * This function retrieves saved state information. If the state information has been lost, - * it will attempt to restart the request by calling the restart URL which is embedded in the - * state information. If there is no restart information available, an exception will be thrown. - * - * @param string $id State identifier (with embedded restart information). - * @param string $stage The stage the state should have been saved in. - * @param bool $allowMissing Whether to allow the state to be missing. - * @return array|NULL State information, or NULL if the state is missing and $allowMissing is TRUE. - */ - public static function loadState($id, $stage, $allowMissing = FALSE) { - assert('is_string($id)'); - assert('is_string($stage)'); - assert('is_bool($allowMissing)'); - SimpleSAML\Logger::debug('Loading state: ' . var_export($id, TRUE)); - - $sid = self::parseStateID($id); - - $session = SimpleSAML_Session::getSessionFromRequest(); - $state = $session->getData('SimpleSAML_Auth_State', $sid['id']); - - if ($state === NULL) { - // Could not find saved data - if ($allowMissing) { - return NULL; - } - - if ($sid['url'] === NULL) { - throw new SimpleSAML_Error_NoState(); - } - - \SimpleSAML\Utils\HTTP::redirectUntrustedURL($sid['url']); - } - - $state = unserialize($state); - assert('is_array($state)'); - assert('array_key_exists(self::ID, $state)'); - assert('array_key_exists(self::STAGE, $state)'); - - // Verify stage - if ($state[self::STAGE] !== $stage) { - /* This could be a user trying to bypass security, but most likely it is just - * someone using the back-button in the browser. We try to restart the - * request if that is possible. If not, show an error. - */ - - $msg = 'Wrong stage in state. Was \'' . $state[self::STAGE] . - '\', should be \'' . $stage . '\'.'; - - SimpleSAML\Logger::warning($msg); - - if ($sid['url'] === NULL) { - throw new Exception($msg); - } - - \SimpleSAML\Utils\HTTP::redirectUntrustedURL($sid['url']); - } - - return $state; - } - - - /** - * Delete state. - * - * This function deletes the given state to prevent the user from reusing it later. - * - * @param array &$state The state which should be deleted. - */ - public static function deleteState(&$state) { - assert('is_array($state)'); - - if (!array_key_exists(self::ID, $state)) { - // This state hasn't been saved - return; - } - - SimpleSAML\Logger::debug('Deleting state: ' . var_export($state[self::ID], TRUE)); - - $session = SimpleSAML_Session::getSessionFromRequest(); - $session->deleteData('SimpleSAML_Auth_State', $state[self::ID]); - } - - - /** - * Throw exception to the state exception handler. - * - * @param array $state The state array. - * @param SimpleSAML_Error_Exception $exception The exception. - */ - public static function throwException($state, SimpleSAML_Error_Exception $exception) { - assert('is_array($state)'); - - if (array_key_exists(self::EXCEPTION_HANDLER_URL, $state)) { - - // Save the exception - $state[self::EXCEPTION_DATA] = $exception; - $id = self::saveState($state, self::EXCEPTION_STAGE); - - // Redirect to the exception handler - \SimpleSAML\Utils\HTTP::redirectTrustedURL($state[self::EXCEPTION_HANDLER_URL], array(self::EXCEPTION_PARAM => $id)); - - } elseif (array_key_exists(self::EXCEPTION_HANDLER_FUNC, $state)) { - // Call the exception handler - $func = $state[self::EXCEPTION_HANDLER_FUNC]; - assert('is_callable($func)'); - - call_user_func($func, $exception, $state); - assert(FALSE); - - } else { - /* - * No exception handler is defined for the current state. - */ - throw $exception; - } - - } - - - /** - * Retrieve an exception state. - * - * @param string|NULL $id The exception id. Can be NULL, in which case it will be retrieved from the request. - * @return array|NULL The state array with the exception, or NULL if no exception was thrown. - */ - public static function loadExceptionState($id = NULL) { - assert('is_string($id) || is_null($id)'); - - if ($id === NULL) { - if (!array_key_exists(self::EXCEPTION_PARAM, $_REQUEST)) { - // No exception - return NULL; - } - $id = $_REQUEST[self::EXCEPTION_PARAM]; - } - - $state = self::loadState($id, self::EXCEPTION_STAGE); - assert('array_key_exists(self::EXCEPTION_DATA, $state)'); - - return $state; - } - - - /** - * Get the ID and (optionally) a URL embedded in a StateID, in the form 'id:url'. - * - * @param string $stateId The state ID to use. - * - * @return array A hashed array with the ID and the URL (if any), in the 'id' and 'url' keys, respectively. If - * there's no URL in the input parameter, NULL will be returned as the value for the 'url' key. - * - * @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no> - * @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no> - */ - public static function parseStateID($stateId) { - $tmp = explode(':', $stateId, 2); - $id = $tmp[0]; - $url = null; - if (count($tmp) === 2) { - $url = $tmp[1]; - } - return array('id' => $id, 'url' => $url); - } + /** + * The index in the cloned state array which contains the identifier of the + * original state. + */ + const CLONE_ORIGINAL_ID = 'SimpleSAML_Auth_State.cloneOriginalId'; + + + /** + * The index in the state array which contains the current stage. + */ + const STAGE = 'SimpleSAML_Auth_State.stage'; + + + /** + * The index in the state array which contains the restart URL. + */ + const RESTART = 'SimpleSAML_Auth_State.restartURL'; + + + /** + * The index in the state array which contains the exception handler URL. + */ + const EXCEPTION_HANDLER_URL = 'SimpleSAML_Auth_State.exceptionURL'; + + /** + * The index in the state array which contains the exception handler function. + */ + const EXCEPTION_HANDLER_FUNC = 'SimpleSAML_Auth_State.exceptionFunc'; + + + /** + * The index in the state array which contains the exception data. + */ + const EXCEPTION_DATA = 'SimpleSAML_Auth_State.exceptionData'; + + + /** + * The stage of a state with an exception. + */ + const EXCEPTION_STAGE = 'SimpleSAML_Auth_State.exceptionStage'; + + + /** + * The URL parameter which contains the exception state id. + */ + const EXCEPTION_PARAM = 'SimpleSAML_Auth_State_exceptionId'; + + + /** + * State timeout. + */ + private static $stateTimeout = null; + + + /** + * Get the persistent authentication state from the state array. + * + * @param array $state The state array to analyze. + * + * @return array The persistent authentication state. + */ + public static function getPersistentAuthData(array $state) + { + // save persistent authentication data + $persistent = array(); + + if (array_key_exists('PersistentAuthData', $state)) { + foreach ($state['PersistentAuthData'] as $key) { + if (isset($state[$key])) { + $persistent[$key] = $state[$key]; + } + } + } + + // add those that should always be included + $mandatory = array( + 'Attributes', + 'Expire', + 'LogoutState', + 'AuthInstant', + 'RememberMe', + 'saml:sp:NameID' + ); + foreach ($mandatory as $key) { + if (isset($state[$key])) { + $persistent[$key] = $state[$key]; + } + } + + return $persistent; + } + + + /** + * Retrieve the ID of a state array. + * + * Note that this function will not save the state. + * + * @param array &$state The state array. + * @param bool $rawId Return a raw ID, without a restart URL. Defaults to FALSE. + * + * @return string Identifier which can be used to retrieve the state later. + */ + public static function getStateId(&$state, $rawId = false) + { + assert('is_array($state)'); + assert('is_bool($rawId)'); + + if (!array_key_exists(self::ID, $state)) { + $state[self::ID] = SimpleSAML\Utils\Random::generateID(); + } + + $id = $state[self::ID]; + + if ($rawId || !array_key_exists(self::RESTART, $state)) { + // Either raw ID or no restart URL. In any case, return the raw ID. + return $id; + } + + // We have a restart URL. Return the ID with that URL. + return $id.':'.$state[self::RESTART]; + } + + + /** + * Retrieve state timeout. + * + * @return integer State timeout. + */ + private static function getStateTimeout() + { + if (self::$stateTimeout === null) { + $globalConfig = SimpleSAML_Configuration::getInstance(); + self::$stateTimeout = $globalConfig->getInteger('session.state.timeout', 60 * 60); + } + + return self::$stateTimeout; + } + + + /** + * Save the state. + * + * This function saves the state, and returns an id which can be used to + * retrieve it later. It will also update the $state array with the identifier. + * + * @param array &$state The login request state. + * @param string $stage The current stage in the login process. + * @param bool $rawId Return a raw ID, without a restart URL. + * + * @return string Identifier which can be used to retrieve the state later. + */ + public static function saveState(&$state, $stage, $rawId = false) + { + assert('is_array($state)'); + assert('is_string($stage)'); + assert('is_bool($rawId)'); + + $return = self::getStateId($state, $rawId); + $id = $state[self::ID]; + + // Save stage + $state[self::STAGE] = $stage; + + // Save state + $serializedState = serialize($state); + $session = SimpleSAML_Session::getSessionFromRequest(); + $session->setData('SimpleSAML_Auth_State', $id, $serializedState, self::getStateTimeout()); + + SimpleSAML\Logger::debug('Saved state: '.var_export($return, true)); + + return $return; + } + + + /** + * Clone the state. + * + * This function clones and returns the new cloned state. + * + * @param array $state The original request state. + * + * @return array Cloned state data. + */ + public static function cloneState(array $state) + { + $clonedState = $state; + + if (array_key_exists(self::ID, $state)) { + $clonedState[self::CLONE_ORIGINAL_ID] = $state[self::ID]; + unset($clonedState[self::ID]); + + SimpleSAML\Logger::debug('Cloned state: '.var_export($state[self::ID], true)); + } else { + SimpleSAML\Logger::debug('Cloned state with undefined id.'); + } + + return $clonedState; + } + + + /** + * Retrieve saved state. + * + * This function retrieves saved state information. If the state information has been lost, + * it will attempt to restart the request by calling the restart URL which is embedded in the + * state information. If there is no restart information available, an exception will be thrown. + * + * @param string $id State identifier (with embedded restart information). + * @param string $stage The stage the state should have been saved in. + * @param bool $allowMissing Whether to allow the state to be missing. + * + * @throws SimpleSAML_Error_NoState If we couldn't find the state and there's no URL defined to redirect to. + * @throws Exception If the stage of the state is invalid and there's no URL defined to redirect to. + * + * @return array|NULL State information, or null if the state is missing and $allowMissing is true. + */ + public static function loadState($id, $stage, $allowMissing = false) + { + assert('is_string($id)'); + assert('is_string($stage)'); + assert('is_bool($allowMissing)'); + SimpleSAML\Logger::debug('Loading state: '.var_export($id, true)); + + $sid = self::parseStateID($id); + + $session = SimpleSAML_Session::getSessionFromRequest(); + $state = $session->getData('SimpleSAML_Auth_State', $sid['id']); + + if ($state === null) { + // Could not find saved data + if ($allowMissing) { + return null; + } + + if ($sid['url'] === null) { + throw new SimpleSAML_Error_NoState(); + } + + \SimpleSAML\Utils\HTTP::redirectUntrustedURL($sid['url']); + } + + $state = unserialize($state); + assert('is_array($state)'); + assert('array_key_exists(self::ID, $state)'); + assert('array_key_exists(self::STAGE, $state)'); + + // Verify stage + if ($state[self::STAGE] !== $stage) { + /* This could be a user trying to bypass security, but most likely it is just + * someone using the back-button in the browser. We try to restart the + * request if that is possible. If not, show an error. + */ + + $msg = 'Wrong stage in state. Was \''.$state[self::STAGE]. + '\', should be \''.$stage.'\'.'; + + SimpleSAML\Logger::warning($msg); + + if ($sid['url'] === null) { + throw new Exception($msg); + } + + \SimpleSAML\Utils\HTTP::redirectUntrustedURL($sid['url']); + } + + return $state; + } + + + /** + * Delete state. + * + * This function deletes the given state to prevent the user from reusing it later. + * + * @param array &$state The state which should be deleted. + */ + public static function deleteState(&$state) + { + assert('is_array($state)'); + + if (!array_key_exists(self::ID, $state)) { + // This state hasn't been saved + return; + } + + SimpleSAML\Logger::debug('Deleting state: '.var_export($state[self::ID], true)); + + $session = SimpleSAML_Session::getSessionFromRequest(); + $session->deleteData('SimpleSAML_Auth_State', $state[self::ID]); + } + + + /** + * Throw exception to the state exception handler. + * + * @param array $state The state array. + * @param SimpleSAML_Error_Exception $exception The exception. + * + * @throws SimpleSAML_Error_Exception If there is no exception handler defined, it will just throw the $exception. + */ + public static function throwException($state, SimpleSAML_Error_Exception $exception) + { + assert('is_array($state)'); + + if (array_key_exists(self::EXCEPTION_HANDLER_URL, $state)) { + + // Save the exception + $state[self::EXCEPTION_DATA] = $exception; + $id = self::saveState($state, self::EXCEPTION_STAGE); + + // Redirect to the exception handler + \SimpleSAML\Utils\HTTP::redirectTrustedURL( + $state[self::EXCEPTION_HANDLER_URL], + array(self::EXCEPTION_PARAM => $id) + ); + } elseif (array_key_exists(self::EXCEPTION_HANDLER_FUNC, $state)) { + // Call the exception handler + $func = $state[self::EXCEPTION_HANDLER_FUNC]; + assert('is_callable($func)'); + + call_user_func($func, $exception, $state); + assert(false); + } else { + /* + * No exception handler is defined for the current state. + */ + throw $exception; + } + } + + + /** + * Retrieve an exception state. + * + * @param string|NULL $id The exception id. Can be NULL, in which case it will be retrieved from the request. + * + * @return array|NULL The state array with the exception, or NULL if no exception was thrown. + */ + public static function loadExceptionState($id = null) + { + assert('is_string($id) || is_null($id)'); + + if ($id === null) { + if (!array_key_exists(self::EXCEPTION_PARAM, $_REQUEST)) { + // No exception + return null; + } + $id = $_REQUEST[self::EXCEPTION_PARAM]; + } + + $state = self::loadState($id, self::EXCEPTION_STAGE); + assert('array_key_exists(self::EXCEPTION_DATA, $state)'); + + return $state; + } + + + /** + * Get the ID and (optionally) a URL embedded in a StateID, in the form 'id:url'. + * + * @param string $stateId The state ID to use. + * + * @return array A hashed array with the ID and the URL (if any), in the 'id' and 'url' keys, respectively. If + * there's no URL in the input parameter, NULL will be returned as the value for the 'url' key. + * + * @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no> + * @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no> + */ + public static function parseStateID($stateId) + { + $tmp = explode(':', $stateId, 2); + $id = $tmp[0]; + $url = null; + if (count($tmp) === 2) { + $url = $tmp[1]; + } + return array('id' => $id, 'url' => $url); + } }