Building the LTI POST

In The Launch Action, I showed how the LTI link is launched:

  1. The AssignmentController.Launch method calculates all the LTI fields, saves them in the ViewBag, and loads the Assignment/Launch View.
  2. The View constructs a <form> from the data in ViewBag.
  3. The browser automatically submits the form when the document loads.

Let’s look at how the LTI fields were calculated, starting with BuildLtiRequestViewBag.


private void BuildLtiRequestViewBag(Assignment assignment)
{
// Start with the basic and required parameters
var parameters = BuildBaseLtiRequestData(assignment);
// Add recommended and optional parameters
AddOptionalParameters(assignment, parameters);
// Add version specific parameters
if (assignment.LtiVersionId == LtiVersion.Version10)
AddLti10Parameters(assignment, parameters);
if (assignment.LtiVersionId == LtiVersion.Version11)
AddLti11Parameters(assignment, parameters);
// Finally, calculate the OAuth signature and send the data over to the view
// for rendering in the client browser. See Views/Assignment/Launch
var uri = new Uri(assignment.Url);
var signatureBase = SignatureBase.Create("POST", uri, parameters);
var signatureProvider = new HmacSha1SigningProvider();
ViewBag.Signature = signatureProvider.ComputeSignature(signatureBase, assignment.Secret,
string.Empty);
ViewBag.Action = uri.ToString();
ViewBag.NameValues = HttpUtility.ParseQueryString(parameters.ToQueryStringFormat());
}

view raw

gistfile1.cs

hosted with ❤ by GitHub

LTI requests must be signed by the consumer with an OAuth digital signature using a secret that the consumer and provider share. When the provider receives the request, it will calculate an OAuth signature for the same request using the shared secret. If the signatures match, the provider can be fairly confident that the request is authentic.

The sample app uses the OAuth.Net library to calculate the OAuth digital signature. OAuth.Net includes the OAuthParameters class which can be used to collect all the data to be signed. To get the ball rolling, BuildLtiRequestViewBag calls BuildBaseLtiRequestData to create an OAuthParameters object and fill it with the required OAuth and LTI parameters.


/// <summary>
/// Calculate the data for a basic LTI 1.x request.
/// </summary>
/// <param name="assignment">The Assignment to be launched.</param>
/// <returns>An OAuthParameters object which includes the required paremters
/// for an LTI 1.x request.</returns>
private OAuthParameters BuildBaseLtiRequestData(Assignment assignment)
{
const string lti_version = "LTI-1p0";
const string lti_message_type = "basic-lti-launch-request";
const string oauth_callback = "about:blank";
const string oauth_signature_method = "HMAC-SHA1";
const string oauth_version = "1.0";
// First I calculate some values that I will need to sign the request
// with OAuth.Net.
var ts = DateTime.UtcNow – new DateTime(1970, 1, 1, 0, 0, 0, 0);
var timestamp = Convert.ToInt64(ts.TotalSeconds).ToString();
var nonce = Guid.NewGuid().ToString("N");
var parameters = new OAuthParameters();
parameters.Callback = oauth_callback;
parameters.ConsumerKey = assignment.ConsumerKey;
parameters.Nonce = nonce;
parameters.SignatureMethod = oauth_signature_method;
parameters.Timestamp = timestamp;
parameters.Version = oauth_version;
// Now add LTI specific parameters, starting with any
// parameters that were included in the URL (the LTI spec says
// to include these when the signature is calculated).
var uri = new Uri(assignment.Url);
parameters.AdditionalParameters.Add(HttpUtility.ParseQueryString(uri.Query));
// LTI Header: These identify the request as being an LTI request
parameters.AdditionalParameters.Add("lti_message_type", lti_message_type);
parameters.AdditionalParameters.Add("lti_version", lti_version);
// Resource: These parameters identify the resource. In K-12, a resource is
// equivalent to assignment and the resource_link_id must be unique to each
// context_id (remember that context is equivalent to course or class). In
// this sample, every user has their own course/class/context, so I simply
// concatenate the class id with the assignment id to form the resource_link_id.
var user = db.Users.Find(WebSecurity.CurrentUserId);
var course = new Course(user);
parameters.AdditionalParameters.Add("resource_link_id", string.Format("{0}-{1}",
course.Id, assignment.AssignmentId));
// Note that the title is recommend, but not required.
parameters.AdditionalParameters.Add("resource_link_title", assignment.Name);
return parameters;
}

view raw

gistfile1.cs

hosted with ❤ by GitHub

The LTI spec has several required parameters, but many more optional ones. The LTI certification tests expect that your implementation will support all of the required parameters and most of the recommended ones. This sample Consumer app supports all of the required and enough of the optional parameters to pass the LTI Tool Consumer certification test.

Next, most of the optional LTI parameters are added to the OAuthParameters list by AddOptionalParameters.


/// <summary>
/// Add the optional parameters for an LTI 1.x request.
/// </summary>
/// <param name="assignment">The Assignment to be launched.</param>
/// <param name="parameters">The partially filled OAuthParameters object
/// that is being used to collect the data.</param>
private void AddOptionalParameters(Assignment assignment, OAuthParameters parameters)
{
var user = db.Users.Find(WebSecurity.CurrentUserId);
// Tool Consumer: These identify this consumer to the provider. In K-12, tools
// such as LMS and Portal systems are typically purchased by the district and
// shared by multiple schools in the district. My advice is to use the district
// identity of the tool here (e.g. "Hillsboro School District LMS"). These
// parameters are recommended.
parameters.AdditionalParameters.Add("tool_consumer_instance_name",
"LTI Consumer Sample");
parameters.AdditionalParameters.Add("tool_consumer_instance_guid",
Request.RequestContext.HttpContext.Request.ApplicationPath);
// Context: These next parameters further identify where the request coming from.
// "Context" can be thought of as the course or class. In this sample app, every
// user automatically has their own "class" or list of assignment.
var course = new Course(user);
parameters.AdditionalParameters.Add("context_id", course.Id);
parameters.AdditionalParameters.Add("context_label", course.Label);
parameters.AdditionalParameters.Add("context_title", course.Title);
parameters.AdditionalParameters.Add("context_type", course.LisType);
// User: These parameters identify the user and their roles within the
// context. These parameters are recommended.
parameters.AdditionalParameters.Add("user_id", User.Identity.Name);
parameters.AdditionalParameters.Add("roles", GetLtiRolesForUser());
// Note that the potentially private information is suppressed if
// the user chooses to hide it.
if (user.SendEmail.GetValueOrDefault(true))
{
parameters.AdditionalParameters.Add("lis_person_contact_email_primary",
user.Email ?? string.Empty);
}
if (user.SendName.GetValueOrDefault(true))
{
parameters.AdditionalParameters.Add("lis_person_name_family",
user.LastName ?? string.Empty);
parameters.AdditionalParameters.Add("lis_person_name_given",
user.FirstName ?? string.Empty);
}
// You can use launch_presentation_locale to send the preferred presentation
// langauge, symbols, etc. I am sending the current UI culture (e.g. en-US).
// This parameter is recommended.
parameters.AdditionalParameters.Add("launch_presentation_locale",
CultureInfo.CurrentUICulture.Name);
}

view raw

gistfile1.cs

hosted with ❤ by GitHub

The remaining parameters depend on the version of LTI. As of today, two versions of the LTI spec have been released: 1.0 and 1.1. LTI 1.1 includes two significant capabilities beyond LTI 1.0:

  1. Basic Outcomes Service – Which allows the provider to send scores back to the consumer (which the consumer can display in its gradebook if it has one).
  2. Custom Parameter Substitution – Which allows the provider to ask for a wide variety of data from the consumer at run time.

The Consumer sample app allows the person creating the assignment to specify LTI 1.0 or LTI 1.1 support. I can’t think of a reason to do this in real life, but I do it here so that you can experiment with both. Depending on the version selected, BuildLtiRequestViewBag will call either AddLti10Parameters for LTI 1.0 or AddLti11Parameters for LTI 1.1.


/// <summary>
/// Add optional parameters that are specific to an LTI 1.0 request.
/// </summary>
/// <param name="assignment">The Assignment to be launched.</param>
/// <param name="parameters">The partially filled OAuthParameters object
/// that is being used to collect the data.</param>
private void AddLti10Parameters(Assignment assignment, OAuthParameters parameters)
{
// LTI 1.0 does not include custom parameter substitution, so the custom parameter
// values are added as-is.
if (!string.IsNullOrWhiteSpace(assignment.CustomParameters))
{
var customParams = assignment.CustomParameters.Split(new[] { ",", "\r\n", "\n" },
StringSplitOptions.RemoveEmptyEntries);
foreach (var customParam in customParams)
{
var namevalue = customParam.Split(new[] { "=" },
StringSplitOptions.RemoveEmptyEntries);
if (namevalue.Length == 2)
{
// Note that per the LTI 1.x specs, custom parameter
// names must be lowercase letters or numbers. Any other
// character is replaced with an underscore.
var name = "custom_" +
Regex.Replace(namevalue[0].ToLower(), "[^0-9a-zA-Z]", "_");
var value = namevalue[1];
parameters.AdditionalParameters.Add(name, value);
}
}
}
}

view raw

gistfile1.cs

hosted with ❤ by GitHub


/// <summary>
/// Add optional parameters that are specific to an LTI 1.1 request.
/// </summary>
/// <param name="assignment">The Assignment to be launched.</param>
/// <param name="parameters">The partially filled OAuthParameters object
/// that is being used to collect the data.</param>
private void AddLti11Parameters(Assignment assignment, OAuthParameters parameters)
{
// LTI 1.1 does support custom parameter substitution
if (!string.IsNullOrWhiteSpace(assignment.CustomParameters))
{
var customParams = assignment.CustomParameters.Split(new[] { ",", "\r\n", "\n" },
StringSplitOptions.RemoveEmptyEntries);
foreach (var customParam in customParams)
{
var namevalue = customParam.Split(new[] { "=" },
StringSplitOptions.RemoveEmptyEntries);
if (namevalue.Length == 2)
{
// Note that per the LTI 1.x specs, custom parameter
// names must be lowercase letters or numbers. Any other
// character is replaced with an underscore.
var name = "custom_" +
Regex.Replace(namevalue[0].ToLower(), "[^0-9a-zA-Z]", "_");
var value = SubstituteCustomValue(namevalue[1]);
parameters.AdditionalParameters.Add(name, value);
}
}
}
// Basic Outcomes Service: These parameters tell the provider where to
// send outcomes (if any) for this assignment.
var urlHelper = new UrlHelper(Request.RequestContext);
parameters.AdditionalParameters.Add("lis_outcome_service_url",
urlHelper.Action("Outcome", "Assignment", null, Request.Url.Scheme));
parameters.AdditionalParameters.Add("lis_result_sourcedid",
assignment.AssignmentId.ToString());
}

view raw

gistfile1.cs

hosted with ❤ by GitHub

After all the parameters are collected, the OAuth Signature Base String is created, the OAuth Signature is calculated, and everything is shoved into the ViewBag.

Posted in Uncategorized | Tagged , , | Leave a comment

The Launch Action

In my last post I mentioned that I had just posted a sample LTI Consumer app on CodePlex. Consumer is an ASP.NET MVC 4.5 app (it also uses Entity Framework 5, jQuery, jQuery UI, Chosen, tiny-mce, and qtip). Most of the app is devoted to user and assignment management.

Users have a user profile in which they can select their U.S. state, school district, and school (did I mention that I am K-12 centric?), and set some privacy choices which will come in handy.

Assignments are web links with some additional meta-data including a key and secret used for OAuth signing, and a sharing scope so that users only see the assignments they should see.

If you are here because you want to see a real LTI request being built, then you are in luck because that is what the rest of this post is about. The LTI specification basically says:

LTI Essentials

  • Learning systems (consumers and providers) should use HTTP or HTTPS to talk to each other.
  • Requests should use the POST method.
  • The form-data must include some name/value pairs that identify the request as as LTI request, specify the context (e.g. who is sending the request, what course are they in, and what their role is in the course).
  • The request should also say what the requester wants…launch a tool, display a document, start a game, etc. This can be in the URL (my preference), a combination of the URL and form-data, or just in the form-data (my least favorite). Note that the tool provider gets to choose. The tool consumer should to be flexible enough to work with any provider.
  • And finally, the request must be signed using an OAuth signature.

In the sample Consumer app, when a user clicks on the Launch button next to each assignment, they invoke the Launch action in the AssignmentController. The controller calculates all the form-data fields needed to launch the tool or content and then loads the Launch view.

        //
        // GET: /Assignment/Launch/5

        public ActionResult Launch(int id = 0)
        {
            Assignment assignment = db.Assignments.Find(id);
            if (assignment == null)
            {
                return HttpNotFound();
            }
        
            // Public assignments are displayed on the front page even if the
            // user is not logged in. If the user is anonymous or if the 
            // assignment does not have an LTI key and secret defined, then
            // the Launch reverts to a simple redirect (GET). I'm curious to
            // see how providers handle this.

            if (Request.IsAuthenticated && assignment.IsLtiLink)
            {
                BuildLtiRequestViewBag(assignment);
                return View(assignment);
            }

            // If the user is not logged in or the assignment does not have an LTI
            // key and secret, then treat the launch as a simple link.
            return Redirect(assignment.Url);
        }

The Launch view builds a simple form which is displayed in the browser and automatically submitted it to the provider.

@model Consumer.Models.Assignment

@{
    ViewBag.Title = Model.Name;
    Layout = "~/Views/Shared/_LayoutEmpty.cshtml";
}

@*
The AssignmentController calculated all the form data, and even the
form action. All this view needs to do is create the form and ask
the browser to submit it.
*@
@<form id="form" action="@ViewBag.Action" method="post">
    @foreach (var name in ViewBag.NameValues.AllKeys)
    {
        <input type="hidden" name="@name" value="@ViewBag.NameValues[name]" />
    }
    <input type="hidden" name="oauth_signature" value="@ViewBag.Signature" />
</form>

@section Scripts {
<script>
    $(function () {
        $("#form").submit();
    });
</script>
}

Which results in a page like this which submits itself automatically.

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <title>IMS Test Tool - LTI Tool Consumer</title>
        <link href="/favicon.ico" rel="shortcut icon" type="image/x-icon" />
        <meta name="viewport" content="width=device-width" />
    </head>
    <body>
        


<form id="form" action="http://www.imsglobal.org/developers/LTI/test/v1p1/tool.php" method="post">
        <input type="hidden" name="oauth_callback" value="about:blank" />
        <input type="hidden" name="oauth_consumer_key" value="12345" />
        <input type="hidden" name="oauth_nonce" value="40fa050d9363433785edfb19fc01f240" />
        <input type="hidden" name="oauth_signature_method" value="HMAC-SHA1" />
        <input type="hidden" name="oauth_timestamp" value="1352044832" />
        <input type="hidden" name="oauth_version" value="1.0" />
        <input type="hidden" name="lti_message_type" value="basic-lti-launch-request" />
        <input type="hidden" name="lti_version" value="LTI-1p0" />
        <input type="hidden" name="resource_link_id" value="1-1" />
        <input type="hidden" name="resource_link_title" value="IMS Test Tool" />
        <input type="hidden" name="tool_consumer_instance_name" value="LTI Consumer Sample" />
        <input type="hidden" name="tool_consumer_instance_guid" value="/" />
        <input type="hidden" name="context_id" value="1" />
        <input type="hidden" name="context_label" value="Andy Miller" />
        <input type="hidden" name="context_title" value="Andy Miller's Class" />
        <input type="hidden" name="context_type" value="CourseSection" />
        <input type="hidden" name="user_id" value="teacher" />
        <input type="hidden" name="roles" value="Instructor" />
        <input type="hidden" name="lis_person_name_family" value="Miller" />
        <input type="hidden" name="lis_person_name_given" value="Andy" />
        <input type="hidden" name="launch_presentation_locale" value="en-US" />
        <input type="hidden" name="custom_state_id" value="OR" />
        <input type="hidden" name="custom_district_id" value="00000000002243" />
        <input type="hidden" name="custom_school_id" value="00000000000000001320" />
        <input type="hidden" name="lis_outcome_service_url" value="http://localhost:54641/Assignment/Outcome" />
        <input type="hidden" name="lis_result_sourcedid" value="1" />
    <input type="hidden" name="oauth_signature" value="4t3Mtpe5tvh4W06KsGyGlGlrZ7c=" />
</form>



        <script src="/Scripts/jquery-1.8.2.js"></script>

        
<script>
    $(function () {
        $("#form").submit();
    });
</script>

    </body>
</html>
Posted in Uncategorized | Tagged , , , , | Leave a comment

First up, LTI

I plan on using this space to blog about some stuff I’m doing with learning tools interoperability, including LTI…

“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, or other educational environments. In LTI these learning applications are called Tools (delivered by Tool Providers) and the LMS, or platforms, are called Tool Consumers.” – IMS Global Developer

First up, a sample LTI Tool Consumer application on CodePlex.

This MVC app simulates a simple LMS or portal. Anyone can register and identify them self as a teacher and/or a student. Teachers can create new assignments and choose to keep them private or share them with their school, district, state, or everyone. Students cannot create assignments, but they can launch any assignment that has been shared.

Once I figure out how to incorporate code samples, I’ll talk about some of the decisions I made and why. As far as a roadmap goes, I plan on adding support for LTI 1.1 features (scoring and custom parameter tokens) to the consumer and building out a sample publisher. After that, I’ll add some LTI 2.0 features. Along the way, I’ll probably play with some ideas like how to use OpenID and LTI.

Posted in Uncategorized | Tagged , | 2 Comments