ILIAS  release_10 Revision v10.1-43-ga1241a92c2f
class.ilAuthProviderSaml.php
Go to the documentation of this file.
1 <?php
2 
19 declare(strict_types=1);
20 
25 {
26  private const LOG_COMPONENT = 'auth';
27 
28  private const ERR_WRONG_LOGIN = 'err_wrong_login';
29 
30  private const SESSION_TMP_ATTRIBUTES = 'tmp_attributes';
31  private const SESSION_TMP_RETURN_TO = 'tmp_return_to';
32 
33  private ilSamlIdp $idp;
34  private readonly ilLanguage $lng;
36  private array $attributes = [];
37  private string $return_to = '';
38  private string $uid = '';
39  private bool $force_new_account = false;
40  private string $migration_account = '';
41 
42  public function __construct(ilAuthCredentials $credentials, ?int $a_idp_id = null)
43  {
44  global $DIC;
45 
46  parent::__construct($credentials);
47 
48  $this->lng = $DIC->language();
49 
50  if (null === $a_idp_id || 0 === $a_idp_id) {
51  $this->idp = ilSamlIdp::getFirstActiveIdp();
52  } else {
53  $this->idp = ilSamlIdp::getInstanceByIdpId($a_idp_id);
54  }
55 
56  if ($credentials instanceof ilAuthFrontendCredentialsSaml) {
57  $this->attributes = $credentials->getAttributes();
58  $this->return_to = $credentials->getReturnTo();
59  }
60  }
61 
62  private function determineUidFromAttributes(): void
63  {
64  if (
65  !array_key_exists($this->idp->getUidClaim(), $this->attributes) ||
66  !is_array($this->attributes[$this->idp->getUidClaim()]) ||
67  !array_key_exists(0, $this->attributes[$this->idp->getUidClaim()]) ||
68  $this->attributes[$this->idp->getUidClaim()][0] === ''
69  ) {
70  throw new ilException(sprintf(
71  'Could not find unique SAML attribute for the configured identifier: %s',
72  print_r($this->idp->getUidClaim(), true)
73  ));
74  }
75 
76  $this->uid = $this->attributes[$this->idp->getUidClaim()][0];
77  }
78 
79  public function doAuthentication(ilAuthStatus $status): bool
80  {
81  if ([] === $this->attributes) {
82  $this->getLogger()->warning('Could not parse any attributes from SAML response.');
83  $this->handleAuthenticationFail($status, self::ERR_WRONG_LOGIN);
84 
85  return false;
86  }
87 
88  try {
90 
91  return $this->handleSamlAuth($status);
92  } catch (Exception $e) {
93  $this->getLogger()->warning($e->getMessage());
94  $this->handleAuthenticationFail($status, self::ERR_WRONG_LOGIN);
95 
96  return false;
97  }
98  }
99 
100  private function handleSamlAuth(ilAuthStatus $status): bool
101  {
102  $update_auth_mode = false;
103 
104  ilLoggerFactory::getLogger(self::LOG_COMPONENT)->debug(sprintf(
105  'Login observer called for SAML authentication request of ext_account "%s" and auth_mode "%s".',
106  $this->uid,
107  $this->getUserAuthModeName()
108  ));
109  ilLoggerFactory::getLogger(self::LOG_COMPONENT)->debug(sprintf('Target set to: %s', print_r($this->return_to, true)));
110  ilLoggerFactory::getLogger(self::LOG_COMPONENT)->debug(sprintf(
111  'Trying to find ext_account "%s" for auth_mode "%s".',
112  $this->uid,
113  $this->getUserAuthModeName()
114  ));
115 
116  $internal_account = ilObjUser::_checkExternalAuthAccount(
117  $this->getUserAuthModeName(),
118  $this->uid,
119  false
120  );
121 
122  if (!is_string($internal_account) || $internal_account === '') {
123  $update_auth_mode = true;
124 
125  ilLoggerFactory::getLogger(self::LOG_COMPONENT)->debug(sprintf(
126  'Could not find ext_account "%s" for auth_mode "%s".',
127  $this->uid,
128  $this->getUserAuthModeName()
129  ));
130 
131  $fallback_auth_mode = 'local';
132  ilLoggerFactory::getLogger(self::LOG_COMPONENT)->debug(sprintf(
133  'Trying to find ext_account "%s" for auth_mode "%s".',
134  $this->uid,
135  $fallback_auth_mode
136  ));
137  $internal_account = ilObjUser::_checkExternalAuthAccount($fallback_auth_mode, $this->uid, false);
138 
139  $defaultAuth = ilAuthUtils::AUTH_LOCAL;
140  if ($GLOBALS['DIC']['ilSetting']->get('auth_mode')) {
141  $defaultAuth = $GLOBALS['DIC']['ilSetting']->get('auth_mode');
142  }
143 
144  if (
145  (!is_string($internal_account) || $internal_account === '') &&
146  ($defaultAuth == ilAuthUtils::AUTH_LOCAL || $defaultAuth == $this->getTriggerAuthMode())
147  ) {
148  ilLoggerFactory::getLogger(self::LOG_COMPONENT)->debug(sprintf(
149  'Could not find ext_account "%s" for auth_mode "%s".',
150  $this->uid,
151  $fallback_auth_mode
152  ));
153 
154  $fallback_auth_mode = 'default';
155  ilLoggerFactory::getLogger(self::LOG_COMPONENT)->debug(sprintf(
156  'Trying to find ext_account "%s" for auth_mode "%s".',
157  $this->uid,
158  $fallback_auth_mode
159  ));
160  $internal_account = ilObjUser::_checkExternalAuthAccount($fallback_auth_mode, $this->uid, false);
161  }
162  }
163 
164  if (is_string($internal_account) && $internal_account !== '') {
165  ilLoggerFactory::getLogger(self::LOG_COMPONENT)->debug(sprintf(
166  'Found user "%s" for ext_account "%s" in ILIAS database.',
167  $internal_account,
168  $this->uid
169  ));
170 
171  if ($this->idp->isSynchronizationEnabled()) {
172  ilLoggerFactory::getLogger(self::LOG_COMPONENT)->debug(sprintf(
173  'SAML user synchronisation is enabled, so update existing user "%s" with ext_account "%s".',
174  $internal_account,
175  $this->uid
176  ));
177  $internal_account = $this->importUser($internal_account, $this->uid, $this->attributes);
178  }
179 
180  if ($update_auth_mode) {
181  $usr_id = ilObjUser::_loginExists($internal_account);
182  if ($usr_id > 0) {
184  ilLoggerFactory::getLogger(self::LOG_COMPONENT)->debug(sprintf(
185  'SAML Switched auth_mode of user with login "%s" and ext_account "%s" to "%s".',
186  $internal_account,
187  $this->uid,
188  $this->getUserAuthModeName()
189  ));
190  } else {
191  ilLoggerFactory::getLogger(self::LOG_COMPONENT)->debug(sprintf(
192  'SAML Could not switch auth_mode of user with login "%s" and ext_account "%s" to "%s".',
193  $internal_account,
194  $this->uid,
195  $this->getUserAuthModeName()
196  ));
197  }
198  }
199 
200  ilLoggerFactory::getLogger(self::LOG_COMPONENT)->debug(sprintf(
201  'Authentication succeeded: Found internal login "%s for ext_account "%s" and auth_mode "%s".',
202  $internal_account,
203  $this->uid,
204  $this->getUserAuthModeName()
205  ));
206 
208  $status->setAuthenticatedUserId(ilObjUser::_lookupId($internal_account));
209  ilSession::set('used_external_auth_mode', $this->getTriggerAuthMode());
210 
211  return true;
212  }
213 
214  ilLoggerFactory::getLogger(self::LOG_COMPONENT)->debug(sprintf(
215  'Could not find an existing user for ext_account "%s" for any relevant auth_mode.',
216  $this->uid
217  ));
218  if ($this->idp->isSynchronizationEnabled()) {
219  ilLoggerFactory::getLogger(self::LOG_COMPONENT)->debug(sprintf(
220  'SAML user synchronisation is enabled, so determine action for ext_account "%s" and auth_mode "%s".',
221  $this->uid,
222  $this->getUserAuthModeName()
223  ));
224  if (!$this->force_new_account && $this->idp->isAccountMigrationEnabled()) {
225  ilSession::set(self::SESSION_TMP_ATTRIBUTES, $this->attributes);
226  ilSession::set(self::SESSION_TMP_RETURN_TO, $this->return_to);
227 
228  ilLoggerFactory::getLogger(self::LOG_COMPONENT)->debug(sprintf(
229  'Account migration is enabled, so redirecting ext_account "%s" to account migration screen.',
230  $this->uid
231  ));
232 
233  $this->setExternalAccountName($this->uid);
235 
236  return false;
237  }
238 
239  $new_name = $this->importUser(null, $this->uid, $this->attributes);
240  ilLoggerFactory::getLogger(self::LOG_COMPONENT)->debug(sprintf(
241  'Created new user account with login "%s" and ext_account "%s".',
242  $new_name,
243  $this->uid
244  ));
245 
246  ilSession::set(self::SESSION_TMP_ATTRIBUTES, null);
247  ilSession::set(self::SESSION_TMP_RETURN_TO, null);
248  ilSession::set('used_external_auth_mode', $this->getTriggerAuthMode());
249 
251  $status->setAuthenticatedUserId(ilObjUser::_lookupId($new_name));
252 
253  return true;
254  }
255 
256  ilLoggerFactory::getLogger(self::LOG_COMPONENT)->debug("SAML user synchronisation is not enabled, auth failed.");
257  $this->handleAuthenticationFail($status, 'err_auth_saml_no_ilias_user');
258 
259  return false;
260  }
261 
262  public function migrateAccount(ilAuthStatus $status): void
263  {
264  }
265 
266  public function createNewAccount(ilAuthStatus $status): void
267  {
268  if (
269  !is_array(ilSession::get(self::SESSION_TMP_ATTRIBUTES)) ||
270  [] === ilSession::get(self::SESSION_TMP_ATTRIBUTES) ||
271  $this->getCredentials()->getUsername() === ''
272  ) {
273  $this->getLogger()->warning('Cannot find user id for external account: ' . $this->getCredentials()->getUsername());
274  $this->handleAuthenticationFail($status, self::ERR_WRONG_LOGIN);
275  return;
276  }
277 
278  $this->uid = $this->getCredentials()->getUsername();
279  $this->attributes = ilSession::get(self::SESSION_TMP_ATTRIBUTES);
280  $this->return_to = (string) ilSession::get(self::SESSION_TMP_RETURN_TO);
281 
282  $this->force_new_account = true;
283 
284  $this->handleSamlAuth($status);
285  }
286 
287  public function setExternalAccountName(string $a_name): void
288  {
289  $this->migration_account = $a_name;
290  }
291 
292  public function getExternalAccountName(): string
293  {
295  }
296 
297  public function getTriggerAuthMode(): string
298  {
299  return ilAuthUtils::AUTH_SAML . '_' . $this->idp->getIdpId();
300  }
301 
302  public function getUserAuthModeName(): string
303  {
304  return 'saml_' . $this->idp->getIdpId();
305  }
306 
307  private function importUser(?string $a_internal_login, string $a_external_account, array $a_user_data = []): string
308  {
309  $mapping = new ilExternalAuthUserAttributeMapping('saml', $this->idp->getIdpId());
310 
311  $xml_writer = new ilXmlWriter();
312  $xml_writer->xmlStartTag('Users');
313  if (null === $a_internal_login) {
314  $login = $a_user_data[$this->idp->getLoginClaim()][0];
315  $login = ilAuthUtils::_generateLogin($login);
316 
317  $xml_writer->xmlStartTag(
318  'User',
319  [
320  'Action' => 'Insert',
321  'Language' => $this->lng->getDefaultLanguage()
322  ]
323  );
324  $xml_writer->xmlElement('Login', [], $login);
325 
326  $xml_writer->xmlElement('Role', [
327  'Id' => $this->idp->getDefaultRoleId(),
328  'Type' => 'Global',
329  'Action' => 'Assign'
330  ]);
331 
332  $xml_writer->xmlElement('Active', [], "true");
333  $xml_writer->xmlElement('TimeLimitOwner', [], USER_FOLDER_ID);
334  $xml_writer->xmlElement('TimeLimitUnlimited', [], 1);
335  $xml_writer->xmlElement('TimeLimitFrom', [], time());
336  $xml_writer->xmlElement('TimeLimitUntil', [], time());
337  $xml_writer->xmlElement(
338  'AuthMode',
339  ['type' => $this->getUserAuthModeName()],
340  $this->getUserAuthModeName()
341  );
342  $xml_writer->xmlElement('ExternalAccount', [], $a_external_account);
343 
344  $mapping = new ilExternalAuthUserCreationAttributeMappingFilter($mapping);
345  } else {
346  $login = $a_internal_login;
347  $usr_id = ilObjUser::_lookupId($a_internal_login);
348 
349  $xml_writer->xmlStartTag('User', ['Action' => 'Update', 'Id' => $usr_id]);
350 
351  $loginClaim = $a_user_data[$this->idp->getLoginClaim()][0];
352  if (ilStr::strToLower($login) !== ilStr::strToLower($loginClaim)) {
353  $login = ilAuthUtils::_generateLogin($loginClaim);
354  $xml_writer->xmlElement('Login', [], $login);
355  }
356 
357  $mapping = new ilExternalAuthUserUpdateAttributeMappingFilter($mapping);
358  }
359 
360  foreach ($mapping as $rule) {
361  try {
362  $attributeValueParser = new ilSamlMappedUserAttributeValueParser($rule, $a_user_data);
363  $value = $attributeValueParser->parse();
364  $this->buildUserAttributeXml($xml_writer, $rule, $value);
365  } catch (ilSamlException $e) {
366  $this->getLogger()->warning($e->getMessage());
367  continue;
368  }
369  }
370 
371  $xml_writer->xmlEndTag('User');
372  $xml_writer->xmlEndTag('Users');
373 
374  ilLoggerFactory::getLogger(self::LOG_COMPONENT)->debug(sprintf(
375  'Started import of user "%s" with ext_account "%s" and auth_mode "%s".',
376  $login,
377  $a_external_account,
378  $this->getUserAuthModeName()
379  ));
380  $importParser = new ilUserImportParser();
381  $importParser->setXMLContent($xml_writer->xmlDumpMem(false));
382  $importParser->setRoleAssignment([
383  $this->idp->getDefaultRoleId() => $this->idp->getDefaultRoleId(),
384  ]);
385  $importParser->setFolderId(USER_FOLDER_ID);
386  $importParser->setUserMappingMode(ilUserImportParser::IL_USER_MAPPING_ID);
387  $importParser->startParsing();
388 
389  return $login;
390  }
391 
392  private function buildUserAttributeXml(
393  ilXmlWriter $xml_writer,
395  string $value
396  ): void {
397  switch (strtolower($rule->getAttribute())) {
398  case 'gender':
399  $gender_attr = 'Gender';
400  match (strtolower($value)) {
401  'n', 'neutral' => $xml_writer->xmlElement($gender_attr, [], 'n'),
402  'm', 'male' => $xml_writer->xmlElement($gender_attr, [], 'm'),
403  // no break
404  default => $xml_writer->xmlElement($gender_attr, [], 'f'),
405  };
406  break;
407 
408  case 'firstname':
409  $xml_writer->xmlElement('Firstname', [], $value);
410  break;
411 
412  case 'lastname':
413  $xml_writer->xmlElement('Lastname', [], $value);
414  break;
415 
416  case 'email':
417  $xml_writer->xmlElement('Email', [], $value);
418  break;
419 
420  case 'second_email':
421  $xml_writer->xmlElement('SecondEmail', [], $value);
422  break;
423 
424  case 'institution':
425  $xml_writer->xmlElement('Institution', [], $value);
426  break;
427 
428  case 'department':
429  $xml_writer->xmlElement('Department', [], $value);
430  break;
431 
432  case 'hobby':
433  $xml_writer->xmlElement('Hobby', [], $value);
434  break;
435 
436  case 'title':
437  $xml_writer->xmlElement('Title', [], $value);
438  break;
439 
440  case 'street':
441  $xml_writer->xmlElement('Street', [], $value);
442  break;
443 
444  case 'city':
445  $xml_writer->xmlElement('City', [], $value);
446  break;
447 
448  case 'zipcode':
449  $xml_writer->xmlElement('PostalCode', [], $value);
450  break;
451 
452  case 'country':
453  $xml_writer->xmlElement('Country', [], $value);
454  break;
455 
456  case 'phone_office':
457  $xml_writer->xmlElement('PhoneOffice', [], $value);
458  break;
459 
460  case 'phone_home':
461  $xml_writer->xmlElement('PhoneHome', [], $value);
462  break;
463 
464  case 'phone_mobile':
465  $xml_writer->xmlElement('PhoneMobile', [], $value);
466  break;
467 
468  case 'fax':
469  $xml_writer->xmlElement('Fax', [], $value);
470  break;
471 
472  case 'referral_comment':
473  $xml_writer->xmlElement('Comment', [], $value);
474  break;
475 
476  case 'matriculation':
477  $xml_writer->xmlElement('Matriculation', [], $value);
478  break;
479 
480  case 'birthday':
481  $xml_writer->xmlElement('Birthday', [], $value);
482  break;
483 
484  default:
485  if (!str_starts_with($rule->getAttribute(), 'udf_')) {
486  break;
487  }
488 
489  $udf_data = explode('_', $rule->getAttribute());
490  if (!isset($udf_data[1])) {
491  break;
492  }
493 
494  $definition = ilUserDefinedFields::_getInstance()->getDefinition((int) $udf_data[1]);
495  if (empty($definition)) {
496  ilLoggerFactory::getLogger('auth')->warning(sprintf(
497  "Invalid/Orphaned UD field mapping detected: %s",
498  $rule->getAttribute()
499  ));
500  break;
501  }
502 
503  $xml_writer->xmlElement(
504  'UserDefinedField',
505  ['Id' => $definition['il_id'], 'Name' => $definition['field_name']],
506  $value
507  );
508  break;
509  }
510  }
511 }
doAuthentication(ilAuthStatus $status)
static get(string $a_var)
Class ilSamlException.
static _generateLogin(string $a_login)
generate free login by starting with a default string and adding postfix numbers
Interface of auth credentials.
static getLogger(string $a_component_id)
Get component logger.
const USER_FOLDER_ID
Definition: constants.php:33
__construct(ilAuthCredentials $credentials, ?int $a_idp_id=null)
importUser(?string $a_internal_login, string $a_external_account, array $a_user_data=[])
getExternalAccountName()
Get external account name.
static _writeAuthMode(int $a_usr_id, string $a_auth_mode)
static _lookupId($a_user_str)
static _checkExternalAuthAccount(string $a_auth, string $a_account, bool $tryFallback=true)
check whether external account and authentication method matches with a user
static getInstanceByIdpId(int $a_idp_id)
handleAuthenticationFail(ilAuthStatus $status, string $a_reason)
Handle failed authentication.
Base class for authentication providers (ldap, apache, ...)
Class ilAuthFrontendCredentialsSaml.
Class ilExternalAuthUserAttributeMapping.
$GLOBALS["DIC"]
Definition: wac.php:30
readonly ilLanguage $lng
setStatus(int $a_status)
Set auth status.
createNewAccount(ilAuthStatus $status)
Create new ILIAS account for external_account.
static _loginExists(string $a_login, int $a_user_id=0)
check if a login name already exists You may exclude a user from the check by giving his user id as 2...
global $DIC
Definition: shib_login.php:25
migrateAccount(ilAuthStatus $status)
Create new account.
ilAuthCredentials $credentials
getUserAuthModeName()
Get user auth mode name ldap_1 for ldap account migration with server id 1 apache for apache auth...
getTriggerAuthMode()
Get auth mode which triggered the account migration 2_1 for ldap account migration with server id 1 1...
getLogger()
Get logger.
handleSamlAuth(ilAuthStatus $status)
Class ilAuthProviderSaml.
Class ilSamlIdp.
__construct(Container $dic, ilPlugin $plugin)
static strToLower(string $a_string)
Definition: class.ilStr.php:72
buildUserAttributeXml(ilXmlWriter $xml_writer, ilExternalAuthUserAttributeMappingRule $rule, string $value)
setAuthenticatedUserId(int $a_id)
xmlElement(string $tag, $attrs=null, $data=null, $encode=true, $escape=true)
Writes a basic element (no children, just textual content)
const STATUS_ACCOUNT_MIGRATION_REQUIRED
static set(string $a_var, $a_val)
Set a value.
setExternalAccountName(string $a_name)
static getFirstActiveIdp()