cancel
Showing results for 
Show  only  | Search instead for 
Did you mean: 
z_dusatko
Community Participant

Grade Passback

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

Labels (1)
11 Replies
pklove
Community Champion

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 Smiley Happy

pklove
Community Champion

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

z_dusatko
Community Participant

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.

pklove
Community Champion

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.

z_dusatko
Community Participant

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 oauth_body_hash = crypto.createHash('sha1').update(xmlBody).digest('base64');
var timestamp = Math.floor(new Date().getTime() / 1000);
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
};
pklove
Community Champion

One more thing, the URL for the oauth_signature generation should be the service URL.

pklove
Community Champion

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) { ....

smithb_ccsd
Community Participant

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.

z_dusatko
Community Participant

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