Last modified: Thursday, July 25, 2024

Sample Perl code of LTI tool provider

LTI, Perl, CGI, LMS, Moodle

Illustration of a camel with a board with LTI written on it in its mouth. This image is generated by DALL-E.

Image generated by DALL-E

I needed to call a Perl CGI script from Moodle via the LTI (Learning Tools Interoperability) interface. However, at that time (spring 2021), there were only samples of the "call an outer tool using LTI" side, and there were not many references on how to create a tool provider. Since I was able to get it to work through trial and error, I have posted the sample code I used to check the operation.

I remember wondering whether to use & or & for the delimiter & in the $text and $key in the code below.

If you use only the authentication part, I think you can apply it in various ways. For example, if you want to show a web page only to students enrolled in a particular Moodle course, you can write a script that outputs HTML after authentication. This is more appropriate than checking whether or not the client's IP address is within the university.

Download (lti_sample.pl) [Download]



#!/usr/bin/perl

use utf8;
use CGI;
use Digest::HMAC_SHA1 qw(hmac_sha1);
use MIME::Base64 qw(encode_base64);
use URI::Escape qw(uri_escape);
use POSIX qw(strftime);

# URL where this script is located
$cgi_url = "https://www.example.com/cgi-bin/lti_test.cgi";

# Consumer Key (like a User ID)
$consumer_key = "ltitest";

# Shared Secret Key (like a password corresponding to a user ID)
$shared_secret = "testtesttest";

my $q = CGI->new;

print $q->header(-type=>'text/html', -charset=>'utf-8');
print $q->start_html(-title=>"LTI test");
print $q->Dump(); # Dump parameters passed to this CGI
print "<hr>\n";

# In practice, compare $consumer_key (user ID assumed by this script) 
# with the value of $q->param('oauth_consumer_key'), and if there is a 
# mismatch, an error is raised. (Omitted here)

my @key_list = $q->param;
$param="";
foreach (sort @key_list) {
    # Sort parameters (other than oauth_signature) passed to CGI (REQUIRED)
    if (! ( /^oauth_signature$/) ) {
        $param .= $_ . "=".uri_escape(scalar $q->param($_))."&";
    }
}
chop($param);
$param = uri_escape($param);

$myurl = uri_escape($cgi_url);

# Concatenate the contents of the environment variable REQUEST_METHOD 
# with the contents of $myurl and $param, separated by &
# (this will be the text part $text)
$text = uri_escape($ENV{'REQUEST_METHOD'})."&".$myurl."&".$param;

# Append & to the end of the uri_escaped shared secret key $shared_secret. 
# (this will be the key part $key)
$key = uri_escape($shared_secret) . "&";

# Call hmac_sha1 with the generated $text and $key. Pass the result to 
# encode_base64 to obtain a signature.
$digest = hmac_sha1($text, $key);
$enc_digest = encode_base64($digest);
chomp $enc_digest;

$timestmp = $q->param('oauth_timestamp');
$sec_from_epoch = time;
if ($timestmp < $sec_from_epoch - 60*3) {
    # Error if this request has expired
    exit 1;
}


$timestr = strftime "%Y/%m/%d %H:%M:%S", localtime($timestmp);
print "<p>Timestamp: $timestr</p>\n";
print "<p>Current Time: ". strftime("%Y/%m/%d %H:%M:%S", localtime(time)) ."</p>\n";

print "<p>Text: $text</p><p>Key: $key</p>\n";
print "Generated signature: <strong>".$enc_digest."</strong>\n";
if ($enc_digest eq $q->param('oauth_signature')) {
    # OK if the signature generated by this script matches the value received 
    # as oauth_signature
    print "<p><strong>Signature matched.</strong></p>\n";
} else {
    # Error if not matched
    print "<p><strong>Signature mismatched.</strong> oauth_signature = "
        . $q->param('oauth_signature') . "</p>\n";
}
print $q->end_html;


Atsushi NUNOME (nunome@kit.ac.jp)