Giter Club home page Giter Club logo

pylti1.3's People

Contributors

alain1405 avatar asavoy avatar cclauss avatar claytonturner avatar cmurtaugh avatar cwinters avatar dmitry-viskov avatar dversoza avatar hmoffatt avatar libre-man avatar michaelwheeler avatar mvillalba avatar nomysz avatar shinsean 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  avatar  avatar

pylti1.3's Issues

Trouble with using nrps and canvas

Hello,

Awesome package!

I'm using this package with fastapi and am working on an integration with canvas. I am able to launch the tool just fine and also to deep link a resource and launch that - but whenever I try to use one of the "services" such as nrps (specifically here the get_members() function) I am getting the following error
pylti1p3.exception.LtiServiceException: HTTP response [https://canvas.instructure.com/login/oauth2/token]: 400 - {"error":"invalid_request","error_description":"JWS signature invalid."}

more complete stack:

File "/usr/local/lib/python3.7/site-packages/pylti1p3/names_roles.py", line 66, in get_members

members, members_url = self.get_members_page(members_url)

File "/usr/local/lib/python3.7/site-packages/pylti1p3/names_roles.py", line 50, in get_members_page

accept='application/vnd.ims.lti-nrps.v2.membershipcontainer+json',

File "/usr/local/lib/python3.7/site-packages/pylti1p3/service_connector.py", line 107, in make_service_request

access_token = self.get_access_token(scopes)

File "/usr/local/lib/python3.7/site-packages/pylti1p3/service_connector.py", line 84, in get_access_token

raise LtiServiceException(r)

pylti1p3.exception.LtiServiceException: HTTP response [https://canvas.instructure.com/login/oauth2/token]: 400 - {"error":"invalid_request","error_description":"JWS signature invalid."}

I have tested the same setup with this platform testing tool https://saltire.lti.app/platform and nrps works fine I am able to get the member list just fine. I'm not sure where to look next to debug..

Any help would be greatly appreciated!

Thanks,
Joe

Facing difficulty integrating with Canvas

Hi Dmitry,

Thank you for the great work.

However, I am facing difficulty integrating the setup with canvas, specifically in the step 2 of the authentication process.

If I set the auth_login_url our implementation-specific: https://learn.acehsc.com.au/api/lti/authorize

Then I get the error "message":"An error occurred.","error_code":"internal_server_error"

This is the form data that gets posted to my tool\login at step 1:

iss: https://canvas.instructure.com

login_hint: c02ea24550b257158e3b4f8e1e077c79bb2ee590

client_id: 10000000000011

target_link_uri: https://acehsc.online/launch/

lti_message_hint: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ2ZXJpZmllciI6IjVjNTdlYWFlOTM5YjRmZDJjM2UxZWRmM2Y5ZjFkODVlMTQ2ZmNhNDJmMzhkY2NiMTk4NTQyNjBkMDE2OGE4ZTQ3NmM1NmQ2OTA4ZjBlNGI3MTU3MjJkYTc3YjQ4MGJiNmU4YjUxMDdlOThiYzRjZjM4YjU3ZTU0YzRmZTEwZTQzIiwiY2FudmFzX2RvbWFpbiI6ImxlYXJuLmFjZWhzYy5jb20uYXUiLCJjb250ZXh0X3R5cGUiOiJDb3Vyc2UiLCJjb250ZXh0X2lkIjoxMDAwMDAwMDAwMDA1NiwiZXhwIjoxNTk0MTEzNjE3fQ.Tf-bqiP5hJajFY0Y_6gkQrPF372Ussvn7ECeOzjF2V0

canvas_region: not_configured

This is what my tool returns while redirecting to the https://learn.acehsc.com.au/api/lti/authorize:

client_id: 10000000000011

login_hint: c02ea24550b257158e3b4f8e1e077c79bb2ee590

lti_message_hint: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ2ZXJpZmllciI6IjVjNTdlYWFlOTM5YjRmZDJjM2UxZWRmM2Y5ZjFkODVlMTQ2ZmNhNDJmMzhkY2NiMTk4NTQyNjBkMDE2OGE4ZTQ3NmM1NmQ2OTA4ZjBlNGI3MTU3MjJkYTc3YjQ4MGJiNmU4YjUxMDdlOThiYzRjZjM4YjU3ZTU0YzRmZTEwZTQzIiwiY2FudmFzX2RvbWFpbiI6ImxlYXJuLmFjZWhzYy5jb20uYXUiLCJjb250ZXh0X3R5cGUiOiJDb3Vyc2UiLCJjb250ZXh0X2lkIjoxMDAwMDAwMDAwMDA1NiwiZXhwIjoxNTk0MTEzNjE3fQ.Tf-bqiP5hJajFY0Y_6gkQrPF372Ussvn7ECeOzjF2V0

nonce: d644906d47504bb682d6f4ed71d689d86136d3c4c03211eab109027500258144

prompt: none

redirect_uri: https://acehsc.online/launch/

response_mode: form_post

response_type: id_token

scope: openid

state: state-061e1b4b-e187-42ae-97ff-cd525d8746a9

As you can see the lti_message_hint is the same, everything is right but it still returns internal_server_error . I've been scratching my head over this for the last three days and any help would be highly appreciated.

If I set the auth_login_url to https://canvas.instructure.com/api/lti/authorize_redirect then I simply get Invalid lti_message_hint error.

My JSON configuration file is attached below with the generic canvas configuration, I've tried changing and trying out different combinations of settings to no avail.

{
     "https://canvas.instructure.com": {
        "default": true,
        "client_id": "10000000000011",
        "auth_login_url": "https://canvas.instructure.com/api/lti/authorize_redirect",
        "auth_token_url": "https://canvas.instructure.com/login/oauth2/token",
        "key_set_url": "https://canvas.instructure.com/api/lti/security/jwks",
        "key_set": null,
        "private_key_file": "private.key",
        "public_key_file": "public.key",
        "deployment_ids": ["6:48a8d8c493e8d2aa9ce833888b4721865af0e06e"]
     }
}

Error: JWK must contain a "kty" parameter"

Tying to run flask example tool in Moodle. The game loaded successfully, and when JS trying get a score from Moodle, got this error.

127.0.0.1 - - [14/Oct/2021 03:20:09] "GET /api/scoreboard/lti1p3-launch-adcbe674-c539-46d7-bd71-a88fa3d41a6e/ HTTP/1.1" 500 -
Traceback (most recent call last):
File "C:\Users\disazoz\AppData\Roaming\Python\Python37\site-packages\flask\app.py", line 2464, in call
return self.wsgi_app(environ, start_response)
File "c:\Users\disazoz\Desktop\test\pylti1.3-flask-example\game\app.py", line 26, in call
return self.app(environ, start_response)
File "C:\Users\disazoz\AppData\Roaming\Python\Python37\site-packages\flask\app.py", line 2450, in wsgi_app
response = self.handle_exception(e)
File "C:\Users\disazoz\AppData\Roaming\Python\Python37\site-packages\flask\app.py", line 1867, in handle_exception
reraise(exc_type, exc_value, tb)
File "C:\Users\disazoz\AppData\Roaming\Python\Python37\site-packages\flask_compat.py", line 39, in reraise
raise value
File "C:\Users\disazoz\AppData\Roaming\Python\Python37\site-packages\flask\app.py", line 2447, in wsgi_app
response = self.full_dispatch_request()
File "C:\Users\disazoz\AppData\Roaming\Python\Python37\site-packages\flask\app.py", line 1952, in full_dispatch_request
rv = self.handle_user_exception(e)
File "C:\Users\disazoz\AppData\Roaming\Python\Python37\site-packages\flask\app.py", line 1821, in handle_user_exception
reraise(exc_type, exc_value, tb)
File "C:\Users\disazoz\AppData\Roaming\Python\Python37\site-packages\flask_compat.py", line 39, in reraise
raise value
File "C:\Users\disazoz\AppData\Roaming\Python\Python37\site-packages\flask\app.py", line 1950, in full_dispatch_request
rv = self.dispatch_request()
File "C:\Users\disazoz\AppData\Roaming\Python\Python37\site-packages\flask\app.py", line 1936, in dispatch_request
return self.view_functionsrule.endpoint
File "c:\Users\disazoz\Desktop\test\pylti1.3-flask-example\game\app.py", line 222, in scoreboard
scores = ags.get_grades(score_line_item)
File "C:\Programs\WPy64-3771\python-3.7.7.amd64\lib\site-packages\pylti1p3\assignments_grades.py", line 200, in get_grades
lineitem = self.find_or_create_lineitem(lineitem, find_by=find_by)
File "C:\Programs\WPy64-3771\python-3.7.7.amd64\lib\site-packages\pylti1p3\assignments_grades.py", line 157, in find_or_create_lineitem
lineitem = self.find_lineitem_by_tag(tag)
File "C:\Programs\WPy64-3771\python-3.7.7.amd64\lib\site-packages\pylti1p3\assignments_grades.py", line 142, in find_lineitem_by_tag
return self.find_lineitem('tag', tag)
File "C:\Programs\WPy64-3771\python-3.7.7.amd64\lib\site-packages\pylti1p3\assignments_grades.py", line 117, in find_lineitem
lineitems, lineitems_url = self.get_lineitems_page(lineitems_url)
File "C:\Programs\WPy64-3771\python-3.7.7.amd64\lib\site-packages\pylti1p3\assignments_grades.py", line 83, in get_lineitems_page
accept='application/vnd.ims.lis.v2.lineitemcontainer+json'
File "C:\Programs\WPy64-3771\python-3.7.7.amd64\lib\site-packages\pylti1p3\service_connector.py", line 110, in make_service_request
access_token = self.get_access_token(scopes)
File "C:\Programs\WPy64-3771\python-3.7.7.amd64\lib\site-packages\pylti1p3\service_connector.py", line 87, in get_access_token
raise LtiServiceException(r)
pylti1p3.exception.LtiServiceException: HTTP response [https://learn.qweasd.su/mod/lti/token.php]: 400 - {
"error" : "JWK must contain a "kty" parameter"
}

Not sure if it related, but i'm using private and public keys that were in the example git repo. The Moodle platform just self hosted instance.

define standard LTI role URIs

It would be very helpful if this module had symbols defined for the URIs of the standard user roles. Symbols for various other URIs commonly used in LTI support would be helpful, too.

allow caller to set HTTP User-Agent

It would be useful to be able to provide custom HTTP headers, in particular User-Agent. Possibly the caller could provide an optional Requests session object to use.

We have seen LMSs use content filters that block requests with a User-Agent of 'python-requests'.

Seeking clarification on the django login process, and nonces

I have a [I think] sane django app... the service has been running for a few years, and happily uses both LTI 1.1 and SAML2 based authentication.

For reference: Django 3.1.13, running in a kubernetes cluster.

I'm now adding LTI 1.3 authentication, and my launch process was failing.

I believe I've narrowed it down to the login process not setting a nonce ... for the launch process to then pick up on & verify.

This in my login code:

def lti1p3login(request):
    logger.info(f"#### lti 1.3 login called")

    tool_conf = get_tool_conf()
    launch_data_storage = get_launch_data_storage()

    oidc_login = DjangoOIDCLogin(request, tool_conf, launch_data_storage=launch_data_storage)
    target_link_uri = get_launch_url(request)
    foo = oidc_login.enable_check_cookies().redirect(target_link_uri)
    # .redirect should have called ._prepare_redirect_url - which should have set stuff
    logger.info(f"    oidc example nonce (pylti1p3/oidc_login - lines 114-116): {oidc_login._generate_nonce()}")  # lti1p3-nonce
    nonce_key = oidc_login._session_service._get_key('nonce')
    logger.info(f"    oidc nonce (pylti1p3/oidc_login - lines 114-116): {oidc_login._session_service._get_value(nonce_key)}")  # lti1p3-nonce
    session_service = oidc_login._session_service
    logger.info(f"    launch storage : {launch_data_storage}")
    logger.info(f"Returning oidc_login redirect {foo}")
   return foo

This logs:

{"message": "#### lti 1.3 login called", "timestamp": "2022-03-28T10:17:39.186512+00:00", "level": "INFO"}
{"message": "    oidc example nonce (pylti1p3/oidc_login - lines 114-116): 9f5e401c7def42cba7049935bc1971aa4bc4c44aae8011ecaec4c6252675a8d8", "timestamp": "2022-03-28T10:17:39.187694+00:00", "level": "INFO"}
{"message": "    oidc nonce (pylti1p3/oidc_login - lines 114-116): None", "timestamp": "2022-03-28T10:17:39.199934+00:00", "level": "INFO"}
{"message": "    launch storage : <pylti1p3.contrib.django.launch_data_storage.cache.DjangoCacheDataStorage object at 0x7f105f9fd0d0>", "timestamp": "2022-03-28T10:17:39.200087+00:00", "level": "INFO"}
{"message": "Returning oidc_login redirect <HttpResponse status_code=200, \"text/html; charset=utf-8\">", "timestamp": "2022-03-28T10:17:39.200663+00:00", "level": "INFO"}
127.0.0.1 - - [28/Mar/2022:11:17:39 +0100] "POST /lti1p3/login/ HTTP/1.1" 200 3246 .....<snip>.....

Am I wrong in assuming that the redirect() call should have set something.... and that something should be retrievable from oidc_login._session_service._get_value(nonce_key)?


Further info - after some investigation - it may be a cookie issue, if one of my colleagues does some "clever magic" so things appear as localhost, it all works.

Use DB in Flask for Tool Credentials

Hey Dmitry,

We wrote a small wrapper in our own code to use Flask-SQLAlchemy to pull LTI tool configs from our database rather than the ToolConfJson. I know it's easier to include it in Django because an ORM is built in. I would love it we could somehow get it included in the Flask contrib as an optional setting?

class LTIConfig(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    iss = db.Column(db.String)
    client_id = db.Column(db.String)
    auth_login_url = db.Column(db.String)
    auth_token_url = db.Column(db.String)
    key_set_url = db.Column(db.String)
    private_key_file = db.Column(db.String)
    public_key_file = db.Column(db.String)
    deployment_id = db.Column(db.String)


def get_lti_config(iss, client_id):
    lti = LTIConfig.query.filter_by(iss=iss, client_id=client_id).first()
    settings = {
            lti.iss: [{
                "client_id": lti.client_id,
                "auth_login_url": lti.auth_login_url,
                "auth_token_url": lti.auth_token_url,
                "auth_audience": "null",
                "key_set_url": lti.key_set_url,
                "key_set": None,
                "deployment_ids": [lti.deployment_id]
            }]
        }

    private_key = lti.private_key_file
    public_key = lti.public_key_file
    tool_conf = ToolConfDict(settings)

    tool_conf.set_private_key(iss, private_key, client_id=client_id)
    tool_conf.set_public_key(iss, public_key, client_id=client_id)

    return tool_conf

@app.route('/login/', methods=['GET', 'POST'])
def login():
    session['iss'] = request.values.get('iss')
    session['client_id'] = request.values.get('client_id')

    tool_conf = get_lti_config(session['iss'], session['client_id'])
    ...

@app.route('/launch/', methods=['POST'])
def launch():
    tool_conf = get_lti_config(session['iss'], session['client_id'])

    flask_request = FlaskRequest()
    ...

I have no idea how to add this to the library as an optional item though.

Unable to link a programmatic grade

Hello everyone,

I wanted to confirm whether this an issue with pylti1.3 or if I'm missing something, when adding multiple assignment grades through the LTI Assigments and Grades Services with the programmatic mode active, rather than the declarative/default grading mode.

I'm able to register and read back grades (line items) into the LMS. But none of them are registered as the actual score on the LMS side. If my understanding is correct, one of these line items needs to be selected by specifying the resourceLinkId. pylti1.3 does allow setting the resource_id, but not the resource_link_id.

Am I missing something?

I'm using as reference this score() function from the repo

def score(launch_id, earned_score, time_spent):
    tool_conf = ToolConfJsonFile(get_lti_config_path())
    flask_request = FlaskRequest()
    launch_data_storage = get_launch_data_storage()
    message_launch = ExtendedFlaskMessageLaunch.from_cache(launch_id, flask_request, tool_conf,
                                                           launch_data_storage=launch_data_storage)

    resource_link_id = message_launch.get_launch_data() \
        .get('https://purl.imsglobal.org/spec/lti/claim/resource_link', {}).get('id')

    if not message_launch.has_ags():
        raise Forbidden("Don't have grades!")

    sub = message_launch.get_launch_data().get('sub')
    timestamp = datetime.datetime.utcnow().isoformat() + 'Z'
    earned_score = int(earned_score)
    time_spent = int(time_spent)

    grades = message_launch.get_ags()
    sc = Grade()
    sc.set_score_given(earned_score) \
        .set_score_maximum(100) \
        .set_timestamp(timestamp) \
        .set_activity_progress('Completed') \
        .set_grading_progress('FullyGraded') \
        .set_user_id(sub)

    sc_line_item = LineItem()
    sc_line_item.set_tag('score') \
        .set_score_maximum(100) \
        .set_label('Score')
    if resource_link_id:
        sc_line_item.set_resource_id(resource_link_id)

    grades.put_grade(sc, sc_line_item)

    tm = Grade()
    tm.set_score_given(time_spent) \
        .set_score_maximum(999) \
        .set_timestamp(timestamp) \
        .set_activity_progress('Completed') \
        .set_grading_progress('FullyGraded') \
        .set_user_id(sub)

    tm_line_item = LineItem()
    tm_line_item.set_tag('time') \
        .set_score_maximum(999) \
        .set_label('Time Taken')
    if resource_link_id:
        tm_line_item.set_resource_id(resource_link_id)

    result = grades.put_grade(tm, tm_line_item)

    return jsonify({'success': True, 'result': result.get('body')})

get grades

Hi Dimitry,

I have a connection with a brightspace instance and try to retrieve grades.
I now receive a HTTP response [https://auth.brightspace.com/core/connect/token]: 400 - {"error":"invalid_grant","error_description":"Error validating assertion:

I just some code found in your test script.

{{{
if not message_launch.has_ags():
raise Exception("Don't have assignments and grades!")
else:

    score_line_item = LineItem()
    score_line_item.set_tag('score') \
        .set_score_maximum(100) \
        .set_label('Score')

    scores = message_launch.validate_registration().get_ags().get_grades(score_line_item)

}}}

Invalid Nonce with Nginx + uWSGI

Hey All,

Trying to deploy a Django <-> uWSGI <-> Nginx application using pylti1.3 and whenever I put nginx in front of the application I get an Invalid Nonce error.

It's on AWS, nginx on http, ALB is https doing http-https conversion.

Any thoughts?

It works fine using Django built in server and uwsgi as the http server.

Support access token retrieval from Moodle LMSes

Hey! I was just debugging through the get_access_token method of the ServiceConnector class and saw it expects successful (2xx or 3xx) responses to contain an "access_token" claim:

self._access_tokens[scope_key] = response['access_token']

I was recently working on an integration with a Moodle-based LMS, and realized that APIs for those kind of LMSes return a claim with a different name for the access token. Particularly, their OAuth Token URL's endpoint lives at the /login/token.php, and the token itself is provided as a "token" claim. e.g.:

{
  "token": "8b1cb0bebd237b919c71d7a8f487b4d2",
  "privatetoken": "U7XZRAaVmielUhkGcYVBKRxXEGhQ9wWCOeHQ2MyMR9jb6oA9TqZ2YdCKhJ7UgxGE"
}

The Moodle-sided code that returns this claim is defined here: https://github.com/moodle/moodle/blob/5ee0b620ae99bebeca9ee2c18078ee010913ae65/login/token.php#L104. Hence, I think that a KeyError would be raised in the case where a Moodle API is hit to retrieve an access token, since the "access_token" claim would not be present even though the response is successful.

If you think this is case is worth considering, I would be happy to help in implementing it.

Best regards,
Pepe

504- Gateway Timeout

I am constantly getting Gateway Timeout. When same request is performed by another tool to same url with same parameters it succeeds. I am on python 3.8

Success Using Insomnia
image

Exception in the code
image

I am getting the data that has been passed to requests.post(auth_url, data=auth_request)

AGS put_grade with no line item raises missing tag exception

You can call put_grade() without a line item, and if none is provided in the message launch data, it will try to create one:

if not lineitem:
lineitem = LineItem()
lineitem.set_label('default')\
.set_score_maximum(100)
lineitem = self.find_or_create_lineitem(lineitem)

However, the one it creates has no tag, so find_or_create_lineitem raises an exception:

if find_by == 'tag':
tag = new_lineitem.get_tag()
if not tag:
raise LtiException('Tag value is not specified')

Regarding LTI Oidc authentication and Deep Link

Hello,

we have a few questions:

  1. Are we necessary to have platform private key for JWT generation which then verified the signature with the public key? If yes then how to get it, that will verify the signature with JWK set?
    Because you are taking the same private key for both(means for platform private key and tools private key)
  2. How to get us deep link return URL of any LMS for LTI 1.3(Ex.Blackboard)?

Django 4.0

Django 4.0 removed ugettext and ugettext_lazy, which has been deprecated since Django 3.0. There is a patch available here but a PR has not been issued yet.

Could you integrate this?

override redirect_uri

The OIDCLogin.redirect() method takes the launch_url as a parameter, but get_cookies_allowed_js_check() always uses the target_link_uri from the original request rather than the launch_url passed.

I have multiple launch URLs in my application including some with parameters, but OpenID Connect security requires the redirect URIs to be whitelisted in at the OP (LTI tool) and must match exactly (no query strings or wildcards). So I want to use a fixed redirect_uri by passing that uri into OIDCLogin.redirect(). But get_cookies_allowed_js_check() ignores that.

When the provider redirects back the original target_link_uri is available as a claim in the jwt. Or it could be saved in the session using pass_params_to_launch(). Or both, and verified.

Compatibility with Moodle

After many tries I still don't understand the problem between Moodle and pylti1.3.
I able to manage LMS moodle 3.11 with LTI plugin WP, LTI handshake is good.

Add LTI Advantage : I use "auto registration/configuration" when everyone is compatible it is happiness. This is not the case of Pylti1.3, but not blocking. -> You have understood this mechanism saves time and avoids filling out a form and making mistakes

I need your help, I can't get PyLti1.3 to work with Moodle. I have a strange behavior.

Moodle : says one and only thing : Invalid Request
Capture d’écran 2022-04-05 à 15 51 53

Here is a summary of the requests when it goes well with WP LTI TOOL plugin , I use like a "good example" :

POST Status code 200 ?lti-tool -> TOOL
params:

Capture d’écran 2022-04-05 à 15 59 02

POST 200 auth.php -> LMS
Capture d’écran 2022-04-05 à 16 01 31

POST 302 ?lti-tool
Capture d’écran 2022-04-05 à 16 03 25

Here is a summary of the requests when it goes wrong with pyLTI1.3 :

POST 200 /login/ --> TOOL pylti1.3
Capture d’écran 2022-04-05 à 16 05 20
GET 302 http://xxxx:9001/login/
Capture d’écran 2022-04-05 à 16 10 32

GET 404 auth.php
Capture d’écran 2022-04-05 à 16 14 26

SSL issue

Hi from Brazil!

I really dont have any experience with servers and security, but following some tutorials I managed to make work a xampp server, where my moodle instance is running with a self generated certificate, to test my Tool, that it is also running with a self generated certificate. But i am facing some problem with the SSL part during the launch flow, in these routes:

@lti_routes.route(tool.configs.initiate_login_uri, methods=['GET', 'POST'])
def oidc_init():
    config = ToolConfJsonFile(r'RemoteLabFPGA\lti\utils\register_config.json')
    flask_request = FlaskRequest()
    launch_data_storage = FlaskCacheDataStorage(cache)
    login = FlaskOIDCLogin(request=flask_request, tool_config=config, launch_data_storage=launch_data_storage)
    return login.enable_check_cookies().redirect(tool.claims.redirect_uris[0])


@lti_routes.route(tool.configs.redirect_uri, methods=['GET', 'POST'])
def oidc_launch():
    config = ToolConfJsonFile(r'RemoteLabFPGA\lti\utils\register_config.json')
    flask_request = FlaskRequest()
    launch_data_storage = FlaskCacheDataStorage(cache)
    message_launch = FlaskMessageLaunch(request=flask_request, tool_config=config, launch_data_storage=launch_data_storage)
    message_launch_data = message_launch.get_launch_data()
    return redirect(url_for('lab_routes.target_link'))

During the first route, the flow makes a call to the redirect function, that calls the CookiesAllowedCheckPage function, and then makes de JS function, and reload the same route. During this process of reload this route, my page changes from secure connection with valide certificate to not secure connection but stills with a valide certificate. After this, in this second run in this route, the redirect function call the second route, making the launch. In this part, the flow always broke during the FlaskMessageLaunch, when trying to get_public_key for the validate_jwt_signature, saying that is a SSL problem, that I believe is caused from the changing in the connection type (but may be wrong lol).

self.validate()
File "RemoteLabFPGA\venv\lib\site-packages\pylti1p3\message_launch.py", line 284, in validate
return self.validate_state()\
File "RemoteLabFPGA\venv\lib\site-packages\pylti1p3\message_launch.py", line 671, in validate_jwt_signature
public_key = self.get_public_key()
File "RemoteLabFPGA\venv\lib\site-packages\pylti1p3\message_launch.py", line 559, in get_public_key
public_key_set = self.fetch_public_key(key_set_url)
File "RemoteLabFPGA\venv\lib\site-packages\pylti1p3\message_launch.py", line 540, in fetch_public_key
raise LtiException("Error during fetch URL " + key_set_url + ": " + str(e))
pylti1p3.exception.LtiException: Error during fetch URL https://localhost/moodle/mod/lti/certs.php: HTTPSConnectionPool(host='localhost', port=443): Max retries exceeded with url: /moodle/mod/lti/certs.php (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1045)')))

I really dont know if the problem is with the way I configured the pylti1.3 or the xampp + certificates, but would appreciate some help! I dont understand why the type of connection is changing during the reload.

nonce invalid

Hi,
I put your demo project in Brightspace.
Somtimes it works, I see the game.hml. other times , refresh, I get an nonce exception

Do you have any suggestions how to fix this?

Regards, Marcel

Wiki JWT page update

For Python 3

import json
from jwcrypto.jwk import JWK

f = open("jwtRS256.key.pub", "rb")
public_key = f.read()
f.close()

jwk_obj = JWK.from_pem(public_key)
public_jwk = json.loads(jwk_obj.export_public())
public_jwk['alg'] = 'RS256'
public_jwk['use'] = 'sig'
public_jwk_str = json.dumps(public_jwk)
print(public_jwk_str)

how to test a `launch` function

I've a working system - I can connect from a test moodle to my tool - and now I want to add a test for the code.

My test code starts:

Overall imports

from rest_framework_jwt.settings import api_settings as jwt_settings
from mock import patch
from unittest.mock import Mock
from urllib import parse
from django.test import Client
from myApp.lti1p3_backend import ToolConfMyApp # My version of DjangoDbToolConf
from myApp_utils.test_utils import TestCreators  # some helper methods for creating test objects in the db

class TestToolConfMyApp(TestCreators):

There are a whole suite of tests for the ToolConfMyApp functions: check_iss_has_one_client, check_iss_has_many_clients, get_public_key, get_iss_config, etc...

I've even got POST and GET tests for login - and test the parameters in the response.url from those calls

For the specific launch test - this is what I start with:


    @patch('pylti1p3.message_launch.MessageLaunch.fetch_public_key')
    def test_launch(self, mock_fpk: Mock):

        # Set up the environment, and the initial login
        key = self.create_lti1p3_tool_key(name="test", private_key="foo", public_key=pub_key)
        self.create_lti1p3_tool(
            title="test",
            tool_key=key,
            issuer="example_dot_com",
            client_id="foo",
            auth_login_url="http://example.com/a",
            auth_token_url="http://example.com/b",
            deployment_ids='["test-id-1", "test-id-2"]',
            key_set_url="http://example.com/c",
        )
        c = Client()
        response = c.get("/lti1p3/login/?lti1p3_new_window=1&iss=example_dot_com&login_hint=5&target_link_uri=http%3A%2F%2Fexample.com%2Flti1p3%2Flaunch%2F&lti_message_hint=5&lti_deployment_id=3&client_id=foo")

        # Cookies should now exist, test launch

Then we want to set up the token for actual call:

        query_string = parse.urlparse(response.url)[4]
        query_dict = parse.parse_qs(query_string)
        tool_conf = ToolConfNoteable()
        mock_fpk.return_value = tool_conf.get_jwks("example_dot_com")

        # create id_token that VLE creates - needs nonce to match login nonce!
        source_token = {
            "nonce": query_dict["nonce"][0],
            "iat": 1649251764,
            "exp": 1649251824,
            "iss": "example_dot_com",
            "aud": "foo",
            "https://purl.imsglobal.org/spec/lti/claim/deployment_id": "test-id-1",
            "https://purl.imsglobal.org/spec/lti/claim/target_link_uri": "http://vle.example.com/lti1p3/launch/",
            "sub": "5",
            "https://purl.imsglobal.org/spec/lti/claim/lis": {
                "person_sourcedid": "",
                "course_section_sourcedid": ""
            },
            "https://purl.imsglobal.org/spec/lti/claim/roles": [
                "http://purl.imsglobal.org/vocab/lis/v2/institution/person#Administrator",
                "http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor",
                "http://purl.imsglobal.org/vocab/lis/v2/system/person#Administrator"
            ],
            "https://purl.imsglobal.org/spec/lti/claim/context": {
                "id": "3",
                "label": "sample_course",
                "title": "An Example of a Course Title",
                "type": [
                "CourseSection"
                ]
            },
            "https://purl.imsglobal.org/spec/lti/claim/resource_link": {
                "title": "some link text",
                "id": "3"
            },
            "https://purl.imsglobal.org/spec/lti-bo/claim/basicoutcome": {
                "lis_result_sourcedid": "{\"data\":{\"instanceid\":\"3\",\"userid\":\"5\",\"typeid\":\"3\",\"launchid\":1293835966},\"hash\":\"f85b809cb4b10a0bb65ce2824e8c892a2d08f6a2015edd68d3ccaf014486cf81\"}",
                "lis_outcome_service_url": "https://vle.example.com/lti/service.php"
            },
            "given_name": "Jo",
            "family_name": "Doe",
            "name": "Jo Doe",
            "https://purl.imsglobal.org/spec/lti/claim/ext": {
                "user_username": "student1",
                "lms": "moodle-2"
            },
            "https://purl.imsglobal.org/spec/lti/claim/launch_presentation": {
                "locale": "en",
                "document_target": "window",
                "return_url": "https://vle.example.com/lti/return.php?course=3&launch_container=4&instanceid=3&sesskey=BKH3oeWS3f"
            },
            "https://purl.imsglobal.org/spec/lti/claim/tool_platform": {
                "product_family_code": "moodle",
                "version": "2020061512",
                "guid": "vle.example.com",
                "name": "vle.example.com",
                "description": "VLE for example.com"
            },
            "https://purl.imsglobal.org/spec/lti/claim/version": "1.3.0",
            "https://purl.imsglobal.org/spec/lti/claim/message_type": "LtiResourceLinkRequest",
            "https://purl.imsglobal.org/spec/lti/claim/custom": {
                "course_suffix": "foo",
                "system_setting_url": "https://vle.example.com/lti/services.php/tool/3/custom",
                "context_setting_url": "https://vle.example.com/lti/services.php/CourseSection/3/bindings/tool/3/custom",
                "link_setting_url": "https://vle.example.com/lti/services.php/links/{link_id}/custom"
            },
            "https://purl.imsglobal.org/spec/lti-ags/claim/endpoint": {
                "scope": [
                "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem",
                "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly",
                "https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly",
                "https://purl.imsglobal.org/spec/lti-ags/scope/score"
                ],
                "lineitems": "https://vle.example.com/lti/services.php/3/lineitems?type_id=3",
                "lineitem": "https://vle.example.com/lti/services.php/3/lineitems/5/lineitem?type_id=3"
            }
        }
        jwt_encode_handler = jwt_settings.JWT_ENCODE_HANDLER
        token = jwt_encode_handler(source_token)

        response = c.post("/lti1p3/launch/?", {"state": query_dict["state"][0], "id_token": token})

.... and it fails with a pylti1p3.exception.LtiException: JWT KID not found error.

The full error reads:

Traceback (most recent call last):
  File \"/path/to/lib/python3.7/site-packages/django/core/handlers/exception.py\", line 47, in inner
    response = get_response(request)
  File \"/path/to/lib/python3.7/site-packages/django/core/handlers/base.py\", line 181, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File \"/path/to/lib/python3.7/site-packages/sentry_sdk/integrations/django/views.py\", line 36, in sentry_wrapped_callback
    return callback(request, *args, **kwargs)
  File \"/path/to/lib/python3.7/site-packages/django/views/generic/base.py\", line 70, in view
    return self.dispatch(request, *args, **kwargs)
  File \"/different/path/to/mycomponent/api/views.py\", line 554, in dispatch
    return super().dispatch(*args, **kwargs)
  File \"/different/path/to/mycomponent/api/views.py\", line 494, in dispatch
    payload = self.add_extra_jwt_claims(payload)
  File \"/different/path/to/mycomponent/api/views.py\", line 564, in add_extra_jwt_claims
    message_launch_data = message_launch.get_launch_data()
  File \"/path/to/lib/python3.7/site-packages/pylti1p3/message_launch.py\", line 494, in get_launch_data
    return self._get_jwt_body()
  File \"/path/to/lib/python3.7/site-packages/pylti1p3/message_launch.py\", line 299, in _get_jwt_body
    self.validate()
  File \"/path/to/lib/python3.7/site-packages/pylti1p3/message_launch.py\", line 284, in validate
    return self.validate_state()\\
  File \"/path/to/lib/python3.7/site-packages/pylti1p3/message_launch.py\", line 671, in validate_jwt_signature
    public_key = self.get_public_key()
  File \"/path/to/lib/python3.7/site-packages/pylti1p3/message_launch.py\", line 569, in get_public_key
    raise LtiException(\"JWT KID not found\")
pylti1p3.exception.LtiException: JWT KID not found

Digging into the code, the problem seems to be the contents of self._jwt in the MessageLaunch.get_public_key method has the id_token rather than the jwks

Any hints as to where I'm going wrong?

EDIT:
I can confirm that tool_conf.get_jwks("example_dot_com") returns an appropriate token:

{
    "alg": "RS256",
    "e": "AQAB",
    "kid": "9uXgneuBIDrQQprq9DVtQ0Za-elqN5wHsviNOtLmQdY",
    "kty": "RSA",
    "n": "u9omeEgZmWHV0448LhGFFUzpGynHhLN88sdbbHP7tEd67zcqfbSoeWiIWbVCwXFVrkTZNjXb5YANNcBrj7bOj6nGNvevYMM5YgxhfjvJ2rbAfPNtjeUzaj95la2wO14rc9b32FmMSzLE5xn5I8HqXZUHPH0W5VS3avBp_v-dyLTacXmlvzvDgJ2gNT9vPiUWLXSS9SajIYr7Sj5IILjSlHod5UkHGSyi7sK5vSpArOh3LbzM0fPPCqupIb6bouDgYRxjUqSphvY891nRMCWtQKXO4aOk65AqqQsRnQovLiyDxPYMesc3kZcNsuPlWyF5YNE81EcmdXRnrkBKqowdZQ",
    "use": "sig",
}

Existance of More Extensive Documentation?

I am trying to learn this library, and am having difficulty understanding parts of it. Is there any further documentation than what was included in the README? Some explanation of the individual classes and important functions would be beneficial to my efforts in understanding this library.

support public/private key files in `ToolConfDict`

The ToolConfJsonFile class includes support for reading the public and private key files based on the contents of the public_key_file and private_key_file config properties. The ToolConfDict class, on which it is based, does not. If the dictionary support class could use those same properties to load the key files, it would allow us to simplify our code. We currently end up duplicating the code of ToolConfJsonFile._process_iss_conf_item in our code.

I think if this were supported in the dictionary class, you couldn't support _configs_dir the same way, but it wouldn't be a major problem.

I have a idea of how this could be implemented. I can open a pull request for this, if you'd like.

Some question about LTI 1.3

Hi Dmitry,

I look up the document about LTI 1.3.
When I try to do OIDCAuthorization, I try to call API to https://canvas.instructure.com/api/lti/authorize_redirect, I can't find the specific parameters I should use, and don't know how I generate the parameter state.

Could you please spare some time to talk more?
[https://canvas.instructure.com/doc/api/file.lti_dev_key_config.html]
Many thanks,
Lucy

AGS find_or_create_lineitem does not check scopes

AssignmentsGradesService.find_or_create_lineitem does not check its scopes before making the service call to create a new lineitem. On Moodle the user can configure the tool to access to put scores to the default line item but not to create new lineitems.

Grammar errors in documentation

While reading through the README documentation for this library, I noticed there were a couple of minor grammatical errors that could be fixed to improve documentation readability.

I can detail what errors I noticed and/or make a PR with the corrections.

put_grade error 401

Hey

My name is Taras
We faced with the issue described below:

We have a problem with the tool service.
For example, we have the service "IMS LTI Assignment and Grade Services" in your library is a method "put_grade" this method makes a query "https://learn.catalynk.net/mod/lti/services.php/7/lineitems?type_id=3" in order to see line-items in the log this query returns 401

image
and such an error with "IMS Titles and LTI roles” on this request “https://learn.catalynk.net/mod/lti/services.php/CourseSection/7/bindings/3/memberships”.

image
This error occurred after the deployment on Heroku local errors are not local, I used this docker-compose for moodle

image

I think my moodle settings are correct

image
image

Maybe you'll have some ideas

Also here is a piece of code for a clear understanding

image

Thanks and look forward to hear your thoughts
Taras

Issue with JWKS

Hi,

I have an LTI 1.3 tool that I'm connecting to different LMSes for launching a webpage and then reporting grades back.

On the LMS side, you can usually set up either a JWKS URL or a public key for the tool connection.
If I set up the public key in pem format everything works fine. If instead I set up a JWKS URL, I get 400 errors when reporting a grade back to the LMS (tested with Avendoo and https://saltire.lti.app/platform).

My code is based on the Django example, I'm using the GET /jwks URL.

A call to GET /jwks seems to return a valid response that I can even transform to pem:

{
    "keys": [
        {
            "e": "AQAB",
            "kid": "bIaXjCAdqjr8ffH57teL4mRDam4KZOqmi7XvQe0n79c",
            "kty": "RSA",
            "n": "xWHS1QVRrHMAT9vmn5hQJ2nCoZ11CQSEv6b6tlIupKbyxCBRbk6Te094RsPcPgCaTHBE2TJ_mdQCqgiW5QPJCzPA2TKgjOvS7K8p9IM74imFJe8FkRlAFRF0JObrHWDS5Jw8f43ko3UjMHclGfP59uDN6IpEd8JDnssZA3wmzombTH8zgBpruvoi7W90pSaOnOVPYDdKn-KM2qcEaHnrvu53I5O0SHlBpkmBeiO6uoE1jcuv-qY_z1rlS9Y-xUHa1zWPnW4YpjGhOzSGxzZT5sTHGHXJdNtM2AoxgB3DvgMCr9xJ8LCOLCtJYqcV3yQ1SGHjJ4-IxYGCX0is_U-JhQ",
            "alg": "RS256",
            "use": "sig"
        }
    ]
}

From the LMS side, I don't get many details on the error, just "invalid key".

Has anyone made this work or can point me to a way to get more details on the issue?

AssignmentsGradesService.get_lineitems() needs to account for "next" link

When calling AssignmentsGradesService.get_lineitems() we're only getting the first 200 Line Items since D2L restricts to returning 200 line items.

from IMS docs:

limit - to restrict the number of line items returned; the platform MAY further reduce the number of items returned at its own discretion. If more items exist following the returned ones, a 'Link' http header (per [RFC8288]) with a URL pointing to the next page and a 'rel' value of 'next' MUST be included in the response; The platform MAY also include other relations including 'prev', 'first' and 'last'.
The URL for the 'next' link is left to the discretion of the implementer. The tool must use the 'next' URL as is and not re-apply filters to it. The tool platform must thus make sure the 'next' URL contains enough information to insure the next page displays the right set of elements based on the filters present on the original request.

Link: https://lms.example.com/sections/2923/lineitems/69?p=2; rel="next"

Deployment ID in JSON

Heya,

First of all, thanks for making an awesome library!

I've been looking through the spec, and from our own usage, and I think deployment_id should be removed as a check in the JSON. The deployment ID is sent back via the JWT already, and isn't part of the signing. Having to go and check the deployment ID manually is going to be impossible for many vendors.

For example: We make an LTI and install it in over 200 courses. We would need to then get the autogenerated deployment ids from each course and add them to our JSON.

Faculty are also able to clone a course with our LTI installed. That will change the deployment id, but we won't know that, so even though it's a valid install, our application won't work for them.

I suggest removing the deployment_id check, and just adding the deployment_id from the JWT to a session variable.

Sending login request query params in post data

While permforming a POST login request, i have observed another GET login request with all the post data as query params, Is there any other way to send the query params, I am asking this because our server have request max_length limitation and the query params causes issues.
This happens if .enable_check_cookies() is used for redirection.

First Post request
image

Second Get request
image

LTI consumer

Can we use this library to create an LTI consumer(like moodle or canvas)? If so then what are the steps to do?

Failed to get auth token for NRPS

Hi @dmitry-viskov ,
I was able to call get_members earlier but now I facing server error in both canvas and brightspace setup.

Here is the payload :

{'grant_type': 'client_credentials', 'client_assertion_type': 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', 'scope': 'https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly', 'client_assertion': <jwt_val>'}

Error Message :

image

add functions to check for roles

I'd like to see functions for is_instructor(), is_teaching_assistant(), is_learner(), etc. It seems these need to be implemented in each application that supports LTI, so it would be nice to have them as part of this module.

Unrecognized message type

HI @dmitry-viskov
I am doing "Ultra Proctoring Service Integration - IMS Proctoring Spec + UEF " with Blackboard. I have completed all the integration part but when i launch proctoring tool , I get https://purl.imsglobal.org/spec/lti/claim/message_type': 'LtiStartProctoring in the data and due to this getting pylti1p3.exception.LtiException: Unrecognized message type.

So can we add this type in recognised message type ?
Attaching document link as well screenshot
BB doc - https://docs.blackboard.com/lti/proctoring/ultra-proctoring-service-integration
IMS doc- https://www.imsglobal.org/spec/proctoring/v1p0
Screenshot from 2021-09-16 16-20-48
Screenshot from 2021-09-17 01-05-50

'

Detect Transient LTI Launch

It would be nice if the MessageLaunch class had the ability to detect Transient LTI Launches.

The function could be something like message_launch.check_transient()

This is useful for detecting impersonation of users in Learning Management Systems. I believe the role claim comes through as "http://purl.imsglobal.org/vocab/lis/v2/institution/person#Transient".

IMS Information: http://www.imsglobal.org/lti-when-user-not-real-user

If this change is deemed valid, then I would be happy to help in implementing this change.

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.