Remko
Remko

Reputation: 877

How to track state for OpenID Connect in Safari IFrame with ITP?

Recent specifications such as LTI 1.3 use (IdP-initiated) OpenID Connect to authenticate tools. In LTI, these tools typically run in an iframe on a different domain. The theory is that the entire authentication flow is 'just' a 3-step back-and-forth of browser redirects.

To avoid CSRF, it is recommended to track a state parameter in a session with OpenID connect. However, Safari has different hoops that need to be jumped through before any storage is available in an iframe: Storage may need to be requested (after user interaction), a cookie needs to have been previously set in top-level context, ...

All solutions I can think of to initiate an OpenID connect from within an IFrame (with CSRF protection) requires lots of code and checks, including several back-and-forths between backend (to set http-only session) and frontend (to check and request cookie storage). I can't imagine that a standard as recent as LTI 1.3 would require so much complexity just to get it working, so I was wondering if there were 'recommended' approaches to doing OpenID connect from within an iframe with a state parameter.

Upvotes: 2

Views: 507

Answers (1)

snake
snake

Reputation: 61

1EdTech now provide a specification to address just this - LTI OIDC Login with LTI Client Side postMessages. Yep, it's a mouthful. Of course, the platform needs to provide support for this spec, since it's using the platform as a kind of storage via postMessages. This and other specs can be found on the main LTI page.

The Client side postMessage specification describes the process as follows:

  • 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

and also provides some js examples describing how to put and get your state and nonce data in the platform store:

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() 
});

Upvotes: 0

Related Questions