LTI: Adding advantage services + fix token

pull/3065/head
Angel Fernando Quiroz Campos 6 years ago
parent 6effdeaf4d
commit 2ec596b983
  1. 24
      plugin/ims_lti/Entity/ImsLtiTool.php
  2. 187
      plugin/ims_lti/Entity/Token.php
  3. 5
      plugin/ims_lti/gradebook/service/lineitem.php
  4. 5
      plugin/ims_lti/gradebook/service/result.php
  5. 5
      plugin/ims_lti/gradebook/service/score.php
  6. 15
      plugin/ims_lti/src/ImsLti.php
  7. 82
      plugin/ims_lti/src/Request/LtiTokenRequest.php
  8. 35
      plugin/ims_lti/src/Service/LtiAdvantageService.php
  9. 29
      plugin/ims_lti/src/Service/LtiAssignmentGradesService.php
  10. 61
      plugin/ims_lti/token.php

@ -133,6 +133,13 @@ class ImsLtiTool
*/
private $redirectUrl;
/**
* @var array
*
* @ORM\Column(name="advantage_services", type="json", nullable=true)
*/
private $advantageServices;
/**
* ImsLtiTool constructor.
*/
@ -621,4 +628,21 @@ class ImsLtiTool
return $this;
}
/**
* @return array
*/
public function getAdvantageServices()
{
if (empty($this->advantageServices)) {
return [];
}
return array_merge(
[
'ags' => \LtiAdvantageService::AGS_SIMPLE,
],
$this->advantageServices
);
}
}

@ -0,0 +1,187 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Entity\ImsLti;
use Doctrine\ORM\Mapping as ORM;
/**
* Class Token.
*
* @package Chamilo\PluginBundle\Entity\ImsLti
*
* @ORM\Table(name="plugin_ims_lti_token")
* @ORM\Entity()
*/
class Token
{
const TOKEN_LIFETIME = 3600;
/**
* @var int
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id()
* @ORM\GeneratedValue()
*/
protected $id;
/**
* @var ImsLtiTool
*
* @ORM\ManyToOne(targetEntity="Chamilo\PluginBundle\Entity\ImsLti\ImsLtiTool")
* @ORM\JoinColumn(name="tool_id", referencedColumnName="id")
*/
private $tool;
/**
* @var array
*
* @ORM\Column(name="scope", type="json")
*/
private $scope;
/**
* @var string
*
* @ORM\Column(name="hash", type="string")
*/
private $hash;
/**
* @var int
*
* @ORM\Column(name="created_at", type="integer")
*/
private $createdAt;
/**
* @var int
*
* @ORM\Column(name="expires_at", type="integer")
*/
private $expiresAt;
/**
* @return int
*/
public function getId()
{
return $this->id;
}
/**
* @return ImsLtiTool
*/
public function getTool()
{
return $this->tool;
}
/**
* @param ImsLtiTool $tool
*
* @return Token
*/
public function setTool($tool)
{
$this->tool = $tool;
return $this;
}
/**
* @return array
*/
public function getScope()
{
return $this->scope;
}
/**
* @param array $scope
*
* @return Token
*/
public function setScope($scope)
{
$this->scope = $scope;
return $this;
}
/**
* @return string
*/
public function getHash()
{
return $this->hash;
}
/**
* @param string $hash
*
* @return Token
*/
public function setHash($hash)
{
$this->hash = $hash;
return $this;
}
/**
* @return int
*/
public function getCreatedAt()
{
return $this->createdAt;
}
/**
* @param int $createdAt
*
* @return Token
*/
public function setCreatedAt($createdAt)
{
$this->createdAt = $createdAt;
return $this;
}
/**
* @return int
*/
public function getExpiresAt()
{
return $this->expiresAt;
}
/**
* @param int $expiresAt
*
* @return Token
*/
public function setExpiresAt($expiresAt)
{
$this->expiresAt = $expiresAt;
return $this;
}
/**
* @return string
*/
public function getScopeInString()
{
return implode(' ', $this->scope);
}
/**
* Generate unique hash.
*
* @return Token
*/
public function generateHash()
{
$this->hash = sha1(uniqid(mt_rand()));
return $this;
}
}

@ -0,0 +1,5 @@
<?php
/* For licensing terms, see /license.txt */
require_once __DIR__.'/../../main/inc/global.inc.php';

@ -0,0 +1,5 @@
<?php
/* For licensing terms, see /license.txt */
require_once __DIR__.'/../../main/inc/global.inc.php';

@ -0,0 +1,5 @@
<?php
/* For licensing terms, see /license.txt */
require_once __DIR__.'/../../main/inc/global.inc.php';

@ -3,6 +3,7 @@
use Chamilo\CoreBundle\Entity\Course;
use Chamilo\CoreBundle\Entity\Session;
use Chamilo\PluginBundle\Entity\ImsLti\ImsLtiTool;
use Chamilo\UserBundle\Entity\User;
/**
@ -177,4 +178,18 @@ class ImsLti
return implode(':', $sourceId);
}
/**
* Get instances for LTI Advantage services.
*
* @param ImsLtiTool $tool
*
* @return array
*/
public static function getAdvantageServices(ImsLtiTool $tool)
{
return [
new LtiAssignmentGradesService($tool),
];
}
}

@ -0,0 +1,82 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\PluginBundle\Entity\ImsLti\ImsLtiTool;
use Firebase\JWT\JWT;
/**
* Class LtiTokenRequest.
*/
class LtiTokenRequest
{
/**
* Validate the request's client assertion. Return the right tool.
*
* @param string $clientAssertion
*
* @throws Exception
*
* @return ImsLtiTool
*/
public function validateClientAssertion($clientAssertion)
{
$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');
}
/** @var ImsLtiTool $tool */
$tool = Database::getManager()
->getRepository('ChamiloPluginBundle:ImsLti\ImsLtiTool')
->findOneBy(['clientId' => $claims['sub']]);
if (!$tool || empty($tool->publicKey)) {
throw new Exception('invalid_client');
}
return $tool;
}
/**
* Validate the request' scope. Return the allowed scopes in services.
*
* @param string $scope
* @param ImsLtiTool $tool
*
* @throws Exception
*
* @return array
*/
public function validateScope($scope, ImsLtiTool $tool)
{
if (empty($scope)) {
throw new Exception('invalid_request');
}
$services = ImsLti::getAdvantageServices($tool);
$requested = explode(' ', $scope);
$allowed = [];
/** @var LtiAdvantageService $service */
foreach ($services as $service) {
$allowed = array_merge($allowed, $service->getAllowedScopes());
}
$intersect = array_intersect($requested, $allowed);
if (empty($intersect)) {
throw new Exception('invalid_scope');
}
return $intersect;
}
}

@ -0,0 +1,35 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\PluginBundle\Entity\ImsLti\ImsLtiTool;
/**
* Class LtiAdvantageService.
*/
abstract class LtiAdvantageService
{
const AGS_SIMPLE = 'simple';
const AGS_FULL = 'full';
/**
* @var ImsLtiTool
*/
protected $tool;
/**
* @param ImsLtiTool $tool
*
* @return LtiAdvantageService
*/
public function setTool(ImsLtiTool $tool)
{
$this->tool = $tool;
return $this;
}
/**
* @return array
*/
abstract public function getAllowedScopes();
}

@ -0,0 +1,29 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\PluginBundle\Entity\ImsLti\ImsLtiTool;
class LtiAssignmentGradesService extends LtiAdvantageService
{
const SCOPE_LINE_ITEM = 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem';
const SCOPE_LINE_ITEM_READ = 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly';
const SCOPE_RESULT_READ = 'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly';
const SCOPE_SCORE = 'https://purl.imsglobal.org/spec/lti-ags/scope/score';
public function getAllowedScopes()
{
$scopes = [
self::SCOPE_LINE_ITEM_READ,
self::SCOPE_RESULT_READ,
self::SCOPE_SCORE,
];
$toolServices = $this->tool->getAdvantageServices();
if (self::AGS_FULL === $toolServices['ags']) {
$scopes[] = self::SCOPE_LINE_ITEM;
}
return $scopes;
}
}

@ -2,6 +2,7 @@
/* For licensing terms, see /license.txt */
use Chamilo\PluginBundle\Entity\ImsLti\ImsLtiTool;
use Chamilo\PluginBundle\Entity\ImsLti\Token;
use Firebase\JWT\JWT;
$cidReset = true;
@ -10,14 +11,17 @@ require_once __DIR__.'/../../main/inc/global.inc.php';
$plugin = ImsLtiPlugin::create();
$tokenRequest = new LtiTokenRequest();
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') {
if ('POST' !== $_SERVER['REQUEST_METHOD'] ||
empty($_SERVER['CONTENT_TYPE']) ||
$_SERVER['CONTENT_TYPE'] !== 'application/x-www-form-urlencoded'
) {
throw new Exception('invalid_request');
}
@ -26,7 +30,7 @@ try {
$grantType = !empty($_POST['grant_type']) ? $_POST['grant_type'] : '';
$scope = !empty($_POST['scope']) ? $_POST['scope'] : '';
if (empty($clientAssertion) || empty($clientAssertionType) || empty($grantType) || empty($scope)) {
if (empty($clientAssertionType) || empty($grantType)) {
throw new Exception('invalid_request');
}
@ -36,29 +40,7 @@ try {
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');
}
$tool = $tokenRequest->validateClientAssertion($clientAssertion);
try {
$jwt = JWT::decode($clientAssertion, $tool->publicKey, ['RS256']);
@ -66,18 +48,27 @@ try {
throw new Exception('invalid_client');
}
$requestedScopes = explode(' ', $scope);
$scopes = $requestedScopes;
$allowedScopes = $tokenRequest->validateScope($scope, $tool);
if (empty($scopes)) {
throw new Exception('invalid_scope');
}
$now = time();
$token = new Token();
$token
->generateHash()
->setTool($tool)
->setScope($allowedScopes)
->setCreatedAt($now)
->setExpiresAt($now + Token::TOKEN_LIFETIME);
$em = Database::getManager();
$em->persist($token);
$em->flush();
$json = [
'access_token' => '',
'access_token' => $token->getHash(),
'token_type' => 'Bearer',
'expires_in' => '',
'scope' => implode(' ', $scopes),
'expires_in' => Token::TOKEN_LIFETIME,
'scope' => $token->getScopeInString(),
];
} catch (Exception $exception) {
header("HTTP/1.0 400 Bad Request");

Loading…
Cancel
Save