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

Allow multiple IDPs to be used #2

Open
npm1 opened this issue Jul 22, 2022 · 46 comments
Open

Allow multiple IDPs to be used #2

npm1 opened this issue Jul 22, 2022 · 46 comments

Comments

@npm1
Copy link
Collaborator

npm1 commented Jul 22, 2022

Currently the spec does not because it follows closely the implementation, but it should not be hard to fix this. Basically: iterate over all the providers, and perform all of those fetches. Wait for all before producing a UI which contains all of them.

@samuelgoto
Copy link
Collaborator

@cbiesinger has put out a proposal on how to go about this here.

@hlflanagan
Copy link

See discussion in https://github.com/fedidcg/meetings/blob/main/2022/2022-10-03-notes.md

@npm1 npm1 changed the title Make sure the spec allows multiple IDPs in the get() call Allow multiple IDPs to be used Oct 3, 2022
@tttzach
Copy link

tttzach commented Oct 6, 2022

We want to start tackling this problem by supporting multiple identity providers (IdPs) specified through an array in a single navigator.credentials.get() call:

<script>
const cred = await navigator.credentials.get({
  identity: {
    providers: [
      {
        configUrl: "https://idp1.com/FOO.json",
        clientId: "123",
      },
      {
        configUrl: "https://idp2.com/login/BAR.json",
        clientId: "456",
      }
    ]
  }
});
</script>

We have also received feedback from other browser vendors that supporting multiple IdPs by combining multiple get() calls would be more practical. This is because IdPs can call FedCM on their SDK (e.g. Facebook SDK, Google SDK) so that relying parties (RPs) which already embed these IdPs’ SDKs do not necessarily need to make any changes. We have been brainstorming some ideas on how to allow multi-get FedCM dialogs. That said, we have also received feedback that even a single-get multi-idp solution works for some use-cases.

Here is a sample mock of what we want to achieve in the long term:

image2

The ‘array within get() call’ solution is being prototyped and gated behind a flag for the upcoming dev trials. The Chrome team intends to do an Origin Trial starting in Chrome 126.

Later, we also intend to enable multi-get FedCM dialogs. However, there are still many open questions such as how to group the multiple calls, how to handle aborts (e.g. what happens if a single get() call aborts when we’re currently displaying UI involving multiple get() calls), what should the IdP order be, and whether the API shape will work for other IdPs that have yet to adopt FedCM. Ultimately, we are still exploring if these solutions will be sufficient in tackling the multi IdP problem and if there are other better solutions available.

@bvandersloot-mozilla
Copy link

Re-using navigator.credentials.get as the lever for an IDP to register itself depending on page state being not yet loaded seems like it may be a little confusing for developers. Was a separate init function considered?

Also, I don't understand how the snippet for the second after onload case works because I don't know what FB.login() or google.accounts.id.prompt() do and why calling them in rapid succession would work.

@npm1
Copy link
Collaborator Author

npm1 commented Oct 7, 2022

Re-using navigator.credentials.get as the lever for an IDP to register itself depending on page state being not yet loaded seems like it may be a little confusing for developers. Was a separate init function considered?

Indeed, an alternative solution we considered was using a register()/init() method plus get() to actually show the UI, where get() would probably not require much parameters as all the IDP info would be provided in the registration method. However, we currently do not favor this solution for a couple of reasons:

  • We did not want to introduce incentives for IDPs to call register+get right away and thus force themselves as the only IDP to be shown in the FedCM dialog.
  • If all IDPs do behave sanely, for page load use-cases it would be on the RP to manually decide when to call get, which felt to us like it would introduce RP changes. Note that the get-only solution does not require RP code changes to show a FedCM dialog with multiple IDPs as long as they already have multiple IDP SDKs embedded on their sites. It would only require the RP to give permission to the IDPs (contractually) to show such dialog.
  • If there is a register as well as a get which are separate, it can be complicated to properly support the sign-in button use-case. We'd probably need an unregister as well, which introduces some more complexity to the API.

Also, I don't understand how the snippet for the second after onload case works because I don't know what FB.login() or google.accounts.id.prompt() do and why calling them in rapid succession would work.

The initialize methods would do the work required to determine that FedCM is ok to call on this page, as well as to fetch any required parameters in order to call FedCM, like client_id. Once the initialization is complete, the IDP is expected to simply call get() on the prompt/login methods, which means they are called synchronously within the same task.

@bvandersloot-mozilla
Copy link

* We did not want to introduce incentives for IDPs to call `register`+`get` right away and thus force themselves as the only IDP to be shown in the FedCM dialog.

* If all IDPs do behave sanely, for page load use-cases it would be on the RP to manually decide when to call `get`, which felt to us like it would introduce RP changes. Note that the `get`-only solution does not require RP code changes to show a FedCM dialog with multiple IDPs as long as they already have multiple IDP SDKs embedded on their sites. It would only require the RP to give permission to the IDPs (contractually) to show such dialog.

One solution to both of these would be to require a user activation requirement on get. This is something I generally support, due to a browser chrome UI change occuring on the call to get.

* If there is a `register` as well as a `get` which are separate, it can be complicated to properly support the sign-in button use-case. We'd probably need an `unregister` as well, which introduces some more complexity to the API.

Why would it require an unregister?

Also, I don't understand how the snippet for the second after onload case works because I don't know what FB.login() or google.accounts.id.prompt() do and why calling them in rapid succession would work.

The initialize methods would do the work required to determine that FedCM is ok to call on this page, as well as to fetch any required parameters in order to call FedCM, like client_id. Once the initialization is complete, the IDP is expected to simply call get() on the prompt/login methods, which means they are called synchronously within the same task.

Sure, I get that they are called synchronously from the same task. What I don't understand is how these multiple calls coordinate into a single dialog. Especially since in this example FB.login() could await its get() call to prevent them from occurring concurrently (speaking of adversarial IDPs).

@npm1
Copy link
Collaborator Author

npm1 commented Oct 7, 2022

One solution to both of these would be to require a user activation requirement on get. This is something I generally support, due to a browser chrome UI change occuring on the call to get.

This would break the use case to show the prompt on initial page load. It might introduce incentives for IDPs to add page-wide listeners and show the UI on any user interaction, regardless of whether it is actually a sign in button or not.

Why would it require an unregister?

It would if IDPs register eagerly, i.e. for the initial page load use case. They'd need to be unregistered once the user clicks on an IDP-specific sign in button.

Sure, I get that they are called synchronously from the same task. What I don't understand is how these multiple calls coordinate into a single dialog. Especially since in this example FB.login() could await its get() call to prevent them from occurring concurrently (speaking of adversarial IDPs).

Await would block any further execution within that function from happening sync, but it would not block the caller right? Example:

async function test1() {
  console.log("pre1");
  await 1;
  console.log("post1");
}

test1();
console.log("postcallback");

Prints

pre1
postcallback
post1

@bvandersloot-mozilla
Copy link

One solution to both of these would be to require a user activation requirement on get. This is something I generally support, due to a browser chrome UI change occuring on the call to get.

This would break the use case to show the prompt on initial page load.

I'm not sure it is positive to continue supporting that use case. This especially depends on the UI treatment of this, i.e. a full door-hanger on load is very intrusive.

It might introduce incentives for IDPs to add page-wide listeners and show the UI on any user interaction, regardless of whether it is actually a sign in button or not.

Is this worse than showing the UI on load?

Why would it require an unregister?

It would if IDPs register eagerly, i.e. for the initial page load use case. They'd need to be unregistered once the user clicks on an IDP-specific sign in button.

Alternatively, allow arguments in the get call to only permit a single IDP and have all available IDPs be the default case. This gets to the "double-NASCAR" problem we started to talk about at the end of this week's meeting and didn't have time to talk about or many of the CG's participants to chime in since we ran over.

Sure, I get that they are called synchronously from the same task. What I don't understand is how these multiple calls coordinate into a single dialog. Especially since in this example FB.login() could await its get() call to prevent them from occurring concurrently (speaking of adversarial IDPs).

Await would block any further execution within that function from happening sync, but it would not block the caller right? Example:

🤦‍♂️ Yes, of course.

It still feels strange to me that two consecutive statements would be dependent on each other in this way. This also raises some weird questions like what happens if get() is called while the dialog is up? Does the UI jank to reflect it? Does it get ignored? Can an IDP do something weird like spin-lock the JS interpreter while the chrome shows the dialog to prevent other requests?

@samuelgoto
Copy link
Collaborator

samuelgoto commented Oct 7, 2022

seems like it may be a little confusing for developers

Yeah, I agree that this is an unusual API ... I think that the desire to integrate well with the JS SDKs is what's making us consider the extra awkwardness for the benefit of making that better.

If you haven't already, it is worth taking a closer look at how the major IdPs expose in their SDKs.

Examples:

https://developers.facebook.com/docs/facebook-login/web/
https://developers.google.com/identity/gsi/web/guides/display-google-one-tap

Was a separate init function considered?

Yeah, good point, we should write down the alternatives we considered (and the ones that are still under consideration) and their trade-offs. @tttzach any chance you can share in this thread the various options we considered?

Sure, I get that they are called synchronously from the same task. What I don't understand is how these multiple calls coordinate into a single dialog.

I think that the most important observation to make here is that the "coordination into a single dialog" in this proposal is made by the RP, not the JS SDKs.

That is, the following block is an event listener that the RP controls and uses to coordinate across functions that are exposed by the JS SDKs.

function onButtonClick() {
  // deliberate calls these in parallel, as opposed to calling await, 
  // to allow the IdPs to all call get() without blocking each other.
  FB.login(); // internally, calls await on navigator.credentials.get()
  google.accounts.id.prompt(); // internally, calls await on navigator.credentials.get()
  // both of these get executed and, at the end of it, the prompt shows -- before the task registered 
  // when the navigator.credentials.get() call was made will 
  // execute after the control gets passed back to the event loop.
}

So, with this proposal, the following wouldn't be a concern, because the RP coordinates the calls exposed by the JS SDKs.

Especially since in this example FB.login() could await its get() call to prevent them 
from occurring concurrently (speaking of adversarial IDPs).

@samuelgoto
Copy link
Collaborator

I'm not sure it is positive to continue supporting that use case. This especially depends on the UI treatment of this, i.e. a full door-hanger on load is very intrusive.

Yeah, in agreement that the current formulation is more intrusive than it should be, but I think that the use case that is serves is an important one to support: a website should be able to tell the browser that it allows users to sign-in to it declaratively upfront (i.e. without user interaction). We have been experimenting with a few other variations controlling the "volume" of the UX" (e.g. based on intent).

Here is an example of "quieter" UX that I think would strike a good balance between discovery (that you can login to this website) and intrusion:

Screen Shot 2022-10-07 at 9 12 12 AM

@npm1
Copy link
Collaborator Author

npm1 commented Oct 7, 2022

I'm not sure it is positive to continue supporting that use case. This especially depends on the UI treatment of this, i.e. a full door-hanger on load is very intrusive.

Fair. Ideally we should aim to strike the right balance between non-intrusiveness and minimal user friction. It's hard.

Is this worse than showing the UI on load?

It could be more disruptive if it shows up once you already thought the page was loaded instead of at the very beginning.

Alternatively, allow arguments in the get call to only permit a single IDP and have all available IDPs be the default case. This gets to the "double-NASCAR" problem we started to talk about at the end of this week's meeting and didn't have time to talk about or many of the CG's participants to chime in since we ran over.

I wouldn't want to have a parameter that allows IDPs to exclusively show themselves in the FedCM UI. But need to think more about it.

It still feels strange to me that two consecutive statements would be dependent on each other in this way. This also raises some weird questions like what happens if get() is called while the dialog is up? Does the UI jank to reflect it? Does it get ignored?

Oh yes sorry that this was not clear enough in the comment, but right now any get() called while the dialog is up would be rejected, similar to nowadays with the single IDP case. But it is also feasible to consider adding this new IDP to the existing UI.

Can an IDP do something weird like spin-lock the JS interpreter while the chrome shows the dialog to prevent other requests?

Hmm that would be very disruptive to a user (unlikely to fly) since the page would become frozen. I don't think an IDP would try that. In any case we wait for IDPs until either onload (cannot stop other embedded scripts from running before that) or the end of the task (cannot force the existing task to prematurely end), so this would not work.

@bvandersloot-mozilla
Copy link

Fair. Ideally we should aim to strike the right balance between non-intrusiveness and minimal user friction. It's hard.

Agreed :)

Oh yes sorry that this was not clear enough in the comment, but right now any get() called while the dialog is up would be rejected, similar to nowadays with the single IDP case. But it is also feasible to consider adding this new IDP to the existing UI.

This is where my race condition sensor goes off, if only because I don't see a clear definition of how the dialog timing is treated in the spec. select an account is in the sequential path of the main call to [[DiscoverFromExternalSource]] in the spec and blocks, and I assume a "select a provider" would be similar. Without some state tracking when a re-entrance is allowed or disallowed explicitly I am just having a hard time following.

This may just be cleared up with revisions for w3c-fedid/FedCM#260.

@npm1
Copy link
Collaborator Author

npm1 commented Oct 7, 2022

This is where my race condition sensor goes off, if only because I don't see a clear definition of how the dialog timing is treated in the spec. select an account is in the sequential path of the main call to [[DiscoverFromExternalSource]] in the spec and blocks, and I assume a "select a provider" would be similar. Without some state tracking when a re-entrance is allowed or disallowed explicitly I am just having a hard time following.

This may just be cleared up with revisions for w3c-fedid/FedCM#260.

At least for the single IDP case, I attempted to handle this in w3c/webappsec-credential-management#207. That will need to change for the multi IDP case though, as you point out.

@achimschloss
Copy link

Was a separate init function considered?

Yeah, good point, we should write down the alternatives we considered (and the ones that are still under consideration) and their trade-offs. @tttzach any chance you can share in this thread the various options we considered?

I guess something like an init function is inevitable to allow for RP controls if the get calls should largely be done from IDP SDKs. I don't even see how the simple IDP priority should be handled otherwise - making this depended on timing of SDK loading order (first get call wins) does not seem like a good idea.

@npm1
Copy link
Collaborator Author

npm1 commented Oct 11, 2022

I guess something like an init function is inevitable to allow for RP controls if the get calls should largely be done from IDP SDKs. I don't even see how the simple IDP priority should be handled otherwise - making this depended on timing of SDK loading order (first get call wins) does not seem like a good idea.

If you are thinking about adversarial IDPs, then having init does not really fix the issue since the IDPs can then compete to call init the fastest. If the IDPs are well-behaved, they should have a clear method exposed to the developer in which the get is called and thus the developer can rely on the order of the calls to express their preference for IDP order.

@achimschloss
Copy link

If you are thinking about adversarial IDPs, then having init does not really fix the issue since the IDPs can then compete to call init the fastest. If the IDPs are well-behaved, they should have a clear method exposed to the developer in which the get is called and thus the developer can rely on the order of the calls to express their preference for IDP order.

I was thinking of an init function called by the RP, but this could obviously be misused in case an adversarial IDP has on-site JS-Code running. But I was not really thinking about adversarial IDP / non-cooperation, but keeping things usable for RPs. 'Implicit' assumptions about call orders to various SDKs mapping to business needs may quickly become confusing (as this might just be one of them)

@teamktown
Copy link

Hi.
Read the recent meeting notes and trying to follow the context of the IDP rendering strategy (offering to log into one? have been logged into N+1 of them?) Given the reference to the NASCAR effect I presume this is about signing into an IdP.

Q: What happens in the case where there are many 1000's of IdPs that a user may potentially search against for the right one and then subsequently sign-in from?

As a ecosystem with >5000 live IdPs worldwide the select an account experience / aka Discovery experience being discussed so far appears to be quite challenging. Insight and guidance on where to assess the experience on this welcomed.

@npm1
Copy link
Collaborator Author

npm1 commented Oct 14, 2022

Hmm yea in theory a user would only have account on a very few number of the IDPs but sending 5000 requests from a single page would result in a very slow experience. If the user searches and selects an IDP from the large list, then this is not an issue. Once the user has selected an IDP, you can invoke FedCM with that IDP. Do you have concerns with the flow where the user has to first choose their IDP?

@teamktown
Copy link

Yes, i do have concerns given the offered UX stories so far (e.g. the quieter UX above).
Additionally, how does one get an 'appropriate' list of IdPs?? There are numerous Discovery UX exemplars for the eduGAIN.org / R&E federations experience ( https://seamlessaccess.org/services/ is one) all driven by the notion of 'render a given circle of trusted entities' vs the entirety of the internet.

Existing Discovery UX can be used in front of FedCM like you describe however one would be concerned about implied / purchased / monetized / monopolized ranking of certain IdPs over others given how integrated it will be and first occurrence could dramatically skew functionality. IE show google id in chrome, mozilla persona in firefox, and microsoft's in Edge (despite being chrome) THEN others.

Additionally, (and a case of me not able to exercise FedCM enough) the notion of presence of identity in the browser across tabs. I'm fine to take it as homework to read more BUT the notion of identity in the quiet UX means I'll likely have multiple signed in personas (e.g. a work one, a school one, a personal one for twitter, for github, for facebook) and they will all be available across all tabs. If so, what are the features for appropriate access to said credential(s) if I need or have a way to sign in with two different IdPs to the same site? Again, I'll take it as homework for me and welcome being pointed to the appropriate place where to read and/or exercise the implementation for such use cases.

@cbiesinger
Copy link

@teamktown We have no short/medium-term plans to offer a "global" list of IDPs. It is unlikely that RPs would want this anyway, how do they know whether to trust that random IDP has actually verified who the email address belongs to? For now we just want to allow RPs who support multiple IDPs to show them all in the same dialog.

WRT the quiet UI, I don't understand what you mean with "notion of presence of identity in the browser across tabs". That UI mock is essentially just a different way to show the FedCM dialog -- the RP calls navigator.credentials.get(), we make the requests to the IDP endpoints, and show the name we get in the URL bar. Presumably the browser would show a somewhat different UI if you are signed in to multiple accounts. We have not really explored this UI very deeply yet.

@teamktown
Copy link

teamktown commented Oct 20, 2022

Thanks for the above comments @cbiesinger and apologies for the delay in reply.
to your point:

"We have no short/medium-term plans to offer a "global" list of IDPs. It is unlikely that RPs would want this anyway, how do they know whether to trust that random IDP has actually verified who the email address belongs to? For now we just want to allow RPs who support multiple IDPs to show them all in the same dialog."

I am trying to highlight that yes, RPs do want^h^h^h^h need this and not just a few, but hundreds to thousands of them. The discovery interface/experience for signing on is a critical aspect for R&E federations worldwide. eduGAIN represents 3600+ RPs (Service Providers) and they need an adequate experience on discovery for one's home IdP. The IdPs are spread across 80+ countries and represent a significant user population who use browsers for SSO access

I'm still ramping up on the technical bits on the FedCM so maybe I'm co-mingling the quiet UI and tabs improperly as you point out.

Still, as you can see by the above numbers, polling idp endpoints to the tune of 1000's per browser window and even tab, is not a scalable pattern and welcome dialogue on how this is to be scalably addressed. If there's a place to point me for this aspect of the UX for this, let me know..

@npm1
Copy link
Collaborator Author

npm1 commented Oct 20, 2022

We chatted with Ben to brainstorm ideas for multi IDP, here are the notes: https://docs.google.com/document/d/1t0QuWnjalv2NsLIr0FEcYpEV30Hp-s9KZblsQjsLZqs/edit?resourcekey=0-RvSqpV4ViFpGaXYBnKYQLw

Our team is aligned about proposal 4 hitting the right balances, so I plan to write another doc explaining it more clearly. I will post that once it's done!

@samuelgoto
Copy link
Collaborator

samuelgoto commented Oct 20, 2022

Still, as you can see by the above numbers, polling idp endpoints to the tune of 1000's per browser window and even tab, is not a scalable pattern and welcome dialogue on how this is to be scalably addressed. If there's a place to point me for this aspect of the UX for this, let me know..

I think some of the "polling to 1000s IdPs" problem can get addressed by the IdP Sign-in Status API, if we included metadata about the user there, e.g.:

IdentityProvider.login({
  accounts: [{
    name: "Sam Goto", 
    email: "[email protected]",
    picture: "..."
  }
}]);

Q: What happens in the case where there are many 1000's of IdPs that a user may potentially search against for the right one and then subsequently sign-in from?

I'm thinking that maybe we'll need to come up with a different registration mechanism. E.g.

IdentityProvider.login({
  federation: "eduGAIN" /** maybe this needs to be signed somehow? **/,
  accounts: [{
    name: "Sam Goto", 
    email: "[email protected]",
    picture: "..."
  }}]
);

Which then allows the RPs to request "identities from a specific federation", e.g.

navigator.credentials.get({
  identity: {
    providers: "eduGAIN"
  }
});

And the browser would be able to match the string "eduGAIN" between the get and the login.

I'm still ramping up on the technical bits on the FedCM so maybe I'm co-mingling the quiet UI and tabs improperly as you point out.

I wouldn't get too attached about the "quiet UI": I'm sure you won't be able to fit 1000 IdPs there. We can build larger UXes based on cardinality and user intention / gestures.

@teamktown
Copy link

@npm1 - thank you for the gdoc !
Observations:

  • In evaluation criteria section:
    • there's not much at all about how to shape experience from the Users perspective. Without clarity on what a user expects from the interface, the UI, the activation experience, and subsequent registration needs, how can the API be shaped and adjusted? If these elements are already defined elsewhere, I encourage them to be linked to the doc. If they are not, then I encourage a bit more depth in this area.
      • Rationale for the comments: There appears to be an assumption that there are one or two identity sources AND that the user has already attempted discovery to find their 'source idp' and then registered with them somehow. This is -- problematic at best, punishing the user at the worst. IdP Discovery/NASCAR effect doesn't seem to be addressed very well yet.
    • Additionally there seems to be an '1 person shall use 1 identity' narrative being spun in the work and thus designs are constrained by this (over) simplification. Outside looking in, this mindset constricts the API vs opening it to a more realistic model where a user has numerous credentials. It is not evident yet that the current API design outlook scales to the users needs (subjective opinion of course).
  • Suggestion:
    • Does a more detailed section in fedCM or a reference to user stories exist beyond the flows? If not, this is deeply welcomed and encourage them to be written up and can contribute. (again apologies if I very much missed this and will be going back to review to see if they are there)
      Some resources on this space from the multi-lateral SAML federation space are below to offer a glimpse at the how others have been tackling the multiple IdP problem space:
  • https://seamlessaccess.org/learning-center/challenges-federated-wayf/ <--most recent
  • https://seamlessaccess.atlassian.net/wiki/spaces/DOCUMENTAT/pages/819234/User+Experience+Scenarios
  • https://www.niso.org/standards-committees/espresso
  • https://wiki.refeds.org/display/FBP/Discovery+Best+Practice
  • https://discovery.refeds.org/

While some of these references go way back, the state of things has not changed a lot -- discovery is challenging to do well and supportive of the users needs.

To @samuelgoto above on his comments

Which then allows the RPs to request "identities from a specific federation", e.g. .. eduGAIN

  • Users rarely know their origin set (ie eduGAIN) they know with whom they are affiliated (I work for.. , I an an alum of .., I am faculty at.. etc)
  • Trying to distill all of any sector into one label is challenging. There's a lot of prior art instrumenting metadata to do this (MDUI being one.

The comments above on the API trickle through to rolling up all idps into one 'label' -- it's hard, it needs more insight to be assistive to the user to avoid the pitfall of inappropriately masking/dropping things or making sign-on more difficult than it should be.

@bvandersloot-mozilla
Copy link

bvandersloot-mozilla commented Oct 31, 2022

A slight modification of option 4 from the doc:

I propose using navigator.credentials.get as both a registration and prompt method, using the mediation parameter to differentiate the use cases. Specifically:

  • Registration is signaled by calling navigator.credentials.get({“mediation” : “delayed” …
  • Prompts are signaled by calling navigator.credentials.get({“mediation” : “U\{delayed}”...
  • When mediation is “delayed” the user agent will not create a dialog with the user until a time that either requires such a dialog or is after some user interaction that indicates a desire to make an authentication decision with user agent UI. Examples include additional calls to navigator.credentials.get without delayed mediation and a user entering a credential selection menu in browser UI.

IdentityCredentialRequestOptions is redefined to have optional providers and new optional member providerOrder of a new dictionary type that allows RPs to specify provider order. This may be as simple as providing an ordered list of IDPs or as complicated as providing an enum to select from ordering heuristics and a requested number to show in the first page of the dialog.

Overloading the use of navigator.credentials.get is non-intuitive and requires justification. The core reason is that both the registering callsite and prompt-inducing callsite will probably want to know the user’s selection. I think defining both calls in terms of get with varying mediation requirements allows us to reuse argument semantics and return value semantics. This is the closest representation to the underlying logical structure I could find: at both calls the script is requesting the retrieval of a credential, varying only in how it wants to be treated for UI. This is precisely the point of the mediation parameter. I would be willing to entertain a separate “prompt” call, however it is unclear where to put it in the BOM and how it would interface with other credential types.

Let’s look at how this can work in all cases:

  1. Single-IDP, gesture
    a. The caller provides the IDP information in a get call with mediation in U{delayed}. Dialog comes up with only that IDP.
    b. The IDP script provides their information in a get call with delayed mediation. Later, with user interaction, get is called without delayed mediation and no providers member. Dialog comes up.
  2. Multiple-IDP, gesture
    a. IDP scripts provide their information in a get call with delayed mediation. Later, with user interaction, get is called without delayed mediation and no providers member. The RP can specify constraints on IDP ordering with the new providerOrder member of IdentityCredentialRequestOptions. Dialog comes up.
  3. Single-IDP, no gesture
    a. The IDP script calls get with mediation delayed whenever it wants. The browser may create quiet or dynamic UI to induce interaction to require resolution
  4. Multiple-IDP, no gesture
    a. All IDP scripts call get with mediation delayed whenever they want. The RP can call get with delayed mediation and the new providerOrder member of IdentityCredentialRequestOptions to specify the order hints.

One nice feature here is that these cases are not disjoint and the spec reflects that. In particular, the state of the API use can flow in this way.

@npm1
Copy link
Collaborator Author

npm1 commented Oct 31, 2022

  • there's not much at all about how to shape experience from the Users perspective. Without clarity on what a user expects from the interface, the UI, the activation experience, and subsequent registration needs, how can the API be shaped and adjusted? If these elements are already defined elsewhere, I encourage them to be linked to the doc. If they are not, then I encourage a bit more depth in this area.

User agent UI is not something that is generally specified. It is up to each user agent how they want to surface their UI. But I'll keep this in mind for the doc I'm working on, to include more visual aspects.

  • Does a more detailed section in fedCM or a reference to user stories exist beyond the flows? If not, this is deeply welcomed and encourage them to be written up and can contribute. (again apologies if I very much missed this and will be going back to review to see if they are there)

The CG has been working on https://github.com/fedidcg/use-case-library. These are not specific to multi IDP scenario though so I can take a look at the linked resources.

While some of these references go way back, the state of things has not changed a lot -- discovery is challenging to do well and supportive of the users needs.

I don't know that FedCM could replace all existing discovery mechanisms. I would want to know which ones currently do not work well or are rely on third party cookies as those would be the most urgent ones to have an API for.

@npm1
Copy link
Collaborator Author

npm1 commented Oct 31, 2022

@bvandersloot-mozilla it is arguably a bit weird to also use get() for the ordering of the IDPs. It would also be interesting to understand who gets the actual id tokens... I imagine it would be the get()call which passed the IDP selected in its providers array, right? I think that's the only call that would need to receive the token, but just wanted to clarify that

@bvandersloot-mozilla
Copy link

I agree it is a little weird, but it makes some sense. It is effectively saying "get an identity credential using the providers you already know about, but with this ordering hint, and show a prompt at a deferred time".

I also think that all calls to get() should return a promise that may resolve to an IdentityCredential. A selected IdentityCredential would be resolved if the client is in the list of providers for that call or there is no list of providers. So the RP and chosen IDP would get the Credential.

@achimschloss
Copy link

I also think that all calls to get() should return a promise that may resolve to an IdentityCredential. A selected IdentityCredential would be resolved if the client is in the list of providers for that call or there is no list of providers. So the RP and chosen IDP would get the Credential.

I think it would be highly beneficial to have a few concrete examples for the cases. These get calls all happen on the RP site so it is a bit hard to digest without. I assume that " IDP would get the Credential" boils down to the IDP script on the RPs site would receive it in order to provide it via a bespoke API to the RP again. Or both (IDP Script and RP) could have called get thus the RP would receive it also "directly" using its own resolved promise.

I guess the point is that it should work in any scenario + you can use the get call for additional business logic using the providerOrder and further potential extensions of this (scope ..., sign-in or sing-up)

@npm1
Copy link
Collaborator Author

npm1 commented Nov 2, 2022

Yea that is part of why I think the ordering get() is a bit weird. In general it seems undesirable to have to provide the id_token from a single FedCM dialog to more than one of the get() callers. So I think it would be OK to not provide the token to a get() call that just specifies ordering. That might just return a promise that resolves right away or something...

@bvandersloot-mozilla
Copy link

@asr-enid :

I assume that " IDP would get the Credential" boils down to the IDP script on the RPs site would receive it in order to provide it via a bespoke API to the RP again.

Exactly. s/IDP/IDP-script-callsite/g.

I think it would be highly beneficial to have a few concrete examples for the cases.

Single-IDP, gesture: Login with Facebook button on a page that a user clicks to activate
Single-IDP, no gesture: "Log in with Google" dialog appears in the upper righthand corner on page load.
Multiple-IDP, gesture: Login button on a page that a user clicks to activate and shows the user all of their options to log in
Multiple-IDP, no gesture: At a time of the browser's choosing the user is shown all of their options to log in. This could be when a user clicks on some browser UI related to identity.

@npm1 :

I think it might be reasonable to have the credential sent to multiple gets in the case of multiple-IDP and user-interaction. You might want the registration get() to know it was the one chosen and the user-activation get() to know which is chosen and when.

I'd be fine to change semantics so that an ordering-specific get() (i.e. no providers, and delayed mediation) resolves immediately. How about something like "calls to get() will resolve with an IdentityCredential if a provider they give is chosen or if they have non-delayed mediation and the user selects a credential in the dialog. Otherwise they resolve with undefined."

@npm1
Copy link
Collaborator Author

npm1 commented Nov 7, 2022

Here is the doc I had promised with a little more detail about the proposal born out of the meeting a few weeks ago. I haven't given a lot of thought to the ordering. Having it be specified via a get() call might be OK though I do wonder if there is a more intuitive/ergonomic solution.

https://docs.google.com/document/d/171zldBKTqts0zKtCMSMv-eexGmXpTdzh5IiPYKZSTOQ/edit#

@npm1
Copy link
Collaborator Author

npm1 commented Nov 21, 2022

Just to report back here, I presented the above proposal to the Google Identity Services team and it made me think that perhaps the API making explicit separation of the user interaction use case vs non-interaction is perhaps not worth it? Their feedback was "We wouldn't know whether there is user interaction at the time our API is called, so we'd just try to invoke the API in user interaction mode [this assumes that when such invocation occurs, it degrades to a delayed/non-interaction if there is actually no user interaction]." So I'm now thinking perhaps we need to rethink?

@npm1
Copy link
Collaborator Author

npm1 commented Nov 28, 2022

This was discussed in the call today: notes. Just wanted to add here some thoughts/ideas that were surfaced during the call:

  • IDP having an embedded SDK in the RP means they have full control, and hence it's difficult to counter any misbehavior. However, the benefit of this is having no code changes from the RP's side.
  • There could be some frame header or such for RP to decide the operating mode of FedCM (delayed or not)
  • Perhaps if IDPs need to be in an iframe then the RP can set the controls via permissions (note: wouldn't scale well to use cases with many IDPs)

@npm1
Copy link
Collaborator Author

npm1 commented Dec 13, 2022

My current thinking is that we should go with option 2 from the doc:

  • While having different behavior before vs after onload, the goal of allowing all IDPs to register outweighs the additional complexity. Additionally, we do not anticipate the onload behavior to be very noticeable as the current FedCM invocations are usually very close to or during onload, so in practice they would work the same way as what we propose to be the post-onload behavior (post a task to request the token).
  • Specifying the order of the IDPs in the UI should not be entirely up to the RP. For instance, returning accounts should always be prioritized, as this means the user has already made a clear choice. For new users though, we can expose a new method where the RP can set the priorities of the IDPs, perhaps by pointing to their config files or their domains.
  • Separation of the use-cases was a consideration when thinking about the idea with two distinct mediation parameters, but the feedback we got was very clear: there is no such distinction in the IDP usage today, and thus introducing this would not currently provide any benefit to the IDP and instead might cause some confusion as to which mediation mode they should be using when.
  • Given there wouldn't be two distinct modes, there's no advantage to having registration vs prompt. We believe that enabling FedCM to be shown without requiring RP coding changes is an important goal, and asking the RP to call prompt() is contrary to this. In addition, this likely would put the IDP in the awkward position of calling prompt() themselves just to be sure that FedCM actually ends up showing up in the RP, and this defeats the purpose of prompt().

Based on the above points, I believe option 2 accompanied with a separate method for IDP ordering would strike the right tradeoffs: it would enable sufficient RP control for those who have strong opinions on which IDP they'd prefer, but it would also enable hands-off RPs to successfully use FedCM by just embedding IDP SDKs. Let me know what you think!

@bvandersloot-mozilla
Copy link

While having different behavior before vs after onload, the goal of allowing all IDPs to register outweighs the additional complexity.

I think there is probably a way to write the spec that doesn't include referencing the specifics of the onload event. I'm thinking of something like defining the prompt mechanism to occur at a browser-defined time (which may be onload but is certainly not immediate). We could even tie it into the

Specifying the order of the IDPs in the UI should not be entirely up to the RP.
Shouldn't an RP be able to decide that they don't want to prioritize returning accounts if they don't want to? This is nit-picking though since we can offer this as a boolean in an ordering information struct.

Based on the above points, I believe option 2 accompanied with a separate method for IDP ordering would strike the right tradeoffs: it would enable sufficient RP control for those who have strong opinions on which IDP they'd prefer, but it would also enable hands-off RPs to successfully use FedCM by just embedding IDP SDKs.

Given this, I think that is probably the way forward. How about instead of "the onload event" we talk about "the display of IDP chooser, which must occur after the onload event"? This leaves more flexibility for quiet UI and doesn't artificially constrain the "early calls" to before onload when there may be more time. Also, we could add an option to the RP's ordering suggestion call that can suggest to the browser to use a louder UI.

@npm1
Copy link
Collaborator Author

npm1 commented Dec 14, 2022

I think there is probably a way to write the spec that doesn't include referencing the specifics of the onload event. I'm thinking of something like defining the prompt mechanism to occur at a browser-defined time (which may be onload but is certainly not immediate). We could even tie it into the

Seems your reply cut off suddenly, but agree that perhaps we can give some leeway to the user agent instead of specifying precisely when the UI should be shown.

Given this, I think that is probably the way forward. How about instead of "the onload event" we talk about "the display of IDP chooser, which must occur after the onload event"? This leaves more flexibility for quiet UI and doesn't artificially constrain the "early calls" to before onload when there may be more time. Also, we could add an option to the RP's ordering suggestion call that can suggest to the browser to use a louder UI.

All of these suggestions sound good to me. Having the bar 'show at least after onload' achieves the goal of enabling all IDPs to register while preserving flexibility on the timing. This coupled with RP provided ordering seems like a good path forward. I like how this is shaping up!

@bvandersloot-mozilla
Copy link

Seems your reply cut off suddenly,

Ignore that fragment- my editing skills are poor. I incorporated that point into the below quote

I like how this is shaping up!

👍

Given this, I think that is probably the way forward. How about instead of "the onload event" we talk about "the display of IDP chooser, which must occur after the onload event"? This leaves more flexibility for quiet UI and doesn't artificially constrain the "early calls" to before onload when there may be more time. Also, we could add an option to the RP's ordering suggestion call that can suggest to the browser to use a louder UI.

All of these suggestions sound good to me. Having the bar 'show at least after onload' achieves the goal of enabling all IDPs to register while preserving flexibility on the timing. This coupled with RP provided ordering seems like a good path forward. I like how this is shaping up!

@npm1
Copy link
Collaborator Author

npm1 commented Jan 16, 2024

Some updates for the record (this has been discussed in FedID CG calls):

  • Chrome reported back that based on metrics we have been gathering, waiting until onload introduces significant performance penalties to an IDP that wants to show FedCM when the page is first shown. Therefore, we may need to go back to the brainstorm board to find ways to support amalgamating multiple get calls.
  • However, despite this there still is desire to support multiple IDPs even if just allowing a single get call for now. For example, this is a prerequisite for IDP registration (where RP can use any IDP that the user has registered with). So Chrome is currently focusing on the simpler problem of supporting multiple IDPs with a single get call.
  • In the last call I provided a desktop demo showing how we have fixed the code so multi IDP works with the currently shipping FedCM API in Chrome. Note that it does not currently work with other experimental Chrome features, such as the button mode. Please try it out (remember to enable the multi IDP flag in chrome://flags and use Chrome Canary) and let us know what you think or here or as a chromium bug report!

@philsmart
Copy link

I gave this a quick test in my testbed using 2 IdPs. Outcomes below:

  • Signed out of both IdPs
    • Clicked the custom sign-in button on the RP (i.e. initiated FedCM with two providers).
    • Asked to sign in to either IdP.
    • Clicked on IdP1.
      • Login_url popup window displayed.
      • Authenticated, the popup disappears and the usual dialog window is displayed.
    • Seems to fix the issues I observed with it popping up windows for all signed-out IdPs in the list.
  • I signed out of both and then tried this again, but this time it popped up a window from the login_url of the first provider. It then proceeded to show me a new (I think) dialog in the centre of the screen (see below[1]). Sometimes it crashed Chrome (so maybe I have some experimental features running it does not like).
  • Signed into both
    • Accounts from both are displayed in the usual dialog
    • I can select one as expected.
  • Signed into IdP 1, but not IdP 2
    • The usual dialog is displayed, I can either select an account from the signed-in IdP or select the other to log in.
    • Both do the expected thing. One pops up a window of the login_url, the other fetches the token.

[1] New window?
new-sign-in-window

@philsmart
Copy link

philsmart commented Feb 6, 2024

I ensured to disable the button mode feature, and I no longer get that specific popup.

My only issue now would be. If I am logged out of both, it asks me which to sign in with (which is fine, although now I am confused with the button flow). But once I have selected one to use, when I try to initiate FedCM again the choice is gone, and it just uses what I last choose (somehow).

@npm1
Copy link
Collaborator Author

npm1 commented Feb 6, 2024

I ensured to disable the button mode feature, and I no longer get that specific popup.

I did find some problem with that flag. I filed https://issues.chromium.org/issues/324102291 which might be what you encountered (button-like behavior despite not using the button mode in the get() call).

My only issue now would be. If I am logged out of both, it asks me which to sign in with (which is fine, although now I am confused with the button flow). But once I have selected one to use, when I try to initiate FedCM again the choice is gone, and it just uses what I last choose (somehow).

Yes, that is indeed how it works. In the widget mode, you only get a login url in the mismatch case. In addition, once you get that mismatch the IDPs have their login status set to logged out. So only the IDP you login to will have their status changed to login. If you go try FedCM again right away you will only see that IDP since its status is logged in whereas the other has status set to logged out.

@npm1
Copy link
Collaborator Author

npm1 commented Feb 21, 2024

Want to provide a concrete update on the Chrome side. We are planning to do an Origin Trial for the specific version of the problem where we allow a single get() call to use multiple IDPs. Any get() call invoked while there is a pending one will still be rejected. This allows us to tackle a subset of the problem without requiring solving all of the hard questions we've been discussing. Feel free to comment on this issue if this is something you'd be interested in trying out!

@judielaine
Copy link

When a RP supports multiple IdPs , eg: choice 1,2, 3 ... N , if the user has active sessions at IdP 1 but has an account at IdP3 that the browser is not aware of, the user should not only be presented IdP 1 but also see that there are other options so that they can discover that they could use IdP 3.

@npm1
Copy link
Collaborator Author

npm1 commented Jun 21, 2024

We have the 'use a different account' feature, it is currently in the button mode OT but I can see it potentially being used in widget mode as well when we have to show some UI anyways. I think that would address your issue.

@wseltzer
Copy link
Contributor

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

No branches or pull requests