GithubHelp home page GithubHelp logo

pylti's Introduction

PyLTI - LTI done right

PyLTI:Python implementation of LTI
Author:MIT Office of Digital Learning
Homepage:http://odl.mit.edu
License:BSD
https://secure.travis-ci.org/mitodl/pylti.png?branch=develop https://coveralls.io/repos/mitodl/pylti/badge.png?branch=develop

PyLTI is a Python implementation of the LTI specification [1]. It supports LTI 1.1.1 and LTI 2.0. While it was written with edX [2] as its LTI consumer, it is a complete implementation of the LTI specification and can be used with any learning management system that supports LTI.

A feature of PyLTI is the way it is used in the creation of an LTI tool. PyLTI is written as a library that exposes an API. This separation of concerns enables a developer to focus on the business logic of their tool and support of their framework of choice.

To demonstrate this usage, there are also a collection of example LTI tools written to support different Python web frameworks.

Framework Example
Flask mit_lti_flask_sample A skeleton example for the Flask framework that consumes the PyLTI library

Dependencies:

  • Python 2.7+ or Python 3.4+
  • oauth2 1.9.0+
  • httplib2 0.9+
  • six 1.10.0+

Development dependencies:

  • Flask 0.10.1
  • httpretty 0.8.3
  • oauthlib 0.6.3
  • pyflakes 1.2.3
  • pytest 2.9.2
  • pytest-cache 1.0
  • pytest-cov 2.3.0
  • pytest-flakes 1.0.1
  • pytest-pep8 1.0.6
  • sphinx 1.2.3

Documentation is available on readthedocs.

Licensing

PyLTI is licensed under the BSD license, version January 9, 2008. See license.rst for the full text of the license.

Footnotes

[1]The Learning Tools Interoperability (LTI) specification is an initiative of IMS. Their site http://developers.imsglobal.org/ contains a description of LTI as well as the current LTI specification.
[2]EdX offers interactive online classes and MOOCs from the world’s best universities. Online courses from MITx, HarvardX, BerkeleyX, UTx and many other universities. EdX is a non-profit online initiative created by founding partners Harvard and MIT. code.edx.org

pylti's People

Contributors

amir-qayyum-khan avatar bdero avatar blarghmatey avatar carsongee avatar guillaumederval avatar iceraj avatar imidulti avatar jolyonb avatar jtriley avatar layus avatar mbertrand avatar nikolas avatar notationist17 avatar pdpinch avatar pwilkins avatar sampaccoud 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

Watchers

 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

pylti's Issues

Reauthentication needs session clear

When a user successfully logs into an LTI session, pyLTI currently assumes that they want to remain logged into this session until the session cookie expires. If they attempt to log into another session with different credentials, those new credentials are not checked. This means that old credentials can be passed into an application.

Add `client_certificate` property to LTI class

Since a common case is using client certificate auth for the grade postback, add a property that allows specifying the path to a client certificate for the post back methods to use.

Issue with Moodle 3.1

I added a comment to a closed issue ( #52 ), but wanted to open another because I'm having trouble identifying the problem.

I'm trying to use pylti using the flask demo application and having problems. I am able to use the sample tool provider at http://ltiapps.net/test/tp.php with no problems when Moodle is the tool consumer. Likewise, I can use the sample tool consumer at http://ltiapps.net/test/tc.php against the PyLTI demo tool provider with no problems. The problem arises when using Moodle against the pylti flask demo.

There's a full dump of the debug log below. The problem is that a file (consumer_key.pem) is being checked for validity by the oauth2 library. That file is created in config.py.

The primary difference between my moodle setup and http://ltiapps.net/test/tc.php is that the moodle (http://moodle.cs.colorado.edu) uses HTTPS and the LTIAPPS uses HTTP for the response/grade URL.

make run-lti
Makefile:10: warning: overriding recipe for target 'build-lti'
Makefile:7: warning: ignoring old recipe for target 'build-lti'
docker run --name lti --rm -it --net host --cap-add SYS_PTRACE lti
werkzeug - * Running on http://0.0.0.0:5000/
werkzeug - * Restarting with reloader
pylti.flask - verify request=initial
pylti.flask - {'lti_version': u'LTI-1p0', 'lis_result_sourcedid': u'{"data":{"instanceid":"2","use
rid":"8","typeid":null,"launchid":193556859},"hash":"bd7c9ddd241a6c766e5be269c4a377feae508ba0f0edb
bee4915a28074b13f4c"}', 'resource_link_description': u'', 'tool_consumer_info_version': u'20160523
00', 'tool_consumer_instance_guid': u'moodle.cs.colorado.edu', 'oauth_signature': u'cioDfvyOZ8A7ur
CiK7v6UOCwsuQ=', 'context_label': u'test-lti', 'lti_message_type': u'basic-lti-launch-request', 'e
xt_user_username': u'grunwald', 'lis_person_name_full': u'Dirk Grunwald', 'context_title': u'XXXX \

  • lti test', 'user_id': u'8', 'tool_consumer_instance_description': u'Computer Science Moodle', 'o
    auth_consumer_key': u'consumer_key', 'launch_presentation_locale': u'en', 'context_id': u'46',
    'lis_outcome_service_url': u'https://moodle.cs.colorado.edu/mod/lti/service.php', 'tool_consumer_
    info_product_family_code': u'moodle', 'oauth_callback': u'about:blank', 'lis_person_name_family':
    u'Grunwald', 'oauth_nonce': u'408cdf4e9b48be41d1609087fe3bfca6', 'oauth_timestamp': u'1471802462',
    'resource_link_title': u'Test-1', 'oauth_signature_method': u'HMAC-SHA1', 'oauth_version': u'1.0'
    , 'lis_course_section_sourcedid': u'', 'lis_person_sourcedid': u'', 'tool_consumer_instance_name':
    u'CS-Moodle', 'resource_link_id': u'2', 'lis_person_contact_email_primary': u'dirk.grunwald@Color
    ado.EDU', 'roles': u'Instructor', 'context_type': u'CourseSection', 'ext_lms': u'moodle-2', 'lis_p
    erson_name_given': u'Dirk', 'launch_presentation_return_url': u'https://moodle.cs.colorado.edu/mod\
    /lti/return.php?course=46&launch_container=4&instanceid=2&sesskey=oWz84rvSwo', 'launch_presentatio
    n_document_target': u'window', 'custom_chapter': u'4'}
    pylti.flask - verify_request?
    pylti.common - consumers {'consumer_key': {'secret': 'lti_secret', 'cert': '/tmp/app/mit_l
    ti_flask_sample/consumer_key.pem'}}
    pylti.common - url http://sdr.cs.colorado.edu:5000/lti/
    pylti.common - method POST
    pylti.common - headers Cookie: __hssrc=1; hsfirstvisit=http%3A%2F%2Fcuengineeringonline.colorado.e
    du%2F|https%3A%2F%2Fwww.google.com%2F|1383786398465; hubspotutk=29d285d349396e6a072017fed175468b;
    III_EXPT_FILE=aa1196; III_SESSION_ID=14f532c5f85c7e07e6892b411ec87f0b; __lc.visitor_id.3158172=S14
    30601947.1623642990; lc_window_state=minimized; __utma=122534169.1425320077.1312478881.1440443128.
    1440512453.28; __utmc=122534169; __unam=ef2afea-14ff58a5725-2488baa3-7; SESSION_LANGUAGE=eng; __ut
    ma=72446465.1106481800.1324397981.1469115538.1470863218.150; __utmc=72446465; __utmz=72446465.1464
    233562.147.57.utmcsr=google|utmccn=(organic)|utmcmd=organic|utmctr=(not%20provided); __utma=137969
    394.1106481800.1324397981.1470878412.1470883484.2; __utmc=137969394; __utmz=137969394.1470878412.1
    .1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); MoodleSession=tv4r7r3s6lt3mdi8ji0jfdute4; ga=GA
    1.2.1106481800.1324397981; session=.eJyVU11r20AQ_CvlHvKU2NaHFclgWmic4FI7lLoEF4M43a2ss0935j6cOsH_vX
    uym2JaCn3U7OxodnbvlTCtHPxwpeBkRNKMXL8hklYgEXRg3Y10IpSsqUunt6DI6JW8q7D6eHe_Wb582swe5s3j0zJdttPh_Gku
    Z-3k8H3z5TBfTJP5YpLMHu43s836ebmZRLP1eEyO10RSr1hT7gxYUI46oVVpwHmjSm_CrxvndnbU77dacwk9ZntMS20o1z3gHm
    HeR2P9U09v1-zeM-2NhXGaXZ3FwzRUKDDj9Eoo66hiIPg4vrJg7RYOY_30kqdm__VZ44BS2FJ7x3QLpQWzFwz-z8q5KXg5y-3A
    WJyr88FcCS0VEkcWLTUH1OXCbHtr49UzlfzDx1-ak7tvl_2KoqWatkKGrodzw984Xga_d6j77h-0tdiHLXa8y7LFCBnw7iCssK
    MijWnOeXFm4bK8dBes1xXh1NEVHsWK_A4Zv1ckXpHrFfGYyxnIO8AddieGQrv4fdpWh0SDuMiyJE6KIxYaapuu77ZII2BZWkFW
    pzSP0RXPqwiKdEDzvOJZXCe3tzSP6HBAi4oCReNRPcyqIuYcUPkYJnCipN41eG-CUQdo3xkPp8IeA8AbxIk-L6Y30W6ADTqww

    asb8GUeDFYLi-AsnzjKY2TIyGBIqtpPozqJE1YPuQ51EWWZ4M4GTLaZYk5dhGWUqjt6fnFf8BOOBn0FuENRqGsJVgEppiy8cxp
    g2BI96SQk-NPkMJRcg.CptlcQ.FWI2_LzIOtFcYzXj3JGoNJ7Hgjg
    Origin: null
    Content-Length: 1556
    User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko)
    Chrome/52.0.2743.116 Safari/537.36
    Connection: keep-alive
    Dnt: 1
    Host: sdr.cs.colorado.edu:5000
    Upgrade-Insecure-Requests: 1
    Cache-Control: max-age=0
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,/;q=0.8
    Accept-Language: en-US,en;q=0.8
    Content-Type: application/x-www-form-urlencoded
    Accept-Encoding: gzip, deflate

ylti.common - params {'lti_version': u'LTI-1p0', 'lis_result_sourcedid': u'{"data":{"instanceid":
"2","userid":"8","typeid":null,"launchid":193556859},"hash":"bd7c9ddd241a6c766e5be269c4a377feae508
ba0f0edbbee4915a28074b13f4c"}', 'resource_link_description': u'', 'tool_consumer_info_version': u'
2016052300', 'tool_consumer_instance_guid': u'moodle.cs.colorado.edu', 'oauth_signature': u'cioDfv
yOZ8A7urCiK7v6UOCwsuQ=', 'context_label': u'test-lti', 'lti_message_type': u'basic-lti-launch-requ
est', 'ext_user_username': u'grunwald', 'lis_person_name_full': u'Dirk Grunwald', 'context_title':
u'XXXX - lti test', 'user_id': u'8', 'tool_consumer_instance_description': u'Computer Science Moo
dle', 'oauth_consumer_key': u'consumer_key', 'launch_presentation_locale': u'en', 'context_id'
: u'46', 'lis_outcome_service_url': u'https://moodle.cs.colorado.edu/mod/lti/service.php', 'tool_c
onsumer_info_product_family_code': u'moodle', 'oauth_callback': u'about:blank', 'lis_person_name_f
amily': u'Grunwald', 'oauth_nonce': u'408cdf4e9b48be41d1609087fe3bfca6', 'oauth_timestamp': u'1471
802462', 'resource_link_title': u'Test-1', 'oauth_signature_method': u'HMAC-SHA1', 'oauth_version'
: u'1.0', 'lis_course_section_sourcedid': u'', 'lis_person_sourcedid': u'', 'tool_consumer_instanc
e_name': u'CS-Moodle', 'resource_link_id': u'2', 'lis_person_contact_email_primary': u'dirk.grunwa
[email protected]', 'roles': u'Instructor', 'context_type': u'CourseSection', 'ext_lms': u'moodle-2'
, 'lis_person_name_given': u'Dirk', 'launch_presentation_return_url': u'https://moodle.cs.colorado\
.edu/mod/lti/return.php?course=46&launch_container=4&instanceid=2&sesskey=oWz84rvSwo', 'launch_pre
sentation_document_target': u'window', 'custom_chapter': u'4'}
pylti.flask - verify_request success
pylti.flask - params oauth_consumer_key=consumer_key
pylti.flask - params launch_presentation_return_url=https://moodle.cs.colorado.edu/mod/lti/return.\
php?course=46&launch_container=4&instanceid=2&sesskey=oWz84rvSwo
pylti.flask - params user_id=8
pylti.flask - params oauth_nonce=408cdf4e9b48be41d1609087fe3bfca6
pylti.flask - params context_label=test-lti
pylti.flask - params context_id=46
pylti.flask - params resource_link_title=Test-1
pylti.flask - params resource_link_id=2
pylti.flask - params lis_person_contact_email_primary=[email protected]
pylti.flask - params lis_person_name_full=Dirk Grunwald
pylti.flask - params lis_person_name_family=Grunwald
pylti.flask - params lis_person_name_given=Dirk
pylti.flask - params lis_result_sourcedid={"data":{"instanceid":"2","userid":"8","typeid":null,"la
unchid":193556859},"hash":"bd7c9ddd241a6c766e5be269c4a377feae508ba0f0edbbee4915a28074b13f4c"}
pylti.flask - params lti_version=LTI-1p0
pylti.flask - params roles=Instructor
pylti.flask - params lis_outcome_service_url=https://moodle.cs.colorado.edu/mod/lti/service.php
pylti.flask - check_role lti_role=Instructor decorator_role=any
werkzeug - 50.183.54.209 - - [21/Aug/2016 18:01:02] "POST /lti/ HTTP/1.1" 200 -
ylti.flask - verify request=session
pylti.flask - check_role lti_role=Instructor decorator_role=any
werkzeug - 50.183.54.209 - - [21/Aug/2016 18:01:05] "GET /add HTTP/1.1" 200 -
pylti.flask - verify request=session
pylti.flask - check_role lti_role=Instructor decorator_role=any
pylti.common - XML Response:

<imsx_POXEnvelopeRequest xmlns="http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0"><imsx_P
OXHeader><imsx_POXRequestHeaderInfo><imsx_version>V1.0</imsx_version><imsx_messageIdentifier>edX_f
ix</imsx_messageIdentifier></imsx_POXRequestHeaderInfo></imsx_POXHeader><imsx_POXBody><replaceResu
ltRequest>{"data":{"instanceid":"2","userid":"8","typeid":nu
ll,"launchid":193556859},"hash":"bd7c9ddd241a6c766e5be269c4a377feae508ba0f0edbbee4915a28074b13f4c"
}en0.0</textString\

</imsx_POXBody></imsx_POXEnvelopeReq
uest>
pylti.common - cert /tmp/app/mit_lti_flask_sample/consumer_key.pem
pylti.common - headers
pylti.common - {'Content-Type': 'application/xml', 'Authorization': u'OAuth realm="https://moodle.\
cs.colorado.edu", oauth_body_hash="ka%2BLAJhOwIQGAknbSlYL2K9dWT0%3D", oauth_nonce="96631772", oaut
h_timestamp="1471802467", oauth_consumer_key="consumer_key", oauth_signature_method="HMAC-SHA1
", oauth_version="1.0", oauth_signature="kXNqNa%2BBJX3ijkER0ZgdxSPHfaQ%3D"'}
werkzeug - 50.183.54.209 - - [21/Aug/2016 18:01:08] "POST /grade HTTP/1.1" 500 -
Traceback (most recent call last):
File "/usr/lib/python2.7/site-packages/flask/app.py", line 1836, in call
return self.wsgi_app(environ, start_response)
File "/usr/lib/python2.7/site-packages/flask/app.py", line 1820, in wsgi_app
response = self.make_response(self.handle_exception(e))
File "/usr/lib/python2.7/site-packages/flask/app.py", line 1403, in handle_exception
reraise(exc_type, exc_value, tb)
File "/usr/lib/python2.7/site-packages/flask/app.py", line 1817, in wsgi_app
response = self.full_dispatch_request()
File "/usr/lib/python2.7/site-packages/flask/app.py", line 1477, in full_dispatch_request
rv = self.handle_user_exception(e)
File "/usr/lib/python2.7/site-packages/flask/app.py", line 1381, in handle_user_exception
reraise(exc_type, exc_value, tb)
File "/usr/lib/python2.7/site-packages/flask/app.py", line 1475, in full_dispatch_request
rv = self.dispatch_request()
File "/usr/lib/python2.7/site-packages/flask/app.py", line 1461, in dispatch_request
return self.view_functionsrule.endpoint
File "/usr/lib/python2.7/site-packages/pylti/flask.py", line 363, in wrapper
return function(_args, *_kwargs)
File "/tmp/app/mit_lti_flask_sample/mit_lti_flask_sample.py", line 97, in grade
lti.post_grade(1 if correct else 0)
File "/usr/lib/python2.7/site-packages/pylti/flask.py", line 279, in post_grade
self.response_url, xml)
File "/usr/lib/python2.7/site-packages/pylti/common.py", line 227, in post_message
content_type,
File "/usr/lib/python2.7/site-packages/pylti/common.py", line 194, in _post_patched_request
headers={'Content-Type': content_type})
File "/usr/lib/python2.7/site-packages/oauth2/init.py", line 682, in request
connection_type=connection_type)
File "/usr/lib/python2.7/site-packages/httplib2/init.py", line 1464, in request
self.disable_ssl_certificate_validation)
File "/usr/lib/python2.7/site-packages/httplib2/init.py", line 929, in init
cert_file=cert_file, strict=strict)
File "/usr/lib64/python2.7/httplib.py", line 1220, in init
context.load_cert_chain(cert_file, key_file)
SSLError: [SSL] PEM lib (_ssl.c:2738)
werkzeug - 50.183.54.209 - - [21/Aug/2016 18:01:08] "GET /grade?debugger=yes&cmd=resource&f=st
yle.css HTTP/1.1" 200 -
werkzeug - 50.183.54.209 - - [21/Aug/2016 18:01:08] "GET /grade?debugger=yes&cmd=resource&f=jq
uery.js HTTP/1.1" 200 -
werkzeug - 50.183.54.209 - - [21/Aug/2016 18:01:08] "GET /grade?debugger=yes&cmd=resource&f=de
bugger.js HTTP/1.1" 200 -
werkzeug - 50.183.54.209 - - [21/Aug/2016 18:01:08] "GET /grade?debugger=yes&cmd=resource&f=ub
untu.ttf HTTP/1.1" 200 -
werkzeug - 50.183.54.209 - - [21/Aug/2016 18:01:08] "GET /grade?debugger=yes&cmd=resource&f=co
nsole.png HTTP/1.1" 200 -
werkzeug - 50.183.54.209 - - [21/Aug/2016 18:01:08] "GET /grade?debugger=yes&cmd=resource&f=so
urce.png HTTP/1.1" 200 -
Dockerfile-lti-centos.txt

Other request types other than posting a grade

The standard (http://www.imsglobal.org/LTI/v1p1p1/ltiIMGv1p1p1.html#_Toc330273035) specifies a few other types of interaction besides just posting the grade.
6.1.2 "readResult" for getting the grade from the LTI Consumer, for instance.

generating the message can be done with generate_request_xml setting operation to "readResult" but posting it with post_message will just yield "success" but not the actual grade

Currently, this can be worked around by calling the internal _post_patched_request directly but I feel this is more than somewhat hackish.

Would be happy to implement a fix to this if you explain how you want it in terms of architecture. My proposal would be to have post_message return either False when request fails but the response xml when it is successful, but there are quite a few other ways this issue could be approached.

Incorrect handling of LTI launch URLs that include query params with percent-encoded chars in decoded value

We ran into a bug with our LTI app which uses PyLTI where LTI launches would always fail if the launch URL contained a query parameter where the value, after parsing the query string into items, contained percent-encoded characters. The OAuth 1.0 signature base string was being generated incorrectly in this case.

I distilled the relevant part of our code into the following reproduction using PyLTI:

Steps to reproduce:

Run the following code in Python 3:

import time
from urllib.parse import urlencode

import pylti.common


consumers = {"TEST_CONSUMER": {"secret": "TEST_SECRET"}}
params = {
    "oauth_consumer_key": "TEST_CONSUMER",
    "oauth_signature_method": "HMAC-SHA1",
    "oauth_timestamp": int(time.time()),
    "oauth_nonce": "foo",
    "oauth_signature": "DUMMY_SIGNATURE",
}
query = urlencode({"url": "https://en.wikipedia.org/wiki/G%C3%B6reme_National_ark"})
url = f"https://example.com/lti_launch?{query}"
pylti.common.verify_request_common(
    consumers=consumers, url=url, method="POST", params=params, headers={}
)

This will fail with an error similar to:

oauth2.Error: Invalid signature. Expected signature base string: b'POST&https%3A%2F%2Fexample.com%2Flti_launch&oauth_consumer_key%3DTEST_CONSUMER%26oauth_nonce%3Dfoo%26oauth_si
gnature_method%3DHMAC-SHA1%26oauth_timestamp%3D1562586532%26url%3Dhttps%253A%252F%252Fen.wikipedia.org%252Fwiki%252FG%25C3%25B6reme_National_ark

If you extract the part of the message starting at "url%3D", and decode it, you'll get the query string param:

"url=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FG%C3%B6reme_National_ark"

Decode this again to get the URL value and you'll get:

"url=https://en.wikipedia.org/wiki/Göreme_National_ark"

Which doesn't match the original "url" param from the query string above. The "ö" character should still be percent-encoded.

If on the other hand the url query param value is passed only in the params:

import time

import pylti.common


consumers = {"TEST_CONSUMER": {"secret": "TEST_SECRET"}}
params = {
    "oauth_consumer_key": "TEST_CONSUMER",
    "oauth_signature_method": "HMAC-SHA1",
    "oauth_timestamp": int(time.time()),
    "oauth_nonce": "foo",
    "oauth_signature": "DUMMY_SIGNATURE",
}
params.update({"url": "https://en.wikipedia.org/wiki/G%C3%B6reme_National_ark"})
url = f"https://example.com/lti_launch"
pylti.common.verify_request_common(
    consumers=consumers, url=url, method="POST", params=params, headers={}
)

Then the error produced is:

oauth2.Error: Invalid signature. Expected signature base string: b'POST&https%3A%2F%2Fexample.com%2Flti_launch&oauth_consumer_key%3DTEST_CONSUMER%26oauth_nonce%3Dfoo%26oauth_si
gnature_method%3DHMAC-SHA1%26oauth_timestamp%3D1562587085%26url%3Dhttps%253A%252F%252Fen.wikipedia.org%252Fwiki%252FG%2525C3%2525B6reme_National_ark'

And if you extract the part from "url%3D" and double-decode it as before, you get the correct output:

"url=https://en.wikipedia.org/wiki/G%C3%B6reme_National_ark"

Related bug:

If a query param appears in both the URL and the "params" dict, the generated signature base string includes the param only once. My reading of the spec, and what oauthlib does, is to include both versions.

Downstream workaround:

Our downstream workaround was to strip the query string from the url argument and pass query params in the params dict instead. See hypothesis/lms#764

Student tests using incorrect url

If I understand the intent of these tests, you should be sending the request to /initial_student? rather than /initial_unknown?. Also, in the first of the two tests you are passing a role of "Staff". Staff is the name used in the decorator, but you should be passing either Administrator or Instructor in the in the request.

def test_access_to_oauth_resource_student_as_staff(self):
"""Verify staff doesn't have access to student only."""
consumers = self.consumers
url = 'http://localhost/initial_unknown?'
staff_url = self.generate_launch_request(
consumers, url, roles='Staff'
)
self.app.get(staff_url)
self.assertTrue(self.has_exception())
def test_access_to_oauth_resource_student_as_unknown(self):
"""Verify staff doesn't have access to student only."""
consumers = self.consumers
url = 'http://localhost/initial_unknown?'
unknown_url = self.generate_launch_request(
consumers, url, roles='FooBar'
)
self.app.get(unknown_url)
self.assertTrue(self.has_exception())

ca_certs in httplib2

Because of new browser rules, I had to install local certificate authority and make localhost available via https for testing. I think it will be more common in the future. For using the locally-issued certificate, httplib2 has a parameter ca_certs; however, it cannot be available through pylti. I solved the problem with monkey patching; however, it seems awkward:

import httplib2
old_init = httplib2.Http.__init__
def new_init(self, *args, **kwargs):
    """Call the original __init__ function with the certificate."""
    old_init(self, *args, **kwargs, ca_certs=ROOT_CERTIFICATE_PATH)
httplib2.Http.__init__ = new_init

Maybe PYLTI_CONFIG could take ca_certs and pass to httplib2:

app.config['PYLTI_CONFIG'] = {
    'consumers': {CONSUMER_KEY: {'secret': CONSUMER_SECRET}},
    'ca_certs': ROOT_CERTIFICATE_PATH,
}

pylti 0.6.0 causes unicode error in python 3.4.3

I get the following error on an Ubuntu 14.04 server using python 3.4.3:

Collecting pylti==0.6.0 (from -r requirements.txt (line 99))
  Downloading https://files.pythonhosted.org/packages/c0/5e/205c8fda20f96ac3d0c64329cf1192185c5cc5f9f1458f630c6b75d39bc8/PyLTI-0.6.0.tar.gz
    Complete output from command python setup.py egg_info:
    Traceback (most recent call last):
      File "<string>", line 1, in <module>
      File "/tmp/pip-build-d_1ba2ei/pylti/setup.py", line 121, in <module>
        README = open('README.rst').read()
      File "/mnt/jenkins/jobs/econplayground/workspace/ve/lib/python3.4/encodings/ascii.py", line 26, in decode
        return codecs.ascii_decode(input, self.errors)[0]
    UnicodeDecodeError: 'ascii' codec can't decode byte 0xe2 in position 2314: ordinal not in range(128)

Safari default permissions and iframe usage

When using a website in an iframe, Safari, by default, does not allow that website to set cookies. This means that session information is not saved for Safari, and hence the LTI authentication data is not saved, and navigating away from the launch page becomes impossible.

I've seen this gotten around by adding the session ID to each webpage as a GET variable.

accessing Custom Parameters sent from edx

When I create an LTI consumer component in the edX course editor, there's an option to enter custom parameters (see fig 1 below). There's edX documentation on how to edit these parameters, but I can't figure out how to access them on the pylti side, in my flask app. The flask example app doesn't cover this. Any ideas?
Thanks!

fig 1:
screenshot-2018-1-10 nick lti test unit world music global rhythms studio

lxml dependency fails

Actually found by cshubert. Basic install on mac failed. lxml library is not found on all OSX machines.

copying src/lxml/isoschematron/resources/xsl/iso-schematron-xslt1/iso_schematron_message.xsl -> build/lib.macosx-10.9-intel-2.7/lxml/isoschematron/resources/xsl/iso-schematron-xslt1

copying src/lxml/isoschematron/resources/xsl/iso-schematron-xslt1/iso_schematron_skeleton_for_xslt1.xsl -> build/lib.macosx-10.9-intel-2.7/lxml/isoschematron/resources/xsl/iso-schematron-xslt1

copying src/lxml/isoschematron/resources/xsl/iso-schematron-xslt1/iso_svrl_for_xslt1.xsl -> build/lib.macosx-10.9-intel-2.7/lxml/isoschematron/resources/xsl/iso-schematron-xslt1

copying src/lxml/isoschematron/resources/xsl/iso-schematron-xslt1/readme.txt -> build/lib.macosx-10.9-intel-2.7/lxml/isoschematron/resources/xsl/iso-schematron-xslt1

running build_ext

building 'lxml.etree' extension

creating build/temp.macosx-10.9-intel-2.7

creating build/temp.macosx-10.9-intel-2.7/src

creating build/temp.macosx-10.9-intel-2.7/src/lxml

cc -fno-strict-aliasing -fno-common -dynamic -arch x86_64 -arch i386 -g -Os -pipe -fno-common -fno-strict-aliasing -fwrapv -DENABLE_DTRACE -DMACOSX -DNDEBUG -Wall -Wstrict-prototypes -Wshorten-64-to-32 -DNDEBUG -g -fwrapv -Os -Wall -Wstrict-prototypes -DENABLE_DTRACE -arch x86_64 -arch i386 -pipe -I/usr/include/libxml2 -I/private/var/folders/j_/13vy2mcx46ncxm0jvsvk82yw0000gp/T/pycharm-packaging8148156407747439394.tmp/lxml/src/lxml/includes -I/System/Library/Frameworks/Python.framework/Versions/2.7/include/python2.7 -c src/lxml/lxml.etree.c -o build/temp.macosx-10.9-intel-2.7/src/lxml/lxml.etree.o -w -flat_namespace

In file included from src/lxml/lxml.etree.c:232:

/private/var/folders/j_/13vy2mcx46ncxm0jvsvk82yw0000gp/T/pycharm-packaging8148156407747439394.tmp/lxml/src/lxml/includes/etree_defs.h:14:10: fatal error: 'libxml/xmlversion.h' file not found

include "libxml/xmlversion.h"

 ^

1 error generated.

error: command 'cc' failed with exit status 1

Command /System/Library/Frameworks/Python.framework/Versions/2.7/Resources/Python.app/Contents/MacOS/Python -c "import setuptools;file='/private/var/folders/j_/13vy2mcx46ncxm0jvsvk82yw0000gp/T/pycharm-packaging8148156407747439394.tmp/lxml/setup.py';exec(compile(open(file).read().replace('\r\n', '\n'), file, 'exec'))" install --single-version-externally-managed --record /var/folders/j_/13vy2mcx46ncxm0jvsvk82yw0000gp/T/pip-yD3rcb-record/install-record.txt failed with error code 1 in /private/var/folders/j_/13vy2mcx46ncxm0jvsvk82yw0000gp/T/pycharm-packaging8148156407747439394.tmp/lxml

Exception information:
Traceback (most recent call last):
File "/Library/Python/2.7/site-packages/pip-1.1-py2.7.egg/pip/basecommand.py", line 104, in main
status = self.run(options, args)
File "/Library/Python/2.7/site-packages/pip-1.1-py2.7.egg/pip/commands/install.py", line 250, in run
requirement_set.install(install_options, global_options)
File "/Library/Python/2.7/site-packages/pip-1.1-py2.7.egg/pip/req.py", line 1133, in install
requirement.install(install_options, global_options)
File "/Library/Python/2.7/site-packages/pip-1.1-py2.7.egg/pip/req.py", line 577, in install
cwd=self.source_dir, filter_stdout=self.filter_install, show_stdout=False)
File "/Library/Python/2.7/site-packages/pip-1.1-py2.7.egg/pip/init.py", line 256, in call_subprocess
% (command_desc, proc.returncode, cwd))
InstallationError: Command /System/Library/Frameworks/Python.framework/Versions/2.7/Resources/Python.app/Contents/MacOS/Python -c "import setuptools;file='/private/var/folders/j
/13vy2mcx46ncxm0jvsvk82yw0000gp/T/pycharm-packaging8148156407747439394.tmp/lxml/setup.py';exec(compile(open(file).read().replace('\r\n', '\n'), file, 'exec'))" install --single-version-externally-managed --record /var/folders/j_/13vy2mcx46ncxm0jvsvk82yw0000gp/T/pip-yD3rcb-record/install-record.txt failed with error code 1 in /private/var/folders/j_/13vy2mcx46ncxm0jvsvk82yw0000gp/T/pycharm-packaging8148156407747439394.tmp/lxml

Session is not reset upon new login

If a new LTI login is sent to the framework, the session variables are not reset to the new login. This means that we always have to clear the session variables, because otherwise it may prevent new data from being stored.

Question about state management

I've been looking into the code for both this and the underlying PYLTI Library.

I've run into issues in understanding how state is managed.

When I open the Edx platform. It makes a post request with some information about OAuth and some stuff about LTI to /index .
The backend, upon verifying the OAuth stuff, returns a web-page tailored to my student that is displayed on the Edx platform.

My question is... Let's say 4 users are using this LTI tool simultaneously. When they submit their answers, where does /grade get the information that tells me which student submitted their solution? Is LTI state passed to the add form, then submitted back to the /grade? Or does the pyLTI flask component just magically store user state and disambiguates one user's request from anothers?

Draconian "role" checking

I think it would make sense to allow role checking to be completely disabled if the role specified is 'any' for a few practical concerns

Right now, even with any, the role is required to belong to a fixed list specified in LTI_ROLES. However, I am integrating my service with an LTI consumer that uses quite a few non-standard role names, and I currently have to patch them in by importing LTI_ROLES and then appending the new names to LTI_ROLES['any'].

Which mostly works. However - the consumer I am integrating with actually has a mode where no role is provided at all (for a reasonably valid reason, actually) and that case can only be solved by branching the code and changing it myself.

Making role='any' just bypass the check would allow these non-standard cases to be handled as well while not really impacting the behavior on standard cases, making it potentially usable in much wider settings.

LTI Fields and Aliases should be in common

At least parts of the LTI class should be in common.py. Preferably the flask class would inherit from a base class that defines nickname, and other common properties and methods with the flask version only adding/replacing with flask specific requirements

HTTP_X_FORWARDED_PROTO - Not supported.

Some cloud providers' (AWS, GCP) platforms insert 'HTTP_X_FORWARDED_PROTO" into the request header rather than 'X-Forwarded-Proto' on their managed services. This causes pylti to fail authentication as the protocol is incorrectly switched from https to http.

"user_id" key collision with Flask-Login

The session key "user_id" is used in both LTI authentication and the Flask-Login library[1]. I can't 100% confirm this, but it seems like Flask-Login may decide to clobber the 'user_id' key at times (especially during AJAX calls). In my own fork of PyLTI, I've had to create a duplicate "pylti_user_id" key with the same value as "user_id". This seems to solve the problem I encountered, where the "user_id" was missing in certain AJAX calls.

If there's interest, I can share my patch, but it's a pretty straightforward change and may not be useful for most people. Although in general, it might be polite for PyLTI to add its session keys with "pylti_" or something prepended.

[1] https://github.com/maxcountryman/flask-login/blob/878c5beb505456fc418912b8e81db79cdb7b3433/flask_login.py

Relationship with pylti/lti

As the current primary maintainer of pylti/lti, what is your thought about the relationship between this project and that project / organization? What is the key value that you're targeting here, and is there a collaboration that may be useful to both of us?

I would love to see others interested in improving the pylti/lti codebase. There are lots of things I don't like about it (I didn't write most of it), but I really haven't been able to give it much attention myself. I'd be happy to be a part of helping shepherd work there, or if it's better for everyone, here.

I'd appreciate your thoughts on where you'd like to see the state of LTI libraries in Python heading. Thank you for any feedback you can give.

OAuth signature from Moodle 2.9 does not match signature built from pylti

Hi there,

Not sure if I am doing something wrong but I have set up a moodle instance on my local machine to use the consumer key: __consumer_key__ and secret key: __lti_secret__ with the mit_lti_flask_sample app and I am finding that hitting the / endpoint causes a pylti.flask - verify_request failed error.

I used some breakpoints and I found that the oauth signature that moodle is sending does not match the signature that is being built by pylti. Do you have any insight as to what might be going on here?

Name and Nickname Properties

I know this is narrowly scoped to work with edX, but if we are going to abstract the raw lti fields into more friendly terms I don't think name should prefer to be the username:

lis_person_name_full
lis_person_name_family
lis_person_name_given

fields should definitely go there, and the current name and nickname should be moved to a username property or similarly. For backwards compat we can fall all the way back to the current implementation, but I think you should try a combination of these three properties first.

https handling sometimes faulty

in common.py:
# Check header for SSL before selecting the url
if headers.get('X-Forwarded-Proto', 'http') == 'https':
url = url.replace('http', 'https', 1)

if url already starts with https://, this will make it into httpss://, screwing up the signature and failing validation.

Simple fix: url.replace('http:','https:',1)

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.