Is LTI Integration possible for Meteor Applications

Hi,

I have looked at this package davidsichau:meteor-lti-auth but did not understand how this works.

Can anyone shed some light on this?

We are using Meteor 1.8.1 with Angular-Meteor and Angular-UI-Router.

Thank you.

Hi.

I use Meteor application as LTI provider. To authenticate LMS (ie. Moodle) users to Meteor I did following simplified steps:

  1. Create Accounts.registerLoginHandler (server side) that accepts token

  2. Create server side route “/lti” that accepts POST message and uses https://www.npmjs.com/package/ims-lti provider to validate request. Store provided data.

  3. Create client side route - login route

My flow is following:

A. Moodle triggers POST to server side route “/lti” (step 2)

B. Upon request check if it is valid and store decoded data. Create token and send it back to client (ie redirect to client side route with token - step 3)

C. Client calls Accounts.callLoginMethod with token (step 1)

D. Upon successful login do something (ie. trigger client side route)

Did you use Iron Router or Angular-UI-Router?

I am using Angular-UI-Router.

I don’t know how to create a server-side route and accept a post message.

What I am doing is calling a method and in Meteor.isServer configured new lti.provider and I’m stuck there. Documentation is not very helpful either.

Any more details/help is much appreciated.

That’s not exactly fair to say. Here’s how to create server side routes, from the documentation: https://docs.meteor.com/packages/webapp.html

Then the internet is awash with information on how to authenticate, build a whole REST API, etc. For instance: https://github.com/stubailo/meteor-rest/blob/master/packages/rest-accounts-password/README.md

Not many have bothered building REST functionality for Meteor because it already has superior options for the data layer.

I’m referring to the ims-lti package for Meteor.

I’m using the WebApp package now. I got the OAuth Signature in the req.body, but for some reason the is_valid flag in the Provider.valid_request method is returning false.

I’ll mess around with it a little more.

This is what I did so far:

		WebApp.connectHandlers.use(bodyParser.urlencoded({ extended: true }));
		WebApp.connectHandlers.use(bodyParser.json());
		WebApp.connectHandlers.use((req, res, next) => {
			res.setHeader("Access-Control-Allow-Origin", "*");
			req.connection.encrypted = true;
			if (req.url == "/lti-launch") {
				let provider = new lti.Provider("my_key", "my_secret");
				provider.valid_request(req, req.body, (err, is_valid) => {
					if (!is_valid) {
						throw new Meteor.Error('lti_error', err.message);
					} else {
						console.log(provider);
					}
				});
			} else {
				return next();
			}
		});

I’m using this method in ng-init of my launch URL template body.

The issue I’m having is when I’m in canvas, the route loads but the code is not executed, when I refresh the frame in canvas, my server is logging the provider (code executes), but I’m getting a blank screen.

Any help is appreciated.

Here’s what we do - we catch the lti request on the server, and then use InjectData to pass the data to the frontend, which uses it to log in. Not sure if this is the best way, but we have used it with classes of 300 students logging in simultaneously. Note that we are currently not validating the LTI signatures - would be a nice addition.

WebApp.connectHandlers.use('/lti', (request, response, next) => {
  if (request.method !== 'POST') {
    response.writeHead(403);
    response.end('LTI sessions must use POST requests');
    return;
  }
  const url = require('url').parse(request.url);
  const slug = url.pathname.substring(1);
  const session = slug && Sessions.findOne({ slug: slug.toUpperCase() });
  if (!session) {
    response.writeHead(404);
    response.end('This session does not exist');
    return;
  } else if (session.settings && session.settings.allowLTI === false) {
    response.writeHead(403);
    response.end(
      'This session does not allow LTI login, check the session settings'
    );
    return;
  }
  let user;
  try {
    user = request.body.lis_person_name_full;
  } catch (e) {
    console.error('Error parsing username in lti request', request.body, e);
    user = uuid();
  }
  let id;
  try {
    id = JSON.parse(request.body.lis_result_sourcedid).data.userid;
  } catch (e) {
    console.error('Error parsing userid in lti request', request.body, e);
    id = uuid();
  }
  try {
    const { userId } = Accounts.updateOrCreateUserFromExternalService('frog', {
      id: user
    });
    Meteor.users.update(userId, { $set: { username: user, userid: id } });
    const stampedLoginToken = Accounts._generateStampedLoginToken();
    Accounts._insertLoginToken(userId, stampedLoginToken);
    InjectData.pushData(request, 'login', {
      token: stampedLoginToken.token,
      slug
    });
    next();
  } catch (e) {
    console.error('Error responding to lti request', request.body, e);
    response.writeHead(400);
    response.end('Error responding to LTI request');
  }
});

Hey @houshuang,

Thanks for this code, I was wondering a couple of things if you do not mind adding some clarifications.

Thanks for your help

Hi. All of our code is on https://github.com/chili-epfl/FROG. It’s fairly involved though.

If you want to test it out, go to https://chilifrog.ch, create a new activity or template, and go to the teacher view. On the top left is a LTI link, you can put this in Moodle etc and it will work. (We’re currently rewamping the whole interface, but it should work).

There is no InjectData collection, it’s based on the meteor/staringatlights:inject-data plugin. It bundles some data with the HTML file sent to the client. You can see how I handle it in frog/imports/client/App/index.js and frog/imports/client/App/api.js

I don’t use any of those packages you mentioned.

I don’t use simple-schema (maybe I should), below is an example of an entry in sessions. It just keeps track of the running sessions which students can connect to using a slug (and which activities are currently active, etc).

FROG is the name of our service.

Sessions:
{
“_id” : “cjxyju00i0000sfiy69idawot”,
“fromGraphId” : “cjxyjgrnx00023f72itzr3g2w”,
“name” : “#Unnamed (2)”,
“graphId” : “cjxyju00m0001sfiy0xo3cr99”,
“state” : “STARTED”,
“ownerId” : “S8agd9ZasetDL3Sx9”,
“timeInGraph” : 17.5,
“countdownStartTime” : -1,
“countdownLength” : 10000,
“pausedAt” : null,
“openActivities” : [
“cjxyju00p0003sfiyhfjr8ga8”
],
“slug” : “OSG4”,
“nextActivities” : ,
“startedAt” : 1562842097419.0
}

Let me know if there’s anything else unclear.

Ok a big thanks, things are getting clearer now. With your code I think I will manage to make the integration !