Session Management OP Frame message origin assertion

Issue #1022 resolved
Filip Skokan created an issue

From OpenID Connect Session Management 1.0 - draft 28#4.2. OP iframe

The OP iframe MUST enforce that the caller has the same origin as its parent frame. It MUST reject postMessage requests from any other source origin.

I understand the intention here but would like to raise a few questions/issues.

  1. cross-domain parent origin is not accessible, accessing window.parent.location.origin or window.parent.origin raises a DOMException and other means of reading the url are unreliable and inconsistent at best (accessing document.referrer and building the origin url out of it).
  2. the parent frame (tab) is not actually the source of the message, this would be the RP frame, same origin tho which might very well sit on a different subdomain, resulting in another origin.

I can see the example in the specification is not handling this either.

Steps to reproduce:

  1. Login with any username/password at RP https://tranquil-reef-95185.herokuapp.com, set to login with OP https://guarded-cliffs-8635.herokuapp.com
  2. Open console, switch to opframe js context
  3. Attempt to get parent origin via js to have are reference to compare message origin with

If anything this assertion of message origin should be mentioned in the RP frame, where the RP frame must assert the origin of the message is the OP frame origin, this origin can be easily formed by knowing the OP Frame location and can compare it to the message origin. The example rpFrame in the specification already does this.

Comments (27)

  1. Edmund Jay

    Filip,

    Is seems there that you are proposing:

    1. Delete modify the OP iframe assertion statement.
    2. Modify the OP iframe assertion to say something along the lines of "OP iframe MUST enforce that the caller is from an approved/known origin"
    3. Add a corresponding assertion statement to the RP iframe.
    4. Add CORS language to OP/RP frames? (unknown if this will support the call of window.parent.location.origin)

    or all/combination of above?

  2. Filip Skokan reporter

    Edmund,

    1. Yes - because asserting message origin being the same as parent (or rather RP frame origin, parent is not the sender) origin is not possible due to CORS restrictions.
    2. No - Same as above, to do something like this OP frame would either have to
      • have all known origins of all RPs in the rendered page ready to be asserted to include current message origin presence
      • or rely upon accessing external resources, doing so would beat the mobile friendly nature of session management messages
      • Content-Security-Policy header frame-ancestors section would seem like a good candidate but the OP frame parent is not the origin of the caller, that would be the RP frame origin, which cannot be checked using CSP.
    3. Yes - I believe the RP can take on this responsibility, it has all the necessary information at the time of rendering its frame to statically embed the assertion.
    4. Partially, with the above, the only thing left to do is remove // Validate message origin from OP Frame example and possibly wrap the receiveMessage body in a try / catch block resulting in responding with error

    It would be of great help if others who have also already undergone this implementation would share their findings.

  3. Michael Jones

    Filip, can you summarize the specific text changes you're proposing? I have some guesses but I'm not sure that my guesses are right.

    Also, a the in-person meeting at the beginning of April, the WG decided to take the logout specs to final status. If we're going to make any more changes, now's the time.

  4. Filip Skokan reporter

    Mike, i'm not comfortable proposing changes without having confirmation from others that this OK and that they've run into the same issue. Understanding what the intention behind those statements in security considerations was or how they were intended to be implemented is needed as well as the possible impact of these proposed changes.

    Nevertheless here's a proposal and my comments on why

    Removal of "The OP iframe MUST enforce that the caller has the origin as its parent frame."

    1. it is not possible to reach x-domain for the parent frame origin (DOMException) and it is not possible to embed the expected origin in the frame html since the server does not have the information available.
    2. it is possible the parent frame and caller origins are actually different, i.e. example-rp.com as the main window, example-op.com as the OP. When the redirect_uri was i.e. https://login.example-rp.com/cb (a subdomain, common pattern) then login.example-rp.com must be the rpFrame origin else op frame won't ever be able to calculate the correct state the RP has received in the authorization response. Now there are three origins in the mix and the OP can only read the caller origin.

    OP Frame Message Origin Assertion Removal
    The OP is not able to efficiently and reliably confirm the origin url of where the opFrame is embedded.

    1. same as the above
    2. in order to do this client side once a message is received the op frame would have to either
      1. embed all possible origins in the frame html - this turns out to be a problem for OPs with hundreds and thousands of clients where client information is loaded on-demand
      2. dynamically reach out to the server for validation - since client_id is part of the message this might be possible but would contradict one of the purposes of the spec it is desirable to be able to check the login status at the OP without causing network traffic
  5. Ryan Means

    We have been working on implementing this spec in the current draft form and ran into this same issue. Being unfamiliar with the WG and the process behind this draft to-date.. I'm wondering if another way to accomplish the goal here would be something like this:

    Given a check_session_iframe endpoint of /checksession on the IdP. The iFrame SHOULD be loaded with a query string of ?clientId={clientId}. The IdP when rendering the contents of the check_session_iframe SHOULD validate the clientId is valid and SHOULD reject requests to render the iFrame if the clientId is not provided or not valid. The IdP SHOULD generate the iframe dynamically such the iframe will check for post messages against a registered whitelist with the IdP for that client.

    That would keep things in the spirit of PostMessages and security best practices around the postmessage specification - that origins are always checked against a whitelist and non-white list messages are rejected. IdP's can then (and SHOULD) enforce this by requiring the iframe request to be loaded with a client ID query string which would allow the IdP to generate the contents of the JS within the iframe to match only registered origins for that client. The IdP could optionally support older versions of this draft by not requiring the clientId query string to be passed and as such, would not do a postmessage origin check.

    This would be essentially implementing what @panva had suggested "[OP Iframe would have to] have all known origins of all RPs in the rendered page ready to be asserted to include current message origin presence"

  6. Filip Skokan reporter

    @rmmeans, actually the normative language only says that

    The OP iframe MUST enforce that the caller has the same origin as its parent frame. It MUST reject postMessage requests from any other source origin.

    That, given the security boundaries and DOMException is simply not possible (even with knowing the client_id upfront).

    Question here is if the comment // Validate message origin in the spec's OP frame non-normative example is intended to do? The above? Not possible.

    However, if validating the message origin is a valid one would satisfy the original intent of the normative part above, I agree that adding a client_id would make that a possibility.

    The IdP SHOULD generate the iframe dynamically such the iframe will check for post messages against a registered whitelist with the IdP for that client.

    The registered whitelist being the unique origins (as per RFC6454) of the client's redirect_uris, correct?

  7. Ryan Means

    Yes @panva - sorry, I was agreeing with your findings on how the normative language should be updated - as we have duplicated your findings in this original report.

    I do not believe it is possible, as you stated, to implement the spec in the current draft status due to the CORS issues you found and that we have duplicated. It was a google search that led us to this bug report in the spec after we were trying to figure out how others could have implemented this per the spec with browsers preventing the cross-origin lookup.

    I agree on on using the clients registered redirect_uris - the OP would parse the unique set of origins out of those redirect_uri's and use those to validate the caller. Then one could say something like:

    The OP iframe MUST enforce that the caller has an origin in the clients registered whitelist. It MUST reject postMessage requests from any other source origin.

    While not per the current draft - you could implement what I just stated without requiring that the OP iFrame be loaded with a query string of the clientId like I had suggested previously - thus not requiring any other OSS libraries that are already implementing the RP iframe to change (e.g. via a query string of the clientId like I had suggested).

    Instead, you could parse the the postMessage and per the spec:

    If the postMessage received is syntactically malformed in such a way that the posted Client ID and origin URL cannot be determined or are syntactically invalid, then the OP iframe SHOULD postMessage the string error back to the source.

    Then after that is parsed and a client ID is successfully extracted from the message - the OP iFrame would then retrieve the client ID's settings via an AJAX call (which is on the same origin that the OP Iframe is running on inside of the browser - thus no CORS problems) back to the OP for the set of allowed origins for that client ID. If the origin on the post message did not line up with an origin in that whitelist, then the message is rejected per the original normative language of:

    It MUST reject postMessage requests from any other source origin.

    While this is implementation specific on how this is done by each OP - similar to how the specification states that get_op_browser_state() is entirely up to OP, in order to relieve network traffic back to the OP from the OP Iframe, the OP IFrame could cache the results of the valid list of origins within the browser for that clientId (e.g. cookie, localstorage, or JS global variable)

    Within that spirit, one could implement this now still implementing to this part of the normative language:

    It MUST reject postMessage requests from any other source origin.

    The part that changes is this:

    The OP iframe MUST enforce that the caller has the same origin as its parent frame

    It would instead say:

    The OP iframe MUST enforce that the caller has an origin in the clients registered whitelist

    Where-as how it is done is entirely up to the OP ( network traffic back to the OP on every request OR have the OP Iframe cache the results in the browser for the valid set of origins for a given client)

    A non normative example for the pseudo-code OP iframe could change to this:

    window.addEventListener("message", receiveMessage, false);
    
      function receiveMessage(e){ // e.data has client_id and session_state
    
        // Validate message origin
        var client_id = e.data.split(' ')[0];
        var session_state = e.data.split(' ')[1];
        var salt = session_state.split('.')[1];
    
        // if message syntactically invalid
        //     postMessage('error', e.origin) and return
    
        // if client_id not contained in set of clientOrigins(client_id)
        //    return
    
        // get_op_client_origins(client_id) is an OP defined
        // function that returns the white list of origins for
        // a given client_id. How it is done is entirely up to
        // the OP.
        var clientOrigins = get_op_client_origins(client_id);
    
        // get_op_browser_state() is an OP defined function
        // that returns the browser's login status at the OP.
        // How it is done is entirely up to the OP.
        var opbs = get_op_browser_state();
    
        // Here, the session_state is calculated in this particular way,
        // but it is entirely up to the OP how to do it under the
        // requirements defined in this specification.
        var ss = CryptoJS.SHA256(client_id + ' ' + e.origin + ' ' +
          opbs + ' ' + salt) + "." + salt;
    
        var stat = '';
        if (session_state == ss) {
          stat = 'unchanged';
        } else {
          stat = 'changed';
        }
    
        e.source.postMessage(stat, e.origin);
      };
    
  8. Filip Skokan reporter

    Loading the list dynamically via unauthenticated ajax request is something I would not recommend or suggest and would rather the client_id be REQUIRED so that implementers can rely on this being present.

  9. Ryan Means

    I don't disagree - however, it cannot be considered as more secure to require the client_id in the query string for loading the OP iframe. Either way the full set of registered origins will be discoverable by a third party - either via an unauthenticated AJAX call that the OP iframe calls back to the OP server or simply by viewing the source of the rendered iframe with a given client ID... which can be programmatically driven from a third party to request the dynamic iframe to be rendered with a given client ID then extract the valid set of origins out it ... all server side by the third party.

    From a security perspective only, both are equivalent in terms of risk - both methods would require the iFrame have access to the complete set of white listed origins: either statically in the browser via a dynamically generated iframe on the server side OR programmatically from a static iframe where the iframe requests it over an un-authenticated AJAX call to get the dynamic set of allowed origins to use for that client_id

    I suppose you could run each origin thru a one way hash and then just compare hashes, perhaps where each origin is salted by the client_id. This would still need to be implemented by both solutions if you are trying to hide the set of white listed origins for a given client.

    This leads me to think my preference would be have the OP iframe handle it with the existing contract of how the OP iframe is loaded. This would allow all existing RP implementations to function as-is while denying any postMessages that don't come from an allowed origin.

  10. Filip Skokan reporter

    it cannot be considered as more secure to require the client_id in the query string for loading the OP iframe. Either way the full set of registered origins will be discoverable by a third party

    Completely agree, which makes me lean towards a simple straight forward solution of requiring client_id for this purpose even more. Personally, I am favouring simple straight forward OP solutions with no ambiguity over keeping the current contract.

    BUT, back to the root question tho, is that assertion actually needed? What does it bring, what does accepting a message from unverified source mean? The status sent back will ALWAYS be error or changed afterall anyway, since the postMessage event.origin is used for recalculating the state that's being compared.
    Can we focus on answering that first since it would mean not having to
    a) change the current contract or implementations (since the assertion cannot be achieved anyway)
    b) keep the logic completely client side and light weight as is one of the core goals of this feature.

  11. Ryan Means

    Excellent point! I would concur - that the data exposed doesn't matter - it would always be error or changed as you stated.

  12. Michael Jones

    I'm all for clarifying the language in the Session management spec so that it is correct and actionable. That said, unless there's a compelling reason to change the interface, I believe we should not do so. There have been implementations of this in production for years and we haven't normatively changed Session Management since 2014. It's working fine as-is, as far as I can tell.

    The only reason we hadn't progressed it to being a final specification is that we were waiting for the other logout solutions to be ready to go at the same time. Given that we're preparing to make all three logout solutions final (after a couple of editorial improvements and a working group last call), now's the time to figure out what clarifications we should make to Session Management.

    Suggestions with actual proposed wording changes would be particularly appreciated.

  13. Filip Skokan reporter

    Just checked keycloak's implementation, they do an ajax request with the client_id and origin from within the rendered iframe once upon the receival of first valid formatted message but they're not validating the caller is the same origin as the parent frame. On the other hand they do not use the redirect_uri Origin for the session_state (just going through the spec again now i realized it is not a normative requirement to do so).

    The Session State value is initially calculated on the server. The same Session State value is also recalculated by the OP iframe in the browser client. The generation of suitable Session State values is specified in Section 4.2, and is based on a salted cryptographic hash of Client ID, origin URL, and OP browser state. For the origin URL, the server can use the origin URL of the Authentication Response, following the algorithm specified in Section 4 of RFC 6454 [RFC6454].

    Meaning they also DO NOT act on the normative language per se but implement what @rmmeans suggested, but without the client_id request parameter extension.

    One way or the other verifying the caller origin is the same as parent origin is not possible. A proposed change would be (what Mike suggested earlier)

    OP iframe MUST enforce that the caller is from an approved/known origin.

    And check if there can be made an exception (allow omitting the above) when the redirect_uri Origin is used while calculating the session_state since the state returned would always be changed or error when used from other then intended origin.

  14. Jake Feasel

    @panva It seems like you could implement the assertion exactly as described by using the document.referrer value and comparing it with the event.origin. For example, here is some working code that is designed to run in a check_session_iframe and makes that assertion:

       /*
           Credit to Microsoft for this. I found their check_session_iframe code linked 
           from here: https://login.microsoftonline.com/microsoft.com/.well-known/openid-configuration
       */
    
        // Return the origin (scheme+host+port) portion of a given URL. Makes sure an / in the end is omitted
        function getOriginFromUrl(url) {
            if (url == null) {
                return null;
            }
            var pathArray = url.split('/');
            if (pathArray.length < 3) {
                return null;
            }
            var protocol = pathArray[0];
            var hostAndPort = pathArray[2];
            return protocol + '//' + hostAndPort;
        }
    
        // Return the URL of the parent hosting the current IFrame
        function getParentUrl() {
            var isInIframe = (parent !== window);
            var parentUrl = null;
            if (isInIframe) {
                parentUrl = document.referrer;
            }
            return parentUrl;
        }
        function receiveMessage(e) {        
            // Validating the message origin
    
            // Get origin of the parent window
            var parentUrl = getParentUrl();
            if (parentUrl === null) {
                return;
            }
            var parentOrigin = getOriginFromUrl(parentUrl);
            if (parentOrigin === null) {
                return;
            }
            // Reject message not having the parent origin
            if (e.origin !== parentOrigin) {
                return;
            }
            //continue processing.....
        }
    
  15. Jake Feasel

    @mbj perhaps the example code provided in the final spec could include something similar to what I posted above.

  16. Filip Skokan reporter

    @jfeasel using referrer is unreliable and inconsistent at best.

    1. RPs may block referrers using the Referrer-Policy header
    2. It's easily spoofable

    I ended up going with an once per frame XHR to verify the origin on the OP similar to what other open source software is doing, eventho I believe it's not necessary if the session_state is using the redirect_uri origin. You can see the implementation here

  17. Jake Feasel

    @panva As far as I can tell, in this case the concern is that a malicious third-party site would secretly embed the RP and OP iframes. This malicious site would then get an innocent user that is logged into both to load it. Without this parent/child origin check by the OP iframe, the OP frame would notify the RP frame as if it were used from the legit RP parent. Presumably, the RP iframe might then use postMessage to its parent, assuming its parent is the legit RP. In this way, the malicious third-party site would be informed about the state change at the OP (to what end I can't really imagine, but let's just grant that this is a bad thing).

    Now, if the OP iframe checked the document.referrer like I described, it would be the malicious third-party site and so wouldn't match the event.origin. You're saying that somehow the malicious third-party site can easily spoof the document.referrer value that is exposed to the OP iframe, so that it appears to match the legit RP. Is that true given the circumstances I described above? Can you describe how?

    If the malicious site blocked the referrer with a referrer-policy, then the check for the referrer would simply fail and they would get no message. This is no different than if it failed because it didn't match.

  18. Filip Skokan reporter

    What I'm trying to say is that, aside from referer header being easily spoofable, a final specification recommendation/examples shouldn't be based on a feature an RP might use for very valid privacy reasons (referrer-policy header) that is going to make the use of Session Status Change Notification impossible for this RP. You cannot whitelist origins that referrer-policy is exempt from and it would simply cause the implementer RPs issues with adoption, debugging this would, I imagine, be a pain.

  19. Log in to comment