LTI OIDC Login with LTI Client Side postMessages

LTI OIDC Login with LTI Client Side postMessages

Base Document
Spec Version 0.1
Base Document
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.1 Problem Description: Blocked cookies prevent proper OIDC flow

The OIDC specification, which LTI is built on, 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.2 Problem Solution: Store OIDC state & nonce using browser messages

1.3 Diagrams: Cookies vs JS State

TODO: Current sequence diagram (re-use or update the existing ones for consistency) vs Update simple version sequence diagram

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.2 Tool: Determine whether you can use cookies

How to determine whether you can use cookies:

  • Did the browser send you previously set cookies?
    • You can just proceed with the normal OIDC Auth Request redirect via a 301 response
  • No cookies sent from browser:
    • Render and HTML page with embedded Javascript to determine next steps
      • Provide the state & nonce values so your Javascript have access
    • (Optional) Can send ajax request to check again if cookies were now set with the response for the HTML page
      • If cookie was set can just redirect with normal OIDC Auth Request

The rendered HTML page should then execute Javascript to listen for a postMessage response and send the state & nonce to the platform.

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.
This is a preview of the 1EdTech LTI Client Side postMessages Specification

This preview is being provided to facilitate member communications around implementation of this solution to solve for the browser cookie problem.

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 Franza42 Lines Inc.
Eric PrestonBlackboard, Inc.
Claude VervoortCengage
Martin LenordTurnitin
Maggie SazioD2L Corporation
Viktor HaagD2L Corporation
Xander MoffattInstructure
Bracken Mosbacker1EdTech
Joshua McGhee1EdTech
Charles SeveranceUniversity of Michigan
Mary GwozdzUnicon, Inc.
Justin BallAtomic Jolt

1EdTech™ Consortium, Inc. ("1EdTech") is publishing the information contained in this document ("Specification") for purposes of scientific, experimental, and scholarly collaboration only.

1EdTech makes no warranty or representation regarding the accuracy or completeness of the Specification.

This material is provided on an "As Is" and "As Available" basis.

The Specification is at all times subject to change and revision without notice.

It is your sole responsibility to evaluate the usefulness, accuracy, and completeness of the Specification as it relates to you.

1EdTech would appreciate receiving your comments and suggestions.

Please contact 1EdTech through our website at www.1edtech.org.

Please refer to Document Name: LTI OIDC Login with LTI Client Side postMessages 0.1

Date: October 25th, 2022