Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature request: allow disconnecting accounts #496

Closed
npm1 opened this issue Aug 23, 2023 · 12 comments · Fixed by #515
Closed

Feature request: allow disconnecting accounts #496

npm1 opened this issue Aug 23, 2023 · 12 comments · Fixed by #515

Comments

@npm1
Copy link
Collaborator

npm1 commented Aug 23, 2023

When the user creates an account on an RP via federated login, the IDP typically records the connection. The stored connection allows the IDP to keep track of RPs that they have signed into and optimize their experience. For example, to have a better experience when the user later returns to the RP, the user account with the IDP is treated as a returning account, which allows auto reauthentication, personalized buttons showing the account used, etc. Sometimes, IdPs offer an API to disconnect this connection (for example, here and here). However, a disconnect flow is authenticated and requires the IDP cookies. In a world without third-party cookies, when the user visits the RP, there is no way to access the IDP cookies in order to disconnect the connection. Because there may be multiple IDP accounts from the same IDP linked to a given RP, the disconnect flow requires knowing which account is being disconnected.

@npm1
Copy link
Collaborator Author

npm1 commented Aug 23, 2023

The proposal

We can provide account disconnection when the user has gone through federated login via the FedCM API. If a user has previously used an account via FedCM on the RP with the IDP, the RP may then initiate the disconnection by invoking a new JavaScript API. It will need to pass a configURL, the clientID it uses under the IDP, and some account hint (e.g. email or account ID) to notify the IDP which account is being disconnected (this does not have to match the id that the IDP sends in the account list endpoint):

// Returns Promise<void>, and may throw exception if disconnection is not possible.
// Called from a top-level RP frame.
IdentityCredential.disconnect({
  configURL: "https://idp.com/config.json",
  clientID: "rp123",
  accountHint: "account456"
});

When the RP invokes this method, the user agent will first check whether the user has previously used FedCM on this RP and with the given IDP using any account. If it has not, then the user agent will reject the promise associated with disconnect(). Otherwise, the user agent will need to find a way to notify the IDP that disconnection is happening. To do this, we introduce a new disconnect_endpoint in the existing config file used for the FedCM API:

{
  "accounts_endpoint": "/accounts",
  "id_assertion_endpoint": "/assertion",
...
  "disconnect_endpoint": "/disconnect"
}

Thus, when the disconnect API is called, the user agent just needs to fetch the config and well-known files in order to find the disconnect endpoint. As with the accounts endpoint, it would check that it is same-origin with the config file. It can then send a credentialed POST request to the specified disconnect endpoint, including the client id and the account hint:

POST /disconnect HTTP/1.1
Host: idp.example
Referer: rp.example
Content-Type: application/x-www-form-urlencoded
Cookie: 0x123
Sec-Fetch-Dest: webidentity

account_hint=account456&client_id=rp123

This credentialed request allows the IDP to disconnect the user account for federated login with the RP. That said, because this flow also goes through FedCM, it is desirable for the IDP to also notify the user agent about which account has been disconnected. This allows the user agent to also remove the relevant triples from its connected accounts set, i.e. treat the user as a new user in future FedCM flows. Thus, the IDP response must contain a JSON object in the body which contains the account ID of the disconnected account. If the IDP wishes the user agent to instead disconnect all accounts associated with the RP, they could pass ‘*’ instead.

{
  "account_id": "account456Id"
}

Upon receiving a response, the user agent will remove the relevant account(s) from the connected accounts set. If no valid response is received or the accountId received does not match any account in the connected accounts set, the user agent will reset ALL accounts matching the RP and IDP from the connected accounts set. This should not impact well-behaved IDP (except in rare cases where there are network issues) but prevents an IDP from using the disconnection endpoint as a simple way to communicate with itself once FedCM is used in an RP.

Because the connected accounts set may not be synced across multiple devices or user agents, the IDP can use the approved_clients array to ensure that future FedCM flows in other scenarios do not consider the disconnected account as a returning account.

Privacy considerations

It is acceptable to send a credentialed request to the disconnect endpoint because the user agent only does so after the FedCM API has been successfully used with the (RP, IDP) combination. Thus the user has consented for the RP and IDP to connect in order to authenticate to the RP using the IDP credentials. In addition, the disconnect endpoint has the property that at least one account is removed from the connected accounts set when the endpoint is hit, making it an unfeasible way for the IDP to use it as a tracking mechanism.

Security considerations

The disconnect endpoint does not introduce a new UI. The disconnection request uses Sec-Fetch-Dest: webidentity to notify the IDP that it is coming from the disconnect API instead of a request sent by JavaScript (since JavaScript requests cannot use Sec-* headers).

One risk is that an attacker that controls the RP JavaScript may invoke the disconnect() API. If it fails right away, this tells the attacker that the user has never used FedCM in this RP. This can already be deduced from the auto-reauthn API, using mediation:silent. In addition, invoking the API will also disconnect the user accounts from the IDP. In the case of a poorly behaved IDP, this could be an issue: IDP2 could disconnect the accounts issued by IDP1. However, it should be noted that this will not destroy the user’s account in the RP, but rather reset the IDP connection with the RP, so the user will just need to explicitly make that connection again in the future.

Alternatives considered

Disconnect endpoint separate from config

The disconnect() method could directly point to the disconnect endpoint instead of the config file. This could also work, although it would not let us reuse the IdentityProviderConfig dictionary. To remove entries from the connected set, we would look up the IDP from the origin of the disconnect endpoint.

Requiring account ID

The connected accounts set stores the account ID. If we required the RP to pass the ID, we would not need to get a response from the IDP. However, IDPs may use a variety of identifiers to communicate with RPs like email, phone number, UID, etc., so we prefer providing the IDP with flexibility with regards to what kind of account hint it supports for its disconnect endpoint.

Do nothing

Technically speaking the disconnect could be done without third-party cookies. The IDP could include an encrypted key in the token for the RP to store in the database. When disconnect is needed, they could just send an uncredentialed request to the IDP: disconnect("foo@emaili.com", "key"). Of course, this is a hacky, unacceptable solution. IDPs should not need to store state in the RP.

@scantor
Copy link

scantor commented Aug 24, 2023

When the user creates an account on an RP via federated login, the IDP typically records the connection.

That statement is quite false. In the early days of Liberty, there was a notion that most federation was really account linking, but that notion died very quickly and the general approach of most IdPs is to be as stateless as possible, and it's left to other systems to worry about tracking the use of services. Even pairwise IDs in both SAML and OpenID are typically hashed, and don't reflect a "remembered" relationship.

Tellingly, the SAML features for terminating links has been entirely ignored and unused, so it's quite evidently not a feature that's really seen any traction. I think your need for revocation is more confined to the fact that your API (broken as it is for real world purposes) is about collecting consent and so naturally there does need to be a way to revoke it, but any need to contact either party about that is probably minimal to the point of ignorable in practice. There might be a small benefit to notifying the IdP to revoke any attribute consent state that might have been established for the RP but honestly consent is largely a rarity outside of consumer use cases anyway.

@npm1
Copy link
Collaborator Author

npm1 commented Aug 24, 2023

That statement is quite false.

I pointed out a couple of examples already (Google and Facebook) but every large consumer IDP offers revocation.

Tellingly, the SAML features for terminating links has been entirely ignored and unused, so it's quite evidently not a feature that's really seen any traction. I think your need for revocation is more confined to the fact that your API (broken as it is for real world purposes) is about collecting consent and so naturally there does need to be a way to revoke it, but any need to contact either party about that is probably minimal to the point of ignorable in practice. There might be a small benefit to notifying the IdP to revoke any attribute consent state that might have been established for the RP but honestly consent is largely a rarity outside of consumer use cases anyway.

I think your tone could use improvement. Saying the API is "broken as it is for real world purposes" is not really constructive. In any case, while I agree that revocation is possibly not a commonly used flow, it is still widely supported for federated authentication (not something specific to the API design as you suggest).

@scantor
Copy link

scantor commented Aug 24, 2023

But that's now how the majority of IdPs work, and I'm simply noting that. The majority of IdPs are not "large consumer IdPs".

My concern essentially centers around the possibility that it becomes a requirement to have that sort of stateful behavior as a precondition to using the API, which would be a problem. The historical context was just noting that the idea of revocation hasn't typically seen much adoption elsewhere, but obviously that doesn't make it worthless as an option.

My tone stems from the frustration my community has experienced in trying to get the people behind this whole effort to understand the risk they are creating to the existing deployed base of standard SSO protocols, which are not just going to "change" to adopt something new. It's not that simple. But that's not material to this specific issue.

@npm1
Copy link
Collaborator Author

npm1 commented Aug 24, 2023

But that's now how the majority of IdPs work, and I'm simply noting that. The majority of IdPs are not "large consumer IdPs".

Ok that's fair, I meant 'typically records' when thinking about # of federated logins, not looking at it in terms of # of IDPs. You may be correct in saying that most IDPs don't support revocation because they don't track any state.

My concern essentially centers around the possibility that it becomes a requirement to have that sort of stateful behavior as a precondition to using the API, which would be a problem. The historical context was just noting that the idea of revocation hasn't typically seen much adoption elsewhere, but obviously that doesn't make it worthless as an option.

Ok, I see what you mean now. I will note that stateful behavior just introduces things that some IDPs find nice to have, like auto re-authentication. I think an IDP can be stateless while using FedCM, and they wouldn't need to implement or invoke revocation.

@scantor
Copy link

scantor commented Aug 24, 2023

Ok, thank you. In terms of what you're trying to do here, that was my only concern. We're (for some definition of "we") having difficulty teasing out things that are "there for the consumer IdPs to use but are optional" vs. "things the social IdPs can or want to do that we can't, but FedCM assumes we can".

The former is obviously entirely reasonable. Thank you for clarifying.

@achimschloss
Copy link
Contributor

It will need to pass a configURL, the clientID it uses under the IDP, and some account hint (e.g. email or account ID) to notify the IDP which account is being revoked (this does not have to match the id that the IDP sends in the account list endpoint):

So the assumption is here that the IDP would lookup the correct accountID (as per FedCM spec, likely the sub in OIDC terms) and approve/confirm the revocation?

Upon receiving a response, the user agent will remove the relevant account(s) from the connected accounts set. If no valid response is received or the accountId received does not match any account in the connected accounts set, the user agent will reset ALL accounts matching the RP and IDP from the connected accounts set. This should not impact well-behaved IDP (except in rare cases where there are network issues) but prevents an IDP from using the revocation endpoint as a simple way to communicate with itself once FedCM is used in an RP.

That entails then that if the RP request a revocation with erroneous data the same happens

Generally as long as this is optional it should be fine (not everyone does this), having that said I think one will really need the ability to always have a scope passed to the IDP (during the login flow) when calling the id-assertion endpoint, because not every idp only works with a fixed set of claims being requested and thus would not be able to handle state properly.

Simple background is that an IDP simply speaking records a triplet (account, relying party, approved scopes) and presents this to the user in its own controls UIs. I think keeping this implicit (as it is now) is not really a good solution, also looking at cross-browser compatibility and a potential parallel login flow to the RP which runs through classical OIDC for example.

@npm1
Copy link
Collaborator Author

npm1 commented Oct 26, 2023

So the assumption is here that the IDP would lookup the correct accountID (as per FedCM spec, likely the sub in OIDC terms) and approve/confirm the revocation?

Right. This is only mainly due to the fact that currently IdPs allow RPs to request revocation using stuff other than the account ID, but the user agent's map is based on the account ID so we need the IdP to tell the user agent which was the account ID that was revoked.

That entails then that if the RP request a revocation with erroneous data the same happens

Yea, do you think this is an issue?

Generally as long as this is optional it should be fine (not everyone does this), having that said I think one will really need the ability to always have a scope passed to the IDP (during the login flow) when calling the id-assertion endpoint, because not every idp only works with a fixed set of claims being requested and thus would not be able to handle state properly.

That makes sense to me. If I understand correctly, you are asking for the FedCM with scopes feature, right? I haven't thought too much about how this feature should behave in terms of the permissions: does the user agent need to remember the scopes for which the user accepted to use FedCM, or is it sufficient for the IdP to have these?

Simple background is that an IDP simply speaking records a triplet (account, relying party, approved scopes) and presents this to the user in its own controls UIs. I think keeping this implicit (as it is now) is not really a good solution, also looking at cross-browser compatibility and a potential parallel login flow to the RP which runs through classical OIDC for example.

Sure, again the scopes feature. In terms of how it would behave with revocation, do IdPs offer users to revoke individual scopes for a given (account, rp) pair? Or is revocation generally presented as a all-or-nothing option (i.e. when you revoke that pair, all scopes are revoked)?

@achimschloss
Copy link
Contributor

If I understand correctly, you are asking for the FedCM with scopes feature, right?

Yes + In the current Chrome implementation the browser ui shows a disclosure text which should reflect (email, profile and profile picture I think) to be approved for transfer and passes "disclosure_text_shown" in this case - It would be cleaner to also pass the scope that the standard text in the browser ui aims to reflect. IDPs will then need to review if this text works for them or not (which makes the continue_on quite vital also)

does the user agent need to remember the scopes for which the user accepted to use FedCM, or is it sufficient for the IdP to have these?

No this is something the IDP must handle and have the means to do so.

That entails then that if the RP request a revocation with erroneous data the same happens

Yea, do you think this is an issue?

Just wanted to point it out now, it might not be an issue as there is only a negative effect for the RP itself in the end.

Sure, again the scopes feature. In terms of how it would behave with revocation, do IdPs offer users to revoke individual scopes for a given (account, rp) pair? Or is revocation generally presented as a all-or-nothing option (i.e. when you revoke that pair, all scopes are revoked)?

Speaking for ourselves and out of general experiences revocation is usually all-or-nothing.

@npm1
Copy link
Collaborator Author

npm1 commented Nov 15, 2023

Feedback on FedID CG was that revoke() is a bit unclear because some developers may mistake it as removing the IDP account in some way. So, any thoughts on disconnect() as the name?

@achimschloss
Copy link
Contributor

disconnect works much better.

@npm1 npm1 removed the agenda+ Regular CG meeting agenda items label Nov 20, 2023
@npm1 npm1 changed the title Feature request: allow revocation Feature request: allow disconnecting accounts Nov 22, 2023
@npm1
Copy link
Collaborator Author

npm1 commented Nov 22, 2023

Thanks! Updated the explainer (Initial issue and first comment) to use the new name.

@yi-gu yi-gu added the agenda+ Regular CG meeting agenda items label Nov 30, 2023
@npm1 npm1 closed this as completed in #515 Dec 1, 2023
@npm1 npm1 removed the agenda+ Regular CG meeting agenda items label Feb 5, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants