Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LTI Authentication #53

Merged
merged 6 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/moodle-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ jobs:
uses: Opencast-Moodle/moodle-workflows-opencast/.github/workflows/moodle-ci.yml@main
with:
requires-tool-plugin: true
branch-tool-plugin: master
branch-tool-plugin: main
191 changes: 191 additions & 0 deletions classes/local/lti_helper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.

/**
* LTI helper class for filter opencast.
* @package filter_opencast
* @copyright 2024 Farbod Zamani Boroujeni, ELAN e.V.
* @author Farbod Zamani Boroujeni <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/

namespace filter_opencast\local;

use oauth_helper;
use tool_opencast\local\settings_api;

/**
* LTI helper class for filter opencast.
* @package filter_opencast
* @copyright 2024 Farbod Zamani Boroujeni, ELAN e.V.
* @author Farbod Zamani Boroujeni <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class lti_helper {

/** */
const LTI_LAUNCH_PATH = '/filter/opencast/ltilaunch.php';

/**
* Create necessary lti parameters.
* @param string $consumerkey LTI consumer key.
* @param string $consumersecret LTI consumer secret.
* @param string $endpoint of the opencast instance.
* @param string $customtool the custom tool
* @param int $courseid the course id to add into the parameters
*
* @return array lti parameters
*/
public static function create_lti_parameters($consumerkey, $consumersecret, $endpoint, $customtool, $courseid) {
global $CFG, $USER;

$course = get_course($courseid);

require_once($CFG->dirroot . '/mod/lti/locallib.php');
require_once($CFG->dirroot . '/lib/oauthlib.php');

$helper = new oauth_helper(['oauth_consumer_key' => $consumerkey,
'oauth_consumer_secret' => $consumersecret, ]);

// Set all necessary parameters.
$params = [];
$params['oauth_version'] = '1.0';
$params['oauth_nonce'] = $helper->get_nonce();
$params['oauth_timestamp'] = $helper->get_timestamp();
$params['oauth_consumer_key'] = $consumerkey;

$params['context_id'] = $course->id;
$params['context_label'] = trim($course->shortname);
$params['context_title'] = trim($course->fullname);
$params['resource_link_id'] = 'o' . random_int(1000, 9999) . '-' . random_int(1000, 9999);
$params['resource_link_title'] = 'Opencast';
$params['context_type'] = ($course->format == 'site') ? 'Group' : 'CourseSection';
$params['launch_presentation_locale'] = current_language();
$params['ext_lms'] = 'moodle-2';
$params['tool_consumer_info_product_family_code'] = 'moodle';
$params['tool_consumer_info_version'] = strval($CFG->version);
$params['oauth_callback'] = 'about:blank';
$params['lti_version'] = 'LTI-1p0';
$params['lti_message_type'] = 'basic-lti-launch-request';
$urlparts = parse_url($CFG->wwwroot);
$params['tool_consumer_instance_guid'] = $urlparts['host'];
$params['custom_tool'] = urlencode($customtool);

// User data.
$params['user_id'] = $USER->id;
$params['lis_person_name_given'] = $USER->firstname;
$params['lis_person_name_family'] = $USER->lastname;
$params['lis_person_name_full'] = $USER->firstname . ' ' . $USER->lastname;
$params['ext_user_username'] = $USER->username;
$params['lis_person_contact_email_primary'] = $USER->email;
$params['roles'] = lti_get_ims_role($USER, null, $course->id, false);

if (!empty($CFG->mod_lti_institution_name)) {
$params['tool_consumer_instance_name'] = trim(html_to_text($CFG->mod_lti_institution_name, 0));
} else {
$params['tool_consumer_instance_name'] = get_site()->shortname;
}

$params['launch_presentation_document_target'] = 'iframe';
$params['oauth_signature_method'] = 'HMAC-SHA1';
$params['oauth_signature'] = $helper->sign("POST", $endpoint, $params, $consumersecret . '&');
return $params;
}

/**
* Retrieves the LTI consumer key and consumer secret for a given Opencast instance ID.
*
* @param int $ocinstanceid The ID of the Opencast instance.
*
* @return array An associative array containing the 'consumerkey' and 'consumersecret' for the given Opencast instance.
* If the credentials are not found, an empty array is returned.
*/
public static function get_lti_credentials(int $ocinstanceid) {
$lticonsumerkey = settings_api::get_lticonsumerkey($ocinstanceid);
$lticonsumersecret = settings_api::get_lticonsumersecret($ocinstanceid);
return ['consumerkey' => $lticonsumerkey, 'consumersecret' => $lticonsumersecret];
}

/**
* Checks if LTI credentials are configured for a given Opencast instance.
*
* This function verifies whether both the LTI consumer key and consumer secret
* are set for the specified Opencast instance.
*
* @param int $ocinstanceid The ID of the Opencast instance to check.
*
* @return bool Returns true if both LTI consumer key and secret are configured,
* false otherwise.
*/
public static function is_lti_credentials_configured(int $ocinstanceid) {
$lticredentials = self::get_lti_credentials($ocinstanceid);
return !empty($lticredentials['consumerkey']) && !empty($lticredentials['consumersecret']);
}

/**
* Retrieves an object containing LTI settings for a given Opencast instance.
*
* This function gathers the LTI credentials and API URL for the specified Opencast instance
* and returns them as a structured object.
*
* @param int $ocinstanceid The ID of the Opencast instance for which to retrieve LTI settings.
*
* @return object An object containing the following properties:
* - ocinstanceid: The ID of the Opencast instance.
* - consumerkey: The LTI consumer key for the instance.
* - consumersecret: The LTI consumer secret for the instance.
* - baseurl: The API URL for the Opencast instance.
*/
public static function get_lti_set_object(int $ocinstanceid) {
$lticredentials = self::get_lti_credentials($ocinstanceid);
$baseurl = settings_api::get_apiurl($ocinstanceid);

return (object) [
'ocinstanceid' => $ocinstanceid,
'consumerkey' => $lticredentials['consumerkey'],
'consumersecret' => $lticredentials['consumersecret'],
'baseurl' => $baseurl,
];
}

/**
* Generates the LTI launch URL for the Opencast filter.
*
* This function creates a URL for launching LTI content specific to the Opencast filter,
* incorporating necessary parameters such as course ID, Opencast instance ID, and episode ID.
*
* @param int $ocinstanceid The ID of the Opencast instance.
* @param int $courseid The ID of the course.
* @param string $episodeid The ID of the Opencast episode.
* @param bool $output Optional. If true, returns the URL as a string. If false, returns a moodle_url object. Default is true.
*
* @return string|moodle_url If $output is true, returns the LTI launch URL as a string.
* If $output is false, returns a moodle_url object representing the LTI launch URL.
*/
public static function get_filter_lti_launch_url(int $ocinstanceid, int $courseid, string $episodeid, bool $output = true) {
$params = [
'courseid' => $courseid,
'ocinstanceid' => $ocinstanceid,
'episodeid' => $episodeid,
'sesskey' => sesskey(),
];
$ltilaunchurl = new \moodle_url(self::LTI_LAUNCH_PATH, $params);
if ($output) {
return $ltilaunchurl->out(false);
}
return $ltilaunchurl;
}
}
33 changes: 24 additions & 9 deletions classes/text_filter.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@

namespace filter_opencast;

use filter_opencast\local\lti_helper;
use mod_opencast\local\paella_transform;
use tool_opencast\local\settings_api;
use stdClass;
use moodle_url;


/**
* Automatic opencast videos filter class.
Expand Down Expand Up @@ -57,9 +62,9 @@ private static function get_attribute(string $tag, string $attributename, string
* @return array|null [ocinstanceid, episodeid] or null if there are no matches.
*/
private static function test_url(string $url, array $episodeurls) {
foreach ($episodeurls as [$ocinstanceid, $episoderegex, $baseurl]) {
foreach ($episodeurls as [$ocinstanceid, $episoderegex, $baseurl, $shoulduselti]) {
if (preg_match_all($episoderegex, $url, $matches)) {
return [$ocinstanceid, $matches[1][0]];
return [$ocinstanceid, $matches[1][0], $shoulduselti];
}
}
return null;
Expand All @@ -81,7 +86,7 @@ public function filter($text, array $options = []) {

// First section: (Relatively) quick check if there are episode urls in the text, and only look for these later.
// Improvable by combining all episode urls into one big regex if needed.
$ocinstances = \tool_opencast\local\settings_api::get_ocinstances();
$ocinstances = settings_api::get_ocinstances();
$occurrences = [];
foreach ($ocinstances as $ocinstance) {
$episodeurls = get_config('filter_opencast', 'episodeurl_' . $ocinstance->id);
Expand All @@ -90,6 +95,11 @@ public function filter($text, array $options = []) {
continue;
}

$uselticonfig = get_config('filter_opencast', 'uselti_' . $ocinstance->id);
// Double check.
$hasconfiguredlti = lti_helper::is_lti_credentials_configured($ocinstance->id);
$shoulduselti = $uselticonfig && $hasconfiguredlti;

foreach (explode("\n", $episodeurls) as $episodeurl) {
$episodeurl = trim($episodeurl);

Expand All @@ -105,7 +115,7 @@ public function filter($text, array $options = []) {
if (self::str_contains($text, $baseurl)) {
$episoderegex = "/" . preg_quote($episodeurl, "/") . "/";
$episoderegex = preg_replace('/\\\\\[EPISODEID\\\]/', '([0-9a-zA-Z\-]+)', $episoderegex);
$occurrences[] = [$ocinstance->id, $episoderegex, $baseurl];
$occurrences[] = [$ocinstance->id, $episoderegex, $baseurl, $shoulduselti];
}
}
}
Expand Down Expand Up @@ -139,7 +149,7 @@ public function filter($text, array $options = []) {
if (self::str_starts_with($match, "</$currenttag")) {
$replacement = null;
if ($episode) {
$replacement = $this->render_player($episode[0], $episode[1], $i++, $width, $height);
$replacement = $this->render_player($episode[0], $episode[1], $episode[2], $i++, $width, $height);
}
if ($replacement) {
$newtext .= $replacement;
Expand Down Expand Up @@ -192,14 +202,15 @@ public function filter($text, array $options = []) {
* Render HTML for embedding video player.
* @param int $ocinstanceid Id of ocinstance.
* @param string $episodeid Id opencast episode.
* @param bool $shoulduselti Flag to determine whether to use LTI launch for this video or not.
* @param int $playerid Unique id to assign to player element.
* @param int|null $width Optionally width for player.
* @param int|null $height Optionally height for player.
* @return string|null
*/
protected function render_player(int $ocinstanceid, string $episodeid, int $playerid,
$width = null, $height = null) {
global $OUTPUT, $PAGE;
protected function render_player(int $ocinstanceid, string $episodeid, bool $shoulduselti,
int $playerid, $width = null, $height = null) {
global $OUTPUT, $PAGE, $COURSE;

$data = paella_transform::get_paella_data_json($ocinstanceid, $episodeid);

Expand All @@ -218,7 +229,11 @@ protected function render_player(int $ocinstanceid, string $episodeid, int $play
$mustachedata->data = json_encode($data);
$mustachedata->width = $width;
$mustachedata->height = $height;
$mustachedata->modplayerpath = (new moodle_url('/mod/opencast/player.html'))->out(false);
if ($shoulduselti) {
$mustachedata->ltiplayerpath = lti_helper::get_filter_lti_launch_url($ocinstanceid, $COURSE->id, $episodeid);
} else {
$mustachedata->modplayerpath = (new moodle_url('/mod/opencast/player.html'))->out(false);
}

if (isset($data['streams'])) {
if (count($data['streams']) === 1) {
Expand Down
5 changes: 5 additions & 0 deletions lang/en/filter_opencast.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,14 @@

defined('MOODLE_INTERNAL') || die();
$string['filtername'] = 'Opencast';
$string['ltilaunch_failed'] = 'Performing LTI authentication failed, please try again!';
$string['pluginname'] = 'Opencast Filter';
$string['privacy:metadata'] = 'The Opencast filter plugin does not store any personal data.';
$string['setting_configurl'] = 'URL to Paella config.json';
$string['setting_configurl_desc'] = 'URL of the config.json used by Paella Player. Can either be a absolute URL or a URL relative to the wwwroot.';
$string['setting_episodeurl'] = 'URL templates for filtering';
$string['setting_episodeurl_desc'] = 'URLs matching this template are replaced with the Opencast player. You must use the placeholder [EPISODEID] to indicate where the episode ID is contained in the URL e.g. http://stable.opencast.de/play/[EPISODEID]. If you want to filter for multiple URLs, enter each URL in a new line.';
$string['setting_uselti'] = 'Enable LTI authentication';
$string['setting_uselti_desc'] = 'When enabled, Opencast videos are delivered through LTI authentication using the <strong>default Opencast video player</strong>. This is typically used alongside Secure Static Files in Opencast for enhanced security.';
$string['setting_uselti_nolti_desc'] = 'To enable LTI Authentication for Opencast, you must configure the required credentials (Consumer Key and Consumer Secret) for this instance. Please do so via this link: {$a}';
$string['setting_uselti_ocinstance_name'] = 'Opencast API {$a} Instance';
81 changes: 81 additions & 0 deletions ltilaunch.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.

/**
* LTI Launch page.
* Designed to be called as a link in an iframe to prepare the lti launch data and perform the launch.
*
* @package filter_opencast
* @copyright 2024 Farbod Zamani Boroujeni, ELAN e.V.
* @author Farbod Zamani Boroujeni <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/

use filter_opencast\local\lti_helper;

require(__DIR__ . '/../../config.php');

global $PAGE;

$courseid = required_param('courseid', PARAM_INT);
$ocinstanceid = required_param('ocinstanceid', PARAM_INT);
$episodeid = required_param('episodeid', PARAM_ALPHANUMEXT);

$baseurl = lti_helper::get_filter_lti_launch_url($ocinstanceid, $courseid, $episodeid, false);

$PAGE->set_pagelayout('embedded');
$PAGE->set_url($baseurl);

require_login($courseid, false);

if (confirm_sesskey()) {
$ltisetobject = lti_helper::get_lti_set_object($ocinstanceid);
$customtool = "/play/{$episodeid}";
$endpoint = rtrim($ltisetobject->baseurl, '/') . '/lti';
$ltiparams = lti_helper::create_lti_parameters(
$ltisetobject->consumerkey,
$ltisetobject->consumersecret,
$endpoint,
$customtool,
$courseid
);
$formid = "ltiLaunchForm-{$episodeid}";
$formattributed = [
'action' => $endpoint,
'method' => 'post',
'id' => $formid,
'name' => $formid,
'encType' => 'application/x-www-form-urlencoded',
];
echo html_writer::start_tag('form', $formattributed);

foreach ($ltiparams as $name => $value) {
$attributes = ['type' => 'hidden', 'name' => htmlspecialchars($name), 'value' => htmlspecialchars($value)];
echo html_writer::empty_tag('input', $attributes) . "\n";
}

echo html_writer::end_tag('form');

echo html_writer::script(
"window.onload = function() {
document.getElementById('{$formid}').submit();
};"
);

exit();
}

throw new \moodle_exception('ltilaunch_failed', 'filter_opencast', $baseurl);
Loading
Loading