-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Partially modified the tree structure and reworked Core LTI documenta…
…tion
- Loading branch information
1 parent
9f95db1
commit 46dc256
Showing
77 changed files
with
486 additions
and
1,356 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,4 +20,6 @@ | |
|
||
npm-debug.log* | ||
yarn-debug.log* | ||
yarn-error.log* | ||
yarn-error.log* | ||
|
||
.obsidian |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
LTI 1.1 |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
||
 | ||
|
||
## 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 + | ||
"<i_message_hint=" + | ||
req.query.lti_message_hint + | ||
"&state=" + | ||
state + | ||
"&redirect_uri=" + | ||
encodeURIComponent(redirectUri) + | ||
"&client_id=" + | ||
clientId + | ||
"&nonce=" + | ||
nonce; | ||
|
||
res.redirect(url); | ||
}; | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 --> | ||
|
||
 | ||
|
||
### 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. |
61 changes: 9 additions & 52 deletions
61
docs/blackboard/lti/core/id-token.md → docs/blackboard/lti/1.3/core/jwt-sample.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
||
 | ||
|
||
## 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 | ||
}; | ||
``` |
Oops, something went wrong.