1<?php 2 3 4/** 5 * Consent Authentication Processing filter 6 * 7 * Filter for requesting the user to give consent before attributes are 8 * released to the SP. 9 * 10 * @package SimpleSAMLphp 11 */ 12class sspmod_consent_Auth_Process_Consent extends SimpleSAML_Auth_ProcessingFilter 13{ 14 /** 15 * Button to receive focus 16 * 17 * @var string|null 18 */ 19 private $_focus = null; 20 21 /** 22 * Include attribute values 23 * 24 * @var bool 25 */ 26 private $_includeValues = false; 27 28 /** 29 * Check remember consent 30 * 31 * @var bool 32 */ 33 private $_checked = false; 34 35 /** 36 * Consent backend storage configuration 37 * 38 * @var sspmod_consent_Store|null 39 */ 40 private $_store = null; 41 42 /** 43 * Attributes where the value should be hidden 44 * 45 * @var array 46 */ 47 private $_hiddenAttributes = array(); 48 49 /** 50 * Attributes which should not require consent 51 * 52 * @var aray 53 */ 54 private $_noconsentattributes = array(); 55 56 /** 57 * Whether we should show the "about service"-link on the no consent page. 58 * 59 * @var bool 60 */ 61 private $_showNoConsentAboutService = true; 62 63 64 /** 65 * Initialize consent filter. 66 * 67 * Validates and parses the configuration. 68 * 69 * @param array $config Configuration information. 70 * @param mixed $reserved For future use. 71 * 72 * @throws SimpleSAML_Error_Exception if the configuration is not valid. 73 */ 74 public function __construct($config, $reserved) 75 { 76 assert(is_array($config)); 77 parent::__construct($config, $reserved); 78 79 if (array_key_exists('includeValues', $config)) { 80 if (!is_bool($config['includeValues'])) { 81 throw new SimpleSAML_Error_Exception( 82 'Consent: includeValues must be boolean. '. 83 var_export($config['includeValues'], true).' given.' 84 ); 85 } 86 $this->_includeValues = $config['includeValues']; 87 } 88 89 if (array_key_exists('checked', $config)) { 90 if (!is_bool($config['checked'])) { 91 throw new SimpleSAML_Error_Exception( 92 'Consent: checked must be boolean. '. 93 var_export($config['checked'], true).' given.' 94 ); 95 } 96 $this->_checked = $config['checked']; 97 } 98 99 if (array_key_exists('focus', $config)) { 100 if (!in_array($config['focus'], array('yes', 'no'), true)) { 101 throw new SimpleSAML_Error_Exception( 102 'Consent: focus must be a string with values `yes` or `no`. '. 103 var_export($config['focus'], true).' given.' 104 ); 105 } 106 $this->_focus = $config['focus']; 107 } 108 109 if (array_key_exists('hiddenAttributes', $config)) { 110 if (!is_array($config['hiddenAttributes'])) { 111 throw new SimpleSAML_Error_Exception( 112 'Consent: hiddenAttributes must be an array. '. 113 var_export($config['hiddenAttributes'], true).' given.' 114 ); 115 } 116 $this->_hiddenAttributes = $config['hiddenAttributes']; 117 } 118 119 if (array_key_exists('attributes.exclude', $config)) { 120 if (!is_array($config['attributes.exclude'])) { 121 throw new SimpleSAML_Error_Exception( 122 'Consent: attributes.exclude must be an array. '. 123 var_export($config['attributes.exclude'], true).' given.' 124 ); 125 } 126 $this->_noconsentattributes = $config['attributes.exclude']; 127 } elseif (array_key_exists('noconsentattributes', $config)) { 128 SimpleSAML\Logger::warning("The 'noconsentattributes' option has been deprecated in favour of 'attributes.exclude'."); 129 if (!is_array($config['noconsentattributes'])) { 130 throw new SimpleSAML_Error_Exception( 131 'Consent: noconsentattributes must be an array. '. 132 var_export($config['noconsentattributes'], true).' given.' 133 ); 134 } 135 $this->_noconsentattributes = $config['noconsentattributes']; 136 } 137 138 if (array_key_exists('store', $config)) { 139 try { 140 $this->_store = sspmod_consent_Store::parseStoreConfig($config['store']); 141 } catch (Exception $e) { 142 SimpleSAML\Logger::error( 143 'Consent: Could not create consent storage: '. 144 $e->getMessage() 145 ); 146 } 147 } 148 149 if (array_key_exists('showNoConsentAboutService', $config)) { 150 if (!is_bool($config['showNoConsentAboutService'])) { 151 throw new SimpleSAML_Error_Exception('Consent: showNoConsentAboutService must be a boolean.'); 152 } 153 $this->_showNoConsentAboutService = $config['showNoConsentAboutService']; 154 } 155 } 156 157 158 /** 159 * Helper function to check whether consent is disabled. 160 * 161 * @param mixed $option The consent.disable option. Either an array of array, an array or a boolean. 162 * @param string $entityId The entityID of the SP/IdP. 163 * 164 * @return boolean True if disabled, false if not. 165 */ 166 private static function checkDisable($option, $entityId) 167 { 168 if (is_array($option)) { 169 // Check if consent.disable array has one element that is an array 170 if (count($option) === count($option, COUNT_RECURSIVE)) { 171 // Array is not multidimensional. Simple in_array search suffices 172 return in_array($entityId, $option, true); 173 } 174 175 // Array contains at least one element that is an array, verify both possibilities 176 if (in_array($entityId, $option, true)) { 177 return true; 178 } 179 180 // Search in multidimensional arrays 181 foreach ($option as $optionToTest) { 182 if (!is_array($optionToTest)) { 183 continue; // bad option 184 } 185 186 if (!array_key_exists('type', $optionToTest)) { 187 continue; // option has no type 188 } 189 190 // Option has a type - switch processing depending on type value : 191 if ($optionToTest['type'] === 'regex') { 192 // regex-based consent disabling 193 194 if (!array_key_exists('pattern', $optionToTest)) { 195 continue; // no pattern defined 196 } 197 198 if (preg_match($optionToTest['pattern'], $entityId) === 1) { 199 return true; 200 } 201 } else { 202 // option type is not supported 203 continue; 204 } 205 } // end foreach 206 207 // Base case : no match 208 return false; 209 } else { 210 return (boolean) $option; 211 } 212 } 213 214 215 /** 216 * Process a authentication response 217 * 218 * This function saves the state, and redirects the user to the page where the user can authorize the release of 219 * the attributes. If storage is used and the consent has already been given the user is passed on. 220 * 221 * @param array &$state The state of the response. 222 * 223 * @return void 224 * 225 * @throws SimpleSAML_Error_NoPassive if the request was passive and consent is needed. 226 */ 227 public function process(&$state) 228 { 229 assert(is_array($state)); 230 assert(array_key_exists('UserID', $state)); 231 assert(array_key_exists('Destination', $state)); 232 assert(array_key_exists('entityid', $state['Destination'])); 233 assert(array_key_exists('metadata-set', $state['Destination'])); 234 assert(array_key_exists('entityid', $state['Source'])); 235 assert(array_key_exists('metadata-set', $state['Source'])); 236 237 $spEntityId = $state['Destination']['entityid']; 238 $idpEntityId = $state['Source']['entityid']; 239 240 $metadata = SimpleSAML_Metadata_MetaDataStorageHandler::getMetadataHandler(); 241 242 /** 243 * If the consent module is active on a bridge $state['saml:sp:IdP'] 244 * will contain an entry id for the remote IdP. If not, then the 245 * consent module is active on a local IdP and nothing needs to be 246 * done. 247 */ 248 if (isset($state['saml:sp:IdP'])) { 249 $idpEntityId = $state['saml:sp:IdP']; 250 $idpmeta = $metadata->getMetaData($idpEntityId, 'saml20-idp-remote'); 251 $state['Source'] = $idpmeta; 252 } 253 254 $statsData = array('spEntityID' => $spEntityId); 255 256 // Do not use consent if disabled 257 if (isset($state['Source']['consent.disable']) && 258 self::checkDisable($state['Source']['consent.disable'], $spEntityId) 259 ) { 260 SimpleSAML\Logger::debug('Consent: Consent disabled for entity '.$spEntityId.' with IdP '.$idpEntityId); 261 SimpleSAML_Stats::log('consent:disabled', $statsData); 262 return; 263 } 264 if (isset($state['Destination']['consent.disable']) && 265 self::checkDisable($state['Destination']['consent.disable'], $idpEntityId) 266 ) { 267 SimpleSAML\Logger::debug('Consent: Consent disabled for entity '.$spEntityId.' with IdP '.$idpEntityId); 268 SimpleSAML_Stats::log('consent:disabled', $statsData); 269 return; 270 } 271 272 if ($this->_store !== null) { 273 $source = $state['Source']['metadata-set'].'|'.$idpEntityId; 274 $destination = $state['Destination']['metadata-set'].'|'.$spEntityId; 275 $attributes = $state['Attributes']; 276 277 // Remove attributes that do not require consent 278 foreach ($attributes as $attrkey => $attrval) { 279 if (in_array($attrkey, $this->_noconsentattributes, true)) { 280 unset($attributes[$attrkey]); 281 } 282 } 283 284 SimpleSAML\Logger::debug('Consent: userid: '.$state['UserID']); 285 SimpleSAML\Logger::debug('Consent: source: '.$source); 286 SimpleSAML\Logger::debug('Consent: destination: '.$destination); 287 288 $userId = self::getHashedUserID($state['UserID'], $source); 289 $targetedId = self::getTargetedID($state['UserID'], $source, $destination); 290 $attributeSet = self::getAttributeHash($attributes, $this->_includeValues); 291 292 SimpleSAML\Logger::debug( 293 'Consent: hasConsent() ['.$userId.'|'.$targetedId.'|'. 294 $attributeSet.']' 295 ); 296 297 try { 298 if ($this->_store->hasConsent($userId, $targetedId, $attributeSet)) { 299 // Consent already given 300 SimpleSAML\Logger::stats('consent found'); 301 SimpleSAML_Stats::log('consent:found', $statsData); 302 return; 303 } 304 305 SimpleSAML\Logger::stats('consent notfound'); 306 SimpleSAML_Stats::log('consent:notfound', $statsData); 307 308 $state['consent:store'] = $this->_store; 309 $state['consent:store.userId'] = $userId; 310 $state['consent:store.destination'] = $targetedId; 311 $state['consent:store.attributeSet'] = $attributeSet; 312 } catch (Exception $e) { 313 SimpleSAML\Logger::error('Consent: Error reading from storage: '.$e->getMessage()); 314 SimpleSAML\Logger::stats('Ccnsent failed'); 315 SimpleSAML_Stats::log('consent:failed', $statsData); 316 } 317 } else { 318 SimpleSAML\Logger::stats('consent nostorage'); 319 SimpleSAML_Stats::log('consent:nostorage', $statsData); 320 } 321 322 $state['consent:focus'] = $this->_focus; 323 $state['consent:checked'] = $this->_checked; 324 $state['consent:hiddenAttributes'] = $this->_hiddenAttributes; 325 $state['consent:noconsentattributes'] = $this->_noconsentattributes; 326 $state['consent:showNoConsentAboutService'] = $this->_showNoConsentAboutService; 327 328 // user interaction necessary. Throw exception on isPassive request 329 if (isset($state['isPassive']) && $state['isPassive'] === true) { 330 SimpleSAML_Stats::log('consent:nopassive', $statsData); 331 throw new SimpleSAML\Module\saml\Error\NoPassive( 332 \SAML2\Constants::STATUS_REQUESTER, 333 'Unable to give consent on passive request.' 334 ); 335 } 336 337 // Save state and redirect 338 $id = SimpleSAML_Auth_State::saveState($state, 'consent:request'); 339 $url = SimpleSAML\Module::getModuleURL('consent/getconsent.php'); 340 \SimpleSAML\Utils\HTTP::redirectTrustedURL($url, array('StateId' => $id)); 341 } 342 343 344 /** 345 * Generate a unique identifier of the user. 346 * 347 * @param string $userid The user id. 348 * @param string $source The source id. 349 * 350 * @return string SHA1 of the user id, source id and salt. 351 */ 352 public static function getHashedUserID($userid, $source) 353 { 354 return hash('sha1', $userid.'|'.SimpleSAML\Utils\Config::getSecretSalt().'|'.$source); 355 } 356 357 358 /** 359 * Generate a unique targeted identifier. 360 * 361 * @param string $userid The user id. 362 * @param string $source The source id. 363 * @param string $destination The destination id. 364 * 365 * @return string SHA1 of the user id, source id, destination id and salt. 366 */ 367 public static function getTargetedID($userid, $source, $destination) 368 { 369 return hash('sha1', $userid.'|'.SimpleSAML\Utils\Config::getSecretSalt().'|'.$source.'|'.$destination); 370 } 371 372 373 /** 374 * Generate unique identifier for attributes. 375 * 376 * Create a hash value for the attributes that changes when attributes are added or removed. If the attribute 377 * values are included in the hash, the hash will change if the values change. 378 * 379 * @param string $attributes The attributes. 380 * @param bool $includeValues Whether or not to include the attribute value in the generation of the hash. 381 * 382 * @return string SHA1 of the user id, source id, destination id and salt. 383 */ 384 public static function getAttributeHash($attributes, $includeValues = false) 385 { 386 if ($includeValues) { 387 foreach ($attributes as &$values) { 388 sort($values); 389 } 390 ksort($attributes); 391 $hashBase = serialize($attributes); 392 } else { 393 $names = array_keys($attributes); 394 sort($names); 395 $hashBase = implode('|', $names); 396 } 397 return hash('sha1', $hashBase); 398 } 399} 400