Passed
Push — 1.11.x ( 5cb974...91a93e )
by Angel Fernando Quiroz
09:31 queued 14s
created

OAuth2::getUserInfo()   C

Complexity

Conditions 14
Paths 16

Size

Total Lines 111
Code Lines 81

Duplication

Lines 0
Ratio 0 %

Importance

Changes 6
Bugs 0 Features 0
Metric Value
eloc 81
c 6
b 0
f 0
dl 0
loc 111
rs 5.4278
cc 14
nc 16
nop 2

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/* For license terms, see /license.txt */
3
4
use League\OAuth2\Client\Provider\AbstractProvider;
5
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
6
use League\OAuth2\Client\Provider\GenericProvider;
7
use League\OAuth2\Client\Token\AccessToken;
8
use League\OAuth2\Client\Tool\ArrayAccessorTrait;
9
10
/**
11
 * OAuth2 plugin class.
12
 *
13
 * @author Sébastien Ducoulombier <[email protected]>
14
 * inspired by AzureActiveDirectory plugin class from Angel Fernando Quiroz Campos <[email protected]>
15
 *
16
 * @package chamilo.plugin.oauth2
17
 */
18
class OAuth2 extends Plugin
19
{
20
    use ArrayAccessorTrait;
21
22
    public const SETTING_ENABLE = 'enable';
23
24
    public const SETTING_FORCE_REDIRECT = 'force_redirect';
25
    public const SETTING_SKIP_FORCE_REDIRECT_IN = 'skip_force_redirect_in';
26
27
    public const SETTING_CLIENT_ID = 'client_id';
28
    public const SETTING_CLIENT_SECRET = 'client_secret';
29
30
    public const SETTING_AUTHORIZE_URL = 'authorize_url';
31
    public const SETTING_SCOPES = 'scopes';
32
    public const SETTING_SCOPE_SEPARATOR = 'scope_separator';
33
34
    public const SETTING_ACCESS_TOKEN_URL = 'access_token_url';
35
    public const SETTING_ACCESS_TOKEN_METHOD = 'access_token_method';
36
    // const SETTING_ACCESS_TOKEN_RESOURCE_OWNER_ID = 'access_token_resource_owner_id';
37
38
    public const SETTING_RESOURCE_OWNER_DETAILS_URL = 'resource_owner_details_url';
39
40
    public const SETTING_RESPONSE_ERROR = 'response_error';
41
    public const SETTING_RESPONSE_CODE = 'response_code';
42
    public const SETTING_RESPONSE_RESOURCE_OWNER_ID = 'response_resource_owner_id';
43
44
    public const SETTING_UPDATE_USER_INFO = 'update_user_info';
45
    public const SETTING_CREATE_NEW_USERS = 'create_new_users';
46
    public const SETTING_RESPONSE_RESOURCE_OWNER_FIRSTNAME = 'response_resource_owner_firstname';
47
    public const SETTING_RESPONSE_RESOURCE_OWNER_LASTNAME = 'response_resource_owner_lastname';
48
    public const SETTING_RESPONSE_RESOURCE_OWNER_STATUS = 'response_resource_owner_status';
49
    public const SETTING_RESPONSE_RESOURCE_OWNER_EMAIL = 'response_resource_owner_email';
50
    public const SETTING_RESPONSE_RESOURCE_OWNER_USERNAME = 'response_resource_owner_username';
51
52
    public const SETTING_RESPONSE_RESOURCE_OWNER_URLS = 'response_resource_owner_urls';
53
54
    public const SETTING_LOGOUT_URL = 'logout_url';
55
56
    public const SETTING_BLOCK_NAME = 'block_name';
57
58
    public const SETTING_MANAGEMENT_LOGIN_ENABLE = 'management_login_enable';
59
    public const SETTING_MANAGEMENT_LOGIN_NAME = 'management_login_name';
60
61
    public const SETTING_ALLOW_THIRD_PARTY_LOGIN = 'allow_third_party_login';
62
63
    public const EXTRA_FIELD_OAUTH2_ID = 'oauth2_id';
64
65
    private const DEBUG = false;
66
67
    protected function __construct()
68
    {
69
        parent::__construct(
70
            '0.1',
71
            'Sébastien Ducoulombier',
72
            [
73
                self::SETTING_ENABLE => 'boolean',
74
75
                self::SETTING_FORCE_REDIRECT => 'boolean',
76
                self::SETTING_SKIP_FORCE_REDIRECT_IN => 'text',
77
78
                self::SETTING_CLIENT_ID => 'text',
79
                self::SETTING_CLIENT_SECRET => 'text',
80
81
                self::SETTING_AUTHORIZE_URL => 'text',
82
                self::SETTING_SCOPES => 'text',
83
                self::SETTING_SCOPE_SEPARATOR => 'text',
84
85
                self::SETTING_ACCESS_TOKEN_URL => 'text',
86
                self::SETTING_ACCESS_TOKEN_METHOD => [
87
                    'type' => 'select',
88
                    'options' => [
89
                        AbstractProvider::METHOD_POST => 'POST',
90
                        AbstractProvider::METHOD_GET => 'GET',
91
                    ],
92
                ],
93
                // self::SETTING_ACCESS_TOKEN_RESOURCE_OWNER_ID => 'text',
94
95
                self::SETTING_RESOURCE_OWNER_DETAILS_URL => 'text',
96
97
                self::SETTING_RESPONSE_ERROR => 'text',
98
                self::SETTING_RESPONSE_CODE => 'text',
99
                self::SETTING_RESPONSE_RESOURCE_OWNER_ID => 'text',
100
101
                self::SETTING_UPDATE_USER_INFO => 'boolean',
102
                self::SETTING_CREATE_NEW_USERS => 'boolean',
103
                self::SETTING_RESPONSE_RESOURCE_OWNER_FIRSTNAME => 'text',
104
                self::SETTING_RESPONSE_RESOURCE_OWNER_LASTNAME => 'text',
105
                self::SETTING_RESPONSE_RESOURCE_OWNER_STATUS => 'text',
106
                self::SETTING_RESPONSE_RESOURCE_OWNER_EMAIL => 'text',
107
                self::SETTING_RESPONSE_RESOURCE_OWNER_USERNAME => 'text',
108
109
                self::SETTING_RESPONSE_RESOURCE_OWNER_URLS => 'text',
110
111
                self::SETTING_LOGOUT_URL => 'text',
112
113
                self::SETTING_BLOCK_NAME => 'text',
114
115
                self::SETTING_MANAGEMENT_LOGIN_ENABLE => 'boolean',
116
                self::SETTING_MANAGEMENT_LOGIN_NAME => 'text',
117
118
                self::SETTING_ALLOW_THIRD_PARTY_LOGIN => 'boolean',
119
            ]
120
        );
121
    }
122
123
    /**
124
     * Instance the plugin.
125
     *
126
     * @staticvar null $result
127
     *
128
     * @return $this
129
     */
130
    public static function create(): OAuth2
131
    {
132
        static $result = null;
133
134
        return $result ?: $result = new self();
135
    }
136
137
    public function getProvider(): GenericProvider
138
    {
139
        $options = [
140
            'clientId' => $this->get(self::SETTING_CLIENT_ID),
141
            'clientSecret' => $this->get(self::SETTING_CLIENT_SECRET),
142
            'redirectUri' => api_get_path(WEB_PLUGIN_PATH).'oauth2/src/callback.php',
143
            'urlAuthorize' => $this->get(self::SETTING_AUTHORIZE_URL),
144
            'urlResourceOwnerDetails' => $this->get(self::SETTING_RESOURCE_OWNER_DETAILS_URL),
145
        ];
146
147
        if ('' === $scopeSeparator = (string) $this->get(self::SETTING_SCOPE_SEPARATOR)) {
148
            $scopeSeparator = ' ';
149
        }
150
151
        $options['scopeSeparator'] = $scopeSeparator;
152
153
        if ('' !== $scopes = (string) $this->get(self::SETTING_SCOPES)) {
154
            $options['scopes'] = explode($scopeSeparator, $scopes);
155
        }
156
157
        if ('' !== $urlAccessToken = (string) $this->get(self::SETTING_ACCESS_TOKEN_URL)) {
158
            $options['urlAccessToken'] = $urlAccessToken;
159
        }
160
161
        if ('' !== $accessTokenMethod = (string) $this->get(self::SETTING_ACCESS_TOKEN_METHOD)) {
162
            $options['accessTokenMethod'] = $accessTokenMethod;
163
        }
164
165
//        if ('' !== $accessTokenResourceOwnerId = (string) $this->get(self::SETTING_ACCESS_TOKEN_RESOURCE_OWNER_ID)) {
166
//            $options['accessTokenResourceOwnerId'] = $accessTokenResourceOwnerId;
167
//        }
168
169
        if ('' !== $responseError = (string) $this->get(self::SETTING_RESPONSE_ERROR)) {
170
            $options['responseError'] = $responseError;
171
        }
172
173
        if ('' !== $responseCode = (string) $this->get(self::SETTING_RESPONSE_CODE)) {
174
            $options['responseCode'] = $responseCode;
175
        }
176
177
        if ('' !== $responseResourceOwnerId = (string) $this->get(self::SETTING_RESPONSE_RESOURCE_OWNER_ID)) {
178
            $options['responseResourceOwnerId'] = $responseResourceOwnerId;
179
        }
180
181
        return new GenericProvider($options);
182
    }
183
184
    /**
185
     * @throws IdentityProviderException
186
     *
187
     * @return array user information, as returned by api_get_user_info(userId)
188
     */
189
    public function getUserInfo(GenericProvider $provider, AccessToken $accessToken): array
190
    {
191
        $url = $provider->getResourceOwnerDetailsUrl($accessToken);
192
        $request = $provider->getAuthenticatedRequest($provider::METHOD_GET, $url, $accessToken);
193
        $response = $provider->getParsedResponse($request);
194
        $this->log('response', print_r($response, true));
195
196
        if (false === is_array($response)) {
197
            $this->log('invalid response', print_r($response, true));
198
            throw new UnexpectedValueException($this->get_lang('InvalidJsonReceivedFromProvider'));
199
        }
200
        $resourceOwnerId = $this->getValueByKey(
201
            $response,
202
            $this->get(self::SETTING_RESPONSE_RESOURCE_OWNER_ID)
203
        );
204
        if (empty($resourceOwnerId)) {
205
            $this->log('missing setting', 'response_resource_owner_id');
206
            throw new RuntimeException($this->get_lang('WrongResponseResourceOwnerId'));
207
        }
208
        $extraFieldValue = new ExtraFieldValue('user');
209
        $result = $extraFieldValue->get_item_id_from_field_variable_and_field_value(
210
            self::EXTRA_FIELD_OAUTH2_ID,
211
            $resourceOwnerId
212
        );
213
        if (false === $result) {
214
            $this->log('user not found', "extrafield 'oauth2_id' with value '$resourceOwnerId'");
215
            // authenticated user not found in internal database
216
            if ('true' !== $this->get(self::SETTING_CREATE_NEW_USERS)) {
217
                $this->log('exception', 'create_new_users setting is disabled');
218
                $message = sprintf(
219
                    $this->get_lang('NoUserAccountAndUserCreationNotAllowed'),
220
                    Display::encrypted_mailto_link(api_get_setting('emailAdministrator'))
221
                );
222
                throw new RuntimeException($message);
223
            }
224
225
            $firstName = $this->getValueByKey(
226
                $response,
227
                $this->get(self::SETTING_RESPONSE_RESOURCE_OWNER_FIRSTNAME),
228
                $this->get_lang('DefaultFirstname')
229
            );
230
            $lastName = $this->getValueByKey(
231
                $response,
232
                $this->get(self::SETTING_RESPONSE_RESOURCE_OWNER_LASTNAME),
233
                $this->get_lang('DefaultLastname')
234
            );
235
            $status = $this->getValueByKey(
236
                $response,
237
                $this->get(self::SETTING_RESPONSE_RESOURCE_OWNER_STATUS),
238
                STUDENT
239
            );
240
            $email = $this->getValueByKey(
241
                $response,
242
                $this->get(self::SETTING_RESPONSE_RESOURCE_OWNER_EMAIL),
243
                'oauth2user_'.$resourceOwnerId.'@'.(gethostname() or 'localhost')
244
            );
245
            $username = $this->getValueByKey(
246
                $response,
247
                $this->get(self::SETTING_RESPONSE_RESOURCE_OWNER_USERNAME),
248
                'oauth2user_'.$resourceOwnerId
249
            );
250
251
            $userInfo = api_get_user_info_from_username($username);
252
253
            if (false !== $userInfo && !empty($userInfo['id']) && 'platform' === $userInfo['auth_source']) {
254
                $this->log('platform user exists', print_r($userInfo, true));
255
                $userId = $userInfo['id'];
256
            } else {
257
                require_once __DIR__.'/../../../main/auth/external_login/functions.inc.php';
258
259
                $userInfo = [
260
                    'firstname' => $firstName,
261
                    'lastname' => $lastName,
262
                    'status' => $status,
263
                    'email' => $email,
264
                    'username' => $username,
265
                    'auth_source' => 'oauth2',
266
                ];
267
                $userId = external_add_user($userInfo);
268
                if (false === $userId) {
269
                    $this->log('user not created', print_r($userInfo, true));
270
                    throw new RuntimeException($this->get_lang('FailedUserCreation'));
271
                }
272
                $this->log('user created', (string) $userId);
273
            }
274
            $this->updateUser($userId, $response);
275
            // Not checking function update_extra_field_value return value because not reliable
276
            UserManager::update_extra_field_value($userId, self::EXTRA_FIELD_OAUTH2_ID, $resourceOwnerId);
277
            $this->updateUserUrls($userId, $response);
278
        } else {
279
            $this->log('user found', "extrafield 'oauth2_id' with value '$resourceOwnerId'");
280
            // authenticated user found in internal database
281
            if (is_array($result) and array_key_exists('item_id', $result)) {
282
                $userId = $result['item_id'];
283
            } else {
284
                $userId = $result;
285
            }
286
            if ('true' === $this->get(self::SETTING_UPDATE_USER_INFO)) {
287
                $this->updateUser($userId, $response);
288
                $this->updateUserUrls($userId, $response);
289
            }
290
        }
291
        $userInfo = api_get_user_info($userId);
292
        if (empty($userInfo)) {
293
            $this->log('user info not found', (string) $userId);
294
            throw new LogicException($this->get_lang('InternalErrorCannotGetUserInfo'));
295
        }
296
297
        $this->log('user info', print_r($userInfo, true));
298
299
        return $userInfo;
300
    }
301
302
    public function getSignInURL(): string
303
    {
304
        return api_get_path(WEB_PLUGIN_PATH).$this->get_name().'/src/callback.php';
305
    }
306
307
    public function getLogoutUrl(): string
308
    {
309
        $token = ChamiloSession::read('oauth2AccessToken');
310
        $idToken = !empty($token['id_token']) ? $token['id_token'] : null;
311
312
        return $this->get(self::SETTING_LOGOUT_URL).'?'.http_build_query(
313
            [
314
                'id_token_hint' => $idToken,
315
                'post_logout_redirect_uri' => api_get_path(WEB_PATH),
316
            ]
317
        );
318
    }
319
320
    /**
321
     * Create extra fields for user when installing.
322
     */
323
    public function install()
324
    {
325
        UserManager::create_extra_field(
326
            self::EXTRA_FIELD_OAUTH2_ID,
327
            ExtraField::FIELD_TYPE_TEXT,
328
            $this->get_lang('OAuth2Id'),
329
            ''
330
        );
331
    }
332
333
    /**
334
     * Extends ArrayAccessorTrait::getValueByKey to return a list of values
335
     * $key can contain wild card character *
336
     * It will be replaced by 0, 1, 2 and so on as long as the resulting key exists in $data
337
     * This is a recursive function, allowing for more than one occurrence of the wild card character.
338
     */
339
    private function getValuesByKey(array $data, string $key, array $default = []): array
340
    {
341
        if (!is_string($key) || empty($key) || !count($data)) {
342
            return $default;
343
        }
344
        $pos = strpos($key, '*');
345
        if ($pos === false) {
346
            $value = $this->getValueByKey($data, $key);
347
348
            return is_null($value) ? [] : [$value];
349
        }
350
        $values = [];
351
        $beginning = substr($key, 0, $pos);
352
        $remaining = substr($key, $pos + 1);
353
        $index = 0;
354
        do {
355
            $newValues = $this->getValuesByKey(
356
                $data,
357
                $beginning.$index.$remaining
358
            );
359
            $values = array_merge($values, $newValues);
360
            $index++;
361
        } while ($newValues);
362
363
        return $values;
364
    }
365
366
    private function updateUser($userId, $response)
367
    {
368
        $user = UserManager::getRepository()->find($userId);
369
        $user->setFirstname(
370
            $this->getValueByKey(
371
                $response,
372
                $this->get(
373
                    self::SETTING_RESPONSE_RESOURCE_OWNER_FIRSTNAME
374
                ),
375
                $user->getFirstname()
376
            )
377
        );
378
        $user->setLastname(
379
            $this->getValueByKey(
380
                $response,
381
                $this->get(self::SETTING_RESPONSE_RESOURCE_OWNER_LASTNAME),
382
                $user->getLastname()
383
            )
384
        );
385
        $user->setUserName(
386
            $this->getValueByKey(
387
                $response,
388
                $this->get(self::SETTING_RESPONSE_RESOURCE_OWNER_USERNAME),
389
                $user->getUsername()
390
            )
391
        );
392
        $user->setEmail(
393
            $this->getValueByKey(
394
                $response,
395
                $this->get(self::SETTING_RESPONSE_RESOURCE_OWNER_EMAIL),
396
                $user->getEmail()
397
            )
398
        );
399
        $user->setStatus(
400
            $this->getValueByKey(
401
                $response,
402
                $this->get(self::SETTING_RESPONSE_RESOURCE_OWNER_STATUS),
403
                $user->getStatus()
404
            )
405
        );
406
        $user->setAuthSource('oauth2');
407
        $configFilePath = __DIR__.'/../config.php';
408
        if (file_exists($configFilePath)) {
409
            require_once $configFilePath;
410
            $functionName = 'oauth2UpdateUserFromResourceOwnerDetails';
411
            if (function_exists($functionName)) {
412
                $functionName($response, $user);
413
            }
414
        }
415
        UserManager::getManager()->updateUser($user);
416
    }
417
418
    /**
419
     * Updates the Access URLs associated to a user
420
     * according to the OAuth2 server response resource owner
421
     * if multi-URL is enabled and SETTING_RESPONSE_RESOURCE_OWNER_URLS defined.
422
     *
423
     * @param $userId integer
424
     * @param $response array
425
     */
426
    private function updateUserUrls($userId, $response)
427
    {
428
        if (api_is_multiple_url_enabled()) {
429
            $key = (string) $this->get(self::SETTING_RESPONSE_RESOURCE_OWNER_URLS);
430
            if (!empty($key)) {
431
                $availableUrls = [];
432
                foreach (UrlManager::get_url_data() as $existingUrl) {
433
                    $urlId = $existingUrl['id'];
434
                    $availableUrls[strval($urlId)] = $urlId;
435
                    $availableUrls[$existingUrl['url']] = $urlId;
436
                }
437
                $allowedUrlIds = [];
438
                foreach ($this->getValuesByKey($response, $key) as $value) {
439
                    if (array_key_exists($value, $availableUrls)) {
440
                        $allowedUrlIds[] = $availableUrls[$value];
441
                    } else {
442
                        $newValue = ($value[-1] === '/') ? substr($value, 0, -1) : $value.'/';
443
                        if (array_key_exists($newValue, $availableUrls)) {
444
                            $allowedUrlIds[] = $availableUrls[$newValue];
445
                        }
446
                    }
447
                }
448
                $grantedUrlIds = [];
449
                foreach (UrlManager::get_access_url_from_user($userId) as $grantedUrl) {
450
                    $grantedUrlIds[] = $grantedUrl['access_url_id'];
451
                }
452
                foreach (array_diff($grantedUrlIds, $allowedUrlIds) as $extraUrlId) {
453
                    UrlManager::delete_url_rel_user($userId, $extraUrlId);
454
                }
455
                foreach (array_diff($allowedUrlIds, $grantedUrlIds) as $missingUrlId) {
456
                    UrlManager::add_user_to_url($userId, $missingUrlId);
457
                }
458
            }
459
        }
460
    }
461
462
    private function log(string $key, string $content)
463
    {
464
        if (self::DEBUG) {
465
            error_log("OAuth2 plugin: $key: $content");
466
        }
467
    }
468
}
469