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