Actualización
This commit is contained in:
563
plugin/oauth2/src/OAuth2.php
Normal file
563
plugin/oauth2/src/OAuth2.php
Normal file
@@ -0,0 +1,563 @@
|
||||
<?php
|
||||
/* For license terms, see /license.txt */
|
||||
|
||||
use Chamilo\CoreBundle\Entity\ExtraFieldValues;
|
||||
use Chamilo\CoreBundle\Entity\TrackELogin;
|
||||
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
|
||||
use League\OAuth2\Client\Provider\AbstractProvider;
|
||||
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
|
||||
use League\OAuth2\Client\Provider\GenericProvider;
|
||||
use League\OAuth2\Client\Token\AccessToken;
|
||||
use League\OAuth2\Client\Tool\ArrayAccessorTrait;
|
||||
|
||||
/**
|
||||
* OAuth2 plugin class.
|
||||
*
|
||||
* @author Sébastien Ducoulombier <seb@ldd.fr>
|
||||
* inspired by AzureActiveDirectory plugin class from Angel Fernando Quiroz Campos <angel.quiroz@beeznest.com>
|
||||
*
|
||||
* @package chamilo.plugin.oauth2
|
||||
*/
|
||||
class OAuth2 extends Plugin
|
||||
{
|
||||
use ArrayAccessorTrait;
|
||||
|
||||
public const SETTING_ENABLE = 'enable';
|
||||
|
||||
public const SETTING_FORCE_REDIRECT = 'force_redirect';
|
||||
public const SETTING_SKIP_FORCE_REDIRECT_IN = 'skip_force_redirect_in';
|
||||
|
||||
public const SETTING_CLIENT_ID = 'client_id';
|
||||
public const SETTING_CLIENT_SECRET = 'client_secret';
|
||||
|
||||
public const SETTING_AUTHORIZE_URL = 'authorize_url';
|
||||
public const SETTING_SCOPES = 'scopes';
|
||||
public const SETTING_SCOPE_SEPARATOR = 'scope_separator';
|
||||
|
||||
public const SETTING_ACCESS_TOKEN_URL = 'access_token_url';
|
||||
public const SETTING_ACCESS_TOKEN_METHOD = 'access_token_method';
|
||||
// const SETTING_ACCESS_TOKEN_RESOURCE_OWNER_ID = 'access_token_resource_owner_id';
|
||||
|
||||
public const SETTING_RESOURCE_OWNER_DETAILS_URL = 'resource_owner_details_url';
|
||||
|
||||
public const SETTING_RESPONSE_ERROR = 'response_error';
|
||||
public const SETTING_RESPONSE_CODE = 'response_code';
|
||||
public const SETTING_RESPONSE_RESOURCE_OWNER_ID = 'response_resource_owner_id';
|
||||
|
||||
public const SETTING_UPDATE_USER_INFO = 'update_user_info';
|
||||
public const SETTING_CREATE_NEW_USERS = 'create_new_users';
|
||||
public const SETTING_RESPONSE_RESOURCE_OWNER_FIRSTNAME = 'response_resource_owner_firstname';
|
||||
public const SETTING_RESPONSE_RESOURCE_OWNER_LASTNAME = 'response_resource_owner_lastname';
|
||||
public const SETTING_RESPONSE_RESOURCE_OWNER_STATUS = 'response_resource_owner_status';
|
||||
public const SETTING_RESPONSE_RESOURCE_OWNER_TEACHER_STATUS = 'response_resource_owner_teacher_status';
|
||||
public const SETTING_RESPONSE_RESOURCE_OWNER_SESSADMIN_STATUS = 'response_resource_owner_sessadmin_status';
|
||||
public const SETTING_RESPONSE_RESOURCE_OWNER_DRH_STATUS = 'response_resource_owner_drh_status';
|
||||
public const SETTING_RESPONSE_RESOURCE_OWNER_STUDENT_STATUS = 'response_resource_owner_student_status';
|
||||
public const SETTING_RESPONSE_RESOURCE_OWNER_ANON_STATUS = 'response_resource_owner_anon_status';
|
||||
public const SETTING_RESPONSE_RESOURCE_OWNER_EMAIL = 'response_resource_owner_email';
|
||||
public const SETTING_RESPONSE_RESOURCE_OWNER_USERNAME = 'response_resource_owner_username';
|
||||
|
||||
public const SETTING_RESPONSE_RESOURCE_OWNER_URLS = 'response_resource_owner_urls';
|
||||
|
||||
public const SETTING_LOGOUT_URL = 'logout_url';
|
||||
|
||||
public const SETTING_BLOCK_NAME = 'block_name';
|
||||
|
||||
public const SETTING_MANAGEMENT_LOGIN_ENABLE = 'management_login_enable';
|
||||
public const SETTING_MANAGEMENT_LOGIN_NAME = 'management_login_name';
|
||||
|
||||
public const SETTING_ALLOW_THIRD_PARTY_LOGIN = 'allow_third_party_login';
|
||||
|
||||
public const EXTRA_FIELD_OAUTH2_ID = 'oauth2_id';
|
||||
|
||||
private const DEBUG = false;
|
||||
|
||||
protected function __construct()
|
||||
{
|
||||
parent::__construct(
|
||||
'0.1',
|
||||
'Sébastien Ducoulombier',
|
||||
[
|
||||
self::SETTING_ENABLE => 'boolean',
|
||||
|
||||
self::SETTING_FORCE_REDIRECT => 'boolean',
|
||||
self::SETTING_SKIP_FORCE_REDIRECT_IN => 'text',
|
||||
|
||||
self::SETTING_CLIENT_ID => 'text',
|
||||
self::SETTING_CLIENT_SECRET => 'text',
|
||||
|
||||
self::SETTING_AUTHORIZE_URL => 'text',
|
||||
self::SETTING_SCOPES => 'text',
|
||||
self::SETTING_SCOPE_SEPARATOR => 'text',
|
||||
|
||||
self::SETTING_ACCESS_TOKEN_URL => 'text',
|
||||
self::SETTING_ACCESS_TOKEN_METHOD => [
|
||||
'type' => 'select',
|
||||
'options' => [
|
||||
AbstractProvider::METHOD_POST => 'POST',
|
||||
AbstractProvider::METHOD_GET => 'GET',
|
||||
],
|
||||
],
|
||||
// self::SETTING_ACCESS_TOKEN_RESOURCE_OWNER_ID => 'text',
|
||||
|
||||
self::SETTING_RESOURCE_OWNER_DETAILS_URL => 'text',
|
||||
|
||||
self::SETTING_RESPONSE_ERROR => 'text',
|
||||
self::SETTING_RESPONSE_CODE => 'text',
|
||||
self::SETTING_RESPONSE_RESOURCE_OWNER_ID => 'text',
|
||||
|
||||
self::SETTING_UPDATE_USER_INFO => 'boolean',
|
||||
self::SETTING_CREATE_NEW_USERS => 'boolean',
|
||||
self::SETTING_RESPONSE_RESOURCE_OWNER_FIRSTNAME => 'text',
|
||||
self::SETTING_RESPONSE_RESOURCE_OWNER_LASTNAME => 'text',
|
||||
self::SETTING_RESPONSE_RESOURCE_OWNER_STATUS => 'text',
|
||||
self::SETTING_RESPONSE_RESOURCE_OWNER_TEACHER_STATUS => 'text',
|
||||
self::SETTING_RESPONSE_RESOURCE_OWNER_SESSADMIN_STATUS => 'text',
|
||||
self::SETTING_RESPONSE_RESOURCE_OWNER_DRH_STATUS => 'text',
|
||||
self::SETTING_RESPONSE_RESOURCE_OWNER_STUDENT_STATUS => 'text',
|
||||
self::SETTING_RESPONSE_RESOURCE_OWNER_ANON_STATUS => 'text',
|
||||
self::SETTING_RESPONSE_RESOURCE_OWNER_EMAIL => 'text',
|
||||
self::SETTING_RESPONSE_RESOURCE_OWNER_USERNAME => 'text',
|
||||
|
||||
self::SETTING_RESPONSE_RESOURCE_OWNER_URLS => 'text',
|
||||
|
||||
self::SETTING_LOGOUT_URL => 'text',
|
||||
|
||||
self::SETTING_BLOCK_NAME => 'text',
|
||||
|
||||
self::SETTING_MANAGEMENT_LOGIN_ENABLE => 'boolean',
|
||||
self::SETTING_MANAGEMENT_LOGIN_NAME => 'text',
|
||||
|
||||
self::SETTING_ALLOW_THIRD_PARTY_LOGIN => 'boolean',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instance the plugin.
|
||||
*
|
||||
* @staticvar null $result
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public static function create(): OAuth2
|
||||
{
|
||||
static $result = null;
|
||||
|
||||
return $result ?: $result = new self();
|
||||
}
|
||||
|
||||
public function getProvider(): GenericProvider
|
||||
{
|
||||
$redirectUri = api_get_path(WEB_PLUGIN_PATH).'oauth2/src/callback.php';
|
||||
// In cases not precisely defined yet, this alternative version might be necessary - see BT#20611
|
||||
//$redirectUri = api_get_path(WEB_PATH).'authorization-code/callback';
|
||||
$options = [
|
||||
'clientId' => $this->get(self::SETTING_CLIENT_ID),
|
||||
'clientSecret' => $this->get(self::SETTING_CLIENT_SECRET),
|
||||
'redirectUri' => $redirectUri,
|
||||
'urlAuthorize' => $this->get(self::SETTING_AUTHORIZE_URL),
|
||||
'urlResourceOwnerDetails' => $this->get(self::SETTING_RESOURCE_OWNER_DETAILS_URL),
|
||||
];
|
||||
|
||||
if ('' === $scopeSeparator = (string) $this->get(self::SETTING_SCOPE_SEPARATOR)) {
|
||||
$scopeSeparator = ' ';
|
||||
}
|
||||
|
||||
$options['scopeSeparator'] = $scopeSeparator;
|
||||
|
||||
if ('' !== $scopes = (string) $this->get(self::SETTING_SCOPES)) {
|
||||
$options['scopes'] = explode($scopeSeparator, $scopes);
|
||||
}
|
||||
|
||||
if ('' !== $urlAccessToken = (string) $this->get(self::SETTING_ACCESS_TOKEN_URL)) {
|
||||
$options['urlAccessToken'] = $urlAccessToken;
|
||||
}
|
||||
|
||||
if ('' !== $accessTokenMethod = (string) $this->get(self::SETTING_ACCESS_TOKEN_METHOD)) {
|
||||
$options['accessTokenMethod'] = $accessTokenMethod;
|
||||
}
|
||||
|
||||
// if ('' !== $accessTokenResourceOwnerId = (string) $this->get(self::SETTING_ACCESS_TOKEN_RESOURCE_OWNER_ID)) {
|
||||
// $options['accessTokenResourceOwnerId'] = $accessTokenResourceOwnerId;
|
||||
// }
|
||||
|
||||
if ('' !== $responseError = (string) $this->get(self::SETTING_RESPONSE_ERROR)) {
|
||||
$options['responseError'] = $responseError;
|
||||
}
|
||||
|
||||
if ('' !== $responseCode = (string) $this->get(self::SETTING_RESPONSE_CODE)) {
|
||||
$options['responseCode'] = $responseCode;
|
||||
}
|
||||
|
||||
if ('' !== $responseResourceOwnerId = (string) $this->get(self::SETTING_RESPONSE_RESOURCE_OWNER_ID)) {
|
||||
$options['responseResourceOwnerId'] = $responseResourceOwnerId;
|
||||
}
|
||||
|
||||
return new GenericProvider($options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws IdentityProviderException
|
||||
*
|
||||
* @return array user information, as returned by api_get_user_info(userId)
|
||||
*/
|
||||
public function getUserInfo(GenericProvider $provider, AccessToken $accessToken): array
|
||||
{
|
||||
$url = $provider->getResourceOwnerDetailsUrl($accessToken);
|
||||
$request = $provider->getAuthenticatedRequest($provider::METHOD_GET, $url, $accessToken);
|
||||
$response = $provider->getParsedResponse($request);
|
||||
$this->log('response', print_r($response, true));
|
||||
|
||||
if (false === is_array($response)) {
|
||||
$this->log('invalid response', print_r($response, true));
|
||||
throw new UnexpectedValueException($this->get_lang('InvalidJsonReceivedFromProvider'));
|
||||
}
|
||||
$resourceOwnerId = $this->getValueByKey(
|
||||
$response,
|
||||
$this->get(self::SETTING_RESPONSE_RESOURCE_OWNER_ID)
|
||||
);
|
||||
if (empty($resourceOwnerId)) {
|
||||
$this->log('missing setting', 'response_resource_owner_id');
|
||||
throw new RuntimeException($this->get_lang('WrongResponseResourceOwnerId'));
|
||||
}
|
||||
$this->log('response resource owner id', $this->get(self::SETTING_RESPONSE_RESOURCE_OWNER_ID));
|
||||
$extraFieldValue = new ExtraFieldValue('user');
|
||||
$result = $extraFieldValue->get_item_id_from_field_variable_and_field_value(
|
||||
self::EXTRA_FIELD_OAUTH2_ID,
|
||||
$resourceOwnerId
|
||||
);
|
||||
if (false === $result) {
|
||||
$this->log('user not found', "extrafield 'oauth2_id' with value '$resourceOwnerId'");
|
||||
|
||||
$username = $this->getValueByKey(
|
||||
$response,
|
||||
$this->get(self::SETTING_RESPONSE_RESOURCE_OWNER_USERNAME),
|
||||
'oauth2user_'.$resourceOwnerId
|
||||
);
|
||||
|
||||
$userInfo = api_get_user_info_from_username($username);
|
||||
|
||||
if (false !== $userInfo && !empty($userInfo['id']) && 'platform' === $userInfo['auth_source']) {
|
||||
$this->log('platform user exists', print_r($userInfo, true));
|
||||
|
||||
$userId = $userInfo['id'];
|
||||
} else {
|
||||
// authenticated user not found in internal database
|
||||
if ('true' !== $this->get(self::SETTING_CREATE_NEW_USERS)) {
|
||||
$this->log('exception', 'create_new_users setting is disabled');
|
||||
$message = sprintf(
|
||||
$this->get_lang('NoUserAccountAndUserCreationNotAllowed'),
|
||||
Display::encrypted_mailto_link(api_get_setting('emailAdministrator'))
|
||||
);
|
||||
throw new RuntimeException($message);
|
||||
}
|
||||
|
||||
require_once __DIR__.'/../../../main/auth/external_login/functions.inc.php';
|
||||
|
||||
$firstName = $this->getValueByKey(
|
||||
$response,
|
||||
$this->get(self::SETTING_RESPONSE_RESOURCE_OWNER_FIRSTNAME),
|
||||
$this->get_lang('DefaultFirstname')
|
||||
);
|
||||
$lastName = $this->getValueByKey(
|
||||
$response,
|
||||
$this->get(self::SETTING_RESPONSE_RESOURCE_OWNER_LASTNAME),
|
||||
$this->get_lang('DefaultLastname')
|
||||
);
|
||||
$status = $this->mapUserStatusFromResponse($response);
|
||||
$email = $this->getValueByKey(
|
||||
$response,
|
||||
$this->get(self::SETTING_RESPONSE_RESOURCE_OWNER_EMAIL),
|
||||
'oauth2user_'.$resourceOwnerId.'@'.(gethostname() or 'localhost')
|
||||
);
|
||||
|
||||
$userInfo = [
|
||||
'firstname' => $firstName,
|
||||
'lastname' => $lastName,
|
||||
'status' => $status,
|
||||
'email' => $email,
|
||||
'username' => $username,
|
||||
'auth_source' => 'oauth2',
|
||||
];
|
||||
$userId = external_add_user($userInfo);
|
||||
if (false === $userId) {
|
||||
$this->log('user not created', print_r($userInfo, true));
|
||||
throw new RuntimeException($this->get_lang('FailedUserCreation'));
|
||||
}
|
||||
$this->log('user created', (string) $userId);
|
||||
}
|
||||
|
||||
$this->updateUser($userId, $response);
|
||||
// Not checking function update_extra_field_value return value because not reliable
|
||||
UserManager::update_extra_field_value($userId, self::EXTRA_FIELD_OAUTH2_ID, $resourceOwnerId);
|
||||
$this->updateUserUrls($userId, $response);
|
||||
} else {
|
||||
$this->log('user found', "extrafield 'oauth2_id' with value '$resourceOwnerId'");
|
||||
// authenticated user found in internal database
|
||||
if (is_array($result) and array_key_exists('item_id', $result)) {
|
||||
$userId = $result['item_id'];
|
||||
} else {
|
||||
$userId = $result;
|
||||
}
|
||||
if ('true' === $this->get(self::SETTING_UPDATE_USER_INFO)) {
|
||||
$this->updateUser($userId, $response);
|
||||
$this->updateUserUrls($userId, $response);
|
||||
|
||||
Event::addEvent(LOG_USER_UPDATE, LOG_USER_ID, $userId);
|
||||
}
|
||||
}
|
||||
$userInfo = api_get_user_info($userId);
|
||||
if (empty($userInfo)) {
|
||||
$this->log('user info not found', (string) $userId);
|
||||
throw new LogicException($this->get_lang('InternalErrorCannotGetUserInfo'));
|
||||
}
|
||||
|
||||
$this->log('user info', print_r($userInfo, true));
|
||||
|
||||
return $userInfo;
|
||||
}
|
||||
|
||||
public function getSignInURL(): string
|
||||
{
|
||||
return api_get_path(WEB_PLUGIN_PATH).$this->get_name().'/src/callback.php';
|
||||
// In cases not precisely defined yet, this alternative version might be necessary - see BT#20611
|
||||
//return api_get_path(WEB_PATH).'authorization-code/callback';
|
||||
}
|
||||
|
||||
public function getLogoutUrl(): string
|
||||
{
|
||||
$token = ChamiloSession::read('oauth2AccessToken');
|
||||
$idToken = !empty($token['id_token']) ? $token['id_token'] : null;
|
||||
|
||||
return $this->get(self::SETTING_LOGOUT_URL).'?'.http_build_query(
|
||||
[
|
||||
'id_token_hint' => $idToken,
|
||||
'post_logout_redirect_uri' => api_get_path(WEB_PATH),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create extra fields for user when installing.
|
||||
*/
|
||||
public function install()
|
||||
{
|
||||
UserManager::create_extra_field(
|
||||
self::EXTRA_FIELD_OAUTH2_ID,
|
||||
ExtraField::FIELD_TYPE_TEXT,
|
||||
$this->get_lang('OAuth2Id'),
|
||||
''
|
||||
);
|
||||
}
|
||||
|
||||
public static function isFirstLoginAfterAuthSource(int $userId): bool
|
||||
{
|
||||
$em = Database::getManager();
|
||||
|
||||
$lastLogin = $em
|
||||
->getRepository(TrackELogin::class)
|
||||
->findOneBy(
|
||||
['loginUserId' => $userId],
|
||||
['loginDate' => 'DESC']
|
||||
)
|
||||
;
|
||||
|
||||
if (!$lastLogin) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$objExtraField = new ExtraField('user');
|
||||
$field = $objExtraField->getHandlerEntityByFieldVariable(self::EXTRA_FIELD_OAUTH2_ID);
|
||||
|
||||
$fieldValue = $em
|
||||
->getRepository(ExtraFieldValues::class)
|
||||
->findOneBy(
|
||||
['itemId' => $userId, 'field' => $field]
|
||||
)
|
||||
;
|
||||
|
||||
if (!$fieldValue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $fieldValue->getCreatedAt() >= $lastLogin->getLoginDate();
|
||||
}
|
||||
|
||||
private function mapUserStatusFromResponse(array $response, int $defaultStatus = STUDENT): int
|
||||
{
|
||||
$status = $this->getValueByKey(
|
||||
$response,
|
||||
$this->get(self::SETTING_RESPONSE_RESOURCE_OWNER_STATUS),
|
||||
$defaultStatus
|
||||
);
|
||||
|
||||
$responseStatus = [];
|
||||
|
||||
if ($teacherStatus = $this->get(self::SETTING_RESPONSE_RESOURCE_OWNER_TEACHER_STATUS)) {
|
||||
$responseStatus[COURSEMANAGER] = $teacherStatus;
|
||||
}
|
||||
|
||||
if ($sessAdminStatus = $this->get(self::SETTING_RESPONSE_RESOURCE_OWNER_SESSADMIN_STATUS)) {
|
||||
$responseStatus[SESSIONADMIN] = $sessAdminStatus;
|
||||
}
|
||||
|
||||
if ($drhStatus = $this->get(self::SETTING_RESPONSE_RESOURCE_OWNER_DRH_STATUS)) {
|
||||
$responseStatus[DRH] = $drhStatus;
|
||||
}
|
||||
|
||||
if ($studentStatus = $this->get(self::SETTING_RESPONSE_RESOURCE_OWNER_STUDENT_STATUS)) {
|
||||
$responseStatus[STUDENT] = $studentStatus;
|
||||
}
|
||||
|
||||
if ($anonStatus = $this->get(self::SETTING_RESPONSE_RESOURCE_OWNER_ANON_STATUS)) {
|
||||
$responseStatus[ANONYMOUS] = $anonStatus;
|
||||
}
|
||||
|
||||
$map = array_flip($responseStatus);
|
||||
|
||||
return $map[$status] ?? $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extends ArrayAccessorTrait::getValueByKey to return a list of values
|
||||
* $key can contain wild card character *
|
||||
* It will be replaced by 0, 1, 2 and so on as long as the resulting key exists in $data
|
||||
* This is a recursive function, allowing for more than one occurrence of the wild card character.
|
||||
*/
|
||||
private function getValuesByKey(array $data, string $key, array $default = []): array
|
||||
{
|
||||
if (!is_string($key) || empty($key) || !count($data)) {
|
||||
return $default;
|
||||
}
|
||||
$pos = strpos($key, '*');
|
||||
if ($pos === false) {
|
||||
$value = $this->getValueByKey($data, $key);
|
||||
|
||||
return is_null($value) ? [] : [$value];
|
||||
}
|
||||
$values = [];
|
||||
$beginning = substr($key, 0, $pos);
|
||||
$remaining = substr($key, $pos + 1);
|
||||
$index = 0;
|
||||
do {
|
||||
$newValues = $this->getValuesByKey(
|
||||
$data,
|
||||
$beginning.$index.$remaining
|
||||
);
|
||||
$values = array_merge($values, $newValues);
|
||||
$index++;
|
||||
} while ($newValues);
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
private function updateUser($userId, $response)
|
||||
{
|
||||
$user = UserManager::getRepository()->find($userId);
|
||||
$user->setFirstname(
|
||||
$this->getValueByKey(
|
||||
$response,
|
||||
$this->get(
|
||||
self::SETTING_RESPONSE_RESOURCE_OWNER_FIRSTNAME
|
||||
),
|
||||
$user->getFirstname()
|
||||
)
|
||||
);
|
||||
$user->setLastname(
|
||||
$this->getValueByKey(
|
||||
$response,
|
||||
$this->get(self::SETTING_RESPONSE_RESOURCE_OWNER_LASTNAME),
|
||||
$user->getLastname()
|
||||
)
|
||||
);
|
||||
$user->setUserName(
|
||||
$this->getValueByKey(
|
||||
$response,
|
||||
$this->get(self::SETTING_RESPONSE_RESOURCE_OWNER_USERNAME),
|
||||
$user->getUsername()
|
||||
)
|
||||
);
|
||||
$user->setEmail(
|
||||
$this->getValueByKey(
|
||||
$response,
|
||||
$this->get(self::SETTING_RESPONSE_RESOURCE_OWNER_EMAIL),
|
||||
$user->getEmail()
|
||||
)
|
||||
);
|
||||
$status = $this->mapUserStatusFromResponse(
|
||||
$response,
|
||||
$user->getStatus()
|
||||
);
|
||||
$user->setStatus($status);
|
||||
$user->setAuthSource('oauth2');
|
||||
$configFilePath = __DIR__.'/../config.php';
|
||||
if (file_exists($configFilePath)) {
|
||||
require_once $configFilePath;
|
||||
$functionName = 'oauth2UpdateUserFromResourceOwnerDetails';
|
||||
if (function_exists($functionName)) {
|
||||
$functionName($response, $user);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
UserManager::getManager()->updateUser($user);
|
||||
} catch (UniqueConstraintViolationException $exception) {
|
||||
throw new Exception(get_lang('UserNameUsedTwice'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the Access URLs associated to a user
|
||||
* according to the OAuth2 server response resource owner
|
||||
* if multi-URL is enabled and SETTING_RESPONSE_RESOURCE_OWNER_URLS defined.
|
||||
*
|
||||
* @param $userId integer
|
||||
* @param $response array
|
||||
*/
|
||||
private function updateUserUrls($userId, $response)
|
||||
{
|
||||
if (api_is_multiple_url_enabled()) {
|
||||
$key = (string) $this->get(self::SETTING_RESPONSE_RESOURCE_OWNER_URLS);
|
||||
if (!empty($key)) {
|
||||
$availableUrls = [];
|
||||
foreach (UrlManager::get_url_data() as $existingUrl) {
|
||||
$urlId = $existingUrl['id'];
|
||||
$availableUrls[strval($urlId)] = $urlId;
|
||||
$availableUrls[$existingUrl['url']] = $urlId;
|
||||
}
|
||||
$allowedUrlIds = [];
|
||||
foreach ($this->getValuesByKey($response, $key) as $value) {
|
||||
if (array_key_exists($value, $availableUrls)) {
|
||||
$allowedUrlIds[] = $availableUrls[$value];
|
||||
} else {
|
||||
$newValue = ($value[-1] === '/') ? substr($value, 0, -1) : $value.'/';
|
||||
if (array_key_exists($newValue, $availableUrls)) {
|
||||
$allowedUrlIds[] = $availableUrls[$newValue];
|
||||
}
|
||||
}
|
||||
}
|
||||
$grantedUrlIds = [];
|
||||
foreach (UrlManager::get_access_url_from_user($userId) as $grantedUrl) {
|
||||
$grantedUrlIds[] = $grantedUrl['access_url_id'];
|
||||
}
|
||||
foreach (array_diff($grantedUrlIds, $allowedUrlIds) as $extraUrlId) {
|
||||
UrlManager::delete_url_rel_user($userId, $extraUrlId);
|
||||
}
|
||||
foreach (array_diff($allowedUrlIds, $grantedUrlIds) as $missingUrlId) {
|
||||
UrlManager::add_user_to_url($userId, $missingUrlId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function log(string $key, string $content)
|
||||
{
|
||||
if (self::DEBUG) {
|
||||
error_log("OAuth2 plugin: $key: $content");
|
||||
}
|
||||
}
|
||||
}
|
||||
100
plugin/oauth2/src/callback.php
Normal file
100
plugin/oauth2/src/callback.php
Normal file
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
/* For license terms, see /license.txt */
|
||||
|
||||
use League\OAuth2\Client\Token\AccessToken;
|
||||
|
||||
require __DIR__.'/../../../main/inc/global.inc.php';
|
||||
|
||||
if (!empty($_GET['error']) && !empty($_GET['state'])) {
|
||||
if ($_GET['state'] === ChamiloSession::read('oauth2state')) {
|
||||
api_not_allowed(
|
||||
true,
|
||||
Display::return_message(
|
||||
$_GET['error_description'] ?? $_GET['error'],
|
||||
'warning'
|
||||
)
|
||||
);
|
||||
} else {
|
||||
ChamiloSession::erase('oauth2state');
|
||||
exit('Invalid state');
|
||||
}
|
||||
}
|
||||
|
||||
$plugin = OAuth2::create();
|
||||
|
||||
if ('true' !== $plugin->get(OAuth2::SETTING_ENABLE)) {
|
||||
api_not_allowed(true);
|
||||
}
|
||||
|
||||
$provider = $plugin->getProvider();
|
||||
|
||||
// If we don't have an authorization code then get one
|
||||
if (!array_key_exists('code', $_GET)) {
|
||||
// Fetch the authorization URL from the provider; this returns the
|
||||
// urlAuthorize option and generates and applies any necessary parameters
|
||||
// (e.g. state).
|
||||
$authorizationUrl = $provider->getAuthorizationUrl();
|
||||
|
||||
// Get the state generated for you and store it to the session.
|
||||
ChamiloSession::write('oauth2state', $provider->getState());
|
||||
|
||||
// Redirect the user to the authorization URL.
|
||||
header('Location: '.$authorizationUrl);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Check given state against previously stored one to mitigate CSRF attack
|
||||
if (!array_key_exists('state', $_GET) || ($_GET['state'] !== ChamiloSession::read('oauth2state'))) {
|
||||
ChamiloSession::erase('oauth2state');
|
||||
exit('Invalid state');
|
||||
}
|
||||
|
||||
try {
|
||||
// Try to get an access token using the authorization code grant.
|
||||
/**
|
||||
* @var $accessToken AccessToken
|
||||
*/
|
||||
$accessToken = $provider->getAccessToken(
|
||||
'authorization_code',
|
||||
['code' => $_GET['code']]
|
||||
);
|
||||
ChamiloSession::write('oauth2AccessToken', $accessToken->jsonSerialize());
|
||||
$userInfo = $plugin->getUserInfo($provider, $accessToken);
|
||||
if ($userInfo['active'] != '1') {
|
||||
throw new Exception($plugin->get_lang('AccountInactive'));
|
||||
}
|
||||
if (api_is_multiple_url_enabled()) {
|
||||
$userId = $userInfo['user_id'];
|
||||
$urlIdsTheUserCanAccess = api_get_access_url_from_user($userId);
|
||||
$userCanAccessTheFirstURL = in_array(1, $urlIdsTheUserCanAccess);
|
||||
$userCanAccessTheCurrentURL = in_array(api_get_current_access_url_id(), $urlIdsTheUserCanAccess)
|
||||
|| UserManager::is_admin($userId)
|
||||
&& $userCanAccessTheFirstURL;
|
||||
|
||||
if (!$userCanAccessTheCurrentURL) {
|
||||
throw new Exception($plugin->get_lang('UserNotAllowedOnThisPortal'));
|
||||
}
|
||||
}
|
||||
} catch (Exception $exception) {
|
||||
$message = Display::return_message($exception->getMessage(), 'error', false);
|
||||
Display::addFlash($message);
|
||||
header('Location: '.api_get_path(WEB_PATH));
|
||||
exit;
|
||||
}
|
||||
|
||||
ConditionalLogin::check_conditions($userInfo);
|
||||
|
||||
$userInfo['uidReset'] = true;
|
||||
|
||||
$_GET['redirect_after_not_allow_page'] = 1;
|
||||
|
||||
$redirectAfterNotAllowPage = ChamiloSession::read('redirect_after_not_allow_page');
|
||||
|
||||
ChamiloSession::clear();
|
||||
|
||||
ChamiloSession::write('redirect_after_not_allow_page', $redirectAfterNotAllowPage);
|
||||
|
||||
ChamiloSession::write('_user', $userInfo);
|
||||
ChamiloSession::write('_user_auth_source', 'oauth2');
|
||||
|
||||
Redirect::session_request_uri(true, $userInfo['user_id']);
|
||||
54
plugin/oauth2/src/oidc_login.php
Normal file
54
plugin/oauth2/src/oidc_login.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
/* For licensing terms, see /license.txt */
|
||||
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request as HttpRequest;
|
||||
|
||||
require __DIR__.'/../../../main/inc/global.inc.php';
|
||||
|
||||
$httpRequest = HttpRequest::createFromGlobals();
|
||||
|
||||
$plugin = OAuth2::create();
|
||||
|
||||
$iss = $httpRequest->get('iss');
|
||||
$loginHint = $httpRequest->get('login_hint');
|
||||
$targetLinkUri = $httpRequest->get('target_link_uri');
|
||||
|
||||
try {
|
||||
if ('true' !== $plugin->get(OAuth2::SETTING_ENABLE)
|
||||
|| 'true' !== $plugin->get(OAuth2::SETTING_ALLOW_THIRD_PARTY_LOGIN)
|
||||
) {
|
||||
throw new Exception(get_lang('NotAllowedHere'));
|
||||
}
|
||||
|
||||
if (empty($iss)) {
|
||||
throw new Exception($plugin->get_lang('IssuerNotFound'));
|
||||
}
|
||||
|
||||
$authorizeUrlSetting = $plugin->get(OAuth2::SETTING_AUTHORIZE_URL);
|
||||
|
||||
if (empty($authorizeUrlSetting) || 0 !== strpos($authorizeUrlSetting, $iss)) {
|
||||
throw new Exception($plugin->get_lang('AuthorizeUrlNotAllowed'));
|
||||
}
|
||||
|
||||
$provider = $plugin->getProvider();
|
||||
|
||||
$authorizationUrl = $provider->getAuthorizationUrl(
|
||||
[
|
||||
'login_hint' => $loginHint,
|
||||
'target_link_uri' => $targetLinkUri,
|
||||
]
|
||||
);
|
||||
|
||||
ChamiloSession::write('oauth2state', $provider->getState());
|
||||
|
||||
$httpResponse = new RedirectResponse($authorizationUrl);
|
||||
$httpResponse->send();
|
||||
} catch (Exception $e) {
|
||||
$message = $e->getMessage()
|
||||
? Display::return_message($e->getMessage(), 'error')
|
||||
: null;
|
||||
|
||||
api_not_allowed(true, $message);
|
||||
}
|
||||
Reference in New Issue
Block a user