Posting grades from external tool to LMS

202 views
Skip to first unread message

Asher Coren

unread,
Mar 5, 2014, 10:09:18 AM3/5/14
to valenc...@googlegroups.com
Hi.
I have an external tool which I connected to my D2L site through LTI. When a student completes his work, I want the tool to send the grade back to the LMS.
I'm using this post as reference, but receive a "Not authenticated" message from D2L when sending the grade with the XML payload. How do I authenticate my grade posting?

Thank you.

Desire2Learn Staff: Viktor

unread,
Mar 5, 2014, 10:25:15 AM3/5/14
to valenc...@googlegroups.com

The authentication required is down to how it must be done as described in the LTI 1.1 (http://www.imsglobal.org/LTI/v1p1/ltiIMGv1p1.html) implementation guide.

In particular (from LTI 1.1 impl guide), pay attention to section 4: LTI Security Model and section 6.1: LTI Basic Outcome Service.

Quick summary of requirements, I believe (as per section 4.3: Security for application/xml Messages):

  • form up the POX (plain 'ol XML) body
  • calculate the body hash value
  • set the 'oauth_body_hash' parm to this value
  • sign the request as per Oauth signing rules, and note that the oauth_body_hash MUST be included in the base string to sign together with the other request parms
  • transmit the oauth_body_hash parm along with the OAuth parms in the signed request

We highly recommend that implementors use a reliable OAuth standard library for their particular platform to do the signature generation and verification, rather than attempt to implement the OAuth signing/verification algorithm on their own.

Note that the Oauth parms in this case get transmitted in the request HEADER and not in the body data. The IMS spec itself contains an example of what the body should look like (sec 4.3, see link above):

POST http://www.imsglobal.org/developers/BLTI/service_handle.php HTTP/1.0

Host: 127.0.0.1:80

Content-Length: 757

Authorization: OAuth realm="",oauth_version="1.0",

  oauth_nonce="29f90c047a44b2ece73d00a09364d49b",

  oauth_timestamp="1313350943",oauth_consumer_key="lmsng.school.edu",

  oauth_body_hash="v%2BxFnmDSHV%2Fj29qhxLwkFILrtPo%3D",

  oauth_signature_method="HMAC-SHA1",

  oauth_signature="8auRpRdPY2KRXUrOyz3HKCs92y8%3D"

Content-type: application/xml

 

<?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>    

    <readResultRequest>     

      <resultRecord>

        <sourcedGUID>

          <sourcedId>3124567</sourcedId>

        </sourcedGUID>

      </resultRecord>   

    </readResultRequest>  

  </imsx_POXBody> 

</imsx_POXEnvelopeRequest>


 

Asher Coren

unread,
Mar 5, 2014, 11:24:37 AM3/5/14
to valenc...@googlegroups.com
Thank you for your reply.
Can you recommend me on any NodeJS library  to do the signature generation and verification? I went over quite a few libraries, but none of them seem to support all what I need (2 leg oauth, xml payload, redirects...).

Thank you.

Asher Coren

unread,
Mar 5, 2014, 12:12:44 PM3/5/14
to valenc...@googlegroups.com
Hi,
I tried using node-oauth. I'm running the following code (where "result_sourcedid" is the result_sourcedid I received through the LTI request from D2L, and xw is the XML parser with the xml payload).
I receive a "connect ECONNREFUSED" error.
Do you any idea what I'm doing wrong?

var oauth = require("oauth");

var request = new oauth.OAuth(null, null, consumerKey, consumerSecret, '1.0', null, 'HMAC-SHA1');

request.post(result_sourcedid, null, null, xw.toString(), 'application/xml', function(error, response, body){
if (!error) {
console.log('STATUS: ' + response.statusCode);
console.log('HEADERS: ' + JSON.stringify(response.headers));
console.log('BODY: ' + body);
}
else{
console.log('problem with request: ' , error.message);
}
});

Asher Coren

unread,
Mar 5, 2014, 12:57:02 PM3/5/14
to valenc...@googlegroups.com
Correction, this is the code I'm running (outcome_service_url instead of result_sourcedid as the url):

var oauth = require("oauth");

var request = new oauth.OAuth(null, null, consumerKey, consumerSecret, '1.0', null, 'HMAC-SHA1');

request.post(outcome_service_url , null, null, xw.toString(), 'application/xml', function(error, response, body){
if (!error) {
console.log('STATUS: ' + response.statusCode);
console.log('HEADERS: ' + JSON.stringify(response.headers));
console.log('BODY: ' + body);
}
else{
console.log('problem with request: ' , error.message);
}
});

Asher Coren

unread,
Mar 5, 2014, 1:21:58 PM3/5/14
to valenc...@googlegroups.com
I've also tried using request, with the following code, and now I'm receiving a status of 403 with a "Not authenticated" message.
What am I doing wrong here?

var oAuth = {callback:null , consumer_key: consumerKey , consumer_secret: consumerSecret};
var options  = {
url: outcome_service_url,
method: "POST",
body: xw.toString(),
oauth:oAuth,
followAllRedirects:"true",
headers: {
'Content-Type': 'application/xml',
'Content-Length': Buffer.byteLength(xw.toString())
}
};
request(options, function(error, response, body){
if (!error) {
console.log('STATUS: ' + response.statusCode);
console.log('HEADERS: ' + JSON.stringify(response.headers));
console.log('BODY: ' + body);
}
else{
console.log('problem with request: ' + error.message);
}
});

Asher Coren

unread,
Mar 5, 2014, 3:31:39 PM3/5/14
to valenc...@googlegroups.com
I've advanced another step, and now I'm adding the calculated hash of the POX to the oauth signature, as ouath_body_hash, but I still get the same Not authenticated" message.
What else am I missing?

Desire2Learn Staff: Viktor

unread,
Mar 6, 2014, 3:05:13 PM3/6/14
to valenc...@googlegroups.com
We have a posting on our developer blog from last October about this general topic:


And a code sample (written in PHP) that you can examine to see how grade return can work:


Hopefully this will assist you?


Asher Coren

unread,
Mar 10, 2014, 2:18:42 PM3/10/14
to valenc...@googlegroups.com
Hi Viktor,
I was given an App ID, App Key, User ID and User Key.
Which ones do I use to authenticate my request to the LMS?

Desire2Learn Staff: Sarah-Beth

unread,
Mar 11, 2014, 9:34:09 AM3/11/14
to valenc...@googlegroups.com
Hi Asher 

The answer is that you use all of those values as part of the Authentication process. Those tokens are added to the query string using the x_a, x_b, x_c, and x_d parameters. Those parameters are explained in detail through the graphic and supporting description in the IDKey Authentication topic. If you want to see a working example of an application that performs the Auth process, check out the API Test Tool. You can manually type in those values and see how they get passed into the URL. 

~S-BB

Asher Coren

unread,
Mar 11, 2014, 10:55:14 AM3/11/14
to valenc...@googlegroups.com
Thank you again.
Here's is what I don't understand: The code sample on your developer blog ( <https://github.com/Desire2Learn-Valence/sample-LTI-WHMIS-quiz>) doesn't use the four keys signing process. It simply signs the request with the consumer_key and consumer_secret the are set up in the LMS.
If all I want to do is to send the grade, as in the post you referred  to (<http://devs.valence.desire2learn.com/2013/10/07/so-you-want-to-extend-your-lms-part-1-lti-primer/>), what signing method do I need to use?

Desire2Learn Staff: Sarah-Beth

unread,
Mar 11, 2014, 11:07:38 AM3/11/14
to valenc...@googlegroups.com
My apologies for the confusion - I didn't read the full thread thoroughly before replying.

The sample you refer to is a pure LTI integration - no need to use the App ID, App Key, User ID, or User Key. Those values are part of the authentication process for the Valence Learning Framework APIs.

LTI authentication is its own process, entirely separate from Valence. I see that Viktor replied to your post with some links to LTI Auth info. Those docs are going to be the definitive resources on the topic.

Have you tried integrating a sample LTI app into your environment? It might be useful to see how to deploy a different tool before attempting to build the authentication piece yourself.

Asher Coren

unread,
Mar 11, 2014, 2:56:30 PM3/11/14
to valenc...@googlegroups.com
I'm posting below the code i'm using in order to submit the grade to the LMS. The code that results in a response of 403 - : Not authenticated.
If anyone can point out to me my mistake, I'd highly appreciate it.
Thank you.

 I run the code by calling:
postGrade( {consumer_key: consumer_key} , sourcedid , outcome_service_url , grade);



var request = require('request');
var crypto = require('crypto');
var shortId = require('shortid');
var DBhandle;
shortId.seed(1000);

//logging
var logger = require('../utils/Utils.js').logger;

function sendPostRequest(options){
request.post(options, function(error, response, body){
if (!error) {
logger.info('STATUS: ' + response.statusCode);
logger.info('HEADERS: ' + JSON.stringify(response.headers));
logger.info('BODY: ' + body);
}
else{
logger.error('problem with request: ' + error.message);
}
});

}

function verifyConsumer(options, postFunction){

var oauth = options.oauth;
if (oauth.consumer_key){

if (!oauth.consumer_secret){
DBhandle = require("../DBHandler");
DBhandle.getConsumerSecret({key:oauth.consumer_key} , function(err, rows){

if (err) {
logger.error("can't get consumer secret :" , err);
return;
}

oauth.consumer_secret = rows[0].consumer_secret;

postFunction(options);
});
}
else{//consumer_secret already exists in oAuth object
postFunction(options);
}
}
else{ //no cosumer_key
logger.info("no consumer key");
return;
}

}

function postGrade(oauth , sourcedId , serviceUrl , grade){

var postBody = getPOXGradeRequest(shortId.generate(), "replaceResultRequest", sourcedId);
var sha1 = crypto.createHash('sha1');
sha1.update(postBody);
var body_hash = sha1.digest('base64');
oauth.body_hash = body_hash;

var options  = {
url: serviceUrl,
method: "POST",
body: postBody,
oauth:oauth,
followAllRedirects:"true",
headers: {
'Content-Type': 'application/xml'
}
};

verifyConsumer(options, sendPostRequest);
}

function getPOXGradeRequest(mID, operation, sourcedId) {
return '<?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>'+mID+'</imsx_messageIdentifier>'+
' </imsx_POXRequestHeaderInfo>'+
' </imsx_POXHeader>'+
' <imsx_POXBody>'+
' <'+operation+'>'+
' <resultRecord>'+
' <sourcedGUID>'+
' <sourcedId>'+sourcedId+'</sourcedId>'+
' </sourcedGUID>'+
' <result>'+
' <resultScore>'+
' <language>en-us</language>'+
' <textString>GRADE</textString>'+
' </resultScore>'+
' </result>'+
' </resultRecord>'+
' </'+operation+'>'+
' </imsx_POXBody>'+
'</imsx_POXEnvelopeRequest>';
}

Reply all
Reply to author
Forward
0 new messages