1<?php 2/* Copyright (c) 1998-2017 ILIAS open source, Extended GPL, see docs/LICENSE */ 3 4/** 5 * Class ilAuthProviderSaml 6 */ 7class ilAuthProviderSaml extends ilAuthProvider implements ilAuthProviderInterface, ilAuthProviderAccountMigrationInterface 8{ 9 /** 10 * @var string 11 */ 12 protected $uid = ''; 13 14 /** 15 * @var array 16 */ 17 protected $attributes = array(); 18 19 /** 20 * @var string 21 */ 22 protected $return_to = ''; 23 24 /** 25 * @var ilSamlIdp 26 */ 27 protected $idp; 28 29 /** 30 * @var bool 31 */ 32 protected $force_new_account = false; 33 34 /** 35 * @var string 36 */ 37 protected $migration_account = ''; 38 39 /** 40 * ilAuthProviderSaml constructor. 41 * @param \ilAuthFrontendCredentials|\ilAuthFrontendCredentialsSaml $credentials 42 * @param int|null $a_idp_id 43 */ 44 public function __construct(\ilAuthFrontendCredentials $credentials, $a_idp_id = null) 45 { 46 parent::__construct($credentials); 47 48 if (null === $a_idp_id || 0 === $a_idp_id) { 49 $this->idp = ilSamlIdp::getFirstActiveIdp(); 50 } else { 51 $this->idp = ilSamlIdp::getInstanceByIdpId($a_idp_id); 52 } 53 54 if ($credentials instanceof \ilAuthFrontendCredentialsSaml) { 55 $this->attributes = $credentials->getAttributes(); 56 $this->return_to = $credentials->getReturnTo(); 57 } 58 } 59 60 /** 61 * @throws \ilException 62 */ 63 private function determineUidFromAttributes() 64 { 65 if ( 66 !array_key_exists($this->idp->getUidClaim(), $this->attributes) || 67 !is_array($this->attributes[$this->idp->getUidClaim()]) || 68 !array_key_exists(0, $this->attributes[$this->idp->getUidClaim()]) || 69 0 === strlen($this->attributes[$this->idp->getUidClaim()][0]) 70 ) { 71 throw new \ilException(sprintf( 72 'Could not find unique SAML attribute for the configured identifier: %s', 73 var_export($this->idp->getUidClaim(), 1) 74 )); 75 } 76 77 $this->uid = $this->attributes[$this->idp->getUidClaim()][0]; 78 } 79 80 /** 81 * @inheritdoc 82 */ 83 public function doAuthentication(\ilAuthStatus $status) 84 { 85 if (!is_array($this->attributes) || 0 === count($this->attributes)) { 86 $this->getLogger()->warning('Could not parse any attributes from SAML response.'); 87 $this->handleAuthenticationFail($status, 'err_wrong_login'); 88 return false; 89 } 90 91 try { 92 $this->determineUidFromAttributes(); 93 94 return $this->handleSamlAuth($status); 95 } catch (\ilException $e) { 96 $this->getLogger()->warning($e->getMessage()); 97 $this->handleAuthenticationFail($status, 'err_wrong_login'); 98 return false; 99 } 100 } 101 102 /** 103 * @param ilAuthStatus $status 104 * @return bool 105 */ 106 public function handleSamlAuth(\ilAuthStatus $status) 107 { 108 $update_auth_mode = false; 109 110 ilLoggerFactory::getLogger('auth')->debug(sprintf('Login observer called for SAML authentication request of ext_account "%s" and auth_mode "%s".', $this->uid, $this->getUserAuthModeName())); 111 ilLoggerFactory::getLogger('auth')->debug(sprintf('Target set to: %s', var_export($this->return_to, 1))); 112 ilLoggerFactory::getLogger('auth')->debug(sprintf('Trying to find ext_account "%s" for auth_mode "%s".', $this->uid, $this->getUserAuthModeName())); 113 114 $internal_account = ilObjUser::_checkExternalAuthAccount( 115 $this->getUserAuthModeName(), 116 $this->uid, 117 false 118 ); 119 120 if (strlen($internal_account) == 0) { 121 $update_auth_mode = true; 122 123 ilLoggerFactory::getLogger('auth')->debug(sprintf('Could not find ext_account "%s" for auth_mode "%s".', $this->uid, $this->getUserAuthModeName())); 124 125 $fallback_auth_mode = 'local'; 126 ilLoggerFactory::getLogger('auth')->debug(sprintf('Trying to find ext_account "%s" for auth_mode "%s".', $this->uid, $fallback_auth_mode)); 127 $internal_account = ilObjUser::_checkExternalAuthAccount($fallback_auth_mode, $this->uid, false); 128 129 $defaultAuth = AUTH_LOCAL; 130 if ($GLOBALS['DIC']['ilSetting']->get('auth_mode')) { 131 $defaultAuth = $GLOBALS['DIC']['ilSetting']->get('auth_mode'); 132 } 133 134 if (strlen($internal_account) == 0 && ($defaultAuth == AUTH_LOCAL || $defaultAuth == $this->getTriggerAuthMode())) { 135 ilLoggerFactory::getLogger('auth')->debug(sprintf('Could not find ext_account "%s" for auth_mode "%s".', $this->uid, $fallback_auth_mode)); 136 137 $fallback_auth_mode = 'default'; 138 ilLoggerFactory::getLogger('auth')->debug(sprintf('Trying to find ext_account "%s" for auth_mode "%s".', $this->uid, $fallback_auth_mode)); 139 $internal_account = ilObjUser::_checkExternalAuthAccount($fallback_auth_mode, $this->uid, false); 140 } 141 } 142 143 if (strlen($internal_account) > 0) { 144 ilLoggerFactory::getLogger('auth')->debug(sprintf('Found user "%s" for ext_account "%s" in ILIAS database.', $internal_account, $this->uid)); 145 146 if ($this->idp->isSynchronizationEnabled()) { 147 ilLoggerFactory::getLogger('auth')->debug(sprintf('SAML user synchronisation is enabled, so update existing user "%s" with ext_account "%s".', $internal_account, $this->uid)); 148 $internal_account = $this->importUser($internal_account, $this->uid, $this->attributes); 149 } 150 151 if ($update_auth_mode) { 152 $usr_id = ilObjUser::_loginExists($internal_account); 153 if ($usr_id > 0) { 154 ilObjUser::_writeAuthMode($usr_id, $this->getUserAuthModeName()); 155 ilLoggerFactory::getLogger('auth')->debug(sprintf('SAML Switched auth_mode of user with login "%s" and ext_account "%s" to "%s".', $internal_account, $this->uid, $this->getUserAuthModeName())); 156 } else { 157 ilLoggerFactory::getLogger('auth')->debug(sprintf('SAML Could not switch auth_mode of user with login "%s" and ext_account "%s" to "%s".', $internal_account, $this->uid, $this->getUserAuthModeName())); 158 } 159 } 160 161 ilLoggerFactory::getLogger('auth')->debug(sprintf('Authentication succeeded: Found internal login "%s for ext_account "%s" and auth_mode "%s".', $internal_account, $this->uid, $this->getUserAuthModeName())); 162 163 $status->setStatus(ilAuthStatus::STATUS_AUTHENTICATED); 164 $status->setAuthenticatedUserId(ilObjUser::_lookupId($internal_account)); 165 ilSession::set('used_external_auth', true); 166 return true; 167 } else { 168 ilLoggerFactory::getLogger('auth')->debug(sprintf('Could not find an existing user for ext_account "%s" for any relevant auth_mode.', $this->uid)); 169 if ($this->idp->isSynchronizationEnabled()) { 170 ilLoggerFactory::getLogger('auth')->debug(sprintf('SAML user synchronisation is enabled, so determine action for ext_account "%s" and auth_mode "%s".', $this->uid, $this->getUserAuthModeName())); 171 if ($this->idp->isAccountMigrationEnabled() && !$this->force_new_account) { 172 ilSession::set('tmp_attributes', $this->attributes); 173 ilSession::set('tmp_return_to', $this->return_to); 174 175 ilLoggerFactory::getLogger('auth')->debug(sprintf('Account migration is enabled, so redirecting ext_account "%s" to account migration screen.', $this->uid)); 176 177 $this->setExternalAccountName($this->uid); 178 $status->setStatus(ilAuthStatus::STATUS_ACCOUNT_MIGRATION_REQUIRED); 179 return false; 180 } 181 182 $new_name = $this->importUser(null, $this->uid, $this->attributes); 183 ilLoggerFactory::getLogger('auth')->debug(sprintf('Created new user account with login "%s" and ext_account "%s".', $new_name, $this->uid)); 184 185 ilSession::set('tmp_attributes', null); 186 ilSession::set('tmp_return_to', null); 187 ilSession::set('used_external_auth', true); 188 189 if (strlen($this->return_to)) { 190 $_GET['target'] = $this->return_to; 191 } 192 193 $status->setStatus(ilAuthStatus::STATUS_AUTHENTICATED); 194 $status->setAuthenticatedUserId(ilObjUser::_lookupId($new_name)); 195 return true; 196 } else { 197 ilLoggerFactory::getLogger('auth')->debug("SAML user synchronisation is not enabled, auth failed."); 198 $this->handleAuthenticationFail($status, 'err_auth_saml_no_ilias_user'); 199 return false; 200 } 201 } 202 } 203 204 /** 205 * @inheritdoc 206 */ 207 public function migrateAccount(ilAuthStatus $status) 208 { 209 } 210 211 /** 212 * @inheritdoc 213 */ 214 public function createNewAccount(\ilAuthStatus $status) 215 { 216 if ( 217 0 === strlen($this->getCredentials()->getUsername()) || 218 !is_array(ilSession::get('tmp_attributes')) || 219 0 === count(ilSession::get('tmp_attributes')) 220 ) { 221 $this->getLogger()->warning('Cannot find user id for external account: ' . $this->getCredentials()->getUsername()); 222 $this->handleAuthenticationFail($status, 'err_wrong_login'); 223 return false; 224 } 225 226 $this->uid = $this->getCredentials()->getUsername(); 227 $this->attributes = ilSession::get('tmp_attributes'); 228 $this->return_to = ilSession::get('tmp_return_to'); 229 230 $this->force_new_account = true; 231 return $this->handleSamlAuth($status); 232 } 233 234 /** 235 * Set external account name 236 * @param string $a_name 237 */ 238 public function setExternalAccountName($a_name) 239 { 240 $this->migration_account = $a_name; 241 } 242 243 /** 244 * @inheritdoc 245 */ 246 public function getExternalAccountName() 247 { 248 return $this->migration_account; 249 } 250 251 /** 252 * @inheritdoc 253 */ 254 public function getTriggerAuthMode() 255 { 256 return AUTH_SAML . '_' . $this->idp->getIdpId(); 257 } 258 259 /** 260 * @inheritdoc 261 */ 262 public function getUserAuthModeName() 263 { 264 return 'saml_' . $this->idp->getIdpId(); 265 } 266 267 /** 268 * @param string|null $a_internal_login 269 * @param string $a_external_account 270 * @param array $a_user_data 271 * @return string 272 */ 273 public function importUser($a_internal_login, $a_external_account, $a_user_data = array()) 274 { 275 $mapping = new ilExternalAuthUserAttributeMapping('saml', $this->idp->getIdpId()); 276 277 $xml_writer = new ilXmlWriter(); 278 $xml_writer->xmlStartTag('Users'); 279 if (null === $a_internal_login) { 280 $login = $a_user_data[$this->idp->getLoginClaim()][0]; 281 $login = ilAuthUtils::_generateLogin($login); 282 283 $xml_writer->xmlStartTag('User', array('Action' => 'Insert')); 284 $xml_writer->xmlElement('Login', array(), $login); 285 286 $xml_writer->xmlElement('Role', array( 287 'Id' => $this->idp->getDefaultRoleId(), 288 'Type' => 'Global', 289 'Action' => 'Assign' 290 )); 291 292 $xml_writer->xmlElement('Active', array(), "true"); 293 $xml_writer->xmlElement('TimeLimitOwner', array(), USER_FOLDER_ID); 294 $xml_writer->xmlElement('TimeLimitUnlimited', array(), 1); 295 $xml_writer->xmlElement('TimeLimitFrom', array(), time()); 296 $xml_writer->xmlElement('TimeLimitUntil', array(), time()); 297 $xml_writer->xmlElement('AuthMode', array('type' => $this->getUserAuthModeName()), $this->getUserAuthModeName()); 298 $xml_writer->xmlElement('ExternalAccount', array(), $a_external_account); 299 300 $mapping = new ilExternalAuthUserCreationAttributeMappingFilter($mapping); 301 } else { 302 $login = $a_internal_login; 303 $usr_id = ilObjUser::_lookupId($a_internal_login); 304 305 $xml_writer->xmlStartTag('User', array('Action' => 'Update', 'Id' => $usr_id)); 306 307 $loginClaim = $a_user_data[$this->idp->getLoginClaim()][0]; 308 if ($login != $loginClaim) { 309 $login = ilAuthUtils::_generateLogin($loginClaim); 310 $xml_writer->xmlElement('Login', array(), $login); 311 } 312 313 $mapping = new ilExternalAuthUserUpdateAttributeMappingFilter($mapping); 314 } 315 316 foreach ($mapping as $rule) { 317 try { 318 $attributeValueParser = new ilSamlMappedUserAttributeValueParser($rule, $a_user_data); 319 $value = $attributeValueParser->parse(); 320 $this->buildUserAttributeXml($xml_writer, $rule, $value); 321 } catch (\ilSamlException $e) { 322 $this->getLogger()->warning($e->getMessage()); 323 continue; 324 } 325 } 326 327 $xml_writer->xmlEndTag('User'); 328 $xml_writer->xmlEndTag('Users'); 329 330 ilLoggerFactory::getLogger('auth')->debug(sprintf('Started import of user "%s" with ext_account "%s" and auth_mode "%s".', $login, $a_external_account, $this->getUserAuthModeName())); 331 include_once './Services/User/classes/class.ilUserImportParser.php'; 332 $importParser = new ilUserImportParser(); 333 $importParser->setXMLContent($xml_writer->xmlDumpMem(false)); 334 $importParser->setRoleAssignment(array( 335 $this->idp->getDefaultRoleId() => $this->idp->getDefaultRoleId() 336 )); 337 $importParser->setFolderId(USER_FOLDER_ID); 338 $importParser->setUserMappingMode(IL_USER_MAPPING_ID); 339 $importParser->startParsing(); 340 341 return $login; 342 } 343 344 /** 345 * @param \ilXmlWriter $xml_writer 346 * @param \ilExternalAuthUserAttributeMappingRule $rule 347 * @param $value 348 */ 349 protected function buildUserAttributeXml(\ilXmlWriter $xml_writer, \ilExternalAuthUserAttributeMappingRule $rule, $value) 350 { 351 switch (strtolower($rule->getAttribute())) { 352 case 'gender': 353 switch (strtolower($value)) { 354 case 'n': 355 case 'neutral': 356 $xml_writer->xmlElement('Gender', array(), 'n'); 357 break; 358 359 case 'm': 360 case 'male': 361 $xml_writer->xmlElement('Gender', array(), 'm'); 362 break; 363 364 case 'f': 365 case 'female': 366 default: 367 $xml_writer->xmlElement('Gender', array(), 'f'); 368 break; 369 } 370 break; 371 372 case 'firstname': 373 $xml_writer->xmlElement('Firstname', array(), $value); 374 break; 375 376 case 'lastname': 377 $xml_writer->xmlElement('Lastname', array(), $value); 378 break; 379 380 case 'email': 381 $xml_writer->xmlElement('Email', array(), $value); 382 break; 383 384 case 'institution': 385 $xml_writer->xmlElement('Institution', array(), $value); 386 break; 387 388 case 'department': 389 $xml_writer->xmlElement('Department', array(), $value); 390 break; 391 392 case 'hobby': 393 $xml_writer->xmlElement('Hobby', array(), $value); 394 break; 395 396 case 'title': 397 $xml_writer->xmlElement('Title', array(), $value); 398 break; 399 400 case 'street': 401 $xml_writer->xmlElement('Street', array(), $value); 402 break; 403 404 case 'city': 405 $xml_writer->xmlElement('City', array(), $value); 406 break; 407 408 case 'zipcode': 409 $xml_writer->xmlElement('PostalCode', array(), $value); 410 break; 411 412 case 'country': 413 $xml_writer->xmlElement('Country', array(), $value); 414 break; 415 416 case 'phone_office': 417 $xml_writer->xmlElement('PhoneOffice', array(), $value); 418 break; 419 420 case 'phone_home': 421 $xml_writer->xmlElement('PhoneHome', array(), $value); 422 break; 423 424 case 'phone_mobile': 425 $xml_writer->xmlElement('PhoneMobile', array(), $value); 426 break; 427 428 case 'fax': 429 $xml_writer->xmlElement('Fax', array(), $value); 430 break; 431 432 case 'referral_comment': 433 $xml_writer->xmlElement('Comment', array(), $value); 434 break; 435 436 case 'matriculation': 437 $xml_writer->xmlElement('Matriculation', array(), $value); 438 break; 439 440 case 'birthday': 441 $xml_writer->xmlElement('Birthday', array(), $value); 442 break; 443 444 default: 445 if (substr($rule->getAttribute(), 0, 4) != 'udf_') { 446 break; 447 } 448 449 $udf_data = explode('_', $rule->getAttribute()); 450 if (!isset($udf_data[1])) { 451 break; 452 } 453 454 $definition = ilUserDefinedFields::_getInstance()->getDefinition($udf_data[1]); 455 $xml_writer->xmlElement( 456 'UserDefinedField', 457 array('Id' => $definition['il_id'], 'Name' => $definition['field_name']), 458 $value 459 ); 460 break; 461 } 462 } 463} 464