Help Needed: LTI 1.3 Deep Linking Response

Ricky.H.288
Ricky.H.288 Posts: 10 🌱
edited June 26 in Development

I'm integrating an LTI 1.3 tool with Brightspace and need help with the deep linking response. The authentication works for both basic launch and deep link request, but the deep link response fails when adding content.

Issue

After the deep link form submis, I see the following error in the LMS: "An error occurred. Please contact your Brightspace administrator."

Request Details

The deep link response request payload is:

{
"iss": "https://myurl.desire2learn.com",
"aud": "7b0be9a4-19ee-4a92-b5ed-aea3bde72bad",
"exp": 1719429969,
"iat": 1719429669,
"nonce": "82574f1a-dc7f-41d7-a491-f78b6370b7c0",
"https://purl.imsglobal.org/spec/lti/claim/message_type": "LtiDeepLinkingResponse",
"https://purl.imsglobal.org/spec/lti/claim/version": "1.3.0",
"https://purl.imsglobal.org/spec/lti/claim/deployment_id": "3a2e7618-6514-4a20-9efc-3e9d9c7b903c",
"https://purl.imsglobal.org/spec/lti-dl/claim/content_items": [
{
"type": "ltiResourceLink",
"title": "Deep Link Content",
"url": "https://myurl.ngrok.io/lti/deeplinkcontent",
"icon": {
"url": "https://example.com/icon.png",
"width": 100,
"height": 100
},
"thumbnail": {
"url": "https://example.com/thumb.png",
"width": 90,
"height": 90
},
"presentation": {
"documentTarget": "iframe",
"width": 800,
"height": 600
},
"lineItem": {
"scoreMaximum": 100,
"label": "Example Line Item",
"resourceId": "abc123",
"tag": "example"
},
"available": {
"startDateTime": "2023-01-01T00:00:00Z",
"endDateTime": "2023-12-31T23:59:59Z"
},
"submission": {
"endDateTime": "2023-12-31T23:59:59Z"
},
"custom": {
"quiz_id": "quiz123",
"duedate": "$ResourceLink.submission.endDateTime"
}
}
],
"https://purl.imsglobal.org/spec/lti-dl/claim/data": "sEYv1nDdkw6t_JiyO5TtPp3nYjGakpXF5vd8NkqgRQ8~"
}

Additional Information

  • The authentication works for both basic launch and deep link request.
  • The deep link response is constructed and sent back to the LMS but results in an error.

Here is the code snippet constructing the deep link response:

[HttpPost("deeplinkresponse")]
public IActionResult DeepLinkResponse([FromForm] string id_token, [FromForm] string state, [FromForm] string contentChoice, [FromForm] string deepLinkingSettings)
{
_logger.LogInformation("Received id_token in DeepLinkResponse method: {IdToken}", id_token);
_logger.LogInformation("Received state in DeepLinkResponse method: {State}", state); if (string.IsNullOrEmpty(id_token) || string.IsNullOrEmpty(state))
{
_logger.LogWarning("Missing id_token or state.");
return BadRequest("Missing id_token or state.");
}

try
{
// Retrieve the LtiRequestInfo from the database using the state
var ltiRequestInfo = _context.LtiRequestInfos.FirstOrDefault(t => t.State == state);
if (ltiRequestInfo == null)
{
_logger.LogWarning("State not found in the database.");
return BadRequest("Invalid state.");
}

// Validate the id_token and extract claims
var jwtToken = LtiHelper.ValidateToken(id_token, _ltiOptions);
var jwtSecurityToken = jwtToken as JwtSecurityToken;

// Extract the deep_link_return_url from the received form data
var deepLinkSettingsDict = JsonConvert.DeserializeObject<Dictionary<string, object>>(deepLinkingSettings);
if (!deepLinkSettingsDict.TryGetValue("deep_link_return_url", out var deepLinkReturnUrl))
{
_logger.LogWarning("Missing deep_link_return_url in deep linking settings.");
return BadRequest("Missing deep_link_return_url.");
}

// Create the deep link response payload
var deepLinkResponse = new
{
type = "ltiResourceLink",
title = "Deep Link Content",
url = $"{_ltiOptions.BaseUrl}/lti/deeplinkcontent",
icon = new
{
url = "https://example.com/icon.png",
width = 100,
height = 100
},
thumbnail = new
{
url = "https://example.com/thumb.png",
width = 90,
height = 90
},
presentation = new
{
documentTarget = "iframe",
width = 800,
height = 600
},
lineItem = new
{
scoreMaximum = 100,
label = "Example Line Item",
resourceId = "abc123",
tag = "example"
},
available = new
{
startDateTime = "2023-01-01T00:00:00Z",
endDateTime = "2023-12-31T23:59:59Z"
},
submission = new
{
endDateTime = "2023-12-31T23:59:59Z"
},
custom = new
{
quiz_id = "quiz123",
duedate = "$ResourceLink.submission.endDateTime"
}
};

// Create the JWT payload using the custom payload class
var payload = new LtiDeepLinkingResponsePayload
{
iss = _ltiOptions.Issuer,
aud = _ltiOptions.ClientId,
exp = new DateTimeOffset(DateTime.UtcNow.AddMinutes(5)).ToUnixTimeSeconds(),
iat = new DateTimeOffset(DateTime.UtcNow).ToUnixTimeSeconds(),
nonce = ltiRequestInfo.Nonce,
MessageType = "LtiDeepLinkingResponse",
Version = "1.3.0",
DeploymentId = _ltiOptions.DeploymentId,
ContentItems = new[] { deepLinkResponse },
Data = deepLinkSettingsDict["data"].ToString()
};

// Create the JWT token
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_ltiOptions.SecretKey));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
var header = new JwtHeader(credentials);

// Serialize the payload manually
var jsonPayload = JsonConvert.SerializeObject(payload);
var encodedPayload = Base64UrlEncoder.Encode(jsonPayload);
var encodedHeader = Base64UrlEncoder.Encode(JsonConvert.SerializeObject(header));
var signature = Base64UrlEncoder.Encode(ComputeSignature(encodedHeader, encodedPayload, _ltiOptions.SecretKey));
var tokenString = $"{encodedHeader}.{encodedPayload}.{signature}";

var formContent = $@"
<html>
<body onload='document.forms[0].submit()'>
<form method='post' action='{deepLinkReturnUrl}'>
<input type='hidden' name='JWT' value='{tokenString}' />
</form>
</body>
</html>";

return Content(formContent, "text/html");
}
catch (SecurityTokenException ex)
{
_logger.LogError(ex, "Security token exception occurred.");
return Unauthorized(ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, "An exception occurred.");
return StatusCode(500, ex.Message);
} } private static byte[] ComputeSignature(string encodedHeader, string encodedPayload, string secret)
{
var bytesToSign = Encoding.UTF8.GetBytes($"{encodedHeader}.{encodedPayload}");
using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret)))
{
return hmac.ComputeHash(bytesToSign);
}
}

Has anyone encountered this issue before or can provide insights into what might be going wrong? Any help would be greatly appreciated.

Thank you!

Tagged:

Answers

  • Hi Richy,

    Could you try switching your 'iss' and 'aud' claims? The audience should be the d2l server and issuer should be the clientId.

    Regards,

    Richard

  • Ricky.H.288
    Ricky.H.288 Posts: 10 🌱

    Hi @Richard.M.314 ,

    Thanks for clarifying that. I've made that change:

    iss = ltiOptions.ClientId,
    aud = _ltiOptions.Issuer
    

    which shows something like this in the decoded JWY

    HEADER:ALGORITHM & TOKEN TYPE

    {
      "alg": "HS256",
      "typ": "JWT"
    }
    

    PAYLOAD:DATA

    {
    "iss": "7b0be9a4-19ee-4a92-b5ed-aea3bde72bad",
    "aud": "https://myurl.desire2learn.com",
    "exp": 1719523749,
    "iat": 1719523449,
    "nonce": "f49d83f8-65ad-4b56-87db-fada083ccf75",
    "https://purl.imsglobal.org/spec/lti/claim/message_type": "LtiDeepLinkingResponse",
    "https://purl.imsglobal.org/spec/lti/claim/version": "1.3.0",
    "https://purl.imsglobal.org/spec/lti/claim/deployment_id": "3a2e7618-6514-4a20-9efc-3e9d9c7b903c",
    "https://purl.imsglobal.org/spec/lti-dl/claim/content_items": [
    {
    "type": "ltiResourceLink",
    "title": "Deep Link Content",
    "url": "https://ecplustest.ngrok.io/lti/deeplinkcontent",
    "icon": {
    "url": "https://example.com/icon.png",
    "width": 100,
    "height": 100
    },
    "thumbnail": {
    "url": "https://example.com/thumb.png",
    "width": 90,
    "height": 90
    },
    "presentation": {
    "documentTarget": "iframe",
    "width": 800,
    "height": 600
    }
    }
    ],
    "https://purl.imsglobal.org/spec/lti-dl/claim/data": "ISMd4An9GyPrvdOEJvqIpZz9o0aYZg-wXrEW48unnS4~"
    }

    I am still seeing "An error occurred. Please contact your Brightspace administrator."

    Anything else that you think could be wrong with this?

  • Hi Ricky,

    I can check the logs if you want to send me those details in a private message.

    Thanks, Richard