WIP - Adding LTI 1.3

pull/3065/head
Angel Fernando Quiroz Campos 6 years ago
parent feef7a7af8
commit 9be4f428a1
  1. 7
      composer.json
  2. 54
      plugin/ims_lti/Entity/Deployment.php
  3. 130
      plugin/ims_lti/Entity/ImsLtiTool.php
  4. 113
      plugin/ims_lti/Entity/Platform.php
  5. 133
      plugin/ims_lti/ImsLtiPlugin.php
  6. 20
      plugin/ims_lti/README.md
  7. 4
      plugin/ims_lti/admin.php
  8. 203
      plugin/ims_lti/auth.php
  9. 49
      plugin/ims_lti/create.php
  10. 47
      plugin/ims_lti/jwks.php
  11. 12
      plugin/ims_lti/lang/english.php
  12. 43
      plugin/ims_lti/login.php
  13. 39
      plugin/ims_lti/platform.php
  14. 56
      plugin/ims_lti/src/Form/FrmAdd.php
  15. 168
      plugin/ims_lti/src/LtiAuthException.php
  16. 19
      plugin/ims_lti/start.php
  17. 90
      plugin/ims_lti/token.php
  18. 3
      plugin/ims_lti/view/admin.tpl

@ -64,6 +64,7 @@
"graphp/graphviz": "~0.2.0",
"guzzlehttp/guzzle": "~6.0",
"imagine/imagine": "0.6.3",
"imsglobal/lti-1p3-tool": "dev-master",
"ircmaxell/password-compat": "~1.0.4",
"jbroadway/urlify": "1.1.0-stable",
"jeroendesloovere/vcard": "~1.7",
@ -123,6 +124,12 @@
"behat/mink-selenium2-driver": "*",
"phpunit/phpunit": "^8.4"
},
"repositories": [
{
"type": "vcs",
"url": "https://github.com/IMSGlobal/lti-1-3-php-library"
}
],
"scripts": {
"pre-install-cmd": [
"Chamilo\\CoreBundle\\Composer\\ScriptHandler::deleteOldFilesFrom19x"

@ -0,0 +1,54 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Entity\ImsLti;
use Doctrine\ORM\Mapping as ORM;
/**
* Class Deployment.
*
* @package Chamilo\PluginBundle\Entity\ImsLti
*
* @ORM\Table(name="plugin_ims_lti_deployment")
* @ORM\Entity()
*/
class Deployment
{
/**
* @var int
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id()
* @ORM\GeneratedValue()
*/
protected $id;
/**
* @var string
*/
private $name;
/**
* @var string
*/
public $deployId;
/**
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* @param string $name
*
* @return Deployment
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
}

@ -105,6 +105,34 @@ class ImsLtiTool
*/
private $children;
/**
* @var string
*
* @ORM\Column(name="client_id", type="string", nullable=true)
*/
private $clientId;
/**
* @var string|null
*
* @ORM\Column(name="public_key", type="text", nullable=true)
*/
public $publicKey;
/**
* @var string|null
*
* @ORM\Column(name="login_url", type="string", nullable=true)
*/
private $loginUrl;
/**
* @var string|null
*
* @ORM\Column(name="redirect_url", type="string", nullable=true)
*/
private $redirectUrl;
/**
* ImsLtiTool constructor.
*/
@ -271,6 +299,24 @@ class ImsLtiTool
return implode("\n", $pairs);
}
public function getCustomParamsAsArray()
{
$params = [];
$lines = explode("\n", $this->customParams);
$lines = array_filter($lines);
foreach ($lines as $line) {
list($key, $value) = explode('=', $line, 2);
$key = self::parseCustomKey($key);
$value = self::parseCurstomValue($value);
$params[$key] = $value;
}
return $params;
}
/**
* @return array
*/
@ -326,6 +372,18 @@ class ImsLtiTool
return $newKey;
}
/**
* @param string $value
*
* @return string
*/
private static function parseCurstomValue($value)
{
$newValue = preg_replace('/\s+/', ' ', $value);
return trim($newValue);
}
/**
* Set activeDeepLinking.
*
@ -491,4 +549,76 @@ class ImsLtiTool
return $this;
}
/**
* Get loginUrl.
*
* @return null|string
*/
public function getLoginUrl()
{
return $this->loginUrl;
}
/**
* Set loginUrl.
*
* @param string|null $loginUrl
*
* @return ImsLtiTool
*/
public function setLoginUrl($loginUrl)
{
$this->loginUrl = $loginUrl;
return $this;
}
/**
* Get redirectUlr.
*
* @return string|null
*/
public function getRedirectUrl()
{
return $this->redirectUrl;
}
/**
* Set redirectUrl.
*
* @param string|null $redirectUrl
*
* @return ImsLtiTool
*/
public function setRedirectUrl($redirectUrl)
{
$this->redirectUrl = $redirectUrl;
return $this;
}
/**
* Get clientId.
*
* @return string
*/
public function getClientId()
{
return $this->clientId;
}
/**
* Set clientId.
*
* @param string $clientId
*
* @return ImsLtiTool
*/
public function setClientId($clientId)
{
$this->clientId = $clientId;
return $this;
}
}

@ -0,0 +1,113 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Entity\ImsLti;
use Doctrine\ORM\Mapping as ORM;
/**
* Class Platform
*
* @package Chamilo\PluginBundle\Entity\ImsLti
*
* @ORM\Table(name="plugin_ims_lti_platform")
* @ORM\Entity()
*/
class Platform
{
/**
* @var int
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id()
* @ORM\GeneratedValue()
*/
protected $id;
/**
* @var string
*
* @ORM\Column(name="kid", type="string")
*/
private $kid;
/**
* @var string
*
* @ORM\Column(name="public_key", type="text")
*/
public $publicKey;
/**
* @var string
*
* @ORM\Column(name="private_key", type="text")
*/
private $privateKey;
/**
* Get id.
*
* @return int
*/
public function getId()
{
return $this->id;
}
/**
* Set id.
*
* @param int $id
*
* @return Platform
*/
public function setId($id)
{
$this->id = $id;
return $this;
}
/**
*
* Get kid.
*
* @return string
*/
public function getKid()
{
return $this->kid;
}
/**
* Set kid.
*
* @param string $kid
*/
public function setKid($kid)
{
$this->kid = $kid;
}
/**
* Get privateKey.
*
* @return string
*/
public function getPrivateKey()
{
return $this->privateKey;
}
/**
* Set privateKey.
*
* @param string $privateKey
*
* @return Platform
*/
public function setPrivateKey($privateKey)
{
$this->privateKey = $privateKey;
return $this;
}
}

@ -7,6 +7,7 @@ use Chamilo\CoreBundle\Entity\Session;
use Chamilo\CoreBundle\Entity\SessionRelCourseRelUser;
use Chamilo\CourseBundle\Entity\CTool;
use Chamilo\PluginBundle\Entity\ImsLti\ImsLtiTool;
use Chamilo\PluginBundle\Entity\ImsLti\Platform;
use Chamilo\UserBundle\Entity\User;
use Doctrine\DBAL\DBALException;
use Doctrine\DBAL\Schema\Schema;
@ -21,6 +22,7 @@ use Symfony\Component\Filesystem\Filesystem;
class ImsLtiPlugin extends Plugin
{
const TABLE_TOOL = 'plugin_ims_lti_tool';
const TABLE_PLATFORM = 'plugin_ims_lti_platform';
public $isAdminPlugin = true;
@ -32,7 +34,14 @@ class ImsLtiPlugin extends Plugin
$version = '1.5.1';
$author = 'Angel Fernando Quiroz Campos';
parent::__construct($version, $author, ['enabled' => 'boolean']);
$message = Display::return_message($this->get_lang('GenerateKeyPairInfo'));
$settings = [
$message => 'html',
'enabled' => 'boolean',
];
parent::__construct($version, $author, $settings);
$this->setCourseSettings();
}
@ -81,6 +90,47 @@ class ImsLtiPlugin extends Plugin
$this->createPluginTables();
}
/**
* Save configuration for plugin.
*
* Generate a new key pair for platform when enabling plugin.
*
* @throws \Doctrine\ORM\OptimisticLockException
*
* @return $this|Plugin
*/
public function performActionsAfterConfigure()
{
$em = Database::getManager();
/** @var Platform $platform */
$platform = $em
->getRepository('ChamiloPluginBundle:ImsLti\Platform')
->findOneBy([]);
if ($this->get('enabled') === 'true') {
if (!$platform) {
$platform = new Platform();
}
$keyPair = self::generatePlatformKeys();
$platform->setKid($keyPair['kid']);
$platform->publicKey = $keyPair['public'];
$platform->setPrivateKey($keyPair['private']);
$em->persist($platform);
} else {
if ($platform) {
$em->remove($platform);
}
}
$em->flush();
return $this;
}
/**
* Unistall plugin. Clear the database
*/
@ -291,6 +341,45 @@ class ImsLtiPlugin extends Plugin
api_is_allowed_to_edit(false, true);
}
/**
* @param User $user
*
* @return array
*/
public static function getRoles(User $user)
{
$roles = ['http://purl.imsglobal.org/vocab/lis/v2/system/person#User'];
if (DRH === $user->getStatus()) {
$roles[] = 'http://purl.imsglobal.org/vocab/lis/v2/membership#Mentor';
$roles[] = 'http://purl.imsglobal.org/vocab/lis/v2/institution/person#Mentor';
return $roles;
}
if (!api_is_allowed_to_edit(false, true)) {
$roles[] = 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner';
$roles[] = 'http://purl.imsglobal.org/vocab/lis/v2/institution/person#Student';
if ($user->getStatus() === INVITEE) {
$roles[] = 'http://purl.imsglobal.org/vocab/lis/v2/institution/person#Guest';
}
return $roles;
}
$roles[] = 'http://purl.imsglobal.org/vocab/lis/v2/institution/person#Instructor';
$roles[] = 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor';
if (api_is_platform_admin_by_id($user->getId())) {
$roles[] = 'http://purl.imsglobal.org/vocab/lis/v2/institution/person#Administrator';
$roles[] = 'http://purl.imsglobal.org/vocab/lis/v2/system/person#SysAdmin';
$roles[] = 'http://purl.imsglobal.org/vocab/lis/v2/system/person#Administrator';
}
return $roles;
}
/**
* @param User $user
*
@ -564,4 +653,46 @@ class ImsLtiPlugin extends Plugin
$em->createQuery('DELETE FROM ChamiloPluginBundle:ImsLti\ImsLtiTool tool WHERE tool.course = :c_id')
->execute(['c_id' => (int) $courseId]);
}
/**
* Generate a key pair and key id for the platform.
*
* Rerturn a associative array like ['kid' => '...', 'private' => '...', 'public' => '...'].
*
* @return array
*/
private static function generatePlatformKeys()
{
// Create the private and public key
$res = openssl_pkey_new(
[
'digest_alg' => 'sha256',
'private_key_bits' => 2048,
'private_key_type' => OPENSSL_KEYTYPE_RSA
]
);
// Extract the private key from $res to $privateKey
$privateKey = '';
openssl_pkey_export($res, $privateKey);
// Extract the public key from $res to $publicKey
$publicKey = openssl_pkey_get_details($res);
return [
'kid' => bin2hex(openssl_random_pseudo_bytes(10)),
'private' => $privateKey,
'public' => $publicKey["key"],
];
}
/**
* @return string
*/
public static function getIssuerUrl()
{
$webPath = api_get_path(WEB_PATH);
return trim($webPath, " /");
}
}

@ -55,6 +55,26 @@ external tool.
Run this changes on database:
## To v1.6.0
```sql
ALTER TABLE plugin_ims_lti_tool
ADD client_id VARCHAR(255) DEFAULT NULL,
ADD public_key LONGTEXT DEFAULT NULL,
ADD login_url VARCHAR(255) DEFAULT NULL,
ADD redirect_url VARCHAR(255) DEFAULT NULL;
CREATE TABLE plugin_ims_lti_platform (
id INT AUTO_INCREMENT NOT NULL,
kid VARCHAR(255) NOT NULL,
public_key LONGTEXT NOT NULL,
private_key LONGTEXT NOT NULL,
PRIMARY KEY(id)
) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB;
CREATE TABLE plugin_ims_lti_deployment (
id INT AUTO_INCREMENT NOT NULL,
PRIMARY KEY(id)
) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB;
```
## To v1.5.1
```sql
ALTER TABLE plugin_ims_lti_tool

@ -8,6 +8,10 @@ api_protect_admin_script();
$plugin = ImsLtiPlugin::create();
if ($plugin->get('enabled') !== 'true') {
api_not_allowed(true);
}
$em = Database::getManager();
$tools = $em->getRepository('ChamiloPluginBundle:ImsLti\ImsLtiTool')->findAll();

@ -0,0 +1,203 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\PluginBundle\Entity\ImsLti\ImsLtiTool;
use Chamilo\PluginBundle\Entity\ImsLti\Platform;
use Firebase\JWT\JWT;
require_once __DIR__.'/../../main/inc/global.inc.php';
//api_protect_course_script(false);
api_block_anonymous_users(false);
$scope = empty($_REQUEST['scope']) ? '' : trim($_REQUEST['scope']);
$responseType = empty($_REQUEST['response_type']) ? '' : trim($_REQUEST['response_type']);
$responseMode = empty($_REQUEST['response_mode']) ? '' : trim($_REQUEST['response_mode']);
$prompt = empty($_REQUEST['prompt']) ? '' : trim($_REQUEST['prompt']);
$clientId = empty($_REQUEST['client_id']) ? '' : trim($_REQUEST['client_id']);
$redirectUri = empty($_REQUEST['redirect_uri']) ? '' : trim($_REQUEST['redirect_uri']);
$state = empty($_REQUEST['state']) ? '' : trim($_REQUEST['state']);
$nonce = empty($_REQUEST['nonce']) ? '' : trim($_REQUEST['nonce']);
$loginHint = empty($_REQUEST['login_hint']) ? '' : trim($_REQUEST['login_hint']);
$ltiMessageHint = empty($_REQUEST['lti_message_hint']) ? '' : trim($_REQUEST['lti_message_hint']);
$em = Database::getManager();
try {
if (empty($scope) || empty($responseType) || empty($clientId) || empty($redirectUri) || empty($loginHint) ||
empty($nonce)
) {
throw LtiAuthException::invalidRequest();
}
if ($scope !== 'openid') {
throw LtiAuthException::invalidScope();
}
if ($responseType !== 'id_token') {
throw LtiAuthException::unsupportedResponseType();
}
if (empty($responseMode)) {
throw LtiAuthException::missingResponseMode();
}
if ($responseMode !== 'form_post') {
throw LtiAuthException::invalidRespondeMode();
}
if ($prompt !== 'none') {
throw LtiAuthException::invalidPrompt();
}
$ltiToolLogin = ChamiloSession::read('lti_tool_login');
if ($ltiToolLogin != $ltiMessageHint) {
throw LtiAuthException::invalidRequest();
}
/** @var ImsLtiTool $tool */
$tool = $em
->find('ChamiloPluginBundle:ImsLti\ImsLtiTool', $ltiToolLogin);
if (empty($tool)) {
throw LtiAuthException::invalidRequest();
}
if ($tool->getClientId() != $clientId) {
throw LtiAuthException::unauthorizedClient();
}
$user = api_get_user_entity(api_get_user_id());
if ($user->getId() != $loginHint) {
throw LtiAuthException::accessDenied();
}
if ($redirectUri !== $tool->getRedirectUrl()) {
throw LtiAuthException::unregisteredRedirectUri();
}
/** @var Platform|null $platform */
$platform = $em
->getRepository('ChamiloPluginBundle:ImsLti\Platform')
->findOneBy([]);
$session = api_get_session_entity(api_get_session_id());
$course = api_get_course_entity(api_get_course_int_id());
$toolUserId = ImsLtiPlugin::generateToolUserId($user->getId());
$platformDomain = str_replace(['https://', 'http://'], '', api_get_setting('InstitutionUrl'));
$jwtContent = [];
$jwtContent['iss'] = ImsLtiPlugin::getIssuerUrl();
$jwtContent['sub'] = (string) $user->getId();
$jwtContent['aud'] = $tool->getClientId();
$jwtContent['iat'] = time();
$jwtContent['exp'] = time() + 60;
$jwtContent['nonce'] = md5(microtime().mt_rand());
// User info
if ($tool->isSharingName()) {
$jwtContent['name'] = $user->getFullname();
$jwtContent['given_name'] = $user->getFirstname();
$jwtContent['family_name'] = $user->getLastname();
}
if ($tool->isSharingPicture()) {
$jwtContent['picture'] = UserManager::getUserPicture($user->getId());
}
if ($tool->isSharingEmail()) {
$jwtContent['email'] = $user->getEmail();
}
// Course (context) info
$jwtContent['https://purl.imsglobal.org/spec/lti/claim/context'] = [
'id' => (string) $course->getId(),
'title' => $course->getTitle(),
'label' => $course->getCode(),
'type' => ['http://purl.imsglobal.org/vocab/lis/v2/course#CourseSection'],
];
// Deployment info
$jwtContent['https://purl.imsglobal.org/spec/lti/claim/deployment_id'] = $session
? "{$session->getId()}-{$course->getId()}-{$tool->getId()}"
: "{$course->getId()}-{$tool->getId()}";
$jwtContent['https://purl.imsglobal.org/spec/lti/claim/target_link_uri'] = $tool->getLaunchUrl();
// Resource link
$jwtContent['https://purl.imsglobal.org/spec/lti/claim/resource_link'] = [
'id' => (string) $tool->getId(),
'title' => $tool->getName(),
'description' => $tool->getDescription(),
];
// Platform info
$jwtContent['https://purl.imsglobal.org/spec/lti/claim/tool_platform'] = [
'guid' => $platformDomain,
'contact_email' => api_get_setting('emailAdministrator'),
'name' => api_get_setting('siteName'),
'family_code' => 'Chamilo LMS',
'version' => api_get_version(),
];
// Launch info
$jwtContent['https://purl.imsglobal.org/spec/lti/claim/launch_presentation'] = [
'locale' => api_get_language_isocode($user->getLanguage()),
'document_target' => 'iframe',
//'height' => 320,
//'wdith' => 240,
//'return_url' => api_get_course_url(),
];
// LIS info
$jwtContent['https://purl.imsglobal.org/spec/lti/claim/lis'] = [
'person_sourcedid' => "$platformDomain:$toolUserId",
'course_offering_sourcedid' => "$platformDomain:{$course->getId()}"
.($session ? ":{$session->getId()}" : ''),
];
// LTI info
$jwtContent['https://purl.imsglobal.org/spec/lti/claim/version'] = '1.3.0';
// Roles info
$jwtContent['https://purl.imsglobal.org/spec/lti/claim/roles'] = ImsLtiPlugin::getRoles($user);
// Message type info
$jwtContent['https://purl.imsglobal.org/spec/lti/claim/message_type'] = 'LtiResourceLinkRequest';
// Custom params info
$customParams = $tool->getCustomParamsAsArray();
if (!empty($customParams)) {
$jwtContent['https://purl.imsglobal.org/spec/lti/claim/custom'] = $customParams;
}
// Sign
$jwt = JWT::encode(
$jwtContent,
$platform->getPrivateKey(),
'RS256',
$platform->getKid()
);
$params = [
'id_token' => $jwt,
'state' => $state,
];
} catch (LtiAuthException $authException) {
$params = [
'error' => $authException->getType(),
'error_description' => $authException->getMessage(),
];
}
?>
<!DOCTYPE html>
<html>
<form action="<?php echo $tool->getLaunchUrl() ?>" name="ltiLaunchForm" method="post">
<input type="hidden" name="id_token" value="<?php echo $jwt ?>">
<input type="hidden" name="state" value="<?php echo $state ?>">
</form>
<script>document.ltiLaunchForm.submit();</script>
</html>

@ -37,28 +37,37 @@ if ($form->validate()) {
isset($formValues['share_picture'])
);
if (empty($formValues['consumer_key']) && empty($formValues['shared_secret'])) {
try {
$launchUrl = $plugin->getLaunchUrlFromCartridge($formValues['launch_url']);
} catch (Exception $e) {
Display::addFlash(
Display::return_message($e->getMessage(), 'error')
);
header('Location: '.api_get_path(WEB_PLUGIN_PATH).'ims_lti/admin.php');
exit;
}
$externalTool->setLaunchUrl($launchUrl);
} else {
if ('1p3' === $formValues['version']) {
$externalTool
->setLaunchUrl($formValues['launch_url'])
->setConsumerKey(
empty($formValues['consumer_key']) ? null : $formValues['consumer_key']
)
->setSharedSecret(
empty($formValues['shared_secret']) ? null : $formValues['shared_secret']
);
->setClientId($formValues['client_id'])
->setLoginUrl($formValues['login_url'])
->setRedirectUrl($formValues['redirect_url'])
->publicKey = $formValues['public_key'];
} else {
if (empty($formValues['consumer_key']) && empty($formValues['shared_secret'])) {
try {
$launchUrl = $plugin->getLaunchUrlFromCartridge($formValues['launch_url']);
} catch (Exception $e) {
Display::addFlash(
Display::return_message($e->getMessage(), 'error')
);
header('Location: '.api_get_path(WEB_PLUGIN_PATH).'ims_lti/admin.php');
exit;
}
$externalTool->setLaunchUrl($launchUrl);
} else {
$externalTool
->setLaunchUrl($formValues['launch_url'])
->setConsumerKey(
empty($formValues['consumer_key']) ? null : $formValues['consumer_key']
)
->setSharedSecret(
empty($formValues['shared_secret']) ? null : $formValues['shared_secret']
);
}
}
$em->persist($externalTool);

@ -0,0 +1,47 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\PluginBundle\Entity\ImsLti\Platform;
use Firebase\JWT\JWT;
use phpseclib\Crypt\RSA;
$cidReset = true;
require_once __DIR__.'/../../main/inc/global.inc.php';
$plugin = ImsLtiPlugin::create();
if ($plugin->get('enabled') !== 'true') {
die;
}
/** @var Platform $platform */
$platform = Database::getManager()
->getRepository('ChamiloPluginBundle:ImsLti\Platform')
->findOneBy([]);
if (!$platform) {
exit;
}
$jwks = [];
$key = new RSA();
$key->setHash('sha256');
$key->loadKey($platform->getPrivateKey());
$key->setPublicKey(false, RSA::PUBLIC_FORMAT_PKCS8);
if ($key->publicExponent) {
$jwks = [
'kty' => 'RSA',
'alg' => 'RS256',
'use' => 'sig',
'e' => JWT::urlsafeB64Encode($key->publicExponent->toBytes()),
'n' => JWT::urlsafeB64Encode($key->modulus->toBytes()),
'kid' => $platform->getKid(),
];
}
header('Content-Type: application/json');
echo json_encode(['keys' => [$jwks]]);

@ -4,6 +4,8 @@ $strings['plugin_title'] = 'IMS/LTI';
$strings['plugin_comment'] = 'IMS/LTI';
$strings['enabled'] = 'Enabled';
$strings['client_id'] = 'Client ID';
$strings['client_id_help'] = 'Client ID to be used for tools by default. You can customize the client_id for each tool.';
$strings['ImsLtiDescription'] = '<p>Learning Tools Interoperability® (LTI®) is a specification developed by IMS Global Learning Consortium. The principal concept of LTI is to establish a standard way of integrating rich learning applications (often remotely hosted and provided through third-party services) with platforms like learning management systems, portals, learning object repositories, or other educational environments.</p>';
$strings['ManageToolButton'] = '<p>To manage the tools go to <a href="%s">Tool list</a></p>';
@ -38,3 +40,13 @@ $strings['ToolAddedOnCourseX'] = 'Tool addeed on course <strong>%s</strong>.';
$strings['SupportDeppLinkingHelp'] = 'Contact your Tool Provider to verify if Deep Linking support is mandatory';
$strings['NoAccessToUrl'] = 'No access to URL';
$strings['LaunchUrlNotFound'] = 'Launch URL not found';
$strings['GenerateKeyPairInfo'] = 'A new private and public key pair will be created when enabling.';
$strings['ConfigurePlatform'] = 'Configure platform';
$strings['Keys'] = 'Keys';
$strings['KeyId'] = 'Key ID';
$strings['PublicKey'] = 'Public key';
$strings['PrivateKey'] = 'Private key';
$strings['PlatformDateUpdated'] = 'Platform data updated';
$strings['LtiVersion'] = 'LTI Version';
$strings['LoginUrl'] = 'Login URL';
$strings['RedirectUrl'] = 'Redirect URL';

@ -0,0 +1,43 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\PluginBundle\Entity\ImsLti\ImsLtiTool;
require_once __DIR__.'/../../main/inc/global.inc.php';
api_protect_course_script(false);
api_block_anonymous_users(false);
$em = Database::getManager();
/** @var ImsLtiTool $tool */
$tool = isset($_GET['id'])
? $em->find('ChamiloPluginBundle:ImsLti\ImsLtiTool', $_GET['id'])
: null;
if (!$tool) {
api_not_allowed(true);
}
$user = api_get_user_entity(api_get_user_id());
ChamiloSession::write('lti_tool_login', $tool->getId());
$params = [
'iss' => ImsLtiPlugin::getIssuerUrl(),
'target_link_uri' => $tool->getLaunchUrl(),
'login_hint' => $user->getId(),
'lti_message_hint' => $tool->getId(),
];
?>
<!DOCTYPE html>
<body>
<form action="<?php echo $tool->getLoginUrl() ?>" method="post" name="lti_1p3_login" id="lti_1p3_login"
enctype="application/x-www-form-urlencoded" class="form-horizontal">
<?php foreach ($params as $name => $value) { ?>
<input type="hidden" name="<?php echo $name ?>" value="<?php echo $value ?>">
<?php } ?>
</form>
<script>document.lti_1p3_login.submit();</script>
</body>

@ -0,0 +1,39 @@
<?php
/* For license terms, see /license.txt */
use Chamilo\PluginBundle\Entity\ImsLti\Platform;
$cidReset = true;
require_once __DIR__.'/../../main/inc/global.inc.php';
api_protect_admin_script();
$plugin = ImsLtiPlugin::create();
if ($plugin->get('enabled') !== 'true') {
api_not_allowed(true);
}
/** @var Platform $platform */
$platform = Database::getManager()
->getRepository('ChamiloPluginBundle:ImsLti\Platform')
->findOneBy([]);
$table = new HTML_Table(['class' => 'table table-striped']);
$table->setHeaderContents(0, 0, $plugin->get_lang('KeyId'));
$table->setHeaderContents(0, 1, $plugin->get_lang('PublicKey'));
$table->setHeaderContents(0, 2, $plugin->get_lang('PrivateKey'));
$table->setCellContents(1, 0, $platform ? $platform->getKid() : '');
$table->setCellContents(1, 1, $platform ? nl2br($platform->publicKey) : '');
$table->setCellContents(1, 2, $platform ? nl2br($platform->getPrivateKey()) : '');
$table->updateCellAttributes(1, 1, ['style' => 'font-family: monospace; font-size: 10px;']);
$table->updateCellAttributes(1, 2, ['style' => 'font-family: monospace; font-size: 10px;']);
$interbreadcrumb[] = ['url' => api_get_path(WEB_CODE_PATH).'admin/index.php', 'name' => get_lang('PlatformAdmin')];
$interbreadcrumb[] = ['url' => api_get_path(WEB_PLUGIN_PATH).'ims_lti/admin.php', 'name' => $plugin->get_title()];
$template = new Template($plugin->get_lang('ConfigurePlatform'));
$template->assign('header', $plugin->get_lang('ConfigurePlatform'));
$template->assign('content', $table->toHtml());
$template->display_one_col_template();

@ -43,8 +43,24 @@ class FrmAdd extends FormValidator
if (null === $this->baseTool) {
$this->addUrl('launch_url', $plugin->get_lang('LaunchUrl'), true);
$this->addRadio(
'version',
$plugin->get_lang('LtiVersion'),
['1p0' => 'LTI 1.0 / 1.1', '1p3' => 'LTI 1.3.0']
);
$this->addHtml('<div id="lti_1p0" style="display: block;">');
$this->addText('consumer_key', $plugin->get_lang('ConsumerKey'), false);
$this->addText('shared_secret', $plugin->get_lang('SharedSecret'), false);
$this->addHtml('</div>');
$this->addHtml('<div id="lti_1p3" style="display: none;">');
$this->addTextarea(
'public_key',
$plugin->get_lang('PublicKey'),
['style' => 'font-family: monospace;', 'rows' => 5]
);
$this->addUrl('login_url', $plugin->get_lang('LoginUrl'), false);
$this->addUrl('redirect_url', $plugin->get_lang('RedirectUrl'), false);
$this->addHtml('</div>');
}
$this->addButtonAdvancedSettings('lti_adv');
@ -77,17 +93,37 @@ class FrmAdd extends FormValidator
public function setDefaultValues()
{
$defaults = [];
$defaults['version'] = '1p0';
if (null !== $this->baseTool) {
$this->setDefaults(
[
'name' => $this->baseTool->getName(),
'description' => $this->baseTool->getDescription(),
'custom_params' => $this->baseTool->getCustomParams(),
'share_name' => $this->baseTool->isSharingName(),
'share_email' => $this->baseTool->isSharingEmail(),
'share_picture' => $this->baseTool->isSharingPicture(),
]
);
$defaults['name'] = $this->baseTool->getName();
$defaults['description'] = $this->baseTool->getDescription();
$defaults['version'] = '1p0';
$defaults['custom_params'] = $this->baseTool->getCustomParams();
$defaults['share_name'] = $this->baseTool->isSharingName();
$defaults['share_email'] = $this->baseTool->isSharingEmail();
$defaults['share_picture'] = $this->baseTool->isSharingPicture();
$defaults['public_key'] = $this->baseTool->publicKey;
$defaults['login_url'] = $this->baseTool->getLoginUrl();
$defaults['redirect_url'] = $this->baseTool->getRedirectUrl();
}
$this->setDefaults($defaults);
}
public function returnForm()
{
$js = "<script>
\$(function () {
\$('[name=\"version\"]').on('change', function () {
$('#lti_1p0, #lti_1p3').hide();
$('#lti_' + this.value).show();
})
});
</script>";
return $js.parent::returnForm(); // TODO: Change the autogenerated stub
}
}

@ -0,0 +1,168 @@
<?php
/* For licensing terms, see /license.txt */
/**
* Class LtiAuthException.
*/
class LtiAuthException extends Exception
{
const INVALID_REQUEST = 1;
const INVALID_SCOPE = 2;
const UNSUPPORTED_RESPONSE_TYPE = 3;
const UNAUTHORIZED_CLIENT = 4;
const ACCESS_DENIED = 5;
const UNREGISTERED_REDIRECT_URI = 6;
const INVALID_RESPONSE_MODE = 7;
const MISSING_RESPONSE_MODE = 8;
const INVALID_PROMPT = 9;
/**
* @var string
*/
private $type;
/**
* LtiAuthException constructor.
*
* @param int $code
* @param Throwable|null $previous
*/
public function __construct($code = 0, Throwable $previous = null)
{
switch ($code) {
case self::INVALID_SCOPE:
$this->type = 'invalid_scope';
$message = 'Invalid scope';
break;
case self::UNSUPPORTED_RESPONSE_TYPE:
$this->type = 'unsupported_response_type';
$message = 'Unsupported responde type';
break;
case self::UNAUTHORIZED_CLIENT:
$this->type = 'unauthorized_client';
$message = 'Unauthorized client';
break;
case self::ACCESS_DENIED:
$this->type = 'access_denied';
$message = 'Access denied';
break;
case self::UNREGISTERED_REDIRECT_URI:
$message = 'Unregistered redirect_uri';
break;
case self::INVALID_RESPONSE_MODE:
$message = 'Invalid response_mode';
break;
case self::MISSING_RESPONSE_MODE:
$message = 'Missing response_mode';
break;
case self::INVALID_PROMPT:
$message = 'Invalid prompt';
break;
case self::INVALID_REQUEST:
default:
$this->type = 'invalid_request';
$message = 'Invalid request';
break;
}
parent::__construct($message, $code, $previous);
}
/**
* @return string
*/
public function getType()
{
return $this->type;
}
/**
* @param Throwable|null $previous
*
* @return LtiAuthException
*/
public static function invalidRequest(Throwable $previous = null)
{
return new self(self::INVALID_REQUEST, $previous);
}
/**
* @param Throwable|null $previous
*
* @return LtiAuthException
*/
public static function invalidScope(Throwable $previous = null)
{
return new self(self::INVALID_SCOPE, $previous);
}
/**
* @param Throwable|null $previous
*
* @return LtiAuthException
*/
public static function unsupportedResponseType(Throwable $previous = null)
{
return new self(self::UNSUPPORTED_RESPONSE_TYPE, $previous);
}
/**
* @param Throwable|null $previous
*
* @return LtiAuthException
*/
public static function unauthorizedClient(Throwable $previous = null)
{
return new self(self::UNAUTHORIZED_CLIENT, $previous);
}
/**
* @param Throwable|null $previous
*
* @return LtiAuthException
*/
public static function accessDenied(Throwable $previous = null)
{
return new self(self::ACCESS_DENIED, $previous);
}
/**
* @param Throwable|null $previous
*
* @return LtiAuthException
*/
public static function unregisteredRedirectUri(Throwable $previous = null)
{
return new self(self::UNREGISTERED_REDIRECT_URI, $previous);
}
/**
* @param Throwable|null $previous
*
* @return LtiAuthException
*/
public static function invalidRespondeMode(Throwable $previous = null)
{
return new self(self::INVALID_RESPONSE_MODE, $previous);
}
/**
* @param Throwable|null $previous
*
* @return LtiAuthException
*/
public static function missingResponseMode(Throwable $previous = null)
{
return new self(self::MISSING_RESPONSE_MODE, $previous);
}
/**
* @param Throwable|null $previous
*
* @return LtiAuthException
*/
public static function invalidPrompt(Throwable $previous = null)
{
return new self(self::INVALID_PROMPT, $previous);
}
}

@ -11,6 +11,9 @@ $em = Database::getManager();
/** @var ImsLtiTool $tool */
$tool = isset($_GET['id']) ? $em->find('ChamiloPluginBundle:ImsLti\ImsLtiTool', intval($_GET['id'])) : null;
$user = api_get_user_entity(
api_get_user_id()
);
if (!$tool) {
api_not_allowed(true);
@ -20,12 +23,20 @@ $imsLtiPlugin = ImsLtiPlugin::create();
$pageTitle = Security::remove_XSS($tool->getName());
$is1p3 = !empty($tool->publicKey) && !empty($tool->getClientId()) &&
!empty($tool->getLoginUrl()) && !empty($tool->getRedirectUrl());
if ($is1p3) {
$launchUrl = api_get_path(WEB_PLUGIN_PATH).'ims_lti/login.php?id='.$tool->getId();
} else {
$launchUrl = api_get_path(WEB_PLUGIN_PATH).'ims_lti/form.php?'.http_build_query(['id' => $tool->getId()]);
}
$template = new Template($pageTitle);
$template->assign('tool', $tool);
$template->assign(
'launch_url',
api_get_path(WEB_PLUGIN_PATH).'ims_lti/form.php?'.http_build_query(['id' => $tool->getId()])
);
$template->assign('launch_url', $launchUrl);
$content = $template->fetch('ims_lti/view/start.tpl');

@ -0,0 +1,90 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\PluginBundle\Entity\ImsLti\ImsLtiTool;
use Firebase\JWT\JWT;
$cidReset = true;
require_once __DIR__.'/../../main/inc/global.inc.php';
$plugin = ImsLtiPlugin::create();
try {
if ($plugin->get('enabled') !== 'true') {
throw new Exception('unsupported');
}
$contenttype = isset($_SERVER['CONTENT_TYPE']) ? explode(';', $_SERVER['CONTENT_TYPE'], 2)[0] : '';
if ('POST' !== $_SERVER['REQUEST_METHOD'] || $contenttype !== 'application/x-www-form-urlencoded') {
throw new Exception('invalid_request');
}
$clientAssertion = empty($_POST['client_assertion']) ? $_POST['client_assertion'] : '';
$clientAssertionType = empty($_POST['client_assertion_type']) ? $_POST['client_assertion_type'] : '';
$grantType = empty($_POST['grant_type']) ? $_POST['grant_type'] : '';
$scope = empty($_POST['scope']) ? $_POST['scope'] : '';
if (empty($clientAssertion) || empty($clientAssertionType) || empty($grantType) || empty($scope)) {
throw new Exception('invalid_request');
}
if ('urn:ietf:params:oauth:client-assertion-type:jwt-bearer' !== $clientAssertionType ||
$grantType !== 'client_credentials'
) {
throw new Exception('unsupported_grant_type');
}
$parts = explode('.', $clientAssertion);
if (count($parts) !== 3) {
throw new Exception('invalid_request');
}
$payload = JWT::urlsafeB64Decode($parts[1]);
$claims = json_decode($payload, true);
if (empty($claims) || empty($claims['sub'])) {
throw new Exception('invalid_request');
}
$em = Database::getManager();
/** @var ImsLtiTool $tool */
$tool = $em
->getRepository('ChamiloPluginBundle:ImsLti\ImsLtiTool')
->findOneBy(['clientId' => $claims['sub']]);
if (!$tool || empty($tool->publicKey)) {
throw new Exception('invalid_client');
}
try {
$jwt = JWT::decode($clientAssertion, $tool->publicKey, ['RS256']);
} catch (Exception $exception) {
throw new Exception('invalid_client');
}
$requestedScopes = explode(' ', $scope);
$scopes = $requestedScopes;
if (empty($scopes)) {
throw new Exception('invalid_scope');
}
$json = [
'access_token' => '',
'token_type' => 'Bearer',
'expires_in' => '',
'scope' => implode(' ', $scopes),
];
} catch (Exception $exception) {
header("HTTP/1.0 400 Bad Request");
$json = ['error' => $exception->getMessage()];
}
header('Content-Type: application/json');
echo json_encode($json);

@ -2,6 +2,9 @@
{% autoescape 'html' %}
<div class="btn-toolbar">
<a href="{{ _p.web_plugin }}ims_lti/platform.php" class="btn btn-primary">
<span class="fa fa-cogs fa-fw" aria-hidden="true"></span> {{ 'ConfigurePlatform'|get_plugin_lang('ImsLtiPlugin') }}
</a>
<a href="{{ _p.web_plugin }}ims_lti/create.php" class="btn btn-primary">
<span class="fa fa-plus fa-fw" aria-hidden="true"></span> {{ 'AddExternalTool'|get_plugin_lang('ImsLtiPlugin') }}
</a>

Loading…
Cancel
Save