Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,20 @@ Once you find the `oauth_consumer_secret` based on the `oauth_consumer_key` in t

```coffeescript
lti = require 'ims-lti'
hmac = require 'ims-lti/lib/hmac-sha1'

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

# To use x-forwarded-* headers
provider = new lti.Provider consumer_key, consumer_secret, trustProxy: true

# To provide the nonce store
store = new lti.Stores.MemoryStore
provider = new lti.Provider consumer_key, consumer_secret, nonceStore: store

# To provide the signer
sig = new hmac.HMAC_SHA1 trustProxy: false
provider = new lti.Provider consumer_key, consumer_secret, signer: sig
```

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.
Expand Down
28 changes: 22 additions & 6 deletions src/hmac-sha1.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ _clean_request_body = (body, query) ->

class HMAC_SHA1

constructor: (options) ->
@trustProxy = (options and options.trustProxy) or false

toString: () ->
'HMAC_SHA1'

Expand All @@ -52,24 +55,37 @@ class HMAC_SHA1

@sign_string sig.join('&'), consumer_secret, token

host: (req) ->
if not @trustProxy
return req.headers.host

req.headers['x-forwarded-host'] or req.headers.host

protocol: (req) ->
xprotocol = req.headers['x-forwarded-proto']
if @trustProxy and xprotocol
return xprotocol

if req.protocol
return req.protocol

if req.connection.encrypted then 'https' else 'http'

build_signature: (req, body, consumer_secret, token) ->
hapiRawReq = req.raw and req.raw.req
if hapiRawReq
req = hapiRawReq

originalUrl = req.originalUrl or req.url
protocol = req.protocol
host = @host req
protocol = @protocol req

# Since canvas includes query parameters in the body we can omit the query string
if body.tool_consumer_info_product_family_code == 'canvas'
originalUrl = url.parse(originalUrl).pathname

if protocol is undefined
encrypted = req.connection.encrypted
protocol = (encrypted and 'https') or 'http'

parsedUrl = url.parse originalUrl, true
hitUrl = protocol + '://' + req.headers.host + parsedUrl.pathname
hitUrl = protocol + '://' + host + parsedUrl.pathname

@build_signature_raw hitUrl, parsedUrl, req.method, body, consumer_secret, token

Expand Down
14 changes: 9 additions & 5 deletions src/provider.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,23 @@ extensions = require './extensions'


class Provider
constructor: (consumer_key, consumer_secret, nonceStore, signature_method=(new HMAC_SHA1()) ) ->
constructor: (consumer_key, consumer_secret, optionsOrNonceStore, signature_method) ->

if typeof consumer_key is 'undefined' or consumer_key is null
throw new errors.ConsumerError 'Must specify consumer_key'

if typeof consumer_secret is 'undefined' or consumer_secret is null
throw new errors.ConsumerError 'Must specify consumer_secret'

if not nonceStore
nonceStore = new MemoryNonceStore()
if optionsOrNonceStore and optionsOrNonceStore.isNonceStore?()
options = {}
nonceStore = optionsOrNonceStore
else
options = optionsOrNonceStore or {}
nonceStore = options.nonceStore or new MemoryNonceStore()

if not nonceStore.isNonceStore?()
throw new errors.ParameterError 'Fourth argument must be a nonceStore object'
if not signature_method
signature_method = options.signer or new HMAC_SHA1(options)

@consumer_key = consumer_key
@consumer_secret = consumer_secret
Expand Down
28 changes: 27 additions & 1 deletion test/Provider.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ describe 'LTI.Provider', () ->
provider.consumer_key.should.equal consumer_key
provider.consumer_secret.should.equal consumer_secret
provider.signer.toString().should.equal sig.toString()

provider.signer.trustProxy.should.equal false


it 'should accept (consumer_key, consumer_secret, nonceStore, sig)', () =>
Expand All @@ -44,6 +44,32 @@ describe 'LTI.Provider', () ->
provider.nonceStore.should.equal nonceStore


it 'should accept (consumer_key, consumer_secret, nonceStore: store)', () =>
nonceStore =
isNonceStore: ()->true
isNew: ()->return
setUsed: ()->return

provider = new @lti.Provider('10204','secret-shhh',nonceStore:nonceStore,trustProxy:true)
provider.nonceStore.should.equal nonceStore
provider.signer.trustProxy.should.equal true


it 'should accept (consumer_key, consumer_secret, signer: sig)', () =>
sig =
me: 3
you: 1
total: 4

provider = new @lti.Provider('10204','secret-shhh',signer:sig)
provider.signer.should.equal sig


it 'should accept (consumer_key, consumer_secret, trustProxy: true)', () =>
provider = new @lti.Provider('10204','secret-shhh',trustProxy:true)
provider.signer.trustProxy.should.equal true


it 'should throw an error if no consumer_key or consumer_secret', () =>
(()=>provider = new @lti.Provider()).should.throw(lti.Errors.ConsumerError)
(()=>provider = new @lti.Provider('consumer-key')).should.throw(lti.Errors.ConsumerError)
Expand Down
36 changes: 32 additions & 4 deletions test/Signer.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,10 @@ should = require 'should'

HMAC_SHA1 = require '../lib/hmac-sha1'



signer = new HMAC_SHA1

describe 'Signer', () ->

it 'should include query params', (done) ->
signer = new HMAC_SHA1
req =
url: '/developers/LTI/test/v1p1/tool.php?foo=123&foo=bar'
method: 'POST'
Expand Down Expand Up @@ -36,3 +33,34 @@ describe 'Signer', () ->

done()

it 'should support x-forwarded-*', (done) ->
signer = new HMAC_SHA1 trustProxy: true
req =
url: '/developers/LTI/test/v1p1/tool.php?foo=123&foo=bar'
method: 'POST'
connection:
encrypted: true
headers:
host: 'localhost:5000'
'x-forwarded-host': 'www.imsglobal.org'
'x-forwarded-proto': 'http'
body =
resource_link_id: 'rsc1',
oauth_callback: 'about:blank',
lis_outcome_service_url: 'http://www.imsglobal.org/developers/LTI/test/v1p1/common/tool_consumer_outcome.php?b64=MTIzNDU6OjpzZWNyZXQ=',
lis_result_sourcedid: 'feb-123-456-2929::28883',
launch_presentation_return_url: 'http://www.imsglobal.org/developers/LTI/test/v1p1/lms_return.php',
lti_version: 'LTI-1p0',
lti_message_type: 'basic-lti-launch-request',
oauth_version: '1.0',
oauth_nonce: '7ee33f6dc94117e792ff529898ce3953',
oauth_timestamp: '1397708483',
oauth_consumer_key: '12345',
oauth_signature_method: 'HMAC-SHA1',
oauth_signature: 'dHORwwJqwh5hQQAlvaA9csSIOhc='

signature = signer.build_signature req, body, 'secret'
signature.should.equal body.oauth_signature

done()