diff --git a/spec/index.bs b/spec/index.bs index d640c3adb..6a3d510cf 100644 --- a/spec/index.bs +++ b/spec/index.bs @@ -442,6 +442,7 @@ This specification introduces a new type of {{Credential}}, called an {{Identity static Promise<undefined> disconnect(IdentityCredentialDisconnectOptions options); readonly attribute USVString? token; readonly attribute boolean isAutoSelected; + readonly attribute USVString configURL; }; @@ -455,6 +456,9 @@ This specification introduces a new type of {{Credential}}, called an {{Identity :: {{IdentityCredential/isAutoSelected}}'s attribute getter returns the value it is set to. It represents whether the user's identity credential was automatically selected when going through the UI flow which resulted in this {{IdentityCredential}}. + : {{IdentityCredential/configURL}} + :: The {{IdentityCredential/configURL}}'s attribute getter returns the value it is set to. + It represents the config URL corresponding to the [=IDP=] which issued this credential. : {{Credential/[[type]]}} :: The {{IdentityCredential}}'s {{Credential/[[type]]}}'s value is "identity". : {{Credential/[[discovery]]}} @@ -729,20 +733,22 @@ algorithm is invoked, the user agent MUST execute the following steps. This retu {{IdentityCredential}} (or throws an error to the caller). 1. Assert: These steps are running [=in parallel=]. - 1. If the [=list/size=] of + 1. If |options|["{{CredentialRequestOptions/identity}}"]["{{IdentityCredentialRequestOptions/providers}}"] - is not equal to 1, [=queue a global task=] on the [=DOM manipulation task source=] given - |globalObject| to throw a new "{{NetworkError}}" {{DOMException}} + is [=list/empty=], [=queue a global task=] on the [=DOM manipulation task source=] given + |globalObject| to throw a new "{{NetworkError}}" {{DOMException}}. Note: The |globalObject| is not currently passed onto the {{Credential/[[DiscoverFromExternalSource]](origin, options, sameOriginWithAncestors)}} algorithm. See issue. + 1. Let |providerList| be |options|["{{CredentialRequestOptions/identity}}"]["{{IdentityCredentialRequestOptions/providers}}"]. + 1. Let |credential| be the result of running [=create an IdentityCredential=] with |providerList|, + |options|, and |globalObject|. + Note: The |globalObject| is not currently passed onto the + {{Credential/[[DiscoverFromExternalSource]](origin, options, sameOriginWithAncestors)}} + algorithm. See issue. - Issue: Support choosing accounts from multiple [=IDP=]s, as described [here](https://github.com/fedidcg/FedCM/issues/319). - 1. Let |provider| be |options|["{{CredentialRequestOptions/identity}}"]["{{IdentityCredentialRequestOptions/providers}}"][0]. - 1. Let |credential| be the result of running [=create an IdentityCredential=] with |provider|, - |options|, and |globalObject|. 1. If |credential| is a pair: 1. Let |throwImmediately| be the value of the second element of the pair. 1. The user agent SHOULD wait a random amount of time @@ -773,23 +779,24 @@ The create an IdentityCredential algorithm invokes the various FedCM fetc agent UI, and creates the {{IdentityCredential}} that is then returned to the [=RP=].
-To create an IdentityCredential given an {{IdentityProviderRequestOptions}} -|provider|, a {{CredentialRequestOptions}} |options|, and a +To create an IdentityCredential given a [=sequence=] of {{IdentityProviderRequestOptions}} +|providerList|, a {{CredentialRequestOptions}} |options|, and a |globalObject|, run the following steps. This returns an {{IdentityCredential}} or a pair (failure, bool), where the bool indicates whether to skip delaying the exception thrown. 1. Assert: These steps are running [=in parallel=]. 1. Let |mode| be |options|'s {{IdentityCredentialRequestOptions/mode}}. 1. If |mode| is {{IdentityCredentialRequestOptionsMode/active}}: + 1. If |providerList|'s [=list/size=] is not equal to 1, return (failure, true). 1. Assert: |globalObject| is a {{Window}}. 1. If |globalObject| does not have [=transient activation=], return (failure, true). 1. Otherwise, if there is a pending request where {{IdentityCredentialRequestOptionsMode}} is {{IdentityCredentialRequestOptionsMode/passive}} on |globalObject|'s [=Window/navigable=]'s [=navigable/top-level traversable=] or on any of its descendants, reject the pending request with a "{{NetworkError}}" {{DOMException}}. - 1. The user agent MAY wait an arbitrary amount of time before continuing with the - next steps. The user agent MAY also return (failure, false) at this point or - after some arbitrary wait. + 1. The user agent MAY wait an arbitrary amount of time before continuing with the next steps. + The user agent MAY also return (failure, false) at this point or after some arbitrary wait. + The user agent MAY also set |providerList| to a subset of itself after some arbitrary wait.
NOTE: For example, the user agent could do any of the following: @@ -800,51 +807,114 @@ the exception thrown. * Use previous user behavior to determine not to show any UI to the user and silently reject the request.
- 1. Let |loginStatus| be the result of [=get the login status=] with - the [=/origin=] of |provider|'s {{IdentityProviderConfig/configURL}}. - 1. If |loginStatus| is [=unknown=], a user agent MAY set it to [=logged-out=]. - 1. If |loginStatus| is [=logged-out=]: - 1. If |mode| is {{IdentityCredentialRequestOptionsMode/active}}: - 1. Let |result| be the result of running - [=fetch the config file and show an IDP login dialog=] with - |provider| and |globalObject|. - 1. If |result| is failure, return (failure, true). - 1. Otherwise, the user agent MUST do one of the following: + 1. Let |mediation| be |options|'s {{CredentialRequestOptions/mediation}}. + 1. Let |providerMap| be a new [=map=]. + 1. For each |provider| in |providerList|: + Note: The fetches performed in these steps to populate |providerMap| can be performed at the + same time, e.g., the second [=IDP=]'s fetches can be started before the first ones finish, + since these are indepedent. + 1. Let |providerOrigin| be the [=/origin=] of |provider|'s {{IdentityProviderConfig/configURL}}. + 1. If |providerMap| [=map/contains=] |providerOrigin|, return (failure, true). + 1. Let |loginStatus| be the result of [=get the login status=] with |providerOrigin|. + 1. If |loginStatus| is [=unknown=], a user agent MAY set it to [=logged-out=]. + 1. If |loginStatus| is [=logged-out=]: + 1. If |mode| is {{IdentityCredentialRequestOptionsMode/active}}: + 1. Let |result| be the result of running + [=fetch the config file and show an IDP login dialog=] with + |provider| and |globalObject|. + 1. If |result| is failure, return (failure, true). + 1. Otherwise, set |providerMap|[|providerOrigin|] to "logged-out" and [=continue=]. + 1. Let |requiresUserMediation| be |providerOrigin|'s [=requires user mediation=]. + 1. If |requiresUserMediation| is true and |mediation| is + "{{CredentialMediationRequirement/silent}}", [=continue=]. + 1. Let |config| be the result of running [=fetch the config file=] with + |provider| and |globalObject|. + 1. If |config| is failure, [=continue=]. + 1. Fetch accounts step: Let |accountsList| be the result of + [=fetch the accounts=] with |config|, |provider|, and |globalObject|. + 1. If |accountsList| is failure, or the size of |accountsList| is 0: + 1. [=Set the login status=] of |providerOrigin| to [=logged-out=]. + A user agent may decide to skip this step if no credentials were + sent to server. + + Note: For example, if the fetch failed due to a DNS error, no + credentials were sent and therefore the [=IDP=] did not learn + the user's identity. In this situation, we do not know whether + the user is signed in or not and so we may not want to reset + the status. + 1. If |loginStatus| is [=logged-in=], set |providerMap|[|providerOrigin|] to "mismatch" + and [=continue=]. + 1. Assert: |accountsList| is not failure and the size of |accountsList| is not 0. + 1. [=Set the login status=] for |providerOrigin| to [=logged-in=]. + 1. For each |acc| in |accountsList|: + 1. If |acc|["{{IdentityProviderAccount/picture}}"] is present, [=fetch the account picture=] + with |acc| and |globalObject|. If the [=user agent=] displays this picture to + the user at any point, it MUST reuse the result of this fetch instead of redownloading + the picture. + + Note: We require downloading the pictures here before we potentially filter the account + list so that the identity provider cannot determine what hints were provided + based on which fetches occurred. + 1. If |provider|'s {{IdentityProviderRequestOptions/loginHint}} is not empty: + 1. For every |account| in |accountList|, remove |account| from |accountList| if |account|'s + {{IdentityProviderAccount/login_hints}} does not [=list/contain=] |provider|'s + {{IdentityProviderRequestOptions/loginHint}}. + 1. If |accountList| is now empty, set |providerMap|[|providerOrigin|] to "mismatch" and + continue. + 1. If |provider|'s {{IdentityProviderRequestOptions/domainHint}} is not empty: + 1. For every |account| in |accountList|: + 1. If {{IdentityProviderRequestOptions/domainHint}} is "any": + 1. If |account|'s {{IdentityProviderAccount/domain_hints}} is empty, remove + |account| from |accountList|. + 1. Otherwise, remove |account| from |accountList| if |account|'s + {{IdentityProviderAccount/domain_hints}} does not [=list/contain=] |provider|'s + {{IdentityProviderRequestOptions/domainHint}}. + 1. If |accountList| is now empty, set |providerMap|[|providerOrigin|] to "mismatch" and + continue. + 1. If |config|.{{IdentityProviderAPIConfig/account_label}} is present: + 1. For every |account| in |accountList|, remove |account| from |accountList| if |account|'s + {{IdentityProviderAccount/label_hints}} does not [=list/contain=] + |config|.{{IdentityProviderAPIConfig/account_label}}. + 1. If |accountList| is now empty, set |providerMap|[|providerOrigin|] to "mismatch" and + continue. + 1. Set |providerMap|[|providerOrigin|] to |accountsList|. + 1. If |providerMap| [=map/is empty=], return (failure, false). + 1. Let |registeredAccount| and |numRegisteredAccounts| be null and 0, respectively. + 1. Let |selectedAccount| be null. + 1. For each (|providerOrigin|, |value|) in |providerMap|: + 1. If |value| is not a [=list=] |accountsList|, [=continue=]. + 1. For each |acc| in |accountsList|: + 1. If |acc| is [=eligible for auto reauthentication=] given the relevant |provider|, and + |globalObject|, set |registeredAccount| to |acc|, increase |numRegisteredAccounts| by 1, + and set |requiresUserMediation| be |providerOrigin|'s [=requires user mediation=]. + 1. If |mediation| is not "{{CredentialMediationRequirement/required}}", |requiresUserMediation| + is false, |numRegisteredAccounts| is equal to 1, and |providerMap|'s [=map/values=] do not + [=map/contain=] "mismatch": + 1. Set |selectedAccount| to |registeredAccount| and |permission| to true. When doing this, the user + agent MAY show some UI to the user indicating that they are being + auto-reauthenticated. + 1. Set |isAutoSelected| to true. + 1. Otherwise, if |mediation| is "{{CredentialMediationRequirement/silent}}" and |providerMap|'s + [=map/values=] do not [=map/contain=] "mismatch", return (failure, true). + 1. Let |permission|, |permissionRequested|, and |isAutoSelected| be set to false. + 1. Let |allAccounts| be an empty [=list=]. + 1. Build UI by adding the following for each (|providerOrigin|, |value|) in |providerMap|: + 1. If |value| is "logged-out", the user agent adds one of the following: + * Nothing: no UI is shown regarding this [=IDP=]. + * Prompt to continue with this [=IDP=]. If the user continues, the user + agent SHOULD set the login status to [=unknown=]. This MAY include an * Return (failure, false). * Prompt the user whether to continue. If the user continues, the user agent SHOULD set |loginStatus| to [=unknown=]. This MAY include an affordance to [=show an IDP login dialog=]. - * If the user cancels this dialog, return (failure, true). * If the user triggers this affordance: 1. Let |result| be the result of running [=fetch the config file and show an IDP login dialog=] - with |provider| and |globalObject|. + with the relevant |provider| and |globalObject|. 1. If |result| is failure, return (failure, true). - 1. Let |requiresUserMediation| be |provider|'s {{IdentityProviderConfig/configURL}}'s [=/origin=]'s - [=requires user mediation=]. - 1. Let |mediation| be |options|'s {{CredentialRequestOptions/mediation}}. - 1. If |requiresUserMediation| is true and |mediation| is - "{{CredentialMediationRequirement/silent}}", return (failure, true). - 1. Let |config| be the result of running [=fetch the config file=] with - |provider| and |globalObject|. - 1. If |config| is failure, return (failure, false). - 1. Fetch accounts step: Let |accountsList| be the result of - [=fetch the accounts=] with |config|, |provider|, and |globalObject|. - 1. If |accountsList| is failure, or the size of |accountsList| is 0: - 1. [=Set the login status=] for the [=/origin=] of the - {{IdentityProviderConfig/configURL}} to [=logged-out=]. - A user agent may decide to skip this step if no credentials were - sent to server. - - Note: For example, if the fetch failed due to a DNS error, no - credentials were sent and therefore the [=IDP=] did not learn - the user's identity. In this situation, we do not know whether - the user is signed in or not and so we may not want to reset - the status. - 1. Mismatch dialog step: If |loginStatus| is [=logged-in=], show a - dialog to the user. The contents of this dialog are defined by the user - agent. This dialog SHOULD provide an affordance for the user to trigger - the [=show an IDP login dialog=] algorithm with |config| and |provider|; + 1. Otherwise, if |value| is "mismatch", add contents indicating this |providerOrigin| + to the user. The contents SHOULD provide an affordance for the user to trigger + the [=show an IDP login dialog=] algorithm with the relevant |config| and |provider|; this dialog is the confirm IDP login dialog. Note: This situation happens when the browser expects the user @@ -855,94 +925,55 @@ the exception thrown. impossible by always showing UI of some kind when credentials were sent to the server. - 1. Wait until one of the following occurs: - - * If the user closes the dialog, return (failure, true). - - * If the [=show an IDP login dialog=] algorithm was triggered: - - 1. Let |result| be the result of that algorithm. - 1. If |result| is failure, return (failure, true). The user - agent MAY show a dialog to the user before or after - returning failure indicating this failure. - 1. Otherwise, go back to the [=fetch accounts step=]. - - 1. Assert: |accountsList| is not failure and the size of |accountsList| is not 0. - 1. [=Set the login status=] for the [=/origin=] of the - {{IdentityProviderConfig/configURL}} to [=logged-in=]. - 1. For each |acc| in |accountsList|: - 1. If |acc|["{{IdentityProviderAccount/picture}}"] is present, [=fetch the account picture=] - with |acc| and |globalObject|. If the [=user agent=] displays this picture to - the user at any point, it MUST reuse the result of this fetch instead of redownloading - the picture. - - Note: We require downloading the pictures here before we potentially filter the account - list so that the identity provider cannot determine what hints were provided - based on which fetches occurred. - 1. If |provider|'s {{IdentityProviderRequestOptions/loginHint}} is not empty: - 1. For every |account| in |accountList|, remove |account| from |accountList| if |account|'s - {{IdentityProviderAccount/login_hints}} does not [=list/contain=] |provider|'s - {{IdentityProviderRequestOptions/loginHint}}. - 1. If |accountList| is now empty, go to the [=mismatch dialog step=]. - 1. If |provider|'s {{IdentityProviderRequestOptions/domainHint}} is not empty: - 1. For every |account| in |accountList|: - 1. If {{IdentityProviderRequestOptions/domainHint}} is "any": - 1. If |account|'s {{IdentityProviderAccount/domain_hints}} is empty, remove - |account| from |accountList|. - 1. Otherwise, remove |account| from |accountList| if |account|'s - {{IdentityProviderAccount/domain_hints}} does not [=list/contain=] |provider|'s - {{IdentityProviderRequestOptions/domainHint}}. - 1. If |accountList| is now empty, go to the [=mismatch dialog step=]. - 1. If |config|.{{IdentityProviderAPIConfig/account_label}} is present: - 1. For every |account| in |accountList|, remove |account| from |accountList| if |account|'s - {{IdentityProviderAccount/label_hints}} does not [=list/contain=] - |config|.{{IdentityProviderAPIConfig/account_label}}. - 1. If |accountList| is now empty, go to the [=mismatch dialog step=]. - 1. Let |registeredAccount|, |numRegisteredAccounts| be null and 0, respectively. - 1. Let |account| be null. - 1. For each |acc| in |accountsList|: - 1. If |acc| is [=eligible for auto reauthentication=] given |provider|, and |globalObject|, - set |registeredAccount| to |acc| and increase |numRegisteredAccounts| by 1. - 1. Let |permission|, |permissionRequested|, and |isAutoSelected| be set to false. - 1. If |mediation| is not "{{CredentialMediationRequirement/required}}", |requiresUserMediation| - is false, and |numRegisteredAccounts| is equal to 1: - 1. Set |account| to |registeredAccount| and |permission| to true. When doing this, the user - agent MAY show some UI to the user indicating that they are being - auto-reauthenticated. - 1. Set |isAutoSelected| to true. - 1. Otherwise, if |mediation| is "{{CredentialMediationRequirement/silent}}", return (failure, true). - 1. Otherwise, if |accountsList|'s size is 1: - 1. Set |account| to |accountsList|[0]. - 1. If [=compute the connection status=] of |account|, |provider|, and |globalObject| returns - [=compute the connection status/connected=], show a dialog to request user permission to sign - in via |account|, and set the result in |permission|. The user agent MAY use |options|'s - {{IdentityCredentialRequestOptions/context}} and |options|'s - {{IdentityCredentialRequestOptions/mode}} to customize the dialog. - 1. Otherwise, let |permission| be the result of running [=request permission to sign-up=] - algorithm with |account|, |config|, |provider|, and |globalObject|. Also set - |permissionRequested| to true if the user agent [=supports showing a permission prompt=]. - 1. Otherwise: - 1. Set |account| to the result of running the [=select an account=] from the - |accountsList|. - 1. If |account| is failure, return (failure, true). - 1. If [=compute the connection status=] of |account|, |provider| and |globalObject| is - [=compute the connection status/connected=], set |permission| to true. - 1. Otherwise, if |provider|.{{IdentityProviderRequestOptions/fields}} is [=list/empty=], - [=create a connection between the RP and the IdP account=] with |provider|, |account|, - and |globalObject|, and set |permission| to true. - - Note: The connection would normally be created in the [=request permission to sign-up=] - algorithm, but we do not want to show an extra dialog in this case. + * If the [=show an IDP login dialog=] algorithm was triggered: + + 1. Let |result| be the result of that algorithm. + 1. If |result| is failure, return (failure, true). The user + agent MAY show a dialog to the user before or after + returning failure indicating this failure. + 1. Otherwise, go back to the [=fetch accounts step=] to get an updated + value of |providerMap| for this [=IDP=]. + 1. Otherwise, |value| is a [=list=] of accounts. [=list/Extend=] |allAccounts| with |value|. + 1. Also include a UI affordance to close the dialog. If the user closes this dialog, return (failure, + true). + 1. If |allAccounts| is not [=list/empty=], also add UI to present the account options to the user as follows: + 1. If |allAccounts|'s size is 1 and |providerMap|'s [=map/values=] do not [=map/contain=] + "mismatch": + 1. Set |selectedAccount| to |allAccounts|[0]. + 1. If [=compute the connection status=] of |selectedAccount|, the relevant |provider|, + and |globalObject| returns [=compute the connection status/connected=], show a + dialog to request user permission to sign in via |selectedAccount|, and set the + result in |permission|. The user agent MAY use |options|'s + {{IdentityCredentialRequestOptions/context}} and |options|'s + {{IdentityCredentialRequestOptions/mode}} to customize the dialog. + 1. Otherwise, set |permission| to the result of running [=request permission to sign-up=] + algorithm with |selectedAccount|, the relevant |config|, the relevant |provider|, + and |globalObject|. Also set |permissionRequested| to true if the user agent + [=supports showing a permission prompt=]. 1. Otherwise: - 1. Let |permission| be the result of running the [=request permission to sign-up=] - algorithm with |account|, |config|, |provider|, and |globalObject|. - 1. Set |permissionRequested| to true. - 1. Wait until the [=user agent=]'s dialogs requesting for user choice or permission to be - closed, if any are created in the previous steps. - 1. Assert: |account| is not null. + 1. Show UI to allow the user to select an account chooser displaying the options from + |accountsList|. + 1. If the user selects an account, perform the following steps: + 1. Set |selectedAccount| to the chosen {{IdentityProviderAccount}}. + 1. If [=compute the connection status=] of |selectedAccount|, the relevant |provider|, + and |globalObject| is [=compute the connection status/connected=], set |permission| + to true. + 1. Otherwise, if |provider|.{{IdentityProviderRequestOptions/fields}} is [=list/empty=], + [=create a connection between the RP and the IdP account=] with |provider|, |account|, + and |globalObject|, and set |permission| to true. + Note: The connection would normally be created in the [=request permission to sign-up=] + algorithm, but we do not want to show an extra dialog in this case. + 1. Otherwise: + 1. Set |permission| to the result of running the [=request permission to sign-up=] + algorithm with |selectedAccount|, the relevant |config|, the relevant |provider|, + and |globalObject|. + 1. Set |permissionRequested| to true. + 1. If the [=user agent=] created any dialogs requesting user choice or permission in the previous + steps, wait until they are closed. 1. If |permission| is false, then return (failure, true). + 1. Assert: |selectedAccount| is not null. 1. Let |credential| be the result of running the [=fetch an identity assertion=] algorithm with - |account|'s {{IdentityProviderAccount/id}}, |permissionRequested|, |isAutoSelected|, + |selectedAccount|'s {{IdentityProviderAccount/id}}, |permissionRequested|, |isAutoSelected|, |provider|, |config|, and |globalObject|. 1. Return |credential|.
@@ -1354,6 +1385,8 @@ To fetch an identity assertion given a {{USVString}} 1. Set |credential|'s {{IdentityCredential/token}} to |tokenString|. 1. Set |credential|'s {{IdentityCredential/isAutoSelected}} to |isAutoSelected|. + 1. Set |credential|'s {{IdentityCredential/configURL}} to |provider|'s + {{IdentityProviderConfig/configURL}}. 1. Wait for |credential| to be set. 1. Return |credential|. @@ -1369,16 +1402,6 @@ dictionary IdentityAssertionResponse { ### Request permission to sign-up ### {#request-permission-signup} -
-To select an account given an |accountsList|, run the following steps. This returns an -{{IdentityProviderAccount}} or failure. - 1. Assert |accountsList|'s [=list/size=] is greater than 1. - 1. Display an account chooser displaying the options from |accountsList|. - 1. Let |account| be the {{IdentityProviderAccount}} of the account that the user - manually selects from the accounts chooser, or failure if no account is selected. - 1. Return |account|. -
- The request permission to sign-up algorithm fetches the [=client metadata endpoint=] of the [=RP=], waits for the user to grant permission to use the given account, and returns whether the user granted permission or not. @@ -2577,7 +2600,7 @@ The [=remote end steps=] are: This section provides a few of the security considerations for the FedCM API. Note that there is a -separate section for [[#privacy]] considerations. +separate section for [[#privacy]]. In FedCM, there are various assets that need to be protected. All of the endpoints need basic protections, and the credentialed ones need protections from various kinds of attacks and threats.