Skip to content

Commit

Permalink
Partially modified the tree structure and reworked Core LTI documenta…
Browse files Browse the repository at this point in the history
…tion
  • Loading branch information
OneComputerGuy committed Sep 11, 2024
1 parent 9f95db1 commit 46dc256
Show file tree
Hide file tree
Showing 77 changed files with 486 additions and 1,356 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,6 @@

npm-debug.log*
yarn-debug.log*
yarn-error.log*
yarn-error.log*

.obsidian
7 changes: 0 additions & 7 deletions docs/blackboard/caliper/caliper-intro.md

This file was deleted.

1 change: 1 addition & 0 deletions docs/blackboard/lti/1.1/welcome.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
LTI 1.1
Empty file.
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
---
title: Sanctioned LTI Registration and Deployment with Learn
id: lti-registration-deployment
categories: Standards
author: Mark Kauffman
title: Best practices and common issues
id: best-practices
published: ""
edited: ""
sidebar_position: 3
edited: "2024-09-04"
author: Mark Kauffman, Scott Hurrey
---

## Motivation
## Sanctioned method of deployment

At Anthology we want our clients to have the best possible experience with Learn and the products that are integrated with it. To that end we developed a central registration service on our developer portal for use by 3rd-party vendors that are LTI 1.3 Tool providers. Using the central registration service, the Tool provider can go through the complexities of Tool registration and get a Client ID. That Client ID is all our mutual clients need to deploy a Tool. When the Tool is deployed a Deployment ID is generated that the client then provides the Tool vendor. The Client ID and Deployment ID are included in all LTI 1.3 communication, giving the Tool everything needed to identify the client's LMS. Reference [LTI Registration and Deployment](./lti-registration-and-deployment.md).
### tl;dr

## Why Vendor Tool-registration Is Best
For LTI 1.3 only integrations the best client experience is given by a vendor when the vendor registers on the Anthology central registration portal and shares the client_id with the client. There are other processes that involve the vendor asking the client to register the LTI-1.3-only integration. While not ideal, that’s OK, as long as the integration is not using the associated REST API key/secret.

### Motivation

At Anthology we want our clients to have the best possible experience with Learn and the products that are integrated with it. To that end we developed a central registration service on our developer portal for use by 3rd-party vendors that are LTI 1.3 Tool providers. Using the central registration service, the Tool provider can go through the complexities of Tool registration and get a Client ID. That Client ID is all our mutual clients need to deploy a Tool. When the Tool is deployed a Deployment ID is generated that the client then provides the Tool vendor. The Client ID and Deployment ID are included in all LTI 1.3 communication, giving the Tool everything needed to identify the client's LMS.

### Why Vendor Tool-registration Is Best

Why do we only sanction vendor Tool registration? First, the vendor owns the Tool, hence it should be their responsibility to manage it. Second, there are about nine pieces of information to exchange during the registration process. Those are vendor specific settings, and the client doesn't need to be aware of any of those. Third, and especially important, should the vendor need to change any details about their Tool, if they own the registration, they can change it in a one-time action on their registration for all clients with negligible impact on our mutual clients. If the vendor has requested the client register the vendor Tool, then every client will need to make the changes on the central registration system. This latter option is error prone and likely to produce Tool downtime for our mutual clients.

Expand All @@ -24,6 +28,22 @@ Hence, a Tool vendor can always design their system and processes so that our mu

The bottom line is that having the vendor register their Tool(s) to get Client ID(s) and having our mutual clients simply deploy their Client ID in exchange for a Deployment is the best process. But, if a vendor believes that it’s necessary for clients to register an LTI Tool, then clients are free to do so, while knowing that said vendor can likely make the process easier and simpler. We ask that the vendor talk to us and consider improving their architecture and processes to make our mutual clients work easier on deployment. And again, for REST Applications or for LTI Applications using REST APIs, the client should never register except in the case that the client owns, hosts, and manages the REST Application on-premise.

## tl;dr
## Common issues

For LTI 1.3 only integrations the best client experience is given by a vendor when the vendor registers on the Anthology central registration portal and shares the client_id with the client. There are other processes that involve the vendor asking the client to register the LTI-1.3-only integration. While not ideal, that’s OK, as long as the integration is not using the associated REST API key/secret.
### Turn on all the switches

We often hear about this message "The Tool Provider has been disabled by the System Administrator" The usual cause is that one of the 3 LTI switches is disabled. There are 3 places you need to enable LTI: (1) at the tool level, (2) at the course/organization level, and (3) at the LTI Global Properties level. Screenshots follow. **LAST, BUT NOT LEAST - Make certain you are enrolled in the course, as an instructor or student, before using the LTI placements you create in a course! Your launch will fail if you don't have a role in the course.**

#### All the Switches

1. Administrator Panel -> Tools & Utilities section -> Tools

![1-LTIConfigAdminTools.png](/assets/img/turn-on-all-switches-1.png)

2. In a Course/Organization, Start at the Left Nav Menu

![2-LTIConfigCourseOrg.png](/assets/img/turn-on-all-switches-2.png)

3. Administrator Panel -> Integrations section -> LTI Tool Providers -> Manage Global Properties

![3-LTIConfigManageGlobal.png](/assets/img/turn-on-all-switches-3.png)
81 changes: 81 additions & 0 deletions docs/blackboard/lti/1.3/core/authentication.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
---
title: Authenticating using OIDC
id: oidc-auth
sidebar_position: 2
published: "2024-09-04"
edited: "2024-09-04"
---

To complete the LTI message flow we need to perform an OIDC authentication step as seen here

![image of the OIDC authentication part within the LTI workflow](/assets/img/lti-launch-sequence-oidc.png)

## Overview

When a user clicks on an LTI link within Learn, the Learn server receives a GET request from the browser with information about that LTI link. Once it loads the tool configuration associated with that link the first thing it does is initiate the OIDC Login request with a browser redirect to the registered OIDC Login URI provided by your tool when registering on the Developer Portal. The initial login request also passes some information along as query parameters.

The data sent by Learn to the OIDC login endpoint of your tool is the following:

- issuer
- login_hint - an opaque value to the tool that must be returned back
- target_link_uri - the URI configured by the tool for this LTI link
- lti_message_hint - an opaque value to the tool that must be returned back
- lti_deployment_id - this is optional, but Learn always sends it
- client_id - this is optional, but Learn always sends it
- lti_storage_target - for use if cookies aren't possible

Once the request is received, your tool must then redirect to the registered OIDC Authentication Request URI provided by the Developer Portal (shown when the registration process was completed), including a Redirect URI (which must be pre-registered) and a state value, along with the other values passed in by the platform. The Redirect URI declares where the tool wants the subsequent LTI launch to go, and the state is what protects against CSRF. The state should be saved in a cookie, so the tool can verify that the initiator of the request is the same browser that sends the LTI message launch. If a cookie cannot work because of browser restrictions preventing setting of cookies by 3rd parties in iframes, then another option must be pursued such as lti_storage_target.

:::caution Redirect URI
Make sure that the Redirect URI is encoded using URLEncode before redirecting to the OIDC endpoint of the Developer Portal. Failure to encode that value could result on broken workflows
:::

The fields the tool must send on the redirect to the Developer Portal are:

- response_type=id_token
- scope=open_id
- login_hint - received as a parameter on the login request from Learn
- lti_message_hint - received as a parameter on the login request from Learn
- state - a random value generated by your tool to prevent CRSF attacks
- nonce - a random value generated by your tool to prevent duplicate requests
- redirect_uri - a pre-registered URL of your tool where the LTI POST with the data will be sent **(MUST BE ENCODED)**
- client_id - The Client ID (or Application ID) of your tool

:::danger login_hint and lti_message_hint values
The values for the `login_hint` and `lti_message_hint` must be sent to the Developer Portal on the redirect **unaltered** since those contain information critical for the workflow and are verified in subsequent calls
:::

## Sample code

Below you can find sample code that outline how the OIDC redirect can be performed

#### NodeJS

```node
exports.oidcLogin = function(req, res) {
const state = uuid.v4(); // save this locally, such as in a cookie; optional in the OIDC spec
const nonce = uuid.v4(); // Used to prevent playback
const client = // You'll need to determine the client ID for this request from parameters on the request
const redirectUri = // Get the Redirect URI for this client
const oidcAuthUrl = // The URL you were given for this client when you registered your application

let url =
oidcAuthUrl +
"?response_type=id_token" +
"&scope=openid" +
"&login_hint=" +
req.query.login_hint +
"&lti_message_hint=" +
req.query.lti_message_hint +
"&state=" +
state +
"&redirect_uri=" +
encodeURIComponent(redirectUri) +
"&client_id=" +
clientId +
"&nonce=" +
nonce;

res.redirect(url);
};
```
25 changes: 25 additions & 0 deletions docs/blackboard/lti/1.3/core/core-launch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
title: LTI Message Launch Flow
id: core-launch
sidebar_position: 1
published: ""
edited: ""
---

LTI proposes a communication between LMS and the system based on JWT payloads sent between the systems, each with a different intention, however, the Core LTI 1.3 launch is the same for any type of interaction. This core launch is the base from where Deep Linking, Assignments and Grades and Names and Roles Provisioning Service are build upon (These services are outlined in later sections)

The UML sequence diagram below shows all the steps, including some optional ones, for implementing the reception of an LTI message launch. We'll break each step down in the next sections.

<!-- + TODO: Update the diagram of this page and the next sections -->

![LTI Launch Sequence](/assets/img/lti-launch-sequence.png)

### Workflow Steps

The LTI workflow above has three main steps to complete the communication between the two systems.

- First, it performs an OIDC authentication which starts when the user clicks on the LTI item within Learn
- Second, it receives a JWT token from Learn and performs a validation to review if the token received from Learn is correct and hasn't been altered
- And third, it performs additional calls if needed to authenticate to complementary services.

These steps will be reviewed in depth in the following sections.
Original file line number Diff line number Diff line change
@@ -1,65 +1,22 @@
---
layout: post
title: "Handling the Launch with id_token JWT"
id: id-token
title: Sample JWT content
id: jwt-contents
sidebar_position: 5
published: ""
edited: ""
categories: Standards
author: Eric Preston
edited: "2024-09-04"
---

### Overview
The following is a sample of the expected JWT contents received from the form POST request

Once the OIDC login negotiation has succeeded you will receive a FORM POST from the browser with two form paramaters:

1. id_token - this is the JSON Web Token (JWT) that contains all the launch information. See [jwt.io](https://jwt.io) for more information on JWTs. You can also use that site to inspect any JWTs you receive (or create).
2. state - if you sent a state value back in the OIDC auth response you will receive this as a URL parameter. You can (should?) use to to ensure the user who initiated the OIDC login is the same user issuing the launch.

These are the basic steps you should upon receiving the id_token JWT:

1. Verify the token
1. Get the header, body, and signature from the JWT (these parts are separated by a period `.`
2. Verify the signing algorithm is what you expect
3. Get the Client ID and its JWKS URL.
4. Validate the signature, using a JWT library
2. Parse the body into JSON - you now have all the information you need to handle the launch for your tool

### Sample code

Some sample code in Node.js is below:

```
exports.handleIdToken = function(id_token) {
let parts = id_token.split(".");
let header = JSON.parse(Buffer.from(parts[0], "base64").toString());
let body = JSON.parse(Buffer.from(parts[1], "base64").toString());
// Verify launch is from correct party
// aud could be an array or a single entry
let clientId;
if (jwtPayload.body.aud instanceof Array) {
clientId = jwtPayload.body.aud[0];
} else {
clientId = jwtPayload.body.aud;
}
let publicKey = // Get public key from the configured JWKS URL using the kid in the header of the JWT
jwt.verify(id_token, jwk2pem(publicKey));
// All is good so run with the JWT body
};
```

### Sample JWT JSON
:::info
Please keep in mind that this information can change, always review the Core LTI 1.3 specification for details about the included information
:::

```
{
"kid": "53c4573a-1ac8-4484-b036-a7b22b557e8c",
"alg": "RS256"
}
},
{
"sub": "4f1025ffab1846ee9ca0a53299dd51b6",
"https://purl.imsglobal.org/spec/lti/claim/deployment_id": "c3c37f92-d008-43db-9e8a-e10fd139ec2d",
Expand Down
73 changes: 73 additions & 0 deletions docs/blackboard/lti/1.3/core/lti-post.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
---
title: Receiving the id_token
id: idtoken-verification
sidebar_position: 3
published: ""
edited: ""
---

From the same workflow used on previous sections, we can now work on the next part of the flow which is the handling of the POST request which includes the id_token

![image of the LTI workflow outlining the LTI POST to the tool](/assets/img/lti-launch-sequence-idtoken.png)

## Overview

Continuing after the OIDC redirect from the last section, the Developer Portal receives the OIDC authentication request from the tool and validates that the redirect URI is registered. It then, redirects to Learn with a command to authenticate the LTI launch, which builds the id_token (JWT) that contains the LTI payload (user, course, resource, etc.) and signs it with its private key. Learn then auto-submits a form with the POST method to the redirect URI specified with two form parameters:

- `id_token` this is the JSON Web Token (JWT) that contains all the launch information. See [jwt.io](https://jwt.io) for more information on JWTs. You can also use that site to inspect any JWTs you receive (or create).
- `state` - if you sent a state value back in the OIDC auth response you will receive this as a URL parameter. You can (and should) use it to ensure the user who initiated the OIDC login is the same user issuing the launch.

When your tool receives this form POST from the browser, the first thing it should do is validate the state is what it stored in the cookie. If it doesn't match the tool should reject the request since it could have been altered.

The next step is to unpack the JWT and validate the signature. JWTs are comprised of three Base64-encoded strings separated by the period (.) sign. The first part is the header which contains the key ID (kid) and signing algorithm. The second part is the payload in JSON format, and it contains the issuer (iss), client ID (aud), user ID (sub) and other information about the launch. Your tool must look up the platform configuration for that issuer/client ID/deployment ID combination and get the platform's public key from the Developer Portal public keyset URL, using the kid in the JWT header for selecting the particular key the platform used to sign the token. The third part is the signature, which normally your code won't work with directly.

Your tool then uses a library of their choosing to validate the signature of the JWT. If the signature is valid then the tool can proceed to process the rest of the JWT information and render its UI with the information it has about the LTI message.

## tl;dr of the steps needed to validate the token

These are the steps you should follow upon receiving the id_token JWT:

1. Verify the token
1. Get the header, body, and signature from the JWT (these parts are separated by a period `.`)
2. Verify the signing algorithm is what you expect. This can be found by parsing the header portion of the id_token
3. Verify if the launch is for the proper Client ID (Application ID) by checking the `aud` property of the body portion. It should match the Client ID of your tool
4. Get the JWKS key used by Learn to sign the JWT token. This can be found by performing a GET request to the JWKS endpoint given by the Developer Portal when registering the application, from there, you can select the proper key by checking the `kid` value included in the header of the JWT token against the values received from the Developer Portal
5. Validate the signature using a JWT library, using the id_token and the key selected from the `kid`
2. Parse the body into JSON - you now have all the information you need to handle the launch for your tool

:::caution Verify the token with a proper library
To prevent any issues while verifying the token, use a JWT library that has a `verify` function since this will cryptographically validate the token.
:::

## Sample code

Below you can find sample code that outline how the validation of the id_token can be performed

#### NodeJS

```nodejs
const jwt = require("jsonwebtoken");
const jwk2pem = require("pem-jwk");
exports.handleIdToken = function(id_token) {
let parts = id_token.split(".");
let header = JSON.parse(Buffer.from(parts[0], "base64").toString());
let body = JSON.parse(Buffer.from(parts[1], "base64").toString());
// Verify launch is from correct party
// aud could be an array or a single entry
let clientId;
if (jwtPayload.body.aud instanceof Array) {
clientId = jwtPayload.body.aud[0];
} else {
clientId = jwtPayload.body.aud;
}
let publicKey = // Get public key from the configured JWKS URL using the kid in the header of the JWT
jwt.verify(id_token, jwk2pem(publicKey));
// All is good so run with the JWT body
};
```
Loading

0 comments on commit 46dc256

Please sign in to comment.