To get Canvas to launch Hypothesis in the SpeedGrader, Hypothesis first needs to submit a grading URL to Canvas. For any given assignment and student, if Hypothesis has submitted a grading URL for that assignment and student, then Canvas will use that grading URL to launch Hypothesis when an instructor views that assignment and student in SpeedGrader.
In order to submit a grading URL to Canvas when a student launches an assignment, we first need to implement a server-side submit-grading-URL proxy API that our frontend code can call.
See Also
Grade Passback Tools in the Canvas docs. The LTI Launch URL example on this page is the one we'll be following.
Why we need a proxy API for this
It's important that, when the LMS server receives a launch request from Canvas, it doesn't make any HTTP requests to external services synchronously as part of responding to that original request that our server received. For example the server mustn't make synchronous requests to the Canvas API, or to Canvas's grading submission URL. This is because Canvas sites and their response times are out of our control, and could cause our server's responses to become too slow (especially if the server had to make multiple requests to third-party services before sending back its response).
Instead the LMS app backend provides a number of proxy APIs for its own frontend to call, so that the LMS app's frontend code can ask its backend to make requests to Canvas for it. Each proxy API endpoint makes one request to an external service and returns the wrapped response to our frontend. The frontend code can't simply make these requests itself, because the requests require using Canvas developer keys (client_secrets) and access tokens which we want to keep on the backend, and not make directly available to the frontend, for security reasons.
Where the existing proxy API code in the LMS app is
When the LMS app receives a launch request it responds to the LMS with an HTML page. This HTML page contains a Preact SPA. The server renders a JWT into the HTML, which the Preact's JavaScript code can use in the Authorization header of an XHR request to authenticate itself to the proxy API. The JWT in the HTML looks like this:
When the frontend code sends a request with this bearer token in the Authorization header, that gets picked up by the app's authentication policies, which create a request.lti_user object based on the JWT.
request.lti_user is an instance of the LTIUser class which currently specifies the user's LTI user_id, oauth_consumer_key and roles.
Code elsewhere in the app then makes use of request.lti_user in order to know who the user is.
You may need to add more attributes to LTIUser
As you can see from the example code below, making a grading submission to Canvas requires at least:
The lis_outcome_service_url LTI launch param
The lis_result_sourcedid launch param
The oauth_consumer_key launch param
The user_id launch param
The document URL of the assignment
It requires at least those 5. I might have missed some.
But the LTIUser class currently only specifies the user_id, oauth_consumer_key and roles, so those fields (and fields that we can derive from those) are all we know about the user who is making a proxy API request.
If more information about the user is needed to make the submission then we may need to add more fields to LTIUser, and update the validation schemas that serialize and deserialize LTIUser values.
Note that some of the LTI launch params needed for making a submission are only included in the launch params when a student launches an assignment, not when a teacher launches one. See below.
How to make a grading submission request to Canvas
The URL that we need to post to is given to us in the lis_outcome_service_url LTI launch parameter when a student launches the assignment. This URL may change so we shouldn't save it - get it from the launch params every time.
There's also a lis_result_sourcedid launch param that we need to include in the XML. See below
Canvas will not include lis_outcome_service_url and lis_result_sourcedid if the person launching the assignment isn't a learner. For example it's not included for teachers
We have to sign the request using OAuth 1 request signing and the shared secret that we have for this application instance
The grading URL actually has to be our app's registered LTI launch URL (/lti_launches) or Canvas will refuse to launch it. But we can add whatever grading-specific query parameters we want to the URL.
We'll need to add at least two query parameters to the URL that we submit to Canvas. We'll need these to be able to retrieve the correct document and annotations when Canvas launches us in SpeedGrader:
The URL of the assignment's document, or its file_id if it's a Canvas file assignment
An identifier for the student (LTI user) who the submission belongs to
Belongs to epic: MVP Canvas SpeedGrader Support
To get Canvas to launch Hypothesis in the SpeedGrader, Hypothesis first needs to submit a grading URL to Canvas. For any given assignment and student, if Hypothesis has submitted a grading URL for that assignment and student, then Canvas will use that grading URL to launch Hypothesis when an instructor views that assignment and student in SpeedGrader.
In order to submit a grading URL to Canvas when a student launches an assignment, we first need to implement a server-side submit-grading-URL proxy API that our frontend code can call.
See Also
Why we need a proxy API for this
It's important that, when the LMS server receives a launch request from Canvas, it doesn't make any HTTP requests to external services synchronously as part of responding to that original request that our server received. For example the server mustn't make synchronous requests to the Canvas API, or to Canvas's grading submission URL. This is because Canvas sites and their response times are out of our control, and could cause our server's responses to become too slow (especially if the server had to make multiple requests to third-party services before sending back its response).
Instead the LMS app backend provides a number of proxy APIs for its own frontend to call, so that the LMS app's frontend code can ask its backend to make requests to Canvas for it. Each proxy API endpoint makes one request to an external service and returns the wrapped response to our frontend. The frontend code can't simply make these requests itself, because the requests require using Canvas developer keys (
client_secret
s) and access tokens which we want to keep on the backend, and not make directly available to the frontend, for security reasons.Where the existing proxy API code in the LMS app is
lms/views/api/canvas/
contains our existing Canvas proxy API codeFor example here's our proxy API for Canvas's list files API, and here's our proxy API for Canvas's file download URL API. The routes for these views all have URLs like
/api/canvas/*
These proxy APIs all make use of the
CanvasAPIClient
service which does the actual workHow authenticating to the proxy API works
When the LMS app receives a launch request it responds to the LMS with an HTML page. This HTML page contains a Preact SPA. The server renders a JWT into the HTML, which the Preact's JavaScript code can use in the
Authorization
header of an XHR request to authenticate itself to the proxy API. The JWT in the HTML looks like this:When the frontend code sends a request with this bearer token in the Authorization header, that gets picked up by the app's authentication policies, which create a
request.lti_user
object based on the JWT.request.lti_user
is an instance of theLTIUser
class which currently specifies the user's LTIuser_id
,oauth_consumer_key
androles
.Code elsewhere in the app then makes use of
request.lti_user
in order to know who the user is.You may need to add more attributes to
LTIUser
As you can see from the example code below, making a grading submission to Canvas requires at least:
lis_outcome_service_url
LTI launch paramlis_result_sourcedid
launch paramoauth_consumer_key
launch paramuser_id
launch paramIt requires at least those 5. I might have missed some.
But the
LTIUser
class currently only specifies theuser_id
,oauth_consumer_key
androles
, so those fields (and fields that we can derive from those) are all we know about the user who is making a proxy API request.If more information about the user is needed to make the submission then we may need to add more fields to
LTIUser
, and update the validation schemas that serialize and deserializeLTIUser
values.Note that some of the LTI launch params needed for making a submission are only included in the launch params when a student launches an assignment, not when a teacher launches one. See below.
How to make a grading submission request to Canvas
Here's some hacky but working proof of concept code for submitting an LTI launch URL to Canvas for grading. For more details see the Canvas docs: https://canvas.instructure.com/doc/api/file.assignment_tools.html
Notes:
The URL that we need to post to is given to us in the
lis_outcome_service_url
LTI launch parameter when a student launches the assignment. This URL may change so we shouldn't save it - get it from the launch params every time.There's also a
lis_result_sourcedid
launch param that we need to include in the XML. See belowCanvas will not include
lis_outcome_service_url
andlis_result_sourcedid
if the person launching the assignment isn't a learner. For example it's not included for teachersWe have to sign the request using OAuth 1 request signing and the shared secret that we have for this application instance
The grading URL actually has to be our app's registered LTI launch URL (
/lti_launches
) or Canvas will refuse to launch it. But we can add whatever grading-specific query parameters we want to the URL.We'll need to add at least two query parameters to the URL that we submit to Canvas. We'll need these to be able to retrieve the correct document and annotations when Canvas launches us in SpeedGrader:
file_id
if it's a Canvas file assignmentQuestions
Is there a simple and robust way to avoid making multiple submissions of the same assignment, for the same student?