privacycg / storage-access

The Storage Access API
https://privacycg.github.io/storage-access/
212 stars 27 forks source link

Consider: Unblock federated logout by making storage access allowance symmetric #82

Open hpsin opened 3 years ago

hpsin commented 3 years ago

An issue with unblocking federated auth in the absence of 3p cookies using the storage access API is that it is a one-way trust. The permission grants idp.example access to its session cookies on rp.example, allowing auth to occur. However, it does not grant access to rp.example session state while on idp.example.

Federated logout is specified today as a visit to a well-known logout URL on idp.example, which then opens iframes of pre-provided logout URLs on each RP. With the removal of 3p cookies, these iframes (e.g. to rp.example/logout?sid=foo) are opened without session artifacts. For stateless RPs, they lose the ability to reliably signout the user, creating a security issue.

Single page app RPs today perform signout by either deleting their session cookies when receiving an iframe-based signout request, or dropping a tombstone cookie that tells them to delete their session cookies on next use (for cookies with SameSite Lax). Neither of these approaches work any longer.

I propose a solution here, which is that if idp.example has received storage access when embedded in rp.example, rp.example should receive storage access when embedded on idp.example.

This has two known drawbacks:

  1. It's not clear this is appropriate behavior. Abuse vectors are unexplored.
  2. This still doesn't solve logout for embedded web apps. If rp1 gets storage access for IDP.example, and then embeds rp2 that also auths using idp.example (thus requiring a complementary logout signal), there's no mechanism to track that rp2 should have storage access when embedded in idp.example.
samuelgoto commented 3 years ago

Just as an extra idea that we are starting to float around, here is how we are thinking federated logout could work within WebID:

The current belief is that, if the browser can observe the "login to rp.example with idp.example", it should allow idp.example to call navigator.credentials.logout(["https://rp.example"]) with 3rd party cookies in the request (which allows the rp.example to, say, clear its own cookies or its own local storage).

So, the question then becomes, how can a browser observe the "login to rp.example with idp.example" with confidence (i.e. in a way that cannot be abused by an adversary)?

There are two variations here, but just to give you a sense, one of them is to use the WebID mediation flow and the oauth heuristics:

  1. User starts at https://rp.example and clicks on a login with idp.example button.
  2. User is redirected to the idp.example oauth endpoint, which the browser is able to classify/observe/detect
  3. The browser detects an oauth navigation and pauses it.
  4. The browser issues a GET request to fetch a configuration file at https://idp.example/.well-known/webid
  5. The configuration file has an endpoint to fetch the list of the user's accounts. The endpoint is called (with cookies) and results in a list of accounts.
  6. The browser takes the list of accounts and shows an account chooser. It has wording that captures the user's intent to "login to rp.example with one of these X, Y, Z accounts".
  7. When the user picks one of the accounts, the browser continues the redirect that was intercepted at step (3).
  8. The browser now stores in its storage that "the user has logged in to rp.example with account X from idp.example"

When a user wants to logout:

  1. User goes to idp.example, initiates a logout
  2. idp.example calls navigator.credentials.logout(["rp.example", "rp2.example"])
  3. The browser loops through the list of URLs, and for every origin that has a corresponding the user has logged in to Y with idp.example it embeds a third party cookie in the request.

There is a different variation where we are exploring using a generic "permission prompt" as opposed to an "account chooser", but otherwise that's the gist of how we are considering going about this in WebID.

This is a bit social-networky, but here is what the UI for the account chooser is starting to look like (in this case, it is a single account):

gffletch commented 3 years ago

@samuelgoto in steps 4-5, how does the IDP know it should return the list of logged in users? I'm not sure it's a good idea to have an open API that can be called from any browser (where the IDP has been used in the past) that returns the list of logged in users. Also what if no user is logged in but there is a list of users who have logged in from that browser in the past. This list is often presented in an IDP formulated "account chooser". Does the browser need to know those identities as well?

Also, why does the browser need to store which identity the user selected from the account chooser? In the logout flow the IDP isn't being required to identify the user via the same identifier? In the context of OpenID Connect, the value that matters is the session id.

I think I like the idea of the "generic permission" prompt as long as that permission is remembered for the rp:idp pair so that user doesn't need to give permission for every identity flow. I think for logout 3p cookies to flow, the browser just needs to know that "a user" has logged in to this rp:idp pair in this browser.

kenrb commented 3 years ago

@gffletch The list of accounts to be populated in the account selector would be up to the IdP, and depend on the cookies that it has previously set on the client. If there is no signed-in account, or if the user wants to use an account that isn't in the list, then there would have to be a fallback where an IdP sign-in page would have to be loaded, possibly in a modal window.

I don't know if it is a problem for an IdP to expose such an endpoint, since it only returns information to a request with valid cookies. If somebody with ill intent has a user's cookies then I don't know that this makes the situation any worse than it already is.

One concern with linking this with the mediation variation of WebID is that it becomes a hard requirement for any IdP that wants session management capabilities to convert to using that flow, which surrenders much of the sign-in experience to the browser, and could potentially mean high variance in UX between different browsers. The ability to get session management privileges based on a permission has much less of that problem.

samuelgoto commented 3 years ago

On

I think I like the idea of the "generic permission" prompt.

And

The ability to get session management privileges based on a permission has much less of that problem.

The "generic permission" prompt [1] would work equally well (from a privacy perspective), but we ran into UX difficulties that lead us to think it is worth exploring a second option, "a mediated account chooser". There is still much to be learned and gathered from implementation experience, but the UX difficulties were around "branding" and "RP and IDP names" in a privacy preserving fashion (with the alternative of showing just domain names possibly cumbersome). I don't think these are problems that are impossible to solve, but I think there is a lot of details here to be uncovered.

[1] To be concrete: "generic permission" is probably the wrong terminology (i don't think it was your intent), but a browser needs to observe something very specific: a login. so, i'll use going forward "a static login to RP with IDP" prompt rather than a "generic permission" prompt. I hope this matches your intent too.

So, no disagreement that "generic permission" could work too, and so far I'm of the position that both options should be offered and allowed to be picked by the IDP.