LTI OIDC Login with LTI Client Side postMessages
Spec Version 0.1
Document Version: | 5 |
Date Issued: | October 25th, 2022 |
Status: | This document is for review and comment by 1EdTech Contributing Members. |
This version: | https://www.imsglobal.org/spec/lti-cs-oidc/v0p1 |
Latest version: | https://www.imsglobal.org/spec/lti-cs-oidc/v0p1 |
Errata: | https://www.imsglobal.org/spec/lti-cs-oidc/v0p1#revision-history |
IPR and Distribution Notice
Recipients of this document are requested to submit, with their comments, notification of any relevant patent claims or other intellectual property rights of which they may be aware that might be infringed by any implementation of the specification set forth in this document, and to provide supporting documentation.
1EdTech takes no position regarding the validity or scope of any intellectual property or other rights that might be claimed to pertain implementation or use of the technology described in this document or the extent to which any license under such rights might or might not be available; neither does it represent that it has made any effort to identify any such rights. Information on 1EdTech's procedures with respect to rights in 1EdTech specifications can be found at the 1EdTech Intellectual Property Rights webpage: http://www.imsglobal.org/ipr/imsipr_policyFinal.pdf .
The following participating organizations have made explicit license commitments to this specification:
Org name | Date election made | Necessary claims | Type |
---|---|---|---|
D2L Corporation | July 21, 2022 | No | RF RAND (Required & Optional Elements) |
Use of this specification to develop products or services is governed by the license with 1EdTech found on the 1EdTech website: http://www.imsglobal.org/speclicense.html.
Permission is granted to all parties to use excerpts from this document as needed in producing requests for proposals.
The limited permissions granted above are perpetual and will not be revoked by 1EdTech or its successors or assigns.
THIS SPECIFICATION IS BEING OFFERED WITHOUT ANY WARRANTY WHATSOEVER, AND IN PARTICULAR, ANY WARRANTY OF NONINFRINGEMENT IS EXPRESSLY DISCLAIMED. ANY USE OF THIS SPECIFICATION SHALL BE MADE ENTIRELY AT THE IMPLEMENTER'S OWN RISK, AND NEITHER THE CONSORTIUM, NOR ANY OF ITS MEMBERS OR SUBMITTERS, SHALL HAVE ANY LIABILITY WHATSOEVER TO ANY IMPLEMENTER OR THIRD PARTY FOR ANY DAMAGES OF ANY NATURE WHATSOEVER, DIRECTLY OR INDIRECTLY, ARISING FROM THE USE OF THIS SPECIFICATION.
Public contributions, comments and questions can be posted here: http://www.imsglobal.org/forums/ims-glc-public-forums-and-resources .
© 2022 1EdTech™ Consortium, Inc. All Rights Reserved.
Trademark information: http://www.imsglobal.org/copyright.html
Abstract
The OIDC specification relies on browser cookies to validate the user agent starting the authorization workflow is the same one that finishes it. However, since an LTI integration is most often inside an iFrame there can be many issues involved with setting a state cookie for this purpose. This implementation guide explains how to use LTI Client Side postMessages with LTI postMessage Storage to replace the function of cookies in validating the state between stages of an OIDC launch.
1. LTI OIDC Login with LTI Client Side postMessages
1.2 Problem Solution: Store OIDC state & nonce using browser messages
1.4 Sequence Diagram
2. Sequence of events through OIDC
- Platform: Send an OIDC Initiation signal
- Tool: Determine whether you can use cookies
- Tool: Send message to store state & nonce
- Platform: Store data and respond
- Tool: Wait for response and continue with OIDC flow
- Platform: Continue with OIDC Auth Response back to tool
- Tool: Verify state & nonce match
2.1 Platform: Send an OIDC Initiation signal
When the platform initiates an LTI launch and supports this launch flow it will add a query parameter
to the OIDC Initiation Request URL called lti_storage_target
.
Query Parameter | Description |
---|---|
*lti_storage_target | Indicates the platform supports this launch flow and specifies the name of the target frame to send events to. |
To specify the parent frame rather than a subframe within the parent use _parent
.
Will specify that put and get capabilities are available and the target frame is the frame specified by the query parameter
2.3 Tool: Send message to store state & nonce
The tool should send a JavaScript postMessage to the target frame identified by the lti_storage_target
. This message should include lti.put_data
as the subject
, as well as values for the message_id
, key
, and value
being stored.
The state and nonce values that would have been placed inside cookies will now be stored in the platform window via LTI postMessage Storage.
See the example code both in the LTI Client Side postMessages document and below in the JS Example: Sending a put_data message to platform.
2.3.1 Determine the target origin for a tool
As described in the LTI Client Side postMessages document
For the tool, wishing to send a message to the platform, the origin is obtained from the OIDC Authorization URI. The origin is described in the MDN Web Docs, and follows Uniform Resource Identifier (URI): Generic Syntax and maybe also The Web Origin Concept
For example, if the OIDC Authorization URI is https://platform.example.com/auth_url
, then the origin would be
https://platform.example.com
In Javascript you can derive the origin like this:
let platformOrigin = new URL('https://platform.example.com/auth_url').origin;
2.3.2 Generating the messageIds
As described in the LTI postMessage Storage document, a unique message id should be generated for each request. The platform will send this id back in the response message for the tool to validate.
2.3.3 Naming the state and value keys
The state value is meant as a single-use token to validate the user's identity throughout a single LTI launch. It is recommended that you not use your session id for the state value. If needed you could store your session id on the platform, but that should be a separate key/value pair than your state.
Since multiple iframes for the same user can see the same storage namespace, you as a tool need to make sure that each frame can fetch the appropriate state value instead of the one from a separate frame.
State key should be unique, and be able to be constructed from the state value on a per-request basis. This allows multiple tool launches per user per browser window to avoid key conflicts, and also allows verification of the state value during the launch based on the state provided during the launch, and the state stored in the platform storage.
For example, the entire launch flow might look like this: if your state value is a generated GUID like 9e4153e7-c417-4424-a25e-c316ab3c0c8d
, then your state key could be something like my_tool_state_9e4153e7-c417-4424-a25e-c316ab3c0c8d
. You will then send this state to the platform via the put_data post-message and redirect to the platform passing the state GUID 9e4153e7-c417-4424-a25e-c316ab3c0c8d
. In the OIDC Auth Response message (LTI launch) you will receive your id_token and the state parameter. In this case, the state would be 9e4153e7-c417-4424-a25e-c316ab3c0c8d
, so you can use that value to reconstruct your key (my_tool_state_9e4153e7-c417-4424-a25e-c316ab3c0c8d
) and make the get_data post-message request.
This practice can also be applied to the nonce value.
2.4 Platform: Store data and respond
The platform's browser frame that is hosting the tool's iFrame, or the frame listening for these events, must remain consistent throughout this described flow so that when the tool stores a value during the OIDC Initiation step it is still available after the OIDC Auth Response step.
This code example shows how a platform can listen and respond to put_data
and get_data
messages: JS Example: Listen for events and store data.
The primary considerations for a platform at this point are that the data is stored in a bucket that is specific to the origin of the tool and to respond immediately since a user is waiting for these actions to complete.
2.5 Tool: Wait for response and continue with OIDC flow
The tool will add an event listener to catch the responding message from the platform. The tool should set up an event listener before the put_data
message is sent so that the response is not missed. There is an example below in JS example: Listen for put_data response
The necessary validation steps are:
- Check that this is message we're expecting:
- It should have a data element that is an object
- The data.subject should be
lti.put_data.response
. Response subjects are always the original subject with.response
appended to it.
- Validate that the message ID in the response data matches the message ID you sent
- Validate that the origin of the response matches the OIDC Authorization URI's origin
2.6 Platform: Continue with OIDC Auth Response back to tool
The platform doesn't have to do anything different for this step of their OIDC login process other than to make sure the outer frame doesn't reload so that the javascript state is preserved between OIDC stages for the tool in the iFrame.
2.7 Tool: Verify state & nonce match
The OIDC verification step is to receive the state cookie from the browser and compare that to the state in the id_token. Since this flow is for when there is now cookie available, you will have to again render an HTML page with javascript that will execute on load to fetch the state & nonce from the platform's browser frame, then compare it to the id_token values via a validation ajax call or redirect.
2.7.1 Receive OIDC Auth Response state & nonce
As part of the OIDC Auth Response request you will receive the state as a POST parameter, and the nonce value will be inside the id_token. Keep these values handy relative to the current launch happening since you will need to compare them via an ajax call or redirect.
2.7.2 Fetch state & nonce using postMessage Storage
The response from the platform won't come until after the message is sent to get the data, but the listener for the response needs to be initialized before you send it otherwise you are likely to miss the response. So, establish a lister as in the example JS example: Listen for get_data response
Using the same process above for Determine the target origin for a tool send a get_data request to the platform and await the response. There are also JS examples below for this JS Example: Sending get_data message to platform
Once you have the state & nonce values from the platform you can proceed to validating these against the ones received in the OIDC Auth Response.
2.7.3 Compare state & nonce
You are now in an HTML page with running javascript and can either make an ajax call and receive session information or a redirect url, or you can redirect to another page with the values as parameters.
Either way, once you've validated your id_token via the normal OIDC process, and have now compared the state & nonce between the id_token and the platform storage values your user is validated and can be sent on to the appropriate tool page.
3. Tool Implementation details
3.1 JS Example: Sending a put_data message to platform
The tool will send a put_data
message like this:
let platformOrigin = new URL(platformOIDCLoginURL).origin;
let frameName = getQueryParam("lti_storage_target");
let parent = window.parent || window.opener;
let targetFrame = frameName === "_parent" ? parent : parent.frames[frameName];
targetFrame.postMessage({
"subject": "lti.put_data",
"message_id": messageId,
"key": "state_<state_id>",
"value": "<state_id>"
} , platformOrigin )
targetFrame.postMessage({
"subject": "lti.put_data",
"message_id": messageId,
"key": "nonce_<nonce_value>",
"value": "<nonce_value>"
} , platformOrigin )
3.2 JS example: Listen for put_data.response
window.addEventListener('message', function (event) {
// This isn't a message we're expecting
if (typeof event.data !== "object"){
return;
}
// Validate it's the response type you expect
if (event.data.subject !== "lti.put_data.response") {
return;
}
// Validate the message id matches the id you sent
if (event.data.message_id !== messageId) {
// this is not the response you're looking for
return;
}
// Validate that the event's origin is the same as the derived platform origin
if (event.origin !== platformOrigin) {
return;
}
// handle errors
if (event.data.error){
// handle errors
console.log(event.data.error.code)
console.log(event.data.error.message)
return;
}
// It's the response we expected
// The state and nonce values were successfully stored, redirect to Platform
redirect_to_platform(platformOIDCLoginURL);
});
3.3 JS Example: Sending get_data message to platform
The tool will send a get_data
message like this:
let platformOrigin = new URL(platformOIDCLoginURL).origin;
let frameName = getQueryParam("lti_storage_target");
let parent = window.parent || window.opener;
let targetFrame = frameName === "_parent" ? parent : parent.frames[frameName];
targetFrame.postMessage({
"subject": "lti.get_data",
"message_id": messageId,
"key": "state_<state_id>",
} , platformOrigin )
targetFrame.postMessage({
"subject": "lti.get_data",
"message_id": messageId,
"key": "nonce_<nonce_value>",
} , platformOrigin )
3.4 JS example: Listen for get_data response
window.addEventListener('message', function (event) {
// This isn't a message we're expecting
if (typeof event.data !== "object"){
return;
}
// Validate it's the response type you expect
if (event.data.subject !== "lti.get_data.response") {
return;
}
// Validate the message id matches the id you sent
if (event.data.message_id !== messageId) {
// this is not the response you're looking for
return;
}
// Validate that the event's origin is the same as the derived platform origin
if (event.origin !== platformOrigin) {
return;
}
// handle errors
if (event.data.error){
// handle errors
console.log(event.data.error.code)
console.log(event.data.error.message)
return;
}
// It's the response we expected
// The state and nonce values were successfully fetched, validate them
ajax_with_values_or_redirect_again()
});
3.5 Maintaining state beyond OIDC flow
other uses and considerations
4. Platform Implementation details
4.1 JS Example: Listen for events and store data
TODO: Working on platform get/put example code:
(function(){
/**
* Stored data should be scoped by the origin of the event, preventing access
* to other origins that may also be embedded in the page
* Each origin has its own object for the key/value store
* {
* "<event.origin>": {<keys>: <values>}
* }
*
* To avoid accidental data leakage the storage data variable should be scoped
* so that it is only accessible by the event listener code
*/
let storageData = {};
// Declare an event listener for messages to put and get data
window.addEventListener('message', function (event) {
// This isn't a message we're expecting
if (typeof event.data !== "object") {
return;
}
// Check the subject value to determine which call is being made
switch (event.data.subject) {
case 'lti.put_data':
// Do we already have a map for this origin, if not create one
if (!storageData[event.origin]) {
storageData[event.origin] = {};
}
// Store the value against the key scoped by origin
storageData[event.origin][event.data.key] = event.data.value;
// Respond immediately to the tool event's origin and source with a `put_data` response
event.source.postMessage({
subject: 'lti.put_data.response',
message_id: event.data.message_id,
key: event.data.key,
value: event.data.value
}, event.origin);
break;
case 'lti.get_data':
// Check whether we have the data
if (!storageData[event.origin] || !storageData[event.origin][event.data.key]) {
// We don't have data for this origin, or this specific key, return error response
event.source.postMessage({
subject: 'lti.get_data.response',
message_id: event.data.message_id,
error: {
code: 'key_not_found',
message: 'There is no value stored for this key'
}
}, event.origin);
break;
}
// Respond immediately to the tool event's origin and source with a `get_data` response
// including the stored value
event.source.postMessage({
subject: 'lti.get_data.response',
message_id: event.data.message_id,
key: event.data.key,
value: storageData[event.origin][event.data.key]
}, event.origin);
break;
default:
// Ignore subjects we don't know how to handle
}
}, false);
})(); // self-executing function
5. Best practices
- Explain multiple user logins in same browser session: will a tool be able to see the same values for different users launching?
A. Revision History
Document Version No. | Release Date | Comments |
---|---|---|
3 | July 21, 2022 | First release of LTI Platform Storage Base Doc. |
4 | October 17th, 2022 | Completion of initial IP review to make documents publicly available. |
5 | October 25th, 2022 | Minor edits for clarity and fixes. |
B. References
B.1 Informative references
- [LTI-CS-POSTMSG-10]
- LTI Client Side postMessages. IMS Global Learning Consortium. URL: #nolinkyet
- [LTI-POSTMSG-STORAGE]
- LTI postMessage Storage. IMS Global Learning Consortium. URL: #nolinkyet
- [rfc3986]
- Uniform Resource Identifier (URI): Generic Syntax. T. Berners-Lee; R. Fielding; L. Masinter. IETF. January 2005. Internet Standard. URL: https://www.rfc-editor.org/rfc/rfc3986
- [rfc6454]
- The Web Origin Concept. A. Barth. IETF. December 2011. Proposed Standard. URL: https://www.rfc-editor.org/rfc/rfc6454
C. List of Contributors
The following individuals contributed to the development of this document:
Name | Organization |
---|---|
Peter Franza | 42 Lines Inc. |
Eric Preston | Blackboard, Inc. |
Claude Vervoort | Cengage |
Martin Lenord | Turnitin |
Maggie Sazio | D2L Corporation |
Viktor Haag | D2L Corporation |
Xander Moffatt | Instructure |
Bracken Mosbacker | 1EdTech |
Joshua McGhee | 1EdTech |
Charles Severance | University of Michigan |
Mary Gwozdz | Unicon, Inc. |
Justin Ball | Atomic Jolt |