Canvas and Mastery are experiencing issues due to an ongoing AWS incident. Follow the status at AWS Health Dashboard and Instructure Status Page
Found this content helpful? Log in or sign up to leave a like!
Hello,
I am following Canvas Dev tutorial for LTI https://canvas.instructure.com/courses/785215/assignments/2233114?module_item_id=4761766 and I finally got to passing grade back from provider to Canvas. Now the signing process gets confusing, the tutorial says this quote:
"What you'll sent back to the learning platform is a POST request where the body is XML ( hey look, a nice little builder utility to get you started!) with a Content-Type
header of of application/xml
, signed using OAuth header signatures based on the same consumer key and shared secret you used to authorize the initial launch. Note: this is different than the way you received parameters from the learning platform since those all came across as POST multipart/form parameters, but you'll instead be sending auth information using the Authorization
header, something along the lines of OAuth realm="http://sp.example.com/",oauth_consumer_key="0685bd9184jfhq22",oauth_token="ad180jjd733klru7",oauth_signature_method="HMAC-SHA1",oauth_signature="wOJIO9A2W5mFwDgiDvZbTSMK%2FPY%3D",oauth_timestamp="137131200",oauth_nonce="4572616e48616d6d65724c61686176",oauth_version="1.0"
."
Unfortunately it gives me 401 not authorized response with my configuration. I have a couple of questions:
1. Is it what follows OAuth just plain text with comma separated parameters?
2. If yes (according to OAuth Core 1.0 it looks like it is) do I always include empty oauth_token="" when I don't have one?
3. Also do I include all parameters I get from LTI launch or just the above realm plus anything starting with "oauth_"?
4. In regards to signing XML POST body I am trying to do HMAC-SHA1 signature with LTI shared secret (I don't see consumer key requirement) and then adding it as another parameter
oauth_body_hash="my HMAC-SHA1 signature" to above authorization header.
Is this correct? According to this draft OAuth Request Body Hash it should be only SHA1 signature.
I am sorry for this long question. It just shows I might have a lot of misunderstanding. Also if anybody knows some node.js library for this whole thing it would awesome (I found only for HMAC signature).
Thanks for any advice,
Zbynek
A quick reply based on some notes I have form our implementation:
(1) Yes, but note the URL escaping of parameters
(2) We leave oauth_token out. I think oauth_realm can also be left out.
(3) Just the above, without outh_token, plus oauth_body_hash (as you have in 4)
(4) For the body hash I think you just use the XML data, no need to add the LTI secret.
You can add the Saltire ( saLTIre - for your LTI testing needs ) test tool to Canvas and then see the headers and XML that is sent when you use the Basic Outcomes service.
Using an existing code library is easier, but not as much fun, or should that be not as educational, or not as painful
Also, I've added showing the headers to out test tool at https://lti.netkno.nz/tp
If you want to see the auth header and body hash for the XML we send, you can use the tool with the consumer key / secret combination: myschool.edu / letmein
Hi Peter,
are double quotes required if I do encode URI? Still getting not authorized.
Just for reference here's my request configuration:
var xmlBody = "<?xml version = \"1.0\" encoding = \"UTF-8\"?>" +
"<imsx_POXEnvelopeRequest xmlns=\"http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0\">" +
" <imsx_POXHeader>" +
" <imsx_POXRequestHeaderInfo>" +
" <imsx_version>V1.0</imsx_version>" +
" <imsx_messageIdentifier>999999123</imsx_messageIdentifier>" +
" </imsx_POXRequestHeaderInfo>" +
" </imsx_POXHeader>" +
" <imsx_POXBody>" +
" <replaceResultRequest>" +
" <resultRecord>" +
" <sourcedGUID>" +
" <sourcedId>" + req.body.lis_result_sourcedid + "</sourcedId>" +
" </sourcedGUID>" +
" <result>" +
" <resultScore>" +
" <language>en</language>" +
" <textString>" + ASSIGN_SCORE + "</textString>" +
" </resultScore>" +
" </result>" +
" </resultRecord>" +
" </replaceResultRequest>" +
" </imsx_POXBody>" +
"</imsx_POXEnvelopeRequest>";
// SHA1 hash only
var oauth_body_hash = crypto.createHash('sha1').update(xmlBody).digest('hex');
var oauthParams =
'realm="' + encodeURIComponent(LTI_ENDPOINT_URL) + '",' +
'oauth_consumer_key="' + encodeURIComponent(req.body.oauth_consumer_key) + '",' +
'oauth_signature_method="' + encodeURIComponent(req.body.oauth_signature_method) + '",' +
'oauth_signature="' + encodeURIComponent(req.body.oauth_signature) + '",' +
'oauth_timestamp="' + encodeURIComponent(req.body.oauth_timestamp) + '",' +
'oauth_nonce="' + encodeURIComponent(req.body.oauth_nonce) + '",' +
'oauth_version="' + encodeURIComponent(req.body.oauth_version) + '",' +
'oauth_body_hash="' + encodeURIComponent(oauth_body_hash) + '"';
var options = {
url: req.body.lis_outcome_service_url,
method: 'POST',
headers: {
'Authorization': 'OAuth ' + oauthParams,
'Content-Type': 'application/xml'
},
body: xmlBody
};
Thanks,
Z.
At a quick glance, you have digest('hex'), but the oauth_body_hash should be base64 encoded. And depending on what your language's functions do, you may need to check the length and add '=' to the end as needed.
Still getting not authorized. I wonder what I am doing wrong.
For oauth_signature in Authorization header I am calculating HMAC signature with LTI key and secret.
For POST headers I am using 'Content-Type': 'application/xml' and I noticed URL is not escaped (OAuth Core 1.0 )
var timestamp = Math.floor(new Date().getTime() / 1000);var oauth_body_hash = crypto.createHash('sha1').update(xmlBody).digest('base64');
httpMethod = 'POST';
url = 'https://localhost:3443/lti';
parameters = {};
parameters["oauth_body_hash"] = oauth_body_hash;
parameters["oauth_consumer_key"] = req.body.oauth_consumer_key;
parameters["oauth_signature_method"] = req.body.oauth_signature_method;
parameters["oauth_timestamp"] = timestamp;
parameters["oauth_nonce"] = timestamp;
parameters["oauth_version"] = req.body.oauth_version;
var oauth_signature = oauthSign.hmacsign(httpMethod, url, parameters, LTI_SHARED_SECRET);var oauthParams =
'realm="https://localhost:3443/lti", ' +
'oauth_body_hash="' + encodeURIComponent(oauth_body_hash) + '", ' +
'oauth_consumer_key="' + encodeURIComponent(req.body.oauth_consumer_key) + '", ' +
'oauth_signature_method="' + encodeURIComponent(req.body.oauth_signature_method) + '", ' +
'oauth_timestamp="' + encodeURIComponent(timestamp) + '", ' +
'oauth_nonce="' + encodeURIComponent(timestamp) + '", ' +
'oauth_version="' + encodeURIComponent(req.body.oauth_version) + '", ' +
'oauth_signature="' + encodeURIComponent(oauth_signature) + '"';
var options = {
url: req.body.lis_outcome_service_url,
method: 'POST',
headers: {
'Authorization': 'OAuth ' + oauthParams,
'Content-Type': 'application/xml'
},
body: xmlBody
};
One more thing, the URL for the oauth_signature generation should be the service URL.
Hi Zbynek, were you able to get this to work? If so, I would love to see the completed code, as I am attempting something similar.
Hi Bruce,
we were able to get it working eventually with oauth-sign library. Also the key was to include all POST response parameters except the oauth signature, something like this:
console.log("********** CHECK OAUTH SIGN ***********");const oauthSign = require('oauth-sign');const httpMethod = 'POST',parameters = {};
// taking all params from request body except oauth_signature
for (let property in req.body) {
if (property.toString() != "oauth_signature") {
parameters[property] = req.body[property];
}
}
const signature = oauthSign.hmacsign(httpMethod, lti_launch_url, parameters, lti_consumer_secret);
const isSignMatch = signature.toString() === req.body.oauth_signature.toString();
It would be even easier to get it work with some LTI launch library but couldn't get it right.
Thanks,
Zbynek
Thanks Zbynek,
It appears that the code you provided is for verifying the oauth signature as opposed to handling the grade passback...?
Edit: Was able to get the Zbynek's code to work for verifying the Oauth signature by adjust the lti_launch_url. I simply had to add a backslash to the end, like so: http://localhost:3000/. Unfortunately, this does not answer my original question about how to get the grade passback to work. The npm library that is suggested below is written in coffeescript and has too many open issues.
If you, or anybody else, are still wanting a node.js library for this, I just needed to do an LTI proof-of-concept using node.js and found the ims-lti module (ims-lti - npm) ) worked fine.
Posting grades back was really straightforward:
provider.outcome_service.send_replace_result(0.85, function(err, result) { ....
Hi Peter et al.,
First of all, thank you so much for all your great replies on this forum, they have helped me immensely! I'm a bit of beginner when it comes to coding, etc. I have a node server up and running using express, and a mongo database for my client secret and id.
I'm building an app that allows students to do music exercises, then posts their scores back to Canvas. It works fine with my self-generated API token, but I've stumbled quite a bit with the OAuth flow. I finally have my local instance of Canvas accepting my app's launch via OAuth. I used the library you posted above ims-lti to verify the launch. The library also works to passback a test grade on launch (which is useless since the user hasn't done anything yet). But when it comes to actually persisting the provider instance I run into problems.
My app loads a template with the exercise and the client-side javascript then sends the submission info back to my server via a post request. I need the provider instance to then send the grade info to Canvas from my node server. The problem is that the provider instance is gone, as is all the important launch data. I can save the launch data in a server-side session, I even tried storing the provider instance there, but I think the functionality of the provider instance is lost once it has been moved to req.session (maybe it's all just JSON by then?). provider.send_replace_result returns undefined if I try to use it...is there a suggested and safe way to save that provider instance while the server waits for the student to finish their assignment?
Thanks in advance!
Scott
UPDATE: I think I got it! I just instantiated a new lti.OutcomeService with the below options
To interact with Panda Bot, our automated chatbot, you need to sign up or log in:
Sign inTo interact with Panda Bot, our automated chatbot, you need to sign up or log in:
Sign in