Installation

Setting up LTI 1.3 for WeBWorK 2.19

Setting up LTI 1.3 for WeBWorK 2.19

by Michael Nyenhuis -
Number of replies: 17

Hi,

I've been trying to set up LTI 1.3 with Moodle for Webwork 2.19 on RHEL, proxied via httpd. It has been set up, I believe correctly, only Moodle, and I believe I have set things up correctly on Webwork. When I first click on Select Content, I get The "No Webwork course associated to the LMS course" alert, but no extra information. I understand that this means the JWT is not being decoded.

Any help in getting LTI 1.3 working would be greatly appreciated.

Thanks!

The output I get in debug.log is

===> Begin WeBWorK::dispatch() <===

[Tue Jul 29 15:51:55.379643 2025] (eval): Hi, I'm the new dispatcher!
[Tue Jul 29 15:51:55.379985 2025] (eval): --------------------------------------------------------------------------------
[Tue Jul 29 15:51:55.380081 2025] (eval): Okay, I got some basic information:
[Tue Jul 29 15:51:55.380278 2025] (eval): The site location is /webwork2
[Tue Jul 29 15:51:55.380348 2025] (eval): The request method is POST
[Tue Jul 29 15:51:55.380726 2025] (eval): The URI is /webwork2/ltiadvantage/login
[Tue Jul 29 15:51:55.380830 2025] (eval): The argument string is iss=https%3A%2F%2Fsandbox.kpu.ca&target_link_uri=https%3A%2F%2Fwebwork2.kpu.ca%2Fwebwo>
[Tue Jul 29 15:51:55.381029 2025] (eval): --------------------------------------------------------------------------------
[Tue Jul 29 15:51:55.381162 2025] (eval): The path is /ltiadvantage/login/
[Tue Jul 29 15:51:55.381397 2025] (eval): The current route is ltiadvantage_login
[Tue Jul 29 15:51:55.381471 2025] (eval): Here is some information about this route:
[Tue Jul 29 15:51:55.381664 2025] (eval): The display module for this route is WeBWorK::ContentGenerator::LTIAdvantage
[Tue Jul 29 15:51:55.381730 2025] (eval): This route has the following captures:
[Tue Jul 29 15:51:55.381910 2025] (eval):       action => login
[Tue Jul 29 15:51:55.381991 2025] (eval):       controller => LTIAdvantage
[Tue Jul 29 15:51:55.382162 2025] (eval): --------------------------------------------------------------------------------
[Tue Jul 29 15:51:55.382223 2025] (eval): Now we want to look at the parameters we got.
[Tue Jul 29 15:51:55.382379 2025] (eval): The raw params:
[Tue Jul 29 15:51:55.382545 2025] (eval):       lti_message_hint => "{"launchid":"ltilaunch_ContentItemSelectionRequest919966836"}"
[Tue Jul 29 15:51:55.382775 2025] (eval):       target_link_uri => "https://webwork2.kpu.ca/webwork2/ltiadvantage/content_selection"
[Tue Jul 29 15:51:55.382849 2025] (eval):       lti_deployment_id => "10"
[Tue Jul 29 15:51:55.383017 2025] (eval):       iss => "https://sandbox.kpu.ca"
[Tue Jul 29 15:51:55.383083 2025] (eval):       login_hint => "2780"
[Tue Jul 29 15:51:55.383248 2025] (eval):       client_id => "0Zkywptk292RPvD"
[Tue Jul 29 15:51:55.383309 2025] (eval): --------------------------------------------------------------------------------
[Tue Jul 29 15:51:55.410488 2025] (eval): We need to get a course environment (with or without a courseID!)
[Tue Jul 29 15:51:55.415488 2025] (eval): Here's the course environment: WeBWorK::CourseEnvironment=HASH(0x561a6c0f1728)
[Tue Jul 29 15:51:55.452010 2025] (eval): Using authentication module WeBWorK::Authen::LTIAdvantage: WeBWorK::Authen::LTIAdvantage=HASH(0x561a6c08a580)
[Tue Jul 29 15:51:55.452151 2025] (eval): We got a courseID from the route, now we can do some stuff:
[Tue Jul 29 15:51:55.452224 2025] (eval): ...we can create a database object...
[Tue Jul 29 15:51:55.481482 2025] (eval): (here's the DB handle: WeBWorK::DB=HASH(0x561a6c1e9860))
[Tue Jul 29 15:51:55.481773 2025] WeBWorK::Authen::LTIAdvantage::verify: The LTI Advantage login route was accessed with the appropriate parameters.
[Tue Jul 29 15:51:55.694901 2025] (eval):

===> Begin WeBWorK::dispatch() <===

[Tue Jul 29 15:51:55.695291 2025] (eval): Hi, I'm the new dispatcher!
[Tue Jul 29 15:51:55.695549 2025] (eval): --------------------------------------------------------------------------------
[Tue Jul 29 15:51:55.695769 2025] (eval): Okay, I got some basic information:
[Tue Jul 29 15:51:55.695939 2025] (eval): The site location is /webwork2
[Tue Jul 29 15:51:55.696097 2025] (eval): The request method is POST
[Tue Jul 29 15:51:55.696458 2025] (eval): The URI is /webwork2/ltiadvantage/launch
[Tue Jul 29 15:51:55.696778 2025] (eval): The argument string is id_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImNiMmE0OTU1ODUxM2I0ZWMwNWRhIn0.e>
[Tue Jul 29 15:51:55.697115 2025] (eval): --------------------------------------------------------------------------------
[Tue Jul 29 15:51:55.697419 2025] (eval): The path is /ltiadvantage/launch/
[Tue Jul 29 15:51:55.697762 2025] (eval): The current route is ltiadvantage_launch
[Tue Jul 29 15:51:55.697940 2025] (eval): Here is some information about this route:
[Tue Jul 29 15:51:55.698130 2025] (eval): The display module for this route is WeBWorK::ContentGenerator::LTIAdvantage
[Tue Jul 29 15:51:55.698178 2025] (eval): This route has the following captures:
[Tue Jul 29 15:51:55.698225 2025] (eval):       action => launch
[Tue Jul 29 15:51:55.698269 2025] (eval):       controller => LTIAdvantage
[Tue Jul 29 15:51:55.698316 2025] (eval): --------------------------------------------------------------------------------
[Tue Jul 29 15:51:55.698360 2025] (eval): Now we want to look at the parameters we got.
[Tue Jul 29 15:51:55.698404 2025] (eval): The raw params:
[Tue Jul 29 15:51:55.698508 2025] (eval):       id_token => "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImNiMmE0OTU1ODUxM2I0ZWMwNWRhIn0.eyJub25jZSI6Ij>
[Tue Jul 29 15:51:55.698602 2025] (eval):       state => "ab246cc3e221e1346b3a6b742cb9f203f084947f3f82cd7bebccccad88dd4993"
[Tue Jul 29 15:51:55.698659 2025] (eval): --------------------------------------------------------------------------------
[Tue Jul 29 15:51:55.990545 2025] (eval): We need to get a course environment (with or without a courseID!)
[Tue Jul 29 15:51:55.995745 2025] (eval): Here's the course environment: WeBWorK::CourseEnvironment=HASH(0x561a6c45eff0)
[Tue Jul 29 15:51:55.996156 2025] (eval): Using authentication module WeBWorK::Authen::LTIAdvantage: WeBWorK::Authen::LTIAdvantage=HASH(0x561a6c57a788)

In reply to Michael Nyenhuis

Setting up LTI 1.3 for WeBWorK 2.19

by Michael Nyenhuis -
LTI 1.3 is still not working. If an instructor first tries to link to a Webwork course, they get the "No WeBWorK course was found associated to this LMS course" message, but no course ID is given. I've checked debug.log and decoded the JWT using command-line tools. I had been using person_sourcedid (with the purl prefix) for username (that's what it is on the LMS), and no person_sourcedid is given in the JWT. I changed the source to email. No change. I copied the course ID in the JWT to a course in Webwork, again, no change. I've checked all my conf files, and I find no typos.

What steps should I go through to find the problem?

The only things I can think of are
1. base64 complains about invalid input, yet decodes the header and payload. I have been assuming perl can decode the JWT without throwing an error message
2. How ssl has been set up on the server. I can't find any private key file, yet I have been told that the server is properly set up for ssl. Apart from LTI, Webwork is working properly. Does JWT use the ssl private key in some way?

Here's the payload of the JWT:
"nonce": "a39bcb0515eca71623d74154c29e96d1af4ca52d66094d7ead232f7b761100ca",
"iat": 1754688079,
"exp": 1754688139,
"iss": "https://sandbox.kpu.ca",
"aud": "0Zkywptk292RPvD",
"https://purl.imsglobal.org/spec/lti/claim/deployment_id": "10",
"https://purl.imsglobal.org/spec/lti/claim/target_link_uri": "https://webwork2.kpu.ca/webwork2/ltiadvantage/content_selection",
"sub": "2780",
"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/membership#Instructor"
],
"https://purl.imsglobal.org/spec/lti/claim/context": {
"id": "13202",
"label": "test course",
"title": "test course",
"type": [
"CourseSection"
]
},
"https://purl.imsglobal.org/spec/lti/claim/message_type": "LtiDeepLinkingRequest",
"given_name": "Michael",
"family_name": "Nyenhuis",
"name": "Nyenhuis, Michael",
"https://purl.imsglobal.org/spec/lti/claim/ext": {
"user_username": "mnyenhuis",
"lms": "moodle-2"
},
"email": "michael.nyenhuis@kpu.ca",
"https://purl.imsglobal.org/spec/lti/claim/launch_presentation": {
"locale": "en"
},
"https://purl.imsglobal.org/spec/lti/claim/tool_platform": {
"product_family_code": "moodle",
"version": "2022112806.03",
"guid": "535e28cf434e003d29dcfd7f1177e2cc",
"name": "KPU Sandbox",
"description": "KPU Sandbox"
},
"https://purl.imsglobal.org/spec/lti/claim/version": "1.3.0",
"https://purl.imsglobal.org/spec/lti-dl/claim/deep_linking_settings": {
"accept_types": [
"ltiResourceLink"
],
"accept_presentation_document_targets": [
"frame",
"iframe",
"window"
],
"accept_copy_advice": false,
"accept_multiple": true,
"accept_unsigned": false,
"auto_create": false,
"can_confirm": false,
"deep_link_return_url": "https://sandbox.kpu.ca/mod/lti/contentitem_return.php?course=13202&id=10&sesskey=oGiCwz82CO",
"title": "WebWorks",
"text": ""
}
}
In reply to Michael Nyenhuis

Setting up LTI 1.3 for WeBWorK 2.19

by Glenn Rice -

If you get the message "No WeBWorK course was found associated to this LMS course" it does not necessarily mean that the JWT is not being decoded.  That is just one possibility.  Have you enabled the general debugging facility in the conf/webwork2.mojolicious.yml file?  What does the debug log show?

In reply to Glenn Rice

Setting up LTI 1.3 for WeBWorK 2.19

by Glenn Rice -
That was silly, I didn't read your first post. I see that you have debugging enabled and posted the result.

Your debugging log is showing the same thing that another debugging log showed for someone else that I was helping. The debugging log abruptly ends on the launch request and does not complete correctly. The issue in that case was a load balancer. We never actually figured out precisely what the issue was though. Basically, the initial login request was going to one node of the load balancer, and then the launch request to another, and the two nodes were not synchronizing correctly. Every once in a while, we got lucky, and both requests went to the same node, and then the launch was successful.

Do you have a load balancer in the picture that might be causing the same problem for you?
In reply to Glenn Rice

Setting up LTI 1.3 for WeBWorK 2.19

by Michael Nyenhuis -

Thanks for the reply. There is load balancing on WeBWorK, but only one node. It's an AWS server, so there needs to be an Application Load Balancer. I would not be surprised if Moodle has more nodes.

In reply to Michael Nyenhuis

Setting up LTI 1.3 for WeBWorK 2.19

by Michael Nyenhuis -
Turns out the problem was the clock on the server. As I understand it, KPU has its own time server (as any large institution would), and doesn't allow connections to external time servers. The default settings on chrony therefore had it synching to nothing, with the result that the clock was over a minute fast. Once corrected, LTI started working. I still have to test it thoroughly.

I guess the lesson is, if you're setting up WeBWorK on a new server, check that the clock synch's properly.
In reply to Michael Nyenhuis

Setting up LTI 1.3 for WeBWorK 2.19

by Glenn Rice -
Ah, the old clock sync issue. Haven't seen that one mentioned for a while.
In reply to Glenn Rice

Setting up LTI 1.3 for WeBWorK 2.19

by Glenn Rice -

Actually, looking at this closer I realized that the "old clock sync" issue I was thinking of only applies to LTI 1.1.  Although LTI 1.3 has a different time sensitive set up.  Note that the $LTI{v1p3}{stateKeyLifetime} defaults to 60 seconds, but can be increased if needed.  Of course, it is better to synchronize clocks better as you did, but if network lag is enough that would be another reason to need to increase the setting.

In reply to Glenn Rice

Setting up LTI 1.3 for WeBWorK 2.19

by Sean Fitzpatrick -

So now I'm curious: I was having similar issues (which we thought was the load balancer). Could it be this same problem? 

I don't think this ever would have occurred to me, and I don't know how one would diagnose this as the issue. 

For that matter, if it is the issue, I'm not sure how to fix it! I'll do some checking tomorrow. I assume the time sync is done on the WeBWorK side, because I don't have access to the Moodle server.

In reply to Sean Fitzpatrick

Setting up LTI 1.3 for WeBWorK 2.19

by Glenn Rice -
It is possible. One thing that will help here would be for you to try the changes to the code in the pull request at https://github.com/openwebwork/webwork2/pull/2787 that I just opened. That adds a debug log message that should appear in these cases, and may explain what is going on. That debug log message will be at the end of the ltiadvantage_lauch request (the last request that is abruptly terminating currently with no explanation in both of your debug log posts).
In reply to Glenn Rice

Setting up LTI 1.3 for WeBWorK 2.19

by Sean Fitzpatrick -
I've checked out this pull request on my server. I tried content selection with the LTI 1.3 tool, and got the "no associated course" error.

In debug.log, I get the following tail, after a very long id_token string:

[Thu Aug 14 15:45:22.244421 2025] (eval): --------------------------------------------------------------------------------
[Thu Aug 14 15:45:22.444327 2025] (eval): Here's the course environment: WeBWorK::CourseEnvironment=HASH(0x5dcdc2ca8690)
[Thu Aug 14 15:45:22.444940 2025] (eval): Using authentication module WeBWorK::Authen::LTIAdvantage: WeBWorK::Authen::LTIAdvantage=HASH(0x5dcdc2ec6428)
[Thu Aug 14 15:45:22.445303 2025] WeBWorK::ContentGenerator::LTIAdvantage::launch: Failed to decode token received from LMS: JWT: iat claim check failed (1755207925/0 vs. 1755207922) at /opt/webwork/webwork2/lib/WeBWorK/ContentGenerator/LTIAdvantage.pm line 383.
In reply to Sean Fitzpatrick

Setting up LTI 1.3 for WeBWorK 2.19

by Glenn Rice -

Try adding

leeway => 10,

in the %jwt_params hash defined on line 362 of lib/WeBWorK/ContentGenerator/LTIAdvantage.pm.  If that fixes the problem, then I will put in a pull request that adds an option for the value of the leeway there.  The default leeway used by the Crypt::JWT module is 0, and you are showing iat values that differ by 3.  Note that 1755207925/0 means that the iat in the payload is 1755207925 and the leeway is 0.  The /0 is added in the error by the Crypt::JWT module.

In reply to Glenn Rice

Setting up LTI 1.3 for WeBWorK 2.19

by Glenn Rice -

After looking at this closer I see that this is a clock synchronization issue.  The iat in the JWT sent by the LMS was 1755207925 which was 3 seconds in the future relative to the current time of 1755207922 on your webwork2 server.  That means that the clock on the LMS server is at least 3 seconds ahead of the clock on your webwork2 server.  In order for the iat (and exp) value in the JWT to be considered valid they are expected to be before the current time on the webwork2 server.  The "leeway" is the number of seconds that the iat and exp values are allowed to be in the future relative to the current time on the webwork2 server.  So setting that to something like 10 as I mentioned should resolve the issue that you are having.  You might see which server is off and see if the clock on that server can be synchronized.  Although 3 seconds is not a large difference, so using the leeway is probably an acceptable solution in that case.

In reply to Glenn Rice

Setting up LTI 1.3 for WeBWorK 2.19

by Sean Fitzpatrick -

After 10 consecutive successful attempts, I am ready to conclude that this works!

Now I am sitting here laughing at the fact that such a simple thing has caused me two years of headaches.

In reply to Sean Fitzpatrick

Setting up LTI 1.3 for WeBWorK 2.19

by Glenn Rice -
Great. Well, we learned something more about the JWT validation, and the result is an improvement that may make for less headaches in the future! I have added this to the pull request at https://github.com/openwebwork/webwork2/pull/2787. Once that is merged, then you can pull the hotfix, and set $LTI{v1p3}{JWTLeeway} in the conf/authen_LTI_1_3.conf file for this.
In reply to Glenn Rice

Setting up LTI 1.3 for WeBWorK 2.19

by Sean Fitzpatrick -
Thanks Glenn, for making the fix, and thanks also to Michael for identifying the problem! I don't think I ever would have guessed that a three second delay was responsible for all my woes.
In reply to Sean Fitzpatrick

Setting up LTI 1.3 for WeBWorK 2.19

by Danny Glin -

There isn't a time sync being done between the LMS and WeBWorK.  Typically each server would sync time with a network time protocol (NTP) server.  If a server isn't regularly syncing with an NTP server then the system time can drift, which causes problems with certain activities between servers where timestamps are involved (LTI is one example.  Another one I've run into in other contexts is file creation and permissions on network file shares).

In your case (in addition to the new pull request that Glenn referenced), you can check if the server time on your WeBWorK server matches the Moodle server.  WeBWorK displays the time at the bottom of each page, so that's easy to verify.  I'm sure that there are activities in Moodle that record a timestamp, so you would just have to run one of those and see if the time matches what you see from WeBWorK.

In reply to Danny Glin

Setting up LTI 1.3 for WeBWorK 2.19

by Sean Fitzpatrick -

A forum post in Moodle, and opening a page in WeBWorK, suggests that time sync isn't an issue.

(Moodle doesn't display seconds, but we're at least at the same minute on both.)

I'll see if I can find time to test Glenn's PR this afternoon.