/** * Process an authentication response. * * This function saves the state, and if necessary redirects the user to the page where the user * is informed about the expiry date of his/her certificate. * * @param array $state The state of the response. */ public function process(&$state) { assert('is_array($state)'); if (isset($state['isPassive']) && $state['isPassive'] === TRUE) { // We have a passive request. Skip the warning return; } if (!isset($_SERVER['SSL_CLIENT_CERT']) || $_SERVER['SSL_CLIENT_CERT'] == '') { return; } $client_cert = $_SERVER['SSL_CLIENT_CERT']; $client_cert_data = openssl_x509_parse($client_cert); if ($client_cert_data == FALSE) { SimpleSAML\Logger::error('authX509: invalid cert'); return; } $validTo = $client_cert_data['validTo_time_t']; $now = time(); $daysleft = (int) (($validTo - $now) / (24 * 60 * 60)); if ($daysleft > $this->warndaysbefore) { // We have a certificate that will be valid for some time. Skip the warning return; } SimpleSAML\Logger::warning('authX509: user certificate expires in ' . $daysleft . ' days'); $state['daysleft'] = $daysleft; $state['renewurl'] = $this->renewurl; /* Save state and redirect. */ $id = SimpleSAML_Auth_State::saveState($state, 'warning:expire'); $url = SimpleSAML\Module::getModuleURL('authX509/expirywarning.php'); \SimpleSAML\Utils\HTTP::redirectTrustedURL($url, array('StateId' => $id)); }
/** * Get the NameID value. * * @param array $state The state array. * @return string|null The NameID value. */ protected function getValue(array &$state) { if (!isset($state['Destination']['entityid'])) { SimpleSAML\Logger::warning('No SP entity ID - not generating persistent NameID.'); return null; } $spEntityId = $state['Destination']['entityid']; if (!isset($state['Source']['entityid'])) { SimpleSAML\Logger::warning('No IdP entity ID - not generating persistent NameID.'); return null; } $idpEntityId = $state['Source']['entityid']; if (!isset($state['Attributes'][$this->attribute]) || count($state['Attributes'][$this->attribute]) === 0) { SimpleSAML\Logger::warning('Missing attribute ' . var_export($this->attribute, true) . ' on user - not generating persistent NameID.'); return null; } if (count($state['Attributes'][$this->attribute]) > 1) { SimpleSAML\Logger::warning('More than one value in attribute ' . var_export($this->attribute, true) . ' on user - not generating persistent NameID.'); return null; } $uid = array_values($state['Attributes'][$this->attribute]); // just in case the first index is no longer 0 $uid = $uid[0]; if (empty($uid)) { SimpleSAML\Logger::warning('Empty value in attribute ' . var_export($this->attribute, true) . ' on user - not generating persistent NameID.'); return null; } $secretSalt = SimpleSAML\Utils\Config::getSecretSalt(); $uidData = 'uidhashbase' . $secretSalt; $uidData .= strlen($idpEntityId) . ':' . $idpEntityId; $uidData .= strlen($spEntityId) . ':' . $spEntityId; $uidData .= strlen($uid) . ':' . $uid; $uidData .= $secretSalt; return sha1($uidData); }
function oauth2_hook_cron(&$croninfo) { assert('is_array($croninfo)'); assert('array_key_exists("summary", $croninfo)'); assert('array_key_exists("tag", $croninfo)'); $oauth2config = SimpleSAML_Configuration::getOptionalConfig('module_oauth2.php'); if (is_null($oauth2config->getValue('cron_tag', 'hourly'))) { return; } if ($oauth2config->getValue('cron_tag', NULL) !== $croninfo['tag']) { return; } try { $store = SimpleSAML_Store::getInstance(); if (!$store instanceof \SimpleSAML\Modules\DBAL\Store\DBAL) { throw new \SimpleSAML_Error_Exception('OAuth2 module: Only DBAL Store is supported'); } $accessTokenRepository = new AccessTokenRepository(); $accessTokenRepository->removeExpiredAccessTokens(); $authTokenRepository = new AuthCodeRepository(); $authTokenRepository->removeExpiredAuthCodes(); $refreshTokenRepository = new RefreshTokenRepository(); $refreshTokenRepository->removeExpiredRefreshTokens(); $croninfo['summary'][] = 'OAuth2 clean up. Removed expired entries from OAuth2 storage.'; } catch (Exception $e) { $message = 'OAuth2 clean up cron script failed: ' . $e->getMessage(); SimpleSAML\Logger::warning($message); $croninfo['summary'][] = $message; } }
/** * Hook to run a cron job. * * @param array &$croninfo Output */ function statistics_hook_cron(&$croninfo) { assert('is_array($croninfo)'); assert('array_key_exists("summary", $croninfo)'); assert('array_key_exists("tag", $croninfo)'); $statconfig = SimpleSAML_Configuration::getConfig('module_statistics.php'); if (is_null($statconfig->getValue('cron_tag', NULL))) { return; } if ($statconfig->getValue('cron_tag', NULL) !== $croninfo['tag']) { return; } $maxtime = $statconfig->getInteger('time_limit', NULL); if ($maxtime) { set_time_limit($maxtime); } try { $aggregator = new sspmod_statistics_Aggregator(); $results = $aggregator->aggregate(); if (empty($results)) { SimpleSAML\Logger::notice('Output from statistics aggregator was empty.'); } else { $aggregator->store($results); } } catch (Exception $e) { $message = 'Loganalyzer threw exception: ' . $e->getMessage(); SimpleSAML\Logger::warning($message); $croninfo['summary'][] = $message; } }
/** * Process a authentication response. * * This function checks how long it is since the last time the user was authenticated. * If it is to short a while since, we will show a warning to the user. * * @param array $state The state of the response. */ public function process(&$state) { assert('is_array($state)'); if (!array_key_exists('PreviousSSOTimestamp', $state)) { /* * No timestamp from the previous SSO to this SP. This is the first * time during this session. */ return; } $timeDelta = time() - $state['PreviousSSOTimestamp']; if ($timeDelta >= 10) { // At least 10 seconds since last attempt return; } if (array_key_exists('Destination', $state) && array_key_exists('entityid', $state['Destination'])) { $entityId = $state['Destination']['entityid']; } else { $entityId = 'UNKNOWN'; } SimpleSAML\Logger::warning('WarnShortSSOInterval: Only ' . $timeDelta . ' seconds since last SSO for this user from the SP ' . var_export($entityId, TRUE)); // Save state and redirect $id = SimpleSAML_Auth_State::saveState($state, 'core:short_sso_interval'); $url = SimpleSAML\Module::getModuleURL('core/short_sso_interval.php'); \SimpleSAML\Utils\HTTP::redirectTrustedURL($url, array('StateId' => $id)); }
/** * Apply this filter. * * @param array &$request The current request */ public function process(&$request) { assert('is_array($request)'); assert('array_key_exists("Attributes", $request)'); $attributes =& $request['Attributes']; if (!isset($attributes[$this->sourceAttribute])) { return; } // will not overwrite existing attribute if (isset($attributes[$this->targetAttribute])) { return; } $sourceAttrVal = $attributes[$this->sourceAttribute][0]; /* the last position of an @ is usually the beginning of the scope * string */ $scopeIndex = strrpos($sourceAttrVal, '@'); if ($scopeIndex !== FALSE) { $attributes[$this->targetAttribute] = array(); $scope = substr($sourceAttrVal, $scopeIndex + 1); $attributes[$this->targetAttribute][] = $scope; SimpleSAML\Logger::debug('ScopeFromAttribute: Inserted new attribute ' . $this->targetAttribute . ', with scope ' . $scope); } else { SimpleSAML\Logger::warning('ScopeFromAttribute: The configured source attribute ' . $this->sourceAttribute . ' does not have a scope. Did not add attribute ' . $this->targetAttribute . '.'); } }
/** * This temporary autoloader allows loading classes with their old-style names (SimpleSAML_Path_Something) even if they * have been migrated to namespaces, by registering an alias for the new class. If the class has not yet been migrated, * the autoloader will then try to load it. * * @param string $class The full name of the class using underscores to separate the elements of the path, and starting * with 'SimpleSAML_'. * @deprecated This function will be removed in SSP 2.0. */ function temporaryLoader($class) { if (!strstr($class, 'SimpleSAML_')) { return; // not a valid class name for old classes } $original = $class; // list of classes that have been renamed or moved $renamed = array('SimpleSAML_Metadata_MetaDataStorageHandlerMDX' => 'SimpleSAML_Metadata_Sources_MDQ', 'SimpleSAML_Logger_LoggingHandlerSyslog' => 'SimpleSAML_Logger_SyslogLoggingHandler', 'SimpleSAML_Logger_LoggingHandlerErrorLog' => 'SimpleSAML_Logger_ErrorLogLoggingHandler', 'SimpleSAML_Logger_LoggingHandlerFile' => 'SimpleSAML_Logger_FileLoggingHandler', 'SimpleSAML_Logger_LoggingHandler' => 'SimpleSAML_Logger_LoggingHandlerInterface'); if (array_key_exists($class, $renamed)) { // the class has been renamed, try to load it and create an alias $class = $renamed[$class]; } // try to load it from the corresponding file $path = explode('_', $class); $file = dirname(__FILE__) . DIRECTORY_SEPARATOR . join(DIRECTORY_SEPARATOR, $path) . '.php'; if (file_exists($file)) { require_once $file; } // it exists, so it's not yet migrated to namespaces if (class_exists($class, false) || interface_exists($class, false)) { return; } // it didn't exist, try to see if it was migrated to namespaces $new = join('\\', $path); if (class_exists($new, false) || interface_exists($new, false)) { // do not try to autoload it if it doesn't exist! It should! class_alias($new, $original); SimpleSAML\Logger::warning("The class or interface '{$original}' is now using namespaces, please use '{$new}'."); } }
/** * Redirect to page setting CDC. * * @param array &$state The request state. */ public function process(&$state) { assert('is_array($state)'); if (!isset($state['Source']['entityid'])) { SimpleSAML\Logger::warning('saml:CDC: Could not find IdP entityID.'); return; } // Save state and build request $id = SimpleSAML_Auth_State::saveState($state, 'cdc:resume'); $returnTo = SimpleSAML\Module::getModuleURL('cdc/resume.php', array('domain' => $this->domain)); $params = array('id' => $id, 'entityID' => $state['Source']['entityid']); $this->client->sendRequest($returnTo, 'append', $params); }
/** * Get the NameID value. * * @param array $state The state array. * @return string|null The NameID value. */ protected function getValue(array &$state) { if (!isset($state['Attributes'][$this->attribute]) || count($state['Attributes'][$this->attribute]) === 0) { SimpleSAML\Logger::warning('Missing attribute ' . var_export($this->attribute, true) . ' on user - not generating attribute NameID.'); return null; } if (count($state['Attributes'][$this->attribute]) > 1) { SimpleSAML\Logger::warning('More than one value in attribute ' . var_export($this->attribute, true) . ' on user - not generating attribute NameID.'); } $value = array_values($state['Attributes'][$this->attribute]); // just in case the first index is no longer 0 $value = $value[0]; return $value; }
/** * Attempt to log in using the given username and password. * * @param string $username The username the user wrote. * @param string $password The password the user wrote. * @param string $org The organization the user chose. * @return array Associative array with the users attributes. */ protected function login($username, $password, $org, array $sasl_args = null) { assert('is_string($username)'); assert('is_string($password)'); assert('is_string($org)'); if (!array_key_exists($org, $this->ldapOrgs)) { // The user has selected an organization which doesn't exist anymore. SimpleSAML\Logger::warning('Authentication source ' . var_export($this->authId, true) . ': Organization seems to have disappeared while the user logged in.' . ' Organization was ' . var_export($org, true)); throw new SimpleSAML_Error_Error('WRONGUSERPASS'); } if ($this->includeOrgInUsername) { $username = $username . '@' . $org; } return $this->ldapOrgs[$org]->login($username, $password, $sasl_args); }
/** * Continue the logout operation. * * This function will never return. * * @param string $assocId The association that is terminated. * @param string|null $relayState The RelayState from the start of the logout. * @param SimpleSAML_Error_Exception|null $error The error that occurred during session termination (if any). * * @throws SimpleSAML_Error_Exception If the RelayState was lost during logout. */ public function onResponse($assocId, $relayState, SimpleSAML_Error_Exception $error = null) { assert('is_string($assocId)'); assert('is_string($relayState) || is_null($relayState)'); if ($relayState === null) { throw new SimpleSAML_Error_Exception('RelayState lost during logout.'); } $state = SimpleSAML_Auth_State::loadState($relayState, 'core:LogoutTraditional'); if ($error === null) { SimpleSAML\Logger::info('Logged out of ' . var_export($assocId, true) . '.'); $this->idp->terminateAssociation($assocId); } else { SimpleSAML\Logger::warning('Error received from ' . var_export($assocId, true) . ' during logout:'); $error->logWarning(); $state['core:Failed'] = true; } self::logoutNextSP($state); }
/** * Store a NameID to attribute. * * @param array &$state The request state. */ public function process(&$state) { assert('is_array($state)'); if (!isset($state['saml:NameID'][SAML2_Const::NAMEID_PERSISTENT])) { SimpleSAML\Logger::warning('Unable to generate eduPersonTargetedID because no persistent NameID was available.'); return; } $nameID = $state['saml:NameID'][SAML2_Const::NAMEID_PERSISTENT]; if ($this->nameId) { $doc = SAML2_DOMDocumentFactory::create(); $root = $doc->createElement('root'); $doc->appendChild($root); SAML2_Utils::addNameId($root, $nameID); $value = $doc->saveXML($root->firstChild); } else { $value = $nameID['Value']; } $state['Attributes'][$this->attribute] = array($value); }
/** * Hook to run a cron job. * * @param array &$croninfo Output */ function riak_hook_cron(&$croninfo) { assert('is_array($croninfo)'); assert('array_key_exists("summary", $croninfo)'); assert('array_key_exists("tag", $croninfo)'); if ($croninfo['tag'] !== 'hourly') { return; } try { $store = new sspmod_riak_Store_Store(); $result = $store->bucket->indexSearch('expires', 'int', 1, time() - 30); foreach ($result as $link) { $link->getBinary()->delete(); } SimpleSAML\Logger::info(sprintf("deleted %s riak key%s", sizeof($result), sizeof($result) == 1 ? '' : 's')); } catch (Exception $e) { $message = 'riak threw exception: ' . $e->getMessage(); SimpleSAML\Logger::warning($message); $croninfo['summary'][] = $message; } }
/** * Hook to run a cron job. * * @param array &$croninfo Output */ function oauth_hook_cron(&$croninfo) { assert('is_array($croninfo)'); assert('array_key_exists("summary", $croninfo)'); assert('array_key_exists("tag", $croninfo)'); $oauthconfig = SimpleSAML_Configuration::getOptionalConfig('module_statistics.php'); if (is_null($oauthconfig->getValue('cron_tag', 'hourly'))) { return; } if ($oauthconfig->getValue('cron_tag', NULL) !== $croninfo['tag']) { return; } try { $store = new sspmod_core_Storage_SQLPermanentStorage('oauth'); $cleaned = $store->removeExpired(); # if ($cleaned > 0) $croninfo['summary'][] = 'OAuth clean up. Removed ' . $cleaned . ' expired entries from OAuth storage.'; } catch (Exception $e) { $message = 'OAuth clean up cron script failed: ' . $e->getMessage(); SimpleSAML\Logger::warning($message); $croninfo['summary'][] = $message; } }
/** * Initialize the PHP session handling. This constructor is protected because it should only be called from * SimpleSAML_SessionHandler::createSessionHandler(...). */ protected function __construct() { // call the parent constructor in case it should become necessary in the future parent::__construct(); $config = SimpleSAML_Configuration::getInstance(); $this->cookie_name = $config->getString('session.phpsession.cookiename', null); if (function_exists('session_status') && defined('PHP_SESSION_ACTIVE')) { // PHP >= 5.4 $previous_session = session_status() === PHP_SESSION_ACTIVE; } else { $previous_session = session_id() !== '' && session_name() !== $this->cookie_name; } if ($previous_session) { if (session_name() === $this->cookie_name || $this->cookie_name === null) { SimpleSAML\Logger::warning('There is already a PHP session with the same name as SimpleSAMLphp\'s session, or the ' . "'session.phpsession.cookiename' configuration option is not set. Make sure to set " . "SimpleSAMLphp's cookie name with a value not used by any other applications."); } /* * We shouldn't have a session at this point, so it might be an application session. Save the details to * retrieve it later and commit. */ $this->previous_session['cookie_params'] = session_get_cookie_params(); $this->previous_session['id'] = session_id(); $this->previous_session['name'] = session_name(); session_write_close(); } if (!empty($this->cookie_name)) { session_name($this->cookie_name); } else { $this->cookie_name = session_name(); } $params = $this->getCookieParams(); session_set_cookie_params($params['lifetime'], $params['path'], $params['domain'], $params['secure'], $params['httponly']); $savepath = $config->getString('session.phpsession.savepath', null); if (!empty($savepath)) { session_save_path($savepath); } }
/** * Parse an Extensions element. Extensions may appear in multiple elements and certain extension may get inherited * from a parent element. * * @param mixed $element The element which contains the Extensions element. * @param array $parentExtensions An optional array of extensions from the parent element. * * @return array An associative array with the extensions parsed. */ private static function processExtensions($element, $parentExtensions = array()) { $ret = array('scope' => array(), 'tags' => array(), 'EntityAttributes' => array(), 'RegistrationInfo' => array(), 'UIInfo' => array(), 'DiscoHints' => array()); // Some extensions may get inherited from a parent element if (($element instanceof \SAML2\XML\md\EntityDescriptor || $element instanceof \SAML2\XML\md\EntitiesDescriptor) && !empty($parentExtensions['RegistrationInfo'])) { $ret['RegistrationInfo'] = $parentExtensions['RegistrationInfo']; } foreach ($element->Extensions as $e) { if ($e instanceof \SAML2\XML\shibmd\Scope) { $ret['scope'][] = $e->scope; continue; } // Entity Attributes are only allowed at entity level extensions and not at RoleDescriptor level if ($element instanceof \SAML2\XML\md\EntityDescriptor || $element instanceof \SAML2\XML\md\EntitiesDescriptor) { if ($e instanceof \SAML2\XML\mdrpi\RegistrationInfo) { // Registration Authority cannot be overridden (warn only if override attempts to change the value) if (isset($ret['RegistrationInfo']['registrationAuthority']) && $ret['RegistrationInfo']['registrationAuthority'] !== $e->registrationAuthority) { SimpleSAML\Logger::warning('Invalid attempt to override registrationAuthority \'' . $ret['RegistrationInfo']['registrationAuthority'] . "' with '{$e->registrationAuthority}'"); } else { $ret['RegistrationInfo']['registrationAuthority'] = $e->registrationAuthority; } } if ($e instanceof \SAML2\XML\mdattr\EntityAttributes && !empty($e->children)) { foreach ($e->children as $attr) { // only saml:Attribute are currently supported here. The specifications also allows // saml:Assertions, which more complex processing if ($attr instanceof \SAML2\XML\saml\Attribute) { if (empty($attr->Name) || empty($attr->AttributeValue)) { continue; } // attribute names that is not URI is prefixed as this: '{nameformat}name' $name = $attr->Name; if (empty($attr->NameFormat)) { $name = '{' . \SAML2\Constants::NAMEFORMAT_UNSPECIFIED . '}' . $attr->Name; } elseif ($attr->NameFormat !== 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri') { $name = '{' . $attr->NameFormat . '}' . $attr->Name; } $values = array(); foreach ($attr->AttributeValue as $attrvalue) { $values[] = $attrvalue->getString(); } $ret['EntityAttributes'][$name] = $values; } } } } // UIInfo elements are only allowed at RoleDescriptor level extensions if ($element instanceof \SAML2\XML\md\RoleDescriptor) { if ($e instanceof \SAML2\XML\mdui\UIInfo) { $ret['UIInfo']['DisplayName'] = $e->DisplayName; $ret['UIInfo']['Description'] = $e->Description; $ret['UIInfo']['InformationURL'] = $e->InformationURL; $ret['UIInfo']['PrivacyStatementURL'] = $e->PrivacyStatementURL; foreach ($e->Keywords as $uiItem) { if (!$uiItem instanceof \SAML2\XML\mdui\Keywords || empty($uiItem->Keywords) || empty($uiItem->lang)) { continue; } $ret['UIInfo']['Keywords'][$uiItem->lang] = $uiItem->Keywords; } foreach ($e->Logo as $uiItem) { if (!$uiItem instanceof \SAML2\XML\mdui\Logo || empty($uiItem->url) || empty($uiItem->height) || empty($uiItem->width)) { continue; } $logo = array('url' => $uiItem->url, 'height' => $uiItem->height, 'width' => $uiItem->width); if (!empty($uiItem->lang)) { $logo['lang'] = $uiItem->lang; } $ret['UIInfo']['Logo'][] = $logo; } } } // DiscoHints elements are only allowed at IDPSSODescriptor level extensions if ($element instanceof \SAML2\XML\md\IDPSSODescriptor) { if ($e instanceof \SAML2\XML\mdui\DiscoHints) { $ret['DiscoHints']['IPHint'] = $e->IPHint; $ret['DiscoHints']['DomainHint'] = $e->DomainHint; $ret['DiscoHints']['GeolocationHint'] = $e->GeolocationHint; } } if (!$e instanceof \SAML2\XML\Chunk) { continue; } if ($e->localName === 'Attribute' && $e->namespaceURI === \SAML2\Constants::NS_SAML) { $attribute = $e->getXML(); $name = $attribute->getAttribute('Name'); $values = array_map(array('SimpleSAML\\Utils\\XML', 'getDOMText'), SimpleSAML\Utils\XML::getDOMChildren($attribute, 'AttributeValue', '@saml2')); if ($name === 'tags') { foreach ($values as $tagname) { if (!empty($tagname)) { $ret['tags'][] = $tagname; } } } } } return $ret; }
/** * Build an assertion based on information in the metadata. * * @param SimpleSAML_Configuration $idpMetadata The metadata of the IdP. * @param SimpleSAML_Configuration $spMetadata The metadata of the SP. * @param array &$state The state array with information about the request. * @return SAML2_Assertion The assertion. */ private static function buildAssertion(SimpleSAML_Configuration $idpMetadata, SimpleSAML_Configuration $spMetadata, array &$state) { assert('isset($state["Attributes"])'); assert('isset($state["saml:ConsumerURL"])'); $now = time(); $signAssertion = $spMetadata->getBoolean('saml20.sign.assertion', NULL); if ($signAssertion === NULL) { $signAssertion = $idpMetadata->getBoolean('saml20.sign.assertion', TRUE); } $config = SimpleSAML_Configuration::getInstance(); $a = new SAML2_Assertion(); if ($signAssertion) { sspmod_saml_Message::addSign($idpMetadata, $spMetadata, $a); } $a->setIssuer($idpMetadata->getString('entityid')); $a->setValidAudiences(array($spMetadata->getString('entityid'))); $a->setNotBefore($now - 30); $assertionLifetime = $spMetadata->getInteger('assertion.lifetime', NULL); if ($assertionLifetime === NULL) { $assertionLifetime = $idpMetadata->getInteger('assertion.lifetime', 300); } $a->setNotOnOrAfter($now + $assertionLifetime); if (isset($state['saml:AuthnContextClassRef'])) { $a->setAuthnContext($state['saml:AuthnContextClassRef']); } else { $a->setAuthnContext(SAML2_Const::AC_PASSWORD); } $sessionStart = $now; if (isset($state['AuthnInstant'])) { $a->setAuthnInstant($state['AuthnInstant']); $sessionStart = $state['AuthnInstant']; } $sessionLifetime = $config->getInteger('session.duration', 8 * 60 * 60); $a->setSessionNotOnOrAfter($sessionStart + $sessionLifetime); $a->setSessionIndex(SimpleSAML\Utils\Random::generateID()); $sc = new SAML2_XML_saml_SubjectConfirmation(); $sc->SubjectConfirmationData = new SAML2_XML_saml_SubjectConfirmationData(); $sc->SubjectConfirmationData->NotOnOrAfter = $now + $assertionLifetime; $sc->SubjectConfirmationData->Recipient = $state['saml:ConsumerURL']; $sc->SubjectConfirmationData->InResponseTo = $state['saml:RequestId']; /* ProtcolBinding of SP's <AuthnRequest> overwrites IdP hosted metadata configuration. */ $hokAssertion = NULL; if ($state['saml:Binding'] === SAML2_Const::BINDING_HOK_SSO) { $hokAssertion = TRUE; } if ($hokAssertion === NULL) { $hokAssertion = $idpMetadata->getBoolean('saml20.hok.assertion', FALSE); } if ($hokAssertion) { /* Holder-of-Key */ $sc->Method = SAML2_Const::CM_HOK; if (\SimpleSAML\Utils\HTTP::isHTTPS()) { if (isset($_SERVER['SSL_CLIENT_CERT']) && !empty($_SERVER['SSL_CLIENT_CERT'])) { /* Extract certificate data (if this is a certificate). */ $clientCert = $_SERVER['SSL_CLIENT_CERT']; $pattern = '/^-----BEGIN CERTIFICATE-----([^-]*)^-----END CERTIFICATE-----/m'; if (preg_match($pattern, $clientCert, $matches)) { /* We have a client certificate from the browser which we add to the HoK assertion. */ $x509Certificate = new SAML2_XML_ds_X509Certificate(); $x509Certificate->certificate = str_replace(array("\r", "\n", " "), '', $matches[1]); $x509Data = new SAML2_XML_ds_X509Data(); $x509Data->data[] = $x509Certificate; $keyInfo = new SAML2_XML_ds_KeyInfo(); $keyInfo->info[] = $x509Data; $sc->SubjectConfirmationData->info[] = $keyInfo; } else { throw new SimpleSAML_Error_Exception('Error creating HoK assertion: No valid client certificate provided during TLS handshake with IdP'); } } else { throw new SimpleSAML_Error_Exception('Error creating HoK assertion: No client certificate provided during TLS handshake with IdP'); } } else { throw new SimpleSAML_Error_Exception('Error creating HoK assertion: No HTTPS connection to IdP, but required for Holder-of-Key SSO'); } } else { /* Bearer */ $sc->Method = SAML2_Const::CM_BEARER; } $a->setSubjectConfirmation(array($sc)); /* Add attributes. */ if ($spMetadata->getBoolean('simplesaml.attributes', TRUE)) { $attributeNameFormat = self::getAttributeNameFormat($idpMetadata, $spMetadata); $a->setAttributeNameFormat($attributeNameFormat); $attributes = self::encodeAttributes($idpMetadata, $spMetadata, $state['Attributes']); $a->setAttributes($attributes); } /* Generate the NameID for the assertion. */ if (isset($state['saml:NameIDFormat'])) { $nameIdFormat = $state['saml:NameIDFormat']; } else { $nameIdFormat = NULL; } if ($nameIdFormat === NULL || !isset($state['saml:NameID'][$nameIdFormat])) { /* Either not set in request, or not set to a format we supply. Fall back to old generation method. */ $nameIdFormat = $spMetadata->getString('NameIDFormat', NULL); if ($nameIdFormat === NULL) { $nameIdFormat = $idpMetadata->getString('NameIDFormat', SAML2_Const::NAMEID_TRANSIENT); } } if (isset($state['saml:NameID'][$nameIdFormat])) { $nameId = $state['saml:NameID'][$nameIdFormat]; $nameId['Format'] = $nameIdFormat; } else { $spNameQualifier = $spMetadata->getString('SPNameQualifier', NULL); if ($spNameQualifier === NULL) { $spNameQualifier = $spMetadata->getString('entityid'); } if ($nameIdFormat === SAML2_Const::NAMEID_TRANSIENT) { /* generate a random id */ $nameIdValue = SimpleSAML\Utils\Random::generateID(); } else { /* this code will end up generating either a fixed assigned id (via nameid.attribute) or random id if not assigned/configured */ $nameIdValue = self::generateNameIdValue($idpMetadata, $spMetadata, $state); if ($nameIdValue === NULL) { SimpleSAML\Logger::warning('Falling back to transient NameID.'); $nameIdFormat = SAML2_Const::NAMEID_TRANSIENT; $nameIdValue = SimpleSAML\Utils\Random::generateID(); } } $nameId = array('Format' => $nameIdFormat, 'Value' => $nameIdValue, 'SPNameQualifier' => $spNameQualifier); } $state['saml:idp:NameID'] = $nameId; $a->setNameId($nameId); $encryptNameId = $spMetadata->getBoolean('nameid.encryption', NULL); if ($encryptNameId === NULL) { $encryptNameId = $idpMetadata->getBoolean('nameid.encryption', FALSE); } if ($encryptNameId) { $a->encryptNameId(sspmod_saml_Message::getEncryptionKey($spMetadata)); } return $a; }
/** * Delete a metadata entry. * * @param string $entityId The entityId of the metadata entry. * @param string $set The metadata set this metadata entry belongs to. */ public function deleteMetadata($entityId, $set) { assert('is_string($entityId)'); assert('is_string($set)'); $filePath = $this->getMetadataPath($entityId, $set); if (!file_exists($filePath)) { SimpleSAML\Logger::warning('Attempted to erase nonexistent metadata entry ' . var_export($entityId, true) . ' in set ' . var_export($set, true) . '.'); return; } $res = unlink($filePath); if ($res === false) { $error = error_get_last(); SimpleSAML\Logger::error('Failed to delete file ' . $filePath . ': ' . $error['message']); } }
/** * Find data stored with a given key. * * @param string $key The key of the data. * * @return mixed The data stored with the given key, or null if no data matching the key was found. */ public static function get($key) { SimpleSAML\Logger::debug("loading key {$key} from memcache"); $latestInfo = null; $latestTime = 0.0; $latestData = null; $mustUpdate = false; $allDown = true; // search all the servers for the given id foreach (self::getMemcacheServers() as $server) { $serializedInfo = $server->get($key); if ($serializedInfo === false) { // either the server is down, or we don't have the value stored on that server $mustUpdate = true; $up = $server->getstats(); if ($up !== false) { $allDown = false; } continue; } $allDown = false; // unserialize the object $info = unserialize($serializedInfo); /* * Make sure that this is an array with two keys: * - 'timestamp': The time the data was saved. * - 'data': The data. */ if (!is_array($info)) { SimpleSAML\Logger::warning('Retrieved invalid data from a memcache server. Data was not an array.'); continue; } if (!array_key_exists('timestamp', $info)) { SimpleSAML\Logger::warning('Retrieved invalid data from a memcache server. Missing timestamp.'); continue; } if (!array_key_exists('data', $info)) { SimpleSAML\Logger::warning('Retrieved invalid data from a memcache server. Missing data.'); continue; } if ($latestInfo === null) { // first info found $latestInfo = $serializedInfo; $latestTime = $info['timestamp']; $latestData = $info['data']; continue; } if ($info['timestamp'] === $latestTime && $serializedInfo === $latestInfo) { // this data matches the data from the other server(s) continue; } // different data from different servers. We need to update at least one of them to maintain sync $mustUpdate = true; // update if data in $info is newer than $latestData if ($latestTime < $info['timestamp']) { $latestInfo = $serializedInfo; $latestTime = $info['timestamp']; $latestData = $info['data']; } } if ($latestData === null) { if ($allDown) { // all servers are down, panic! $e = new SimpleSAML_Error_Error('MEMCACHEDOWN', null, 503); throw new SimpleSAML_Error_Exception('All memcache servers are down', 503, $e); } // we didn't find any data matching the key SimpleSAML\Logger::debug("key {$key} not found in memcache"); return null; } if ($mustUpdate) { // we found data matching the key, but some of the servers need updating SimpleSAML\Logger::debug("Memcache servers out of sync for {$key}, forcing sync"); self::set($key, $latestData); } return $latestData; }
/** * Print the exception to the log with log level warning. * * This function will write this exception to the log, including a full backtrace. */ public function logWarning() { $lines = $this->format(); foreach ($lines as $line) { SimpleSAML\Logger::warning($line); } }
/** * Save metadata for loading with the 'serialize' metadata loader. * * @param string $outputDir The directory we should save the metadata to. */ public function writeMetadataSerialize($outputDir) { assert('is_string($outputDir)'); $metaHandler = new SimpleSAML_Metadata_MetaDataStorageHandlerSerialize(array('directory' => $outputDir)); /* First we add all the metadata entries to the metadata handler. */ foreach ($this->metadata as $set => $elements) { foreach ($elements as $m) { $entityId = $m['metadata']['entityid']; SimpleSAML\Logger::debug('metarefresh: Add metadata entry ' . var_export($entityId, TRUE) . ' in set ' . var_export($set, TRUE) . '.'); $metaHandler->saveMetadata($entityId, $set, $m['metadata']); } } /* Then we delete old entries which should no longer exist. */ $ct = time(); foreach ($metaHandler->getMetadataSets() as $set) { foreach ($metaHandler->getMetadataSet($set) as $entityId => $metadata) { if (!array_key_exists('expire', $metadata)) { SimpleSAML\Logger::warning('metarefresh: Metadata entry without expire timestamp: ' . var_export($entityId, TRUE) . ' in set ' . var_export($set, TRUE) . '.'); continue; } if ($metadata['expire'] > $ct) { continue; } SimpleSAML\Logger::debug('metarefresh: ' . $entityId . ' expired ' . date('l jS \\of F Y h:i:s A', $metadata['expire'])); SimpleSAML\Logger::debug('metarefresh: Delete expired metadata entry ' . var_export($entityId, TRUE) . ' in set ' . var_export($set, TRUE) . '. (' . ($ct - $metadata['expire']) . ' sec)'); $metaHandler->deleteMetadata($entityId, $set); } } }
/** * Verify signed data. * * This function verifies signed data. * * @param string $signedData The data which is signed. * * @return string|false The data, or false if the signature is invalid. */ private static function _verify($signedData) { assert('is_string($signedData)'); $data = explode(':', $signedData, 2); if (count($data) !== 2) { SimpleSAML\Logger::warning('Consent cookie: Missing signature.'); return false; } $data = $data[1]; $newSignedData = self::_sign($data); if ($newSignedData !== $signedData) { SimpleSAML\Logger::warning('Consent cookie: Invalid signature.'); return false; } return $data; }
/** * Get a session from the session handler. * * @param string|null $sessionId The session we should get, or null to get the current session. * * @return SimpleSAML_Session The session that is stored in the session handler, or null if the session wasn't * found. */ public static function getSession($sessionId = null) { assert('is_string($sessionId) || is_null($sessionId)'); $sh = SimpleSAML_SessionHandler::getSessionHandler(); if ($sessionId === null) { $checkToken = true; $sessionId = $sh->getCookieSessionId(); if ($sessionId === null) { return null; } } else { $checkToken = false; } if (array_key_exists($sessionId, self::$sessions)) { return self::$sessions[$sessionId]; } $session = $sh->loadSession($sessionId); if ($session === null) { return null; } assert('$session instanceof self'); if ($checkToken) { $globalConfig = SimpleSAML_Configuration::getInstance(); if ($session->authToken !== null) { $authTokenCookieName = $globalConfig->getString('session.authtoken.cookiename', 'SimpleSAMLAuthToken'); if (!isset($_COOKIE[$authTokenCookieName])) { SimpleSAML\Logger::warning('Missing AuthToken cookie.'); return null; } if ($_COOKIE[$authTokenCookieName] !== $session->authToken) { SimpleSAML\Logger::warning('Invalid AuthToken cookie.'); return null; } } // run session check function if defined $checkFunction = $globalConfig->getArray('session.check_function', null); if (isset($checkFunction)) { assert('is_callable($checkFunction)'); $check = call_user_func($checkFunction, $session); if ($check !== true) { SimpleSAML\Logger::warning('Session did not pass check function.'); return null; } } } self::$sessions[$sessionId] = $session; return $session; }
/** * Get the NameID value. * * @param array $state The state array. * @return string|null The NameID value. * * @throws sspmod_saml_Error if the NameID creation policy is invalid. */ protected function getValue(array &$state) { if (!isset($state['saml:NameIDFormat']) && !$this->allowUnspecified) { SimpleSAML\Logger::debug('SQLPersistentNameID: Request did not specify persistent NameID format, ' . 'not generating persistent NameID.'); return null; } $validNameIdFormats = @array_filter(array($state['saml:NameIDFormat'], $state['SPMetadata']['NameIDPolicy'], $state['SPMetadata']['NameIDFormat'])); if (count($validNameIdFormats) && !in_array($this->format, $validNameIdFormats) && !$this->allowDifferent) { SimpleSAML\Logger::debug('SQLPersistentNameID: SP expects different NameID format (' . implode(', ', $validNameIdFormats) . '), not generating persistent NameID.'); return null; } if (!isset($state['Destination']['entityid'])) { SimpleSAML\Logger::warning('SQLPersistentNameID: No SP entity ID - not generating persistent NameID.'); return null; } $spEntityId = $state['Destination']['entityid']; if (!isset($state['Source']['entityid'])) { SimpleSAML\Logger::warning('SQLPersistentNameID: No IdP entity ID - not generating persistent NameID.'); return null; } $idpEntityId = $state['Source']['entityid']; if (!isset($state['Attributes'][$this->attribute]) || count($state['Attributes'][$this->attribute]) === 0) { SimpleSAML\Logger::warning('SQLPersistentNameID: Missing attribute ' . var_export($this->attribute, true) . ' on user - not generating persistent NameID.'); return null; } if (count($state['Attributes'][$this->attribute]) > 1) { SimpleSAML\Logger::warning('SQLPersistentNameID: More than one value in attribute ' . var_export($this->attribute, true) . ' on user - not generating persistent NameID.'); return null; } $uid = array_values($state['Attributes'][$this->attribute]); // just in case the first index is no longer 0 $uid = $uid[0]; $value = sspmod_saml_IdP_SQLNameID::get($idpEntityId, $spEntityId, $uid); if ($value !== null) { SimpleSAML\Logger::debug('SQLPersistentNameID: Found persistent NameID ' . var_export($value, true) . ' for user ' . var_export($uid, true) . '.'); return $value; } if ((!isset($state['saml:AllowCreate']) || !$state['saml:AllowCreate']) && !$this->alwaysCreate) { SimpleSAML\Logger::warning('SQLPersistentNameID: Did not find persistent NameID for user, and not allowed to create new NameID.'); throw new sspmod_saml_Error(SAML2_Const::STATUS_RESPONDER, 'urn:oasis:names:tc:SAML:2.0:status:InvalidNameIDPolicy'); } $value = bin2hex(openssl_random_pseudo_bytes(20)); SimpleSAML\Logger::debug('SQLPersistentNameID: Created persistent NameID ' . var_export($value, true) . ' for user ' . var_export($uid, true) . '.'); sspmod_saml_IdP_SQLNameID::add($idpEntityId, $spEntityId, $uid, $value); return $value; }
/** * Set the HTTP return code for this error. * * This should be overridden by subclasses who want a different return code than 500 Internal Server Error. */ protected function setHTTPCode() { // Some mostly used HTTP codes $httpCodesMap = array(400 => 'HTTP/1.0 400 Bad Request', 403 => 'HTTP/1.0 403 Forbidden', 404 => 'HTTP/1.0 404 Not Found', 405 => 'HTTP/1.0 405 Method Not Allowed', 500 => 'HTTP/1.0 500 Internal Server Error', 501 => 'HTTP/1.0 501 Method Not Implemented', 503 => 'HTTP/1.0 503 Service Temporarily Unavailable'); $httpCode = $this->httpCode; if (function_exists('http_response_code')) { http_response_code($httpCode); return; } if (!array_key_exists($this->httpCode, $httpCodesMap)) { $httpCode = 500; SimpleSAML\Logger::warning('HTTP response code not defined: ' . var_export($this->httpCode, true)); } header($httpCodesMap[$httpCode]); }
/** * This function lists all known metadata in the given set. It is returned as an associative array * where the key is the entity id. * * @param string $set The set we want to list metadata from. * * @return array An associative array with the metadata from from the given set. */ public function getList($set = 'saml20-idp-remote') { assert('is_string($set)'); $result = array(); foreach ($this->sources as $source) { $srcList = $source->getMetadataSet($set); foreach ($srcList as $key => $le) { if (array_key_exists('expire', $le)) { if ($le['expire'] < time()) { unset($srcList[$key]); SimpleSAML\Logger::warning("Dropping metadata entity " . var_export($key, true) . ", expired " . SimpleSAML\Utils\Time::generateTimestamp($le['expire']) . "."); } } } /* $result is the last argument to array_merge because we want the content already * in $result to have precedence. */ $result = array_merge($srcList, $result); } return $result; }
/** * Re-authenticate an user. * * This function is called by the IdP to give the authentication source a chance to * interact with the user even in the case when the user is already authenticated. * * @param array &$state Information about the current authentication. */ public function reauthenticate(array &$state) { assert('is_array($state)'); $session = SimpleSAML_Session::getSessionFromRequest(); $data = $session->getAuthState($this->authId); foreach ($data as $k => $v) { $state[$k] = $v; } // check if we have an IDPList specified in the request if (isset($state['saml:IDPList']) && sizeof($state['saml:IDPList']) > 0 && !in_array($state['saml:sp:IdP'], $state['saml:IDPList'], true)) { /* * The user has an existing, valid session. However, the SP provided a list of IdPs it accepts for * authentication, and the IdP the existing session is related to is not in that list. * * First, check if we recognize any of the IdPs requested. */ $mdh = SimpleSAML_Metadata_MetaDataStorageHandler::getMetadataHandler(); $known_idps = $mdh->getList(); $intersection = array_intersect($state['saml:IDPList'], array_keys($known_idps)); if (empty($intersection)) { // all requested IdPs are unknown throw new SimpleSAML\Module\saml\Error\NoSupportedIDP(\SAML2\Constants::STATUS_REQUESTER, 'None of the IdPs requested are supported by this proxy.'); } /* * We have at least one IdP in the IDPList that we recognize, and it's not the one currently in use. Let's * see if this proxy enforces the use of one single IdP. */ if (!is_null($this->idp) && !in_array($this->idp, $intersection)) { // an IdP is enforced but not requested throw new SimpleSAML\Module\saml\Error\NoAvailableIDP(\SAML2\Constants::STATUS_REQUESTER, 'None of the IdPs requested are available to this proxy.'); } /* * We need to inform the user, and ask whether we should logout before starting the authentication process * again with a different IdP, or cancel the current SSO attempt. */ SimpleSAML\Logger::warning("Reauthentication after logout is needed. The IdP '{$state['saml:sp:IdP']}' is not in the IDPList " . "provided by the Service Provider '{$state['core:SP']}'."); $state['saml:sp:IdPMetadata'] = $this->getIdPMetadata($state['saml:sp:IdP']); $state['saml:sp:AuthId'] = $this->authId; self::askForIdPChange($state); } }
/** * Add attributes from an LDAP server. * * @param array &$request The current request */ public function process(&$request) { assert('is_array($request)'); assert('array_key_exists("Attributes", $request)'); $attributes =& $request['Attributes']; // perform a merge on the ldap_search_filter // loop over the attributes and build the search and replace arrays foreach ($attributes as $attr => $val) { $arrSearch[] = '%' . $attr . '%'; if (strlen($val[0]) > 0) { $arrReplace[] = SimpleSAML_Auth_LDAP::escape_filter_value($val[0]); } else { $arrReplace[] = ''; } } // merge the attributes into the ldap_search_filter $filter = str_replace($arrSearch, $arrReplace, $this->search_filter); if (strpos($filter, '%') !== false) { SimpleSAML\Logger::info('AttributeAddFromLDAP: There are non-existing attributes in the search filter. (' . $this->search_filter . ')'); return; } if (!in_array($this->attr_policy, array('merge', 'replace', 'add'))) { SimpleSAML\Logger::warning("AttributeAddFromLDAP: 'attribute.policy' must be one of 'merge'," . "'replace' or 'add'."); return; } // search for matching entries try { $entries = $this->getLdap()->searchformultiple($this->base_dn, $filter, array_values($this->search_attributes), true, false); } catch (Exception $e) { return; // silent fail, error is still logged by LDAP search } // handle [multiple] values foreach ($entries as $entry) { foreach ($this->search_attributes as $target => $name) { if (is_numeric($target)) { $target = $name; } if (isset($attributes[$target]) && $this->attr_policy === 'replace') { unset($attributes[$target]); } $name = strtolower($name); if (isset($entry[$name])) { unset($entry[$name]['count']); if (isset($attributes[$target])) { foreach (array_values($entry[$name]) as $value) { if ($this->attr_policy === 'merge') { if (!in_array($value, $attributes[$target])) { $attributes[$target][] = $value; } } else { $attributes[$target][] = $value; } } } else { $attributes[$target] = array_values($entry[$name]); } } } } }
/** * Add a specific type of metadata to an entity. * * @param string $set The metadata set this metadata comes from. * @param array $metadata The metadata. */ public function addMetadata($set, $metadata) { assert('is_string($set)'); assert('is_array($metadata)'); $this->setExpiration($metadata); switch ($set) { case 'saml20-sp-remote': $this->addMetadataSP20($metadata); break; case 'saml20-idp-remote': $this->addMetadataIdP20($metadata); break; case 'shib13-sp-remote': $this->addMetadataSP11($metadata); break; case 'shib13-idp-remote': $this->addMetadataIdP11($metadata); break; case 'attributeauthority-remote': $this->addAttributeAuthority($metadata); break; default: SimpleSAML\Logger::warning('Unable to generate metadata for unknown type \'' . $set . '\'.'); } }
/** * Delete all consents. * * @param string $userId The hash identifying the user at an IdP. * * @return int Number of consents deleted */ public function deleteAllConsents($userId) { assert('is_string($userId)'); $st = $this->_execute('DELETE FROM ' . $this->_table . ' WHERE hashed_user_id = ?', array($userId)); if ($st === false) { return; } if ($st->rowCount() > 0) { SimpleSAML\Logger::debug('consent:Database - Deleted (' . $st->rowCount() . ') consent(s).'); return $st->rowCount(); } else { SimpleSAML\Logger::warning('consent:Database - Attempted to delete nonexistent consent'); } }