From 9ef002738ba044bba7a5961e654c9391af2924dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Pe=C3=B1a=20Moreno?= Date: Wed, 15 Nov 2023 15:25:21 -0500 Subject: [PATCH 1/8] b --- spec/index.bs | 42 +++++++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/spec/index.bs b/spec/index.bs index 8ba92d0cf..4c8ed0d76 100644 --- a/spec/index.bs +++ b/spec/index.bs @@ -495,8 +495,13 @@ To create a connection between the RP and the IdP account given an This specification introduces a new type of {{Credential}}, called an {{IdentityCredential}}:
+  dictionary IdentityCredentialRevokeOptions : IdentityProviderConfig {
+    USVString accountHint;
+  };
+
   [Exposed=Window, SecureContext]
   interface IdentityCredential : Credential {
+    static Promise<undefined> revoke(optional IdentityCredentialRevokeOptions options = {});
     readonly attribute USVString? token;
   };
 
@@ -560,7 +565,7 @@ enum IdentityCredentialRequestOptionsContext { }; dictionary IdentityCredentialRequestOptions { - required sequence providers; + required sequence providers; IdentityCredentialRequestOptionsContext context = "signin"; }; @@ -572,6 +577,9 @@ the [=RP=] supports (e.g. that it has a pre-registration agreement with). dictionary IdentityProviderConfig { required USVString configURL; required USVString clientId; +}; + +dictionary IdentityProviderRequestOptions : IdentityProviderConfig { USVString nonce; DOMString loginHint; }; @@ -582,11 +590,11 @@ dictionary IdentityProviderConfig { :: The URL of the configuration file for the identity provider. : {{IdentityProviderConfig/clientId}} :: The {{id_assertion_endpoint_request/client_id}} provided to the [=RP=] out of band by the [=IDP=] - : {{IdentityProviderConfig/nonce}} + : {{IdentityProviderRequestOptions/nonce}} :: A random number of the choice of the [=RP=]. It is generally used to associate a client session with a {{IdentityProviderToken/token}} and to mitigate replay attacks. Therefore, this value should have sufficient entropy such that it would be hard to guess. - : {{IdentityProviderConfig/loginHint}} + : {{IdentityProviderRequestOptions/loginHint}} :: A string representing the login hint corresponding to an account which the RP wants the user agent to show to the user. If provided, the user agent will not show accounts which do not match this login hint value. It generally matches some attribute from the desired @@ -684,7 +692,7 @@ 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 {{IdentityProviderConfig}} +To create an IdentityCredential given an {{IdentityProviderRequestOptions}} |provider|, 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 @@ -761,10 +769,10 @@ the exception thrown. 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. If |provider|'s {{IdentityProviderConfig/loginHint}} is not empty: + 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 - {{IdentityProviderConfig/loginHint}}. + {{IdentityProviderRequestOptions/loginHint}}. 1. If |accountList| is now empty, go to the [=mismatch dialog step=]. 1. For each |acc| in |accountsList|: 1. If |acc|["{{IdentityProviderAccount/picture}}"] is present, [=fetch the account picture=] @@ -828,7 +836,7 @@ The fetch the config file algorithm fetches both the [=well-known file=] the [=IDP=], checks that the config file is mentioned in the [=well-known file=], and returns the config.
-To fetch the config file given an {{IdentityProviderConfig}} |provider| and +To fetch the config file given an {{IdentityProviderRequestOptions}} |provider| and |globalObject|, run the following steps. This returns an {{IdentityProviderAPIConfig}} or failure. 1. Let |configUrl| be the result of running [=parse url=] with |provider|'s @@ -975,8 +983,8 @@ FedCM UI to the user.
To fetch the accounts list given an {{IdentityProviderAPIConfig}} |config|, an -{{IdentityProviderConfig}} |provider|, and |globalObject|, run the following steps. This returns an -{{IdentityProviderAccountList}}. +{{IdentityProviderRequestOptions}} |provider|, and |globalObject|, run the following steps. This +returns an {{IdentityProviderAccountList}}. 1. Let |accountsUrl| be the result of [=computing the manifest URL=] given |provider|, |config|["{{IdentityProviderAPIConfig/accounts_endpoint}}"], and |globalObject|. 1. If |accountsUrl| is failure, return an empty list. @@ -1094,15 +1102,15 @@ the token that will be provided to the [=RP=].
To fetch an identity assertion given a {{USVString}} - |accountId|, a boolean |disclosureTextShown|, an {{IdentityProviderConfig}} |provider|, an - {{IdentityProviderAPIConfig}} |config|, and |globalObject|, run the following steps. This + |accountId|, a boolean |disclosureTextShown|, an {{IdentityProviderRequestOptions}} |provider|, + an {{IdentityProviderAPIConfig}} |config|, and |globalObject|, run the following steps. This returns an {{IdentityCredential}} or failure. 1. Let |tokenUrl| be the result of [=computing the manifest URL=] given |provider|, |config|["{{IdentityProviderAPIConfig/id_assertion_endpoint}}"], and |globalObject|. 1. If |tokenUrl| is failure, return failure. 1. Let |requestBody| be the result of running [=urlencoded serializer=] with a list containing: 1. ("client_id", |provider|'s {{IdentityProviderConfig/clientId}}) - 1. ("nonce", |provider|'s {{IdentityProviderConfig/nonce}}) + 1. ("nonce", |provider|'s {{IdentityProviderRequestOptions/nonce}}) 1. ("account_id", |accountId|) 1. ("disclosure_text_shown", |disclosureTextShown|) 1. Let |request| be a new request as follows: @@ -1178,7 +1186,7 @@ granted permission or not.
To request permission to sign-up the user with a given an {{IdentityProviderAccount}} |account|, -an {{IdentityProviderAPIConfig}} |config|, an {{IdentityProviderConfig}} |provider|, and a +an {{IdentityProviderAPIConfig}} |config|, an {{IdentityProviderRequestOptions}} |provider|, and a |globalObject|, run the following steps. This returns a boolean. 1. Assert: These steps are running [=in parallel=]. 1. Let |metadata| be the result of running [=fetch the client metadata=] with |config|, @@ -1203,7 +1211,7 @@ an {{IdentityProviderAPIConfig}} |config|, an {{IdentityProviderConfig}} |provid
To fetch the client metadata given an {{IdentityProviderAPIConfig}} |config| and -an {{IdentityProviderConfig}} |provider|, run the following steps. This returns an +an {{IdentityProviderRequestOptions}} |provider|, run the following steps. This returns an {{IdentityProviderClientMetadata}} or failure. 1. Let |clientMetadataUrl| be the result of [=computing the manifest URL=] given |provider|, |config|["{{IdentityProviderAPIConfig/client_metadata_endpoint}}"], and |globalObject|. @@ -1286,7 +1294,7 @@ To fetch request given a [=/request=] |request|, |globalObject|, and
-When computing the manifest URL given an {{IdentityProviderConfig}} |provider|, a +When computing the manifest URL given an {{IdentityProviderRequestOptions}} |provider|, a [=string=] |manifestString|, and |globalObject|, perform the following steps. This returns a URL or failure. 1. Let |configUrl| be the result of running [=parse url=] with |provider|'s @@ -1681,7 +1689,7 @@ Every {{IdentityProviderAccount}} is expected to have members with the following the Privacy Policy and the Terms of Service. : login_hints :: A list of strings which correspond to all of the login hints which match with this account. - An [=RP=] can use the {{IdentityProviderConfig/loginHint}} to request that only an account + An [=RP=] can use the {{IdentityProviderRequestOptions/loginHint}} to request that only an account matching a given value is shown to the user. @@ -2046,7 +2054,7 @@ The [=remote end steps=] are: 1. For each |account| in |accounts|: 1. Let |accountState| be the result of running the [=compute the connection status=] - algorithm given |account| and the {{IdentityProviderConfig}} of the IDP + algorithm given |account| and the {{IdentityProviderRequestOptions}} of the IDP |account| belongs to 1. [=list/Append=] a [=dictionary=] to |list| with the following properties: 1. `accountId` set to the account's {{IdentityProviderAccount/id}} From 6005a45e2387c1937aa33605ac6e70b93fb71160 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Pe=C3=B1a=20Moreno?= Date: Thu, 16 Nov 2023 18:06:57 -0500 Subject: [PATCH 2/8] b --- spec/index.bs | 216 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 210 insertions(+), 6 deletions(-) diff --git a/spec/index.bs b/spec/index.bs index 4c8ed0d76..9dd351611 100644 --- a/spec/index.bs +++ b/spec/index.bs @@ -452,6 +452,9 @@ the [=/origin=] of the [=RP=], |idp| is the [=/origin=] of the [=IDP=], and |acc an account identifier. It represents the set of triples such that the user has used FedCM to login to the |rp| via the |idp| |account|. +Issue: the [=connected accounts set=] should be double keyed in the [=RP=] (e.g. it should include +both the requester and the embedder, or in other words the iframe and the top-level). + If a user clears browsing data for an |origin| (cookies, localStorage, etc.), the user agent MUST [=list/remove=] all triples with an [=/origin=] matching the |origin| from connected accounts set. @@ -495,13 +498,13 @@ To create a connection between the RP and the IdP account given an This specification introduces a new type of {{Credential}}, called an {{IdentityCredential}}:
-  dictionary IdentityCredentialRevokeOptions : IdentityProviderConfig {
+  dictionary IdentityCredentialDisconnectOptions : IdentityProviderConfig {
     USVString accountHint;
   };
 
   [Exposed=Window, SecureContext]
   interface IdentityCredential : Credential {
-    static Promise<undefined> revoke(optional IdentityCredentialRevokeOptions options = {});
+    static Promise<undefined> disconnect(optional IdentityCredentialDisconnectOptions options = {});
     readonly attribute USVString? token;
   };
 
@@ -522,6 +525,137 @@ This specification introduces a new type of {{Credential}}, called an {{Identity The main entrypoint in this specification is through the entrypoints exposed by the [[CM]] API. + +### The disconnect method ### {#browser-api-identity-credential-disconnect} + + +
+When the static {{IdentityCredential/disconnect}} method is invoked given an +{{IdentityCredentialDisconnectOptions}} |options| perform the following steps: + + 1. Let |globalObject| be the [=current global object=]. + 1. Let |document| be |globalObject|'s [=associated Document=]. + 1. If |document| is not [=allowed to use=] the [=identity-credentials-get=] + [=policy-controlled feature=], throw a "{{NotAllowedError}}" {{DOMException}}. + 1. If |options| does not [=map/contain=] all of the following, then throw a new {{TypeError}}: + * {{IdentityProviderConfig/configURL}} + * {{IdentityProviderConfig/clientId}} + * {{IdentityCredentialDisconnectOptions/accountHint}} + 1. Let |promise| be a new {{Promise}}. + 1. [=In parallel=], [=attempt to disconnect=] given |options|, |promise|, and |globalObject|. + 1. Return |promise|. +
+ +
+When asked to attempt to disconnect given an {{IdentityCredentialDisconnectOptions}} +|options|, a {{Promise}} |promise|, and a |globalObject|, perform the following steps: + + 1. Assert: these steps are running [=in parallel=]. + 1. Let |configUrl| be the result of running [=parse url=] with |options|'s + {{IdentityProviderConfig/configURL}} and |globalObject|. + 1. If |configUrl| is failure, [=reject=] |promise| with an "{{InvalidStateError}}" + {{DOMException}}. + 1. Run a [[!CSP]] check with a [[CSP#directive-connect-src|connect-src]] directive on the URL + passed as |configUrl|. If it fails, [=reject=] |promise| with a "{{NetworkError}}" + {{DOMException}}. + 1. If there is another pending {{IdentityCredential/disconnect}} call for this |globalObject| + (e.g. it has not yet thrown an exception or its associated {{Promise}} has not yet been + resolved), [=reject=] |promise| with a "{{NetworkError}}" {{DOMException}}. + 1. If |configUrl| is not a [=potentially trustworthy origin=], [=reject=] |promise| with a + "{{NetworkError}}" {{DOMException}}. + 1. If the user has disabled the FedCM API on the |globalObject|, [=reject=] |promise| with a + "{{NetworkError}}" {{DOMException}}. + 1. If there does not exist an account |account| such that [=compute the connection status=] of + |options|, |account|, and |globalObject| returns + [=compute the connection status/connected=], then [=reject=] |promise| with a + "{{NetworkError}}" {{DOMException}}. This check can be performed by iterating over the + [=connected accounts set=] or by keeping a separate data structure to make this lookup fast. + 1. Let |config| be the result of running [=fetch the config file=] with + |provider| and |globalObject|. + 1. If |config| is failure, [=reject=] |promise| with a "{{NetworkError}}" {{DOMException}}. + 1. Let |disconnectUrl| be the result of [=computing the manifest URL=] given |provider|, + |config|["{{IdentityProviderAPIConfig/disconnect_endpoint}}"], and |globalObject|. + 1. If |disconnectUrl| is failure, [=reject=] |promise| with a "{{NetworkError}}" + {{DOMException}}. + 1. [=Send a disconnect request=] with |disconnectUrl|, |options|, and |globalObject| and let + |accountId| be the result. + 1. Let |idpOrigin| be the [=url/origin=] corresponding to |configUrl|. + 1. Let |rpOrigin| be |globalObject|'s [=associated Document=]'s [=Document/origin=]. + 1. If |accountId| is not failure: + 1. Let |triple| be (|rpOrigin|, |idpOrigin|, |accountId|). + 1. If [=connected accounts set=] [=list/contains=] |triple|: + 1. [=list/Remove=] |triple| from the [=connected accounts set=]. + 1. [=Resolve=] |promise|. + 1. Return. + 1. For every (|rp|, |idp|, |accountId|) |triple| in the [=connected accounts set=]: + 1. If |rp| equals |rpOrigin| and |idp| equals |idpOrigin|, [=list/remove=] |triple| from the + [=connected accounts set=]. + 1. [=Reject=] |promise| with a "{{NetworkError}}" {{DOMException}}. +
+ + +#### Disconnect request #### {#disconnect-request} + + +The send a disconnect request algorithm sends a request to disconnect an account that has +previously been used for federated login in the [=RP=]. + +
+When asked to send a disconnect request given a URL +|disconnectUrl|, and {{IdentityCredentialDisconnectOptions}} |options|, and a |globalObject|, +perform the following steps. This returns an {{USVString}} or failure. + + 1. Let |requestBody| be the result of running [=urlencoded serializer=] with a list containing: + 1. ("client_id", |options|'s {{IdentityProviderConfig/clientId}}) + 1. ("account_hint", |options|'s {{IdentityCredentialDisconnectOptions/accountHint}}) + 1. Let |request| be a new request as follows: + + : [=request/url=] + :: |disconnectUrl| + : [=request/method=] + :: "POST" + : [=request/body=] + :: the [=UTF-8 encode=] of |requestBody| + : [=request/redirect mode=] + :: "error" + : [=request/client=] + :: null + : [=request/window=] + :: "no-window" + : [=request/service-workers mode=] + :: "none" + : [=request/destination=] + :: "webidentity" + : [=request/origin=] + :: |globalObject|'s [=associated document=]'s [=Document/origin=] + : [=request/header list=] + :: a [=list=] containing a single [=header=] with [=header/name=] set to `Accept` and + [=header/value=] set to `application/x-www-form-urlencoded` + : [=request/credentials mode=] + :: "include" + : [=request/mode=] + :: "cors" + + 1. Let |accountId| be null. + 1. [=Fetch request=] with |request| and |globalObject|, and with + processResponseConsumeBody set to the following steps given a + response |response| and |responseBody|: + 1. Let |json| be the result of [=extract the JSON fetch response=] from |response| and + |responseBody|. + 1. [=converted to an IDL value|Convert=] |json| to a {{DisconnectedAccount}}, |account|. + 1. If one of the previous two steps threw an exception, set |accountId| to failure + and return. + 1. Set |accountId| to |account|'s {{DisconnectedAccount/account_id}}. + 1. Wait for |accountId| to be set. + 1. Return |accountId|. +
+ + +dictionary DisconnectedAccount { + required USVString account_id; +}; + + ### The CredentialRequestOptions ### {#browser-api-credential-request-options} @@ -532,7 +666,7 @@ This section defines the dictionaries passed into the JavaScript call: ```js const credential = await navigator.credentials.get({ identity: { // IdentityCredentialRequestOptions - providers: [{ // sequence + providers: [{ // sequence configURL: "https://idp.example/manifest.json", // IdentityProviderConfig.configURL clientId: "123", // IdentityProviderConfig.clientId nonce: "nonce" // IdentityProviderConfig.nonce @@ -932,7 +1066,7 @@ or failure. 1. [=converted to an IDL value|Convert=] |json| to an {{IdentityProviderAPIConfig}} stored in |config|. 1. If one of the previous two steps threw an exception, set |config| to failure. - 1. Set |config|.{{IdentityProviderAPIConfig/login_url}} to the result of [=computing + 1. Set |config|.{{IdentityProviderAPIConfig/login_url}} to the result of [=computing the manifest URL=] with |provider|, |config| and |globalObject|. 1. If |config|.{{IdentityProviderAPIConfig/login_url}} is null, return failure. 1. Wait for both |config| and |configInWellKnown| to be set. @@ -969,6 +1103,7 @@ dictionary IdentityProviderAPIConfig { required USVString client_metadata_endpoint; required USVString id_assertion_endpoint; required USVString login_url; + USVString disconnect_endpoint; IdentityProviderBranding branding; }; @@ -1449,8 +1584,8 @@ When invoking the {{IdentityProvider/getUserInfo()}} method given an {{IdentityP [=list/contain=] |provider|'s {{IdentityProviderConfig/clientId}}, continue. Note: this allows the [=IDP=] to override whether an account is a returning account. - This could be useful for instance in cases where the user has revoked the account - out of band. + This could be useful for instance in cases where the user has disconnected the + account out of band. 1. [=Compute the connection status=] of |provider|, |account|, and |globalObject|. If the result is [=compute the connection status/connected=], set |hasReturningAccount| to @@ -1485,6 +1620,7 @@ The [=IDP=] exposes a series of HTTP endpoints: 1. An [[#idp-api-accounts-endpoint]] endpoint 1. A [[#idp-api-client-id-metadata-endpoint]] endpoint 1. An [[#idp-api-id-assertion-endpoint]] endpoint +1. An [[#idp-api-disconnect-endpoint]] endpoint if it supports {{IdentityCredential/disconnect}}. The FedCM API introduces the ability for a site to ask the browser to execute a few different network requests. It is important for the browser @@ -1498,6 +1634,7 @@ network requests performed: accounts_endpointyesnono client_metadata_endpointnoyesyes id_assertion_endpointyesyesyes + disconnect_endpointyesyesyes @@ -1572,6 +1709,9 @@ The {{IdentityProviderAPIConfig}} object's members have the following semantics: :: A URL that points to an HTTP API that complies with the [[#idp-api-client-id-metadata-endpoint]] API. : id_assertion_endpoint :: A URL that points to an HTTP API that complies with the [[#idp-api-id-assertion-endpoint]] API. + : disconnect_endpoint + :: A URL that points to an HTTP API that complies with the [[#idp-api-disconnect-endpoint]] + API. : branding :: A set of {{IdentityProviderBranding}} options. @@ -1629,6 +1769,7 @@ For example: "accounts_endpoint": "/accounts", "client_metadata_endpoint": "/metadata", "id_assertion_endpoint": "/assertion", + "disconnect_endpoint": "/disconnect", "branding": { "background_color": "green", "color": "#FFEEAA", @@ -1858,6 +1999,61 @@ For example: ```
+ +## Disconnect endpoint ## {#idp-api-disconnect-endpoint} + + +The disconnect endpoint is responsible for disconnecting a previously made federated +login connection between an [=RP=] and an [=IDP=] account, and returning the account's +{{IdentityProviderAccount/id}} so that the [=user agent=] can remove it from the +[=connected accounts set=]. + +The [=disconnect endpoint=] is fetched when invoking the {{IdentityCredential/disconnect}} +method: + +(a) as a **POST** request, +(b) **with** [=IDP=] cookies, +(c) **with** the [=RP=]'s origin in the Origin header, +(d) **with** the Sec-Fetch-Dest header set to `webidentity`, +(e) **without** following [[RFC9110#section-10.2.2|HTTP redirects]], and +(f) in "cors" [=request/mode=]. + +It will also contain the following in the request body `application/x-www-form-urlencoded`: + +
+ : client_id + :: The [=RP=]'s unique identifier from the [=IDP=] + : account_hint + :: An account hint for the [=IDP=] account being disconnected from the [=RP=]. +
+ +For example: + +
+```http +POST /fedcm_disconnect_endpoint HTTP/1.1 +Host: idp.example +Origin: https://rp.example/ +Content-Type: application/x-www-form-urlencoded +Cookie: 0x23223 +Sec-Fetch-Dest: webidentity +client_id=client1234&account_hint=hint12 +``` +
+ +If the disconnection is unsuccessful, the [=IDP=] may respond with an error. If it is successful, +the response body must be a JSON object that can be [=converted to an IDL value|converted=] to an +{{DisconnectedAccount}} without an exception. + +
+ : account_id + :: The {{IdentityProviderAccount/id}} of the account that was successfully disconnected. +
+ +The [=IDP=] must return the {{DisconnectedAccount/account_id}} since it may be different from the +{{disconnect_endpoint_request/account_hint}}, and the ID is the one which allows the +[=user agent=] to disconnect the account from the [=connected accounts set=]. + # Permissions Policy Integration # {#permissions-policy-integration} @@ -2353,6 +2549,14 @@ origin of the fetched URLs. information required to perform a federated signin/signup. It is not possible for the [=RP=] or the [=IDP=] to force the token fetch to happen without user permission, as the user agent cannot be spoofed or otherwise tricked. + +* The [=disconnect endpoint=] may only be fetched after the user has successfully gone through the + FedCM flow at least once in the [=RP=]. It sends a credentialed request to the [=IDP=], so it + is important that the [=user agent=] does not allow unlimited requests of such type, even after + the user has used FedCM once. For this reason, any time that disconnection sends a credentialed + request, at least one account is removed from the [=connected accounts set=], thus ensuring that + this endpoint does not introduce a way for the [=RP=] to send requests to the [=IDP=] containing + the [=IDP=] cookies forever. ## Attack Scenarios ## {#attack-scenarios} From 141dff10d438be5a0369af72cecdbc9c723f5449 Mon Sep 17 00:00:00 2001 From: npm1 Date: Mon, 20 Nov 2023 11:33:12 -0500 Subject: [PATCH 3/8] Apply suggestions from code review Co-authored-by: Ted Thibodeau Jr --- spec/index.bs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/index.bs b/spec/index.bs index 9dd351611..7b91b7570 100644 --- a/spec/index.bs +++ b/spec/index.bs @@ -452,7 +452,7 @@ the [=/origin=] of the [=RP=], |idp| is the [=/origin=] of the [=IDP=], and |acc an account identifier. It represents the set of triples such that the user has used FedCM to login to the |rp| via the |idp| |account|. -Issue: the [=connected accounts set=] should be double keyed in the [=RP=] (e.g. it should include +Issue: the [=connected accounts set=] should be double keyed in the [=RP=] (i.e., it should include both the requester and the embedder, or in other words the iframe and the top-level). If a user clears browsing data for an |origin| (cookies, localStorage, etc.), the user agent MUST @@ -530,8 +530,8 @@ by the [[CM]] API.
-When the static {{IdentityCredential/disconnect}} method is invoked given an -{{IdentityCredentialDisconnectOptions}} |options| perform the following steps: +When the static {{IdentityCredential/disconnect}} method is invoked, given an +{{IdentityCredentialDisconnectOptions}} |options|, perform the following steps: 1. Let |globalObject| be the [=current global object=]. 1. Let |document| be |globalObject|'s [=associated Document=]. @@ -559,7 +559,7 @@ When asked to attempt to disconnect given an {{IdentityCredentialDisc passed as |configUrl|. If it fails, [=reject=] |promise| with a "{{NetworkError}}" {{DOMException}}. 1. If there is another pending {{IdentityCredential/disconnect}} call for this |globalObject| - (e.g. it has not yet thrown an exception or its associated {{Promise}} has not yet been + (e.g., it has not yet thrown an exception or its associated {{Promise}} has not yet been resolved), [=reject=] |promise| with a "{{NetworkError}}" {{DOMException}}. 1. If |configUrl| is not a [=potentially trustworthy origin=], [=reject=] |promise| with a "{{NetworkError}}" {{DOMException}}. @@ -577,7 +577,7 @@ When asked to attempt to disconnect given an {{IdentityCredentialDisc |config|["{{IdentityProviderAPIConfig/disconnect_endpoint}}"], and |globalObject|. 1. If |disconnectUrl| is failure, [=reject=] |promise| with a "{{NetworkError}}" {{DOMException}}. - 1. [=Send a disconnect request=] with |disconnectUrl|, |options|, and |globalObject| and let + 1. [=Send a disconnect request=] with |disconnectUrl|, |options|, and |globalObject|, and let |accountId| be the result. 1. Let |idpOrigin| be the [=url/origin=] corresponding to |configUrl|. 1. Let |rpOrigin| be |globalObject|'s [=associated Document=]'s [=Document/origin=]. @@ -601,7 +601,7 @@ The send a disconnect request algorithm sends a request to disconnect an previously been used for federated login in the [=RP=].
-When asked to send a disconnect request given a URL +When asked to send a disconnect request, given a URL |disconnectUrl|, and {{IdentityCredentialDisconnectOptions}} |options|, and a |globalObject|, perform the following steps. This returns an {{USVString}} or failure. From c63052ba66603f770e2357931e9a0c9339264a02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Pe=C3=B1a=20Moreno?= Date: Mon, 20 Nov 2023 12:36:20 -0500 Subject: [PATCH 4/8] Add sentence about removal of all accounts --- spec/index.bs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/spec/index.bs b/spec/index.bs index 7b91b7570..c017c4187 100644 --- a/spec/index.bs +++ b/spec/index.bs @@ -2051,8 +2051,10 @@ the response body must be a JSON object that can be [=converted to an IDL value| The [=IDP=] must return the {{DisconnectedAccount/account_id}} since it may be different from the -{{disconnect_endpoint_request/account_hint}}, and the ID is the one which allows the -[=user agent=] to disconnect the account from the [=connected accounts set=]. +{{disconnect_endpoint_request/account_hint}}, and the ID is the one which allows the [=user agent=] +to disconnect the account from the [=connected accounts set=]. If the [=IDP=] returns an error or +the [=user agent=] does not find the account with the ID provided by the [=IDP=], then all accounts +associated with the relevant ([=RP=], [=IDP=]) are removed from the [=connected accounts set=]. # Permissions Policy Integration # {#permissions-policy-integration} From 618c3dca5b1abfdf32c32c1b9537bedb4b12cfb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Pe=C3=B1a=20Moreno?= Date: Wed, 29 Nov 2023 15:13:06 -0500 Subject: [PATCH 5/8] Review --- spec/index.bs | 40 +++++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/spec/index.bs b/spec/index.bs index c017c4187..8d565fdac 100644 --- a/spec/index.bs +++ b/spec/index.bs @@ -453,7 +453,9 @@ an account identifier. It represents the set of triples such that the user has u the |rp| via the |idp| |account|. Issue: the [=connected accounts set=] should be double keyed in the [=RP=] (i.e., it should include -both the requester and the embedder, or in other words the iframe and the top-level). +both the requester and the embedder, or in other words the iframe and the top-level). Otherwise the +top-level's state could be used and modified by the embedder, which introduces leaks and unwanted +cross-origin communication. If a user clears browsing data for an |origin| (cookies, localStorage, etc.), the user agent MUST [=list/remove=] all triples with an [=/origin=] matching the |origin| from connected accounts set. @@ -491,6 +493,20 @@ To create a connection between the RP and the IdP account given an 1. [=set/Append=] |triple| to [=connected accounts set=].
+
+To remove a connection given |accountId|, |rpOrigin|, |idpOrigin|, run the following +steps. It returns whether a connection matching the |accountId| was found and removed: + 1. If |accountId| is not failure: + 1. Let |triple| be (|rpOrigin|, |idpOrigin|, |accountId|). + 1. If [=connected accounts set=] [=list/contains=] |triple|: + 1. [=list/Remove=] |triple| from the [=connected accounts set=]. + 1. Return true. + 1. For every (|rp|, |idp|, |accountId|) |triple| in the [=connected accounts set=]: + 1. If |rp| equals |rpOrigin| and |idp| equals |idpOrigin|, [=list/remove=] |triple| from the + [=connected accounts set=]. + 1. Return false. +
+ ## The IdentityCredential Interface ## {#browser-api-identity-credential-interface} @@ -499,7 +515,7 @@ This specification introduces a new type of {{Credential}}, called an {{Identity
   dictionary IdentityCredentialDisconnectOptions : IdentityProviderConfig {
-    USVString accountHint;
+    required USVString accountHint;
   };
 
   [Exposed=Window, SecureContext]
@@ -537,7 +553,7 @@ When the static {{IdentityCredential/disconnect}} method is invoked, given an
     1. Let |document| be |globalObject|'s [=associated Document=].
     1. If |document| is not [=allowed to use=] the [=identity-credentials-get=]
         [=policy-controlled feature=], throw a "{{NotAllowedError}}" {{DOMException}}.
-    1. If |options| does not [=map/contain=] all of the following, then throw a new {{TypeError}}:
+    1. If any of the following members from |options| are empty, throw a new {{TypeError}}:
         * {{IdentityProviderConfig/configURL}}
         * {{IdentityProviderConfig/clientId}}
         * {{IdentityCredentialDisconnectOptions/accountHint}}
@@ -574,30 +590,24 @@ When asked to attempt to disconnect given an {{IdentityCredentialDisc
         |provider| and |globalObject|.
     1. If |config| is failure, [=reject=] |promise| with a "{{NetworkError}}" {{DOMException}}.
     1. Let |disconnectUrl| be the result of [=computing the manifest URL=] given |provider|,
-        |config|["{{IdentityProviderAPIConfig/disconnect_endpoint}}"], and |globalObject|.
+        |config|.{{IdentityProviderAPIConfig/disconnect_endpoint}}, and |globalObject|.
     1. If |disconnectUrl| is failure, [=reject=] |promise| with a "{{NetworkError}}"
         {{DOMException}}.
     1. [=Send a disconnect request=] with |disconnectUrl|, |options|, and |globalObject|, and let
         |accountId| be the result.
     1. Let |idpOrigin| be the [=url/origin=] corresponding to |configUrl|.
     1. Let |rpOrigin| be |globalObject|'s [=associated Document=]'s [=Document/origin=].
-    1. If |accountId| is not failure:
-        1. Let |triple| be (|rpOrigin|, |idpOrigin|, |accountId|).
-        1. If [=connected accounts set=] [=list/contains=] |triple|:
-            1. [=list/Remove=] |triple| from the [=connected accounts set=].
-            1. [=Resolve=] |promise|.
-            1. Return.
-    1. For every (|rp|, |idp|, |accountId|) |triple| in the [=connected accounts set=]:
-        1. If |rp| equals |rpOrigin| and |idp| equals |idpOrigin|, [=list/remove=] |triple| from the
-            [=connected accounts set=].
-    1. [=Reject=] |promise| with a "{{NetworkError}}" {{DOMException}}.
+    1. [=Remove a connection=] for |accountId|, |rpOrigin|, and |idpOrigin| and let |foundAccount|
+        be the result.
+    1. If |foundAccount|, [=resolve=] |promise|, otherwise [=reject=] |promise| with a
+        "{{NetworkError}}" {{DOMException}}.
 
#### Disconnect request #### {#disconnect-request} -The send a disconnect request algorithm sends a request to disconnect an account that has +The [=send a disconnect request=] algorithm sends a request to disconnect an account that has previously been used for federated login in the [=RP=].
From d4effec931336310d3542aa50e1a218f8d532103 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Pe=C3=B1a=20Moreno?= Date: Wed, 29 Nov 2023 16:57:32 -0500 Subject: [PATCH 6/8] Feedback --- spec/index.bs | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/spec/index.bs b/spec/index.bs index 8d565fdac..d57781e9f 100644 --- a/spec/index.bs +++ b/spec/index.bs @@ -494,17 +494,21 @@ To create a connection between the RP and the IdP account given an
-To remove a connection given |accountId|, |rpOrigin|, |idpOrigin|, run the following -steps. It returns whether a connection matching the |accountId| was found and removed: - 1. If |accountId| is not failure: - 1. Let |triple| be (|rpOrigin|, |idpOrigin|, |accountId|). - 1. If [=connected accounts set=] [=list/contains=] |triple|: - 1. [=list/Remove=] |triple| from the [=connected accounts set=]. - 1. Return true. - 1. For every (|rp|, |idp|, |accountId|) |triple| in the [=connected accounts set=]: +To remove the connection given |accountId|, |rpOrigin|, and |idpOrigin|, run the +following steps. It returns whether the |accountId| connection was successfully removed. + 1. Let |triple| be (|rpOrigin|, |idpOrigin|, |accountId|). + 1. If [=connected accounts set=] [=list/contains=] |triple|: + 1. [=list/Remove=] |triple| from the [=connected accounts set=]. + 1. Return true. + 1. Return false. +
+ +
+To remove all connections given |rpOrigin| and |idpOrigin|, run the following steps: + 1. For every (|rp|, |idp|, accountId) |triple| in the + [=connected accounts set=]: 1. If |rp| equals |rpOrigin| and |idp| equals |idpOrigin|, [=list/remove=] |triple| from the [=connected accounts set=]. - 1. Return false.
@@ -597,9 +601,12 @@ When asked to attempt to disconnect given an {{IdentityCredentialDisc |accountId| be the result. 1. Let |idpOrigin| be the [=url/origin=] corresponding to |configUrl|. 1. Let |rpOrigin| be |globalObject|'s [=associated Document=]'s [=Document/origin=]. - 1. [=Remove a connection=] for |accountId|, |rpOrigin|, and |idpOrigin| and let |foundAccount| - be the result. - 1. If |foundAccount|, [=resolve=] |promise|, otherwise [=reject=] |promise| with a + 1. Let |wasAccountRemoved| be false. + 1. If |accountId| is not failure, [=remove the connection=] given |accountId|, |rpOrigin|, and + |idpOrigin| and let |wasAccountRemoved| be the result. + 1. If |accountId| is failure or |wasAccountRemoved| is false, [=remove all connections=] given + |rpOrigin| and |idpOrigin|. + 1. If |accountId| is not failure, [=resolve=] |promise|, otherwise [=reject=] |promise| with a "{{NetworkError}}" {{DOMException}}.
From 61a479773f6872d7f164d81a548c7c376af1401c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Pe=C3=B1a=20Moreno?= Date: Wed, 29 Nov 2023 17:50:13 -0500 Subject: [PATCH 7/8] Make it pretty --- spec/index.bs | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/spec/index.bs b/spec/index.bs index d57781e9f..9ff548c03 100644 --- a/spec/index.bs +++ b/spec/index.bs @@ -557,10 +557,6 @@ When the static {{IdentityCredential/disconnect}} method is invoked, given an 1. Let |document| be |globalObject|'s [=associated Document=]. 1. If |document| is not [=allowed to use=] the [=identity-credentials-get=] [=policy-controlled feature=], throw a "{{NotAllowedError}}" {{DOMException}}. - 1. If any of the following members from |options| are empty, throw a new {{TypeError}}: - * {{IdentityProviderConfig/configURL}} - * {{IdentityProviderConfig/clientId}} - * {{IdentityCredentialDisconnectOptions/accountHint}} 1. Let |promise| be a new {{Promise}}. 1. [=In parallel=], [=attempt to disconnect=] given |options|, |promise|, and |globalObject|. 1. Return |promise|. @@ -598,16 +594,18 @@ When asked to attempt to disconnect given an {{IdentityCredentialDisc 1. If |disconnectUrl| is failure, [=reject=] |promise| with a "{{NetworkError}}" {{DOMException}}. 1. [=Send a disconnect request=] with |disconnectUrl|, |options|, and |globalObject|, and let - |accountId| be the result. + |result| be the result. 1. Let |idpOrigin| be the [=url/origin=] corresponding to |configUrl|. 1. Let |rpOrigin| be |globalObject|'s [=associated Document=]'s [=Document/origin=]. - 1. Let |wasAccountRemoved| be false. - 1. If |accountId| is not failure, [=remove the connection=] given |accountId|, |rpOrigin|, and - |idpOrigin| and let |wasAccountRemoved| be the result. - 1. If |accountId| is failure or |wasAccountRemoved| is false, [=remove all connections=] given - |rpOrigin| and |idpOrigin|. - 1. If |accountId| is not failure, [=resolve=] |promise|, otherwise [=reject=] |promise| with a - "{{NetworkError}}" {{DOMException}}. + 1. If |result| is failure: + 1. [=Remove all connections=] given |rpOrigin| and |idpOrigin|. + 1. [=Reject=] |promise| with a "{{NetworkError}}" {{DOMException}}. + 1. Return. + 1. Let |accountId| be |result| (note that it is not failure). + 1. [=Remove the connection=] given |accountId|, |rpOrigin|, and |idpOrigin| and let + |wasAccountRemoved| be the result. + 1. If |wasAccountRemoved| is false, [=remove all connections=] given |rpOrigin| and |idpOrigin|. + 1. [=Resolve=] |promise|.
From ad3a7f64aee6c65a539f9d4d309da89e4bf69dcc Mon Sep 17 00:00:00 2001 From: npm1 Date: Fri, 1 Dec 2023 14:20:00 -0500 Subject: [PATCH 8/8] Apply suggestions from code review Co-authored-by: Ted Thibodeau Jr --- spec/index.bs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/index.bs b/spec/index.bs index 9ff548c03..2813a3fbc 100644 --- a/spec/index.bs +++ b/spec/index.bs @@ -494,7 +494,7 @@ To create a connection between the RP and the IdP account given an
-To remove the connection given |accountId|, |rpOrigin|, and |idpOrigin|, run the +To remove a connection: given |accountId|, |rpOrigin|, and |idpOrigin|, run the following steps. It returns whether the |accountId| connection was successfully removed. 1. Let |triple| be (|rpOrigin|, |idpOrigin|, |accountId|). 1. If [=connected accounts set=] [=list/contains=] |triple|: @@ -504,7 +504,7 @@ following steps. It returns whether the |accountId| connection was successfully
-To remove all connections given |rpOrigin| and |idpOrigin|, run the following steps: +To remove all connections: given |rpOrigin| and |idpOrigin|, run the following steps: 1. For every (|rp|, |idp|, accountId) |triple| in the [=connected accounts set=]: 1. If |rp| equals |rpOrigin| and |idp| equals |idpOrigin|, [=list/remove=] |triple| from the @@ -602,7 +602,7 @@ When asked to attempt to disconnect given an {{IdentityCredentialDisc 1. [=Reject=] |promise| with a "{{NetworkError}}" {{DOMException}}. 1. Return. 1. Let |accountId| be |result| (note that it is not failure). - 1. [=Remove the connection=] given |accountId|, |rpOrigin|, and |idpOrigin| and let + 1. [=Remove a connection=] using |accountId|, |rpOrigin|, and |idpOrigin|, and let |wasAccountRemoved| be the result. 1. If |wasAccountRemoved| is false, [=remove all connections=] given |rpOrigin| and |idpOrigin|. 1. [=Resolve=] |promise|.