mitodl / pylti Goto Github PK
View Code? Open in Web Editor NEWPyLTI implementation
License: BSD 2-Clause "Simplified" License
PyLTI implementation
License: BSD 2-Clause "Simplified" License
oauthlib is better maintained, and supports OAuth 2.0: https://github.com/idan/oauthlib
if outcome service is not available stack trace is thrown. it should have better debug logging for that case.
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 \
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
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.
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.
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!
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.
pylti/pylti/tests/test_flask.py
Lines 291 to 309 in 5088b1d
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.
PyLTI will need to be updated at some point as the tests fail with new versions of pytest.
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)
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.
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.
Test and fix pylint errors
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)
Would it be possible to refer to a public sample? The README.rst points to https://github.mit.edu/mitxlti/mit_lti_flask_sample
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.
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.
Adds support for Sakai and Coursera
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?
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,
}
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.
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.
Add session
or requests
property to LTI class to allow controlling how post backs are done (add header, add cert, etc)
@lti
decorator requires parameters. Rethink good defaults for parameters.
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
^
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
Not all frameworks will have app
object. Rethink if we can get config in some other way and refactor the code so it does not require app parameter.
I've been testing passing a parameter in the post data:
custom_problemdata: {"name":"problem name!"}
When this parameter is included, the LTI authentication always fails. I'm not sure exactly where the failure occurs.
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?
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.
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
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
maybe a doc_requirements.txt or something...not really sure what common practice is there
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.