In The Launch Action, I showed how the LTI link is launched:
- The AssignmentController.Launch method calculates all the LTI fields, saves them in the ViewBag, and loads the Assignment/Launch View.
- The View constructs a <form> from the data in ViewBag.
- The browser automatically submits the form when the document loads.
Let’s look at how the LTI fields were calculated, starting with BuildLtiRequestViewBag.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | |
} |
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <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; | |
} |
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <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); | |
} |
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:
- 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).
- 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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <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); | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <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()); | |
} |
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.