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