GithubHelp home page GithubHelp logo

ims-lti's Introduction

ims-lti

Build Status Coverage Status

This is a nodejs library used to help create Tool Providers for the IMS LTI standard. Tool Consumer implmentation is left as an excersise to the reader :P

Install

npm install ims-lti --save

To require the library into your project

require 'ims-lti'

Supported LTI Versions

Usage

The LTI standard won't be covered here, but it would be good to familiarize yourself with the specs. LTI documentation

This library doesn't help you manage or distribute the consumer keys and secrets. The POST parameters will contain the oauth_consumer_key and your application should use that to look up the consumer secret from your own datastore.

This library offers a few interfaces to use for managing the OAuth nonces to make sure the same nonce isn't used twice with the same timestamp. Read the LTI documentation on OAuth. They will be covered below.

Setting up a Tool Provider (TP)

As a TP your app will receive a POST request with LTI launch data that will be signed with OAuth using a key/secret that both the TP and Tool Consumer (TC) share. This is all covered in the LTI security model

Once you find the oauth_consumer_secret based on the oauth_consumer_key in the POST request, you can initialize a Provider object with them and a few other optional parameters:

lti = require 'ims-lti'

provider = new lti.Provider consumer_key, consumer_secret, [nonce_store=MemoryStore], [signature_method=HMAC_SHA1]

Once the provider has been initialized, a reqest object can be validated against it. During validation, OAuth signatures are checked against the passed consumer_secret and signautre_method ( HMAC_SHA1 assumed ). isValid returns true if the request is an lti request and is properly signed.

provider.valid_request req, (err, isValid) ->
  # isValid = Boolean | always false if err
  # err = Error object with method descibing error if err, null if no error

After validating the reqest, the provider object both stores the requests parameters (excluding oauth parameters) and provides convinience accessors to common onces including provider.student, provider.ta, provider.username, and more. All request data can be accessed through provider.body in an effort to namespace the values.

Currently there is not an emplementation for posting back to the Tool Consumer, although there is a boolean accessor provider.outcome_service that will return true if the TC will accept a POSTback.

Nonce Stores

ims-lti does not standardize the way in which the OAuth nonce/timestamp is to be stored. Since it is a crutial part of OAuth security, this library implements an Interface to allow the user to implement their own nonce-stores.

Nonce Interface

All custom Nonce stores should extend the NonceStore class and implment isNew and setUsed

class NonceStore
  isNew:   (nonce,timestamp,callback)=>
    # Sets any new nonce to used
  setUsed: (nonce,timestamp,callback)=>

Two nonce stores have been implemented for convinience.

MemoryNonceStore

The default nonce store (if none is specified) is the Memory Nonce Store. This store simply keeps an array of nocne/timestamp keys. Timestamps must be valid within a 5 minute grace period.

RedisNonceStore

A superior nonce store is the RedisNonceStore. This store requires a secondary input into the constructor, a redis-client. The redis client is used to store the nonce keys and set them to expire within a set amount of time (default 5 minutes). A RedisNonceStore is initialized like:

RedisNonceStore = require '../lib/redis-nonce-store'
client          = require('redis').createClient()
store           = new RedisNonceStore('consumer_key', client)

provider = new lti.Provider consumer_key, consumer_secret, store

Outcomes Extension

The outcomes feature is part of the LTI 1.1 specification and is new to ims-lti 1.0. All of the behind-the-scenes work necessary to get the ball rolling with it is already implemented for you, all you need to do is submit grades.

provider = new lti.Provider consumer_key, consumer_secret

provider.valid_request req, (err, is_valid) ->
  # Check if the request is valid and if the outcomes service exists.
  if (!is_valid || !provider.outcome_service) return false

  # Check if the outcome service supports the result data extension using the
  # text format. Available formats include text and url.
  console.log provider.outcome_service.supports_result_data('text')

  # Replace accepts a value between 0 and 1.
  provider.outcome_service.send_replace_result .5, (err, result) ->
    console.log result # True or false

  provider.outcome_service.send_read_result (err, result) ->
    console.log result # Value of the result already submitted from this embed

  provider.outcome_service.send_delete_result (err, result) ->
    console.log result # True or false

  provider.outcome_service.send_replace_result_with_text .5, 'Hello, world!', (err, result) ->
    console.log result # True or false

  provider.outcome_service.send_replace_result_with_url .5, 'https://google.com', (err, result) ->
    console.log result # True or false

Content Extension

The content extension is an extension supported by most LMS platforms. It provides LTI providers a way to send content back to the LMS in the form of urls, images, files, oembeds, iframes, and even lti launch urls.

provider = new lti.Provider consumer_key, consumer_secret

provider.valid_request req, (err, is_valid) ->
  #check if the request is valid and if the content extension is loaded.
  if (!is_valid || !provider.ext_content) return false

  provider.ext_content.has_return_type 'file' # Does the consumer support files
  provider.ext_content.has_file_extension 'jpg' # Does the consumer support jpg

  # All send requests take a response object as the first parameter. How the
  # response object is manipulated can be overrided by replacing
  # lti.Extensions.Content.redirector with your own function that accepts two
  # parameters, the response object and the url to redirect to.
  provider.ext_content.send_file res, file_url, text, content_mime_type

  provider.ext_content.send_iframe res, iframe_url, title_attribute, width, height

  provider.ext_content.send_image_url res, image_url, text, width, height

  provider.ext_content.send_lti_launch_url res, launch_url, title_attribute, text

  provider.ext_content.send_oembed res, oembed_url, endpoint

  provider.ext_content.send_url res, hyperlink_url, text, title_attribute, target_attribute

Running Tests

To run the test suite first installing the dependencies:

npm install
make test

ims-lti's People

Contributors

jclevy62 avatar jrundquist avatar ktonon avatar lzhang avatar ob1 avatar omsmith avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

ims-lti's Issues

Additional Maintainers?

Hey @omsmith! ๐Ÿ‘‹
Thanks for putting together an awesome project! ๐Ÿ™‡
It looks like things have been a bit quiet around here for a while now.
Would having additional maintainer(s) on the project be helpful?

If it would be helpful, I'd be happy to help maintain the project.

"request" runtime dependency unnecessary or undeclared

The "request" module is declared as a dev dependency in "package.json", but is required at runtime by "ims-lti.coffee" though it seems to be unused there.

Thus, if my project doesn't explicitly require "request" on it's own, the ims-lti code breaks on requiring a nonexistent module.

Consumer javascript implementation for comment

Hi everyone,

I've had a go at implementing the Consumer class in Javascript, reusing as much code as I could from the published Javascript Provider class. I'm short on time right now to bring this to the proper format for a PR, especially because I'm not versed in Coffeescript, but I'd really appreciate comments on the approach.

Also welcome any help bringing this to good enough condition for a pull request.

Implementation:

    var lti = require( 'ims-lti' );
    var consumer = new lti.Consumer( 'oauth_key', 'oauth_secret' );
    var requestBody = {
        lti_version     : 'LTI-1p0',
        lti_message_type: 'basic-lti-launch-request',
        resource_link_id: '0',
        resource_link_title: 'My title',
        context_id: '1234',
        context_title: "My Test",
        context_label: "No label",
        roles: 'Student'
    };
    var ltiUrl = 'https://lti.source.com/launch';
    consumer.validate_request( requestBody, function( err, valid ) {
        if( err || ! valid ) next( err || new Error( "Invalid LTI request" ) );
        else {
            consumer.send( { url: ltiUrl }, function( err, msg, response ) {
                res.send( response );
            }
        }
    }

consumer.js

Outcomes extension fails to submit over HTTPS

Method _send_request seems to be broken if HTTPS is used. It gives the following error message:

[Error: Protocol "http:" not supported. Expected "https:".]

I'm not sure if the current way of selecting the protocol works as expected. This happens when lis_outcome_service_url contains https.

Support x-forwarded-host header

AFAIK, ims-lti relies on req values being x-forwarded-* aware; with express it involves setting 'trust proxy' to a truthy value.

It works for https proxy but it won't affect the host value. Although, express will set req.hostname but it doesn't include the port. ims-lti uses req.headers.host to sign the request.

If ims-lti has to use header values, there should be the option to lookup x-forwarded-* values instead.

Should be able to access Hmac from ims-lti

I am implementing my own Consumer.js. I can access all the objects except HMac.

It would be good if reference to hmacSha1 is included in src/ims-lti.js (all other files in library are included except hmacSha1.js)

 exports = module.exports = {
    version: '0.0.0',
    Provider: require('./provider'),
    Consumer: require('./consumer'),
    Errors: require('./errors'),

     HMacSha1 : require('./hmac-sha1'),

     Stores: {
         RedisStore: require('./redis-nonce-store'),
         MemoryStore: require('./memory-nonce-store'),
         NonceStore: require('./nonce-store')
     },
     supported_versions: ['LTI-1p0']
  };

So that in consumer.js one can do

var lti = require('ims-lti');
var signer = new lti.HmacSha1();

Sending content urls back to consumer beyound express or from the client

#I'm new to LTIs and have an implementation question and hope I can find some direction here.

I'm using meteor, express (to handle POST request) and React(client)/React Router, and experimenting with an LTI that allows instructors to send a URL link of some search result content back to the course page, after launching the LTI from the editor button.

After clicking on the editor button, in canvas, on the server backend, I have successfully been able to take the express's req and res objects and create a valid LTI provider, after validating the post request within a middleware function, where I have access to the request object.

Next, my search app comes up, and at this point, I would like to send a URL hyperlink back to be included in the course content. The only thing is, I only know how to do that on the server using:

provider.ext_content.send_url(res, hyperlink_url, text, title_attribute, target_attribute)

because it seems like it's the only place I can create the lti.provider is within Express, where I have access to the res and req.

Is there a way to send the consumer a URL link using the
provider.ext_content.send_url(res, hyperlink_url, text, title_attribute, target_attribute) outside of express, particularly from the client?

Any help is welcomed, thank you.

Is there any way to expose the base string when a signature fails?

To debug problems with LTI Launches it is essential to be able to compare base strings. The different signature values have no information at all. It would be nice to return the base string in event of provider fail. Something like:

[ { message: 'Invalid Signature' , base: "GET&http..."}, false ]

Would be a nice, clean extension.

redis-nonce-store should catch redis errors

Calling code may want to know when calls to @redis.get or @redis.set fail. For example, in this code, err should be handled when @redis.get fails, perhaps because the redis client can't connect to the redis instance. And then despite this comment, I think you actually want to know when that write failed - otherwise, you're vulnerable to replay attacks.

Additionally, this code currently treats all nonceStore errors from @nonceStore.isNew in _valid_oauth as an "Expired nonce," which may not be the case if it can error out for other reasons (i.e. a call to @redis.get failed). It looks like you already have a StoreError, so you could use that in RedisNonceStore.isNew and then switch on the error type in the @nonceStore.isNew callback to determine which error to send to the next callback and ultimately up to the calling code.

I'm happy to submit a PR for this, just want to make sure you guys are ok with the architectural ideas above. I guess, technically, it's kind of a breaking change if people are relying on "Expired nonce" even when its not?

Is verifying the nonce necessary when the request is done over ssl?

For our application the request can ONLY happen over SSL (we implement no other connection options). So I'm trying to determine if there is any purpose in verifying the oauth_nonce. I believe that the purpose of the nonce is entirely to prevent replay attacks which is already a feature of SSL.

Storing the nonce values will cost money and waste time for each user so I only want to do it if it has some value. Is there value in storing nonces and rejecting any duplicate requests when the request is made over SSL?

I asked this question on StackOverflow as well, but thought someone here might have more specific information:
https://stackoverflow.com/q/44469654/796999

LTI 1.0 signing not up to specs for hmac-sha1

The oauth1 specs state that the consumer secret and token need to be parameter encoded before passing them to hmac-sha1 for signing:

[...] the key is the concatenated values (each first encoded per Parameter Encoding) of the Consumer Secret and Token Secret, separated by an '&' character (ASCII code 38) even if empty.

In provider.coffee and hmac-sha1.coffee however, the consumer secret is passed directly to the signing algorithm without encoding it first.

This means that if the shared secret includes characters that should be encoded (e.g., "secret!key"), the signature test fill fail for a correctly signed message.

What type of request objects are expected?

I'm having trouble understanding what type of request objects are expected. I'm using express, and the body-parser middleware, but my signatures are being signed incorrectly. It would be nice if the expected request object format were documented. Are these just basic node request objects?

Error class gotcha

It looks like the error classes don't behave like a Javascript Error:

it.only 'should return false if nonce already seen', (done) =>
      req =
        url: '/test'
        method: 'POST'
        connection:
          encrypted: undefined
        headers:
          host: 'localhost'
        body:
          lti_message_type: 'basic-lti-launch-request'
          lti_version: 'LTI-1p0'
          resource_link_id: 'http://link-to-resource.com/resource'
          oauth_customer_key: 'key'
          oauth_signature_method: 'HMAC-SHA1'
          oauth_timestamp: Math.round(Date.now()/1000)
          oauth_nonce: Date.now()+Math.random()*100

      #sign the fake request
      signature = @provider.signer.build_signature(req, req.body, 'secret')
      req.body.oauth_signature = signature

      @provider.valid_request req, (err, valid) =>
        should.not.exist err
        valid.should.equal true
        @provider.valid_request req, (err, valid) ->
          should.exist err
          console.log('Message: ', err.message);
          console.log('Stack: ', err.stack);
          err.should.be.instanceof(lti.Errors.NonceError)
          valid.should.equal false
          done()

which will print out:

Message:  Expired nonce
Stack:  undefined

vs changing this line to:

callback new Error('Expired nonce'), false

which will print out:

Message:  Expired nonce
Stack:  Error: Expired nonce
    at /Users/jason/oss/ims-lti/lib/provider.js:204:27
    at MemoryNonceStore.isNew (/Users/jason/oss/ims-lti/lib/memory-nonce-store.js:82:16)
    at Provider._valid_oauth (/Users/jason/oss/ims-lti/lib/provider.js:200:30)
    at Provider.valid_request (/Users/jason/oss/ims-lti/lib/provider.js:169:19)
    at Provider.valid_request (/Users/jason/oss/ims-lti/lib/provider.js:88:59)
    at /Users/jason/oss/ims-lti/test/Provider.coffee:326:19
    at /Users/jason/oss/ims-lti/lib/provider.js:207:18
    at /Users/jason/oss/ims-lti/lib/memory-nonce-store.js:98:20
    at MemoryNonceStore.setUsed (/Users/jason/oss/ims-lti/lib/memory-nonce-store.js:117:14)
    at MemoryNonceStore.isNew (/Users/jason/oss/ims-lti/lib/memory-nonce-store.js:85:19)

    at Provider._valid_oauth (/Users/jason/oss/ims-lti/lib/provider.js:200:30)
    at Provider.valid_request (/Users/jason/oss/ims-lti/lib/provider.js:169:19)
    at Provider.valid_request (/Users/jason/oss/ims-lti/lib/provider.js:88:59)
    at Context.<anonymous> (/Users/jason/oss/ims-lti/test/Provider.coffee:323:17)
    at callFnAsync (/Users/jason/oss/ims-lti/node_modules/mocha/lib/runnable.js:349:8)
    at Test.Runnable.run (/Users/jason/oss/ims-lti/node_modules/mocha/lib/runnable.js:301:7)
    at Runner.runTest (/Users/jason/oss/ims-lti/node_modules/mocha/lib/runner.js:422:10)
    at /Users/jason/oss/ims-lti/node_modules/mocha/lib/runner.js:528:12
    at next (/Users/jason/oss/ims-lti/node_modules/mocha/lib/runner.js:342:14)
    at /Users/jason/oss/ims-lti/node_modules/mocha/lib/runner.js:352:7
    at next (/Users/jason/oss/ims-lti/node_modules/mocha/lib/runner.js:284:14)
    at Immediate.<anonymous> (/Users/jason/oss/ims-lti/node_modules/mocha/lib/runner.js:320:5)
    at runCallback (timers.js:651:20)
    at tryOnImmediate (timers.js:624:5)
    at processImmediate [as _immediateCallback] (timers.js:596:5)

This can cause issues if calling code is expecting to find a useful stack trace or is using something like nested error stacks. There might be other subtle differences, too, but the lack of a stack trace is the one that puzzled me today.

It seems like the class inheritance isn't working as expected? It could be related to this coffeescript bug, but I know literally nothing about coffeescript, so hard to tell.

Tool Provider is signature does not match with ToolConsumer Test Demo Site.

I am trying to use this ToolProvider node plugin to verify Oauth signature and got stuck in one issue for long time.

This plugin is working fine and generating the correct signature of request payload when I am submitting post request from (http://ltiapps.net/test) or from canvas.instructure.com during course assignments tutorial. But this plugin signature is not matching when I am testing from demo site at acme.instructure.com ( https://officehours.acme.instructure.com/ ), Any idean what could go wrong here?

Clear nonces in MemoryNonceStore

Although the module for using Redis exists, in smaller environments MemoryNonceStore is enough. However, it leaks memory because the used nonces are never removed.

If the used nonces were saved with the timestamp, checking new nonces could remove the expired nonces with some probability.

Does it work with Canvas authentication?

Hi, I am using this library to go through the Canvas LTI tutorial. However, there is difference between the oauth_signature generated by the library and the one passed by Canvas. I used https link in Canvas and the req_url that the library uses for the same request is http. I tried hard-coding the link to https in the library. I get a different signature, but still doesn't match with the signature sent from Canvas. There is no oauth_token passed from Canvas. Is it normal?

Was this library tested with Canvas before? Any tips on debugging the issue will be highly helpful.

Thanks!

Are we expected to call nonceStore.setUsed manually?

...from within provider.validate_request? It might make more sense to call it from within there, i.e. just inside the passed next function for this.nonceStore.isNew in Provider.prototype._valid_oauth

If we're expected to call it from within our own implementation of isNew itself, there's little value in documenting that it must be exposed externally, instead part of the contract of isNew should be that on subsequent calls for the same value it should return false.

Reading a student grade when she has none yet results in an error.

When calling send_read_result() for a student that doesn't have any grade yet, some LMS (specifically Moodle) send back a XML response with a body value of "".

Here's an extract of the received XML sent back by a Moodle instance where the student did not have any grade for the activity :

<resultScore>
  <language>en</language>
  <textString/>
</resultScore>

As you can see, the value of textString is "".
This causes the lib to raise an error.

Having followed the chain of method, I've pinpointed the line that, according to me, causes the issue :

score = parseFloat navigateXml(xml, 'imsx_POXBody.readResultResponse.result.resultScore.textString'), 10

The combination of parseFloat and navigateXml in the same line yield a score value of NaN (which is normal).

But the following check of isNan(score) returns an error as if this was an error case (which is not):

callback new errors.OutcomeResponseError('Invalid score response'), false

I would expect a different behavior between a case where there is no grade yet and a case when the LMS sends back an actual invalid value.

I previously thought about returning a 0 value when there is no grade yet. But it might be confusing with the case where the student has received an actual 0 grade ? Dunno.

Invalid LTI consumer response may result in empty error

Code

const lti = require('ims-lti')

const outcome = new lti.OutcomeService({
  consumer_key: 'key',
  consumer_secret: 'secret',
  // Note: localhost:3000 is a server that simply responseds with a status 200, empty body, to any request
  service_url: 'http://localhost:3000',
  source_did: 'sourcedid'
})

outcome.send_replace_result(1.0, (error, result) => {
  if (error) {
    console.error('Encountered an error while sending result:')
    console.error(error)
  }
})

Actual outcome

$ node test.js
Encountered an error while sending result:
{ [Error] message: undefined }

Expected outcome

Maybe a more helpful error message?

Cause

After debugging the program, it looks like the XML parser does not throw an error. But the code from the response is undefined (since the XML is empty), so the library assumes there was a non-success code and tries to throw an error using the message in the XML. But, again, there is nothing in the XML. So msg === undefined

callback new errors.OutcomeResponseError(msg), false

Doesn't work when launching to https

When I launch to a normal http endpoint, the authentication works fine. When I launch the same endpoint but from https, the validation breaks. The signatures are different. I haven't looked into it too much, but I think it's how the headers are handled. There are different headers set (at least in the version of Chrome that I'm using) when performing the https post than when performing the http post.

Provide better errors for validation

As of right now the errors just consist of a simple string which we could just match against, but that's probably not ideal if the text changes. We should probably attach codes to the errors thrown so they can be verified via some sort of constant.

As an example fs.readFile will return an error code of ENOENT if the file or directory does not exist.

I will look into providing an implementation via a pull request since I'm looking to use this functionality getting a provider app verified.

TypeError: Cannot read properties of undefined (reading 'host')

The build_signature function is using raw request headers obtained from Node HTTP to get the host. Direct interaction with the raq request object is not recommended by hapijs and headers are not defined on this object running [email protected].

TypeError: Cannot read properties of undefined (reading 'host')
at HMAC_SHA1.build_signature (PROJECT_DIR/node_modules/ims-lti/lib/hmac-sha1.js:71:47)
at Provider._valid_oauth (PROJECT_DIR/node_modules/ims-lti/lib/provider.js:67:31)
at Provider.valid_request (PROJECT_DIR/node_modules/ims-lti/lib/provider.js:51:19)
at Provider.valid_request (PROJECT_DIR/node_modules/ims-lti/lib/provider.js:4:59)

LTI 1.1, 1.1.1, 1.2, 2.0 implementations

This is definitely a very long term issue, but a few final drafts of the spec have been published (most notably LTI 2.0). It would be cool to implement them. The main issue I foresee here is that very very few platforms actually use any other version than 1.0 as of now, so there's not very many things to test against.

http://www.imsglobal.org/lti/

  • 1.0
  • 1.1 - #19
  • 1.1.1 - #19
  • 2.0

How to specify ext_content in LTI request?

Hi

It is not clear from the documentation how to specify ext_content value. I want to return an iframe in response to an LTI request but not sure how to access LTI provider ext_content to send iframe.

Thanks.

Persisting provider for use later

Hi there,
May I know is there any way to persist the provider for use later (such as saving it in a database) so that I can use the outcome service to submit the grade a few hours later?
By the way will the Express request object be detached from signature verification and what alternative can be used?
Thanks

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.