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

Add multiple IDP support #438

Closed
wants to merge 3 commits into from
Closed

Add multiple IDP support #438

wants to merge 3 commits into from

Conversation

npm1
Copy link
Collaborator

@npm1 npm1 commented Feb 14, 2023

Relevant issue: #319

This PR adds support for multiple IDPs. It does so by adding some state to the document to allow bundling multiple get() calls with potentially multiple providers each. The bundling follows the pattern explained in #319 (comment): all calls before onload are bundled together while after onload we just use a task for bundling. There are also checks added to ensure that dupes in configURLs are not allowed. A few methods now return pairs so as to enable tracking the provider to which the selected account belongs to, which in turn enables determining which get() call to provide the credential to, in case of a successful flow.


Preview | Diff

@npm1
Copy link
Collaborator Author

npm1 commented Feb 14, 2023

@bvandersloot-mozilla I was hoping you could take a look at this before you go on leave! I know it is a bit late, so if you're unable to get a chance then I will bug your teammates :)

spec/index.bs Outdated Show resolved Hide resolved
1. Return |account|.
To <dfn>select an account</dfn> given |allAccountsAndProviders|, run the following steps. This
returns an ({{IdentityProviderAccount}}, {{IdentityProviderAPIConfig}}) [=tuple=] or the [=tuple=]
(failure, failure).
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are doing this so that you always return a 2-tuple? I don't think this is necessary... I'd just say "or failure"

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not necessary but it was cleaner to always return a tuple here instead of maybe returning failure and then having to do checks about it in the caller.

spec/index.bs Outdated Show resolved Hide resolved
spec/index.bs Outdated Show resolved Hide resolved
set |credentialPromise| to a new "{{NotAllowedError}}" {{DOMException}}.
1. Wait until |credentialPromise| is set, and return it.
1. If |document|'s [=fedID task status=] is "no task":
1. If |document|'s {{Document/readyState}} is "complete":
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI, longer term we should add a callback from the HTML spec to FedCM as described here: whatwg/html#8382 (comment)

(and also get rid of the DOM event listener)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea I should probably do that, for the other part (the one using event listener)

spec/index.bs Outdated Show resolved Hide resolved
Copy link
Collaborator

@bvandersloot-mozilla bvandersloot-mozilla left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have one major structural complaint: I'd like to be able to have a provider-chooser before the account chooser. Could we have an optional provider list filter step after the config fetches but before the account list fetches? This would enable both UI designs

I haven't gone through for bugs or deep-dived on the Bikeshed, and think @cbiesinger had some good points about figuring out how to handle the HTML integration and async events better. But other than the line above, this matches my expectations.

spec/index.bs Outdated Show resolved Hide resolved
@@ -461,9 +466,67 @@ algorithm is invoked, the user agent MUST execute the following steps. This retu

1. Assert: These steps are running [=in parallel=].
1. Assert: |options|["{{CredentialRequestOptions/identity}}"]["{{IdentityCredentialRequestOptions/providers}}"] [=map/exists=].
1. Assert: |options|["{{CredentialRequestOptions/identity}}"]["{{IdentityCredentialRequestOptions/providers}}"] [=list/size=] is 1.
1. Let |document| be |globalObject|'s [=associated Document=].
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want to coalesce requests in separate iframes? If so, we should probably unify these to be one per window. For example, using globalObject's top-level traversable's active Document.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. I think for now we do not want to mix requests from different frames.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My thoughts on this depend on the behavior of major IDPs that I don't know off of the top of my head... Do the APIs included via script tags usually iframe themselves off before trying to establish a session on page load? Put another way: does the equivalent of the google.accounts.id.initialize call do the meat of its work inside of an iframe for any major IDP?

If so, I think mixing requests from frames is important to this extension's utility.

spec/index.bs Outdated Show resolved Hide resolved
1. Wait until |credentialPromise| is set, and return it.
1. If |document|'s [=fedID task status=] is "no task":
1. If |document|'s {{Document/readyState}} is "complete":
1. [=queue a global task=] with |globalObject| on the [=DOM manipulation task source=] to
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you consider any alternatives to the DOM manipulation task source? It seems reasonable, but I'm not confident.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could also be the networking task source since those steps will produce a bunch of fetches. I'm open to change this but not sure if it makes a big difference (as long as similar fedcm steps run on the same task source)

spec/index.bs Outdated Show resolved Hide resolved
spec/index.bs Outdated Show resolved Hide resolved
@npm1
Copy link
Collaborator Author

npm1 commented Feb 15, 2023

I have one major structural complaint: I'd like to be able to have a provider-chooser before the account chooser. Could we have an optional provider list filter step after the config fetches but before the account list fetches? This would enable both UI designs

Added

I haven't gone through for bugs or deep-dived on the Bikeshed, and think @cbiesinger had some good points about figuring out how to handle the HTML integration and async events better. But other than the line above, this matches my expectations.

Ah yea I think for now we should monkeypatch the HTML spec instead of what I did. Haven't fixed this part but ptal at the rest

returns an ({{IdentityProviderAccount}}, {{IdentityProviderAPIConfig}}) [=tuple=] or the [=tuple=]
(failure, failure).
1. Assert |allAccountsAndProviders|'s [=list/size=] is greater than 1.
1. Display an account chooser displaying the options from |allAccountsAndProviders|.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no mentioning here how this dialogue is constructed w.r.t. to ordering suggestions by the RP - This was discussed at length - #319 (comment)

@npm1

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

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.

The general context was discussed here #348 and in a fedccg meeting see here - https://github.com/fedidcg/meetings/blob/main/2022/2022-10-10-notes.md

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea we could certainly add the RP-specified ordering, although it is a minor addition in terms of how it impacts the processing model, and I don't think we are settled on the API shape yet. I lean towards having a separate method, something like navigator.credentials.setProviderOrdering(["configURL1", "configURL2"]) but I don't think we have reached consensus on that. Once we do then I can add that to this PR or a later PR if this has merged, WDYT?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be good to include here given the interest and our initial commitments.

If we're hanging it off of navigator.credentials we should generalize it to be applicable to all credentials.

However, I prefer to put it somewhere like window.IdentityProvider.setPromptOrdering, and maybe move the Sign-In status API to window.IdentityProvider.status.*. This makes it clear that it is tied to Identity Provider concerns, rather than general credentials.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

navigator.credentials.setProviderOrdering(["configURL1", "configURL2"])

If we start from the assumption that the relying party is going to be making a JS call (which I'm not sure is a good idea), then we can revisit the entirety of this API design here and take one of the other alternatives that don't rely on batching API calls, that is the Register and Prompt alternative we considered.

We could do:

// Called from JS SDKs
let {token} = await IdentityProvider.register({
  configURL: // ...
});

And then later the RP can call:

await navigator.credentials.get({
  identity: {
    providers: [{
      order: ["configUrl1", "configUrl2", ...]
    }]
  }
});
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, I prefer to put it somewhere like window.IdentityProvider.setPromptOrdering, and maybe move the Sign-In status API to window.IdentityProvider.status.*. This makes it clear that it is tied to Identity Provider concerns, rather than general credentials.

I'm fine with IdentityProvider.setPromptOrdering() too, as we don't have use-cases for making this more general to credentials. Although we might in the future.

If we start from the assumption that the relying party is going to be making a JS call (which I'm not sure is a good idea), then we can revisit the entirety of this API design here and take one of the other alternatives that don't rely on batching API calls, that is the Register and Prompt alternative we considered.

I don't think that suggestion works, and the reasons are stated in the section you linked. We need to fulfill these requirements:

  1. The API should be easily invocable from IDP SDKs, independently.
  2. The IDP ordering should be customizable by an RP that wishes to do so. Note that this customization is entirely optional: an RP does not have to specify an IDP ordering.

Your suggestion does not satisfy 1), a key requirement. We are not assuming that the RP is going to directly make a JS call. I think however it is fair to assume that if the RP wants to specify IDP ordering, then they should be capable of adding a JS call. Note that this JS call only requires very limited info about the IDPs (their origins or configURLs), not the full set of things required from a get() call. But this JS call is also not required: the user agent can decide an IDP ordering if the RP provides no signal about their preference.

An alternative I considered is passing an order parameter directly in the get() call. I worry that this means the IDP is the one setting the value since they are the ones invoking the FedCM API, and there's not a lot of incentive to respect the desired RP value (an IDP would always want to be shown as the first one).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An alternative I considered is passing an order parameter directly in the get() call. I worry that this means the IDP is the one setting the value since they are the ones invoking the FedCM API, and there's not a lot of incentive to respect the desired RP value (an IDP would always want to be shown as the first one).

I agree, this could also lead to conflicting values passed to the get calls. Having some sort of independent way for RPs to define their preferences in general seems sensible outside of some defined default behaviour. Making this optional makes sense to me too. I guess this API surface should then also not be available in IDP IFrames.

However, I prefer to put it somewhere like window.IdentityProvider.setPromptOrdering, and maybe move the Sign-In status API to window.IdentityProvider.status.*.

Agree - this would also leave room for catering to additional needs of RPs (#348) by adding additional means to define this via api surfaces to window.IdentityProvider. (for example a means to differentiate if they want to trigger a sign-in vs. a sign-up flows or don't care (default), which is not really a multi-idp question thus not in scope for this PR)

@bvandersloot-mozilla
Copy link
Collaborator

I have one major structural complaint: I'd like to be able to have a provider-chooser before the account chooser. Could we have an optional provider list filter step after the config fetches but before the account list fetches? This would enable both UI designs

Added

👍 TY!

I haven't gone through for bugs or deep-dived on the Bikeshed, and think @cbiesinger had some good points about figuring out how to handle the HTML integration and async events better. But other than the line above, this matches my expectations.

Ah yea I think for now we should monkeypatch the HTML spec instead of what I did. Haven't fixed this part but ptal at the rest

Agreed. Then once this is graduated to a WG (or more stable) we can upstream the monkeypatch.
I've looked at the rest and it makes logical sense and represents the vision I have.

I don't have the time to deep-dive on the Bikeshed before my leave, unfortunately. This is mostly because of my general unfamiliarity with the weeds of what is allowed "in parallel" and how to play nice with WHATWG HTML. I think that a lot of the complication here stems from the Credential internal methods being run in parallel. Like the "wait for X to be set" lines where promises are natural. However, I don't spot any bugs in the constructions you make and the algorithms you add. Great work here- there is creativity in this I hadn't gotten far enough to see needed in my sketches.

@npm1
Copy link
Collaborator Author

npm1 commented Jan 16, 2024

This seems outdated and I am trying to clean up open pull requests so I'm closing. I do plan to send a PR for multi IDP in the near future, but not including the load event heuristic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
5 participants