Reputation: 877
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
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