Generic OAuth2 plugin - refs BT#16734

pull/3290/head
Sébastien Ducoulombier 6 years ago
parent df4ce2cfe9
commit a00f874c96
  1. 34
      main/auth/external_login/login.oauth2.php
  2. 22
      plugin/oauth2/README.md
  3. 31
      plugin/oauth2/index.php
  4. 8
      plugin/oauth2/install.php
  5. 69
      plugin/oauth2/lang/english.php
  6. 44
      plugin/oauth2/layout/login_form.tpl
  7. 35
      plugin/oauth2/login.php
  8. 11
      plugin/oauth2/plugin.php
  9. 204
      plugin/oauth2/src/OAuth2.php
  10. 53
      plugin/oauth2/src/callback.php
  11. 18
      plugin/oauth2/view/block.tpl
  12. 19
      plugin/oauth2/view/login.tpl

@ -0,0 +1,34 @@
<?php
/* For licensing terms, see /license.txt */
require_once __DIR__.'/functions.inc.php';
/** @var array $uData */
if ($uData['auth_source'] === 'oauth2') {
$plugin = OAuth2::create();
if ('true' !== $plugin->get(OAuth2::SETTING_ENABLE)) {
api_not_allowed(true);
}
$oauth2IdField = new ExtraFieldValue('user');
$oauth2IdValue = $oauth2IdField->get_values_by_handler_and_field_variable(
$uData['user_id'],
OAuth2::EXTRA_FIELD_OAUTH2_ID
);
if (empty($oauth2IdValue) || empty($oauth2IdValue['value'])) {
api_not_allowed(true);
}
$provider = $plugin->getProvider();
$authUrl = $provider->getAuthorizationUrl();
ChamiloSession::write('oauth2state', $provider->getState());
// Redirect to OAuth2 login.
header('Location: '.$authUrl);
// Avoid execution from here in local.inc.php script.
exit;
}

@ -0,0 +1,22 @@
# The OAuth2 Plugin
Allows authentication with a generic OAuth2 provider.
This plugin adds an extra field to users :
- `oauth2_id`, to store each users' OAuth2 identifier.
> This plugin uses the [`league/oauth2-client`](https://oauth2-client.thephpleague.com/) package.
### To configure the OAuth2 server
The OAuth2 server administrator must give you an OAuth2 client identifier and secret and enter this callback URL :
`https://{CHAMILO_URL}/plugin/oauth2/src/callback.php`.
### To configure this plugin
* Enable it
* Fill in the setting parameters (read the help messages)
* assign a region. Preferably `login_bottom`.
Also, you can configure the external login to work with the classic Chamilo form login.
Adding this line in `configuration.php` file.
```php
$extAuthSource["oauth2"]["login"] = $_configuration['root_sys']."main/auth/external_login/login.oauth2.php";
```

@ -0,0 +1,31 @@
<?php
/* For licensing terms, see /license.txt */
/**
* @author Sébastien Ducoulombier <seb@ldd.fr>
* inspired by AzureActiveDirectory plugin from Angel Fernando Quiroz Campos <angel.quiroz@beeznest.com>
*
* @package chamilo.plugin.oauth2
*/
/** @var OAuth2 $oAuth2Plugin */
$oAuth2Plugin = OAuth2::create();
if ($oAuth2Plugin->get(OAuth2::SETTING_ENABLE) === 'true') {
$_template['block_title'] = $oAuth2Plugin->get(OAuth2::SETTING_BLOCK_NAME);
$_template['signin_url'] = $oAuth2Plugin->getSignInURL();
$managementLoginEnabled = 'true' === $oAuth2Plugin->get(OAuth2::SETTING_MANAGEMENT_LOGIN_ENABLE);
$_template['management_login_enabled'] = $managementLoginEnabled;
if ($managementLoginEnabled) {
$managementLoginName = $oAuth2Plugin->get(OAuth2::SETTING_MANAGEMENT_LOGIN_NAME);
if (empty($managementLoginName)) {
$managementLoginName = $oAuth2Plugin->get_lang('ManagementLogin');
}
$_template['management_login_name'] = $managementLoginName;
}
}

@ -0,0 +1,8 @@
<?php
/* For licensing terms, see /license.txt */
if (!api_is_platform_admin()) {
die('You must have admin permissions to install plugins');
}
OAuth2::create()->install();

@ -0,0 +1,69 @@
<?php
/* For licensing terms, see /license.txt */
/**
* Strings to english L10n.
*
* @author Sébastien Ducoulombier <seb@ldd.fr>
* inspired by Angel Fernando Quiroz Campos <angel.quiroz@beeznest.com>
*
* @package chamilo.plugin.oauth2
*/
$strings['plugin_title'] = 'OAuth2';
$strings['plugin_comment'] = 'Allow authentication with an <em>OAuth2</em> server';
$strings['enable'] = 'Enable';
$strings['client_id'] = 'Client ID';
$strings['client_id_help'] = '<strong>The <em>OAuth2</em> client identifier</strong>
the <em>OAuth2</em> server administrator assigned to this Chamilo instance.
<br/>Required.';
$strings['client_secret'] = 'Client Secret';
$strings['client_secret_help'] = '<strong>The secret code</strong> associated to the <em>OAuth2</em> client identifier.
<br/>Required.';
$strings['authorize_url'] = 'Authorize URL';
$strings['authorize_url_help'] = 'The <em>OAuth2</em> server URL to request authorization.
<br/>Required.';
$strings['access_token_url'] = 'Access Token URL';
$strings['access_token_url_help'] = 'The <em>OAuth2</em> server URL to request an access token.
<br/>Required.';
$strings['access_token_method'] = 'Access Token HTTP Method';
$strings['access_token_method_help'] = 'Default value: POST';
$strings['resource_owner_details_url'] = 'Resource Owner Details URL';
$strings['resource_owner_details_url_help'] = 'The <em>OAuth2</em> server URL
returning the identified user information as a <em>JSON</em> array.
Required.';
$strings['response_error'] = 'Response error key';
$strings['response_error_help'] = 'Default is <code>error</code>';
$strings['response_code'] = 'Response code key';
$strings['response_code_help'] = 'By default, an error code retrieval is not attempted';
$strings['response_resource_owner_id'] = 'Response Resource Owner Id key';
$strings['response_resource_owner_id_help'] = 'The array key to the user\'s <em>OAuth2</em> identifier value.
<br/>Default value: <code>id</code>.
<br/>If the identifier is in a subentry of the returned <em>JSON</em> array,
<br/>then please enter successive path keys separated by dots. For example,
<br/><code>data.0.id</code>
<br/>means the identifier is to be found at
<code>$jsonArray["data"][0]["id"]</code>';
$strings['block_name'] = 'Block name';
$strings['block_name_help'] = 'The title shown above the <em>OAuth2</em> Login button';
$strings['management_login_enable'] = 'Management login';
$strings['management_login_enable_help'] = 'Disable the Chamilo login and enable an alternative login page for users.
<br>
You will need copy file <code>/plugin/oauth2/layout/login_form.tpl</code>
to directory <code>/main/template/overrides/layout/</code>.';
$strings['management_login_name'] = 'Name for the management login';
$strings['management_login_name_help'] = 'Default value is "Management Login".';
$strings['OAuth2Id'] = 'OAuth2 identifier';
$strings['ManagementLogin'] = 'Management Login';
$strings['InvalidId'] = 'Login failed - the OAuth2 identifier was not recognized as an existing Chamilo user\'s.';

@ -0,0 +1,44 @@
{% if _u.logged == 0 %}
{% if login_form %}
<div id="login-block" class="panel panel-default">
<div class="panel-body">
{{ login_language_form }}
{% if plugin_login_top is not null %}
<div id="plugin_login_top">
{{ plugin_login_top }}
</div>
{% endif %}
{{ login_failed }}
{% set oauth2_plugin_enabled = 'oauth2'|api_get_plugin_setting('enable') %}
{% set oauth2_plugin_manage_login = 'oauth2'|api_get_plugin_setting('manage_login_enable') %}
{% if 'false' == oauth2_plugin_enabled or 'false' == oauth2_plugin_manage_login %}
{{ login_form }}
{% if "allow_lostpassword" | api_get_setting == 'true' or "allow_registration"|api_get_setting == 'true' %}
<ul class="nav nav-pills nav-stacked">
{% if "allow_registration"|api_get_setting != 'false' %}
<li><a href="{{ _p.web_main }}auth/inscription.php"> {{ 'SignUp'|get_lang }} </a></li>
{% endif %}
{% if "allow_lostpassword"|api_get_setting == 'true' %}
<li>
<a href="{{ _p.web_main }}auth/lostPassword.php">{{ 'LostPassword'|get_lang }}</a>
</li>
{% endif %}
</ul>
{% endif %}
{% endif %}
{% if plugin_login_bottom is not null %}
<div id="plugin_login_bottom">
{{ plugin_login_bottom }}
</div>
{% endif %}
</div>
</div>
{% endif %}
{% endif %}

@ -0,0 +1,35 @@
<?php
/* For license terms, see /license.txt */
require __DIR__.'/../../main/inc/global.inc.php';
$plugin = OAuth2::create();
$pluginEnabled = $plugin->get(OAuth2::SETTING_ENABLE);
$managementLoginEnabled = $plugin->get(OAuth2::SETTING_MANAGEMENT_LOGIN_ENABLE);
if ('true' !== $pluginEnabled || 'true' !== $managementLoginEnabled) {
header('Location: '.api_get_path(WEB_PATH));
exit;
}
$userId = api_get_user_id();
if (!($userId) || api_is_anonymous($userId)) {
$managementLoginName = $plugin->get(OAuth2::SETTING_MANAGEMENT_LOGIN_NAME);
if (empty($managementLoginName)) {
$managementLoginName = $plugin->get_lang('ManagementLogin');
}
$template = new Template($managementLoginName);
// Only display if the user isn't logged in.
$template->assign('login_language_form', api_display_language_form(true, true));
$template->assign('login_form', $template->displayLoginForm());
$content = $template->fetch('oauth2/view/login.tpl');
$template->assign('content', $content);
$template->display_one_col_template();
}

@ -0,0 +1,11 @@
<?php
/* For licensing terms, see /license.txt */
/**
* @author Sébastien Ducoulombier <seb@ldd.fr>, inspired by Angel Fernando Quiroz Campos <angel.quiroz@beeznest.com>
*
* @package chamilo.plugin.oauth2
*/
/** @var OAuth2 $plugin_info */
$plugin_info = OAuth2::create()->get_info();
$plugin_info['templates'] = ['view/block.tpl'];

@ -0,0 +1,204 @@
<?php
/* For license terms, see /license.txt */
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;
const SETTING_ENABLE = 'enable';
const SETTING_CLIENT_ID = 'client_id';
const SETTING_CLIENT_SECRET = 'client_secret';
const SETTING_AUTHORIZE_URL = 'authorize_url';
# const SETTING_SCOPES = 'scopes';
# const SETTING_SCOPE_SEPARATOR = 'scope_separator';
const SETTING_ACCESS_TOKEN_URL = 'access_token_url';
const SETTING_ACCESS_TOKEN_METHOD = 'access_token_method';
# const SETTING_ACCESS_TOKEN_RESOURCE_OWNER_ID = 'access_token_resource_owner_id';
const SETTING_RESOURCE_OWNER_DETAILS_URL = 'resource_owner_details_url';
const SETTING_RESPONSE_ERROR = 'response_error';
const SETTING_RESPONSE_CODE = 'response_code';
const SETTING_RESPONSE_RESOURCE_OWNER_ID = 'response_resource_owner_id';
const SETTING_BLOCK_NAME = 'block_name';
const SETTING_MANAGEMENT_LOGIN_ENABLE = 'management_login_enable';
const SETTING_MANAGEMENT_LOGIN_NAME = 'management_login_name';
const EXTRA_FIELD_OAUTH2_ID = 'oauth2_id';
protected function __construct()
{
parent::__construct(
'0.1',
'Sébastien Ducoulombier',
[
self::SETTING_ENABLE => 'boolean',
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' => [
GenericProvider::METHOD_POST => 'POST',
GenericProvider::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_BLOCK_NAME => 'text',
self::SETTING_MANAGEMENT_LOGIN_ENABLE => 'boolean',
self::SETTING_MANAGEMENT_LOGIN_NAME => 'text',
]
);
}
/**
* Instance the plugin.
*
* @staticvar null $result
*
* @return $this
*/
public static function create()
{
static $result = null;
return $result ? $result : $result = new self();
}
/**
* @return string
*/
public function get_name()
{
return 'oauth2';
}
/**
* @return GenericProvider
*/
public function getProvider()
{
return new GenericProvider(
[
'clientId' => $this->get(self::SETTING_CLIENT_ID),
'clientSecret' => $this->get(self::SETTING_CLIENT_SECRET),
'redirectUri' => api_get_path(WEB_PLUGIN_PATH).'oauth2/src/callback.php',
'urlAuthorize' => $this->get(self::SETTING_AUTHORIZE_URL),
# 'scopes' => $this->get(self::SETTING_SCOPES) or null,
# 'scopeSeparator' => $this->get(self::SETTING_SCOPE_SEPARATOR) ?: ',',
'urlAccessToken' => $this->get(self::SETTING_ACCESS_TOKEN_URL),
'accessTokenMethod' => $this->get(self::SETTING_ACCESS_TOKEN_METHOD) ?: GenericProvider::METHOD_POST,
#'accessTokenResourceOwnerId' => $this->get(self::SETTING_ACCESS_TOKEN_RESOURCE_OWNER_ID)
# ?: GenericProvider::ACCESS_TOKEN_RESOURCE_OWNER_ID,
'urlResourceOwnerDetails' => $this->get(self::SETTING_RESOURCE_OWNER_DETAILS_URL),
'responseError' => $this->get(self::SETTING_RESPONSE_ERROR) ?: 'error',
'responseCode' => $this->get(self::SETTING_RESPONSE_CODE) ?: null,
'responseResourceOwnerId' => $this->get(self::SETTING_RESPONSE_RESOURCE_OWNER_ID) ?: 'id',
]
);
}
/**
* @return array user information, as returned by api_get_user_info(userId)
* @throws IdentityProviderException
* @var AccessToken $accessToken
* @var GenericProvider $provider
*/
public function getUserInfo($provider, $accessToken)
{
$url = $provider->getResourceOwnerDetailsUrl($accessToken);
$request = $provider->getAuthenticatedRequest($provider::METHOD_GET, $url, $accessToken);
$response = $provider->getParsedResponse($request);
if (false === is_array($response)) {
throw new UnexpectedValueException(
get_lang('invalid_json_received_from_provider')
);
}
$resourceOwnerId = $this->getValueByKey(
$response,
$this->get(self::SETTING_RESPONSE_RESOURCE_OWNER_ID)
);
if (empty($resourceOwnerId)) {
throw new RuntimeException(
get_lang('wrong_response_resource_owner_id')
);
}
$extraFieldValue = new ExtraFieldValue('user');
$result = $extraFieldValue->get_item_id_from_field_variable_and_field_value(
OAuth2::EXTRA_FIELD_OAUTH2_ID,
$resourceOwnerId
);
if (false === $result) {
throw new RuntimeException(
get_lang('no_user_has_this_oauth_code')
);
}
if (is_array($result) and array_key_exists('item_id', $result)) {
$userId = $result['item_id'];
} else {
$userId = $result;
}
$userInfo = api_get_user_info($userId);
if (empty($userInfo)) {
throw new LogicException(
get_lang('internal_error_cannot_get_user_info')
);
}
return $userInfo;
}
public function getSignInURL()
{
return api_get_path(WEB_PLUGIN_PATH).$this->get_name().'/src/callback.php';
}
/**
* 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'),
''
);
}
}

@ -0,0 +1,53 @@
<?php
/* For license terms, see /license.txt */
require __DIR__.'/../../../main/inc/global.inc.php';
$plugin = OAuth2::create();
$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.
$accessToken = $provider->getAccessToken('authorization_code', [
'code' => $_GET['code']
]);
$userInfo = $plugin->getUserInfo($provider, $accessToken);
if ($userInfo['active'] != '1') {
throw new Exception(get_lang('AccountInactive'));
}
} catch (Exception $exception) {
$message = Display::return_message($exception->getMessage(), 'error');
Display::addFlash($message);
header('Location: '.api_get_path(WEB_PATH));
exit;
}
$_user['user_id'] = $userInfo['user_id'];
$_user['uidReset'] = true;
ChamiloSession::write('_user', $_user);
ChamiloSession::write('_user_auth_source', 'oauth2');
Redirect::session_request_uri(true, $userInfo['user_id']);

@ -0,0 +1,18 @@
{% if not _u.logged %}
<div id="oauth2-login">
{% if not oauth2.block_title is empty %}
<h4>{{ oauth2.block_title }}</h4>
{% endif %}
{% if not oauth2.signin_url is empty %}
<a href="{{ oauth2.signin_url }}" class="btn btn-default">{{ 'SignIn'|get_lang }}</a>
{% endif %}
{% if oauth2.management_login_enabled %}
<hr>
<a href="{{ _p.web_plugin ~ 'oauth2/login.php' }}">
{{ oauth2.management_login_name }}
</a>
{% endif %}
</div>
{% endif %}

@ -0,0 +1,19 @@
<div class="row">
<div class="col-sm-4 col-sm-offset-4">
{{ login_language_form }}
{{ login_form }}
{% if "allow_lostpassword"|api_get_setting == 'true' or "allow_registration"|api_get_setting == 'true' %}
<ul class="nav nav-pills nav-stacked">
{% if "allow_registration"|api_get_setting != 'false' %}
<li><a href="{{ _p.web_main }}auth/inscription.php">{{ 'SignUp'|get_lang }}</a></li>
{% endif %}
{% if "allow_lostpassword"|api_get_setting == 'true' %}
<li><a href="{{ _p.web_main }}auth/lostPassword.php">{{ 'LostPassword'|get_lang }}</a></li>
{% endif %}
</ul>
{% endif %}
</div>
</div>
Loading…
Cancel
Save