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 loginHint, RP context, and getUserInfo #470

Merged
merged 5 commits into from
Jun 8, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 148 additions & 8 deletions spec/index.bs
Original file line number Diff line number Diff line change
Expand Up @@ -412,10 +412,20 @@ This specification introduces an extension to the {{CredentialRequestOptions}} o
The {{IdentityCredentialRequestOptions}} contains a list of
{{IdentityProviderConfig}}s that the [=RP=] supports and has
pre-registered with (i.e. the [=IDP=] has given the [=RP=] a `clientId`).
The {{IdentityCredentialRequestOptions}} also contains a {{IdentityCredentialRequestOptionsContext}}
which the user agent can use to provide a more meaningful dialog to users.

<xmp class=idl>
enum IdentityCredentialRequestOptionsContext {
"signin",
"signup",
"use",
"continue"
};

dictionary IdentityCredentialRequestOptions {
sequence<IdentityProviderConfig> providers;
IdentityCredentialRequestOptionsContext context = "signin";
};
</xmp>

Expand All @@ -427,6 +437,7 @@ dictionary IdentityProviderConfig {
required USVString configURL;
required USVString clientId;
USVString nonce;
DOMString loginHint;
};
</xmp>

Expand All @@ -439,6 +450,11 @@ dictionary IdentityProviderConfig {
:: 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.
: <b>{{IdentityProviderConfig/loginHint}}</b>
:: 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
{{IdentityProviderAccount}}.
</dl>

<!-- ============================================================ -->
Expand Down Expand Up @@ -502,7 +518,7 @@ algorithm is invoked, the user agent MUST execute the following steps. This retu
before continuing.
1. Let |provider| be |options|["{{CredentialRequestOptions/identity}}"]["{{IdentityCredentialRequestOptions/providers}}"][0].
1. Let |credential| be the result of running [=create an IdentityCredential=] with |provider|,
|options|["{{CredentialRequestOptions/mediation}}"], and |globalObject|.
|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
Expand Down Expand Up @@ -534,20 +550,25 @@ agent UI, and creates the {{IdentityCredential}} that is then returned to the [=

<div algorithm="create an IdentityCredential">
To <dfn>create an IdentityCredential</dfn> given an {{IdentityProviderConfig}}
|provider|, a {{CredentialRequestOptions/mediation}} |mediation|, and a
|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
the exception thrown.
1. Assert: These steps are running [=in parallel=].
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.
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. Let |accountsList| be the result of [=fetch the accounts list=] with |config|, |provider|,
and |globalObject|.
1. If |provider|'s {{IdentityProviderConfig/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}}.
1. If |accountsList| is failure, return (failure, false).
1. For each |acc| in |accountsList|:
1. If |acc|["{{IdentityProviderAccount/picture}}"] is present, [=fetch the account picture=]
Expand Down Expand Up @@ -580,7 +601,8 @@ the exception thrown.
with |account|, |accountState|, |config|, |provider|, and |globalObject|. Also set
|disclosureTextShown| to true.
1. Otherwise, show a dialog to request user permission to sign in via |account|, and set the
result in |permission|.
result in |permission|. The user agent MAY use |options|'s
{{IdentityCredentialRequestOptions/context}} to customize the dialog.
1. Otherwise:
1. Set |account| to the result of running the [=select an account=] from the
|accountsList|.
Expand Down Expand Up @@ -816,6 +838,7 @@ dictionary IdentityProviderAccount {
USVString given_name;
USVString picture;
sequence<USVString> approved_clients;
sequence<DOMString> login_hints;
};
dictionary IdentityProviderAccountList {
sequence<IdentityProviderAccount> accounts;
Expand Down Expand Up @@ -970,6 +993,8 @@ an {{IdentityProviderAPIConfig}} |config|, an {{IdentityProviderConfig}} |provid
is defined, and the |provider|'s {{IdentityProviderConfig/clientId}} is not in the list of
|account|["{{IdentityProviderAccount/approved_clients}}"], then the user agent MUST display
the |metadata|["{{IdentityProviderClientMetadata/terms_of_service_url}}"] link.
1. The user agent MAY use the {{IdentityCredentialRequestOptions/context}} to customize the
dialog shown.
1. If the user does not grant permission, return false.
1. [=Create a connection between the RP and the IdP account=] with |provider|, |account|, and
|globalObject|.
Expand Down Expand Up @@ -1094,6 +1119,116 @@ and a |responseBody|, run the following steps. This returns an [=ordered map=].
1. Return |json|.
</div>

<!-- ============================================================ -->
Copy link
Collaborator

Choose a reason for hiding this comment

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

I need to read into this more, but I have concerns. This seems to duplicate a lot of structure from the IdentityProviderAccount and the Credential discovery algorithms. I worry about desync- there is already a given_name givenName confusion happening.

I get that you probably don't want to just cache the user info to prevent stale information. But I don't think we need to necessarily hang fresh info on the Window object and duplicate structs.

An alternative is to put a similar function on the IdentityCredential interface that takes no arguments and returns an IdentityProviderAccount. This may require adding in the IDP config URI or even just the base domain to the IdentityCredential to facilitate (but I think that should be done anyway). This would make the algorithm a little bit easier because having an IdentityCredential means it was discovered correctly, so some of the sanity checks can be cleared.

Copy link
Collaborator

Choose a reason for hiding this comment

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

This also feels like it is solving a very specific convenience-centered problem while having a more general name and putting itself in a general context.

If the IDP's iframe wants access to the account information and already has gone through a FedCM flow, why wouldn't they just use their token to request it themselves?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We use two dictionaries because one is describing JSON values while the other is describing the dictionary returned in JavaScript. They do need to be different, both due to semantics as well as difference in naming conventions.

I don't think it makes sense to add this to IdentityCredential, as this API does not return a credential, and it also does not require the JavaScript to have a credential around. I'm not sure what problem you are trying to solve with this suggestion.

Regarding why the IDP iframe can't just use the token: the FedCM call is done in the RP, not the IDP iframe. As such, the token should not allow querying updated account information on its own: this is power the IDP should not grant to the RP automatically. Using the token would also require the IDP to store this token somewhere in cookies so it can perform this query. So there really is no way to do this in a reasonable way, in the absence of third-party cookies.

Copy link
Collaborator

Choose a reason for hiding this comment

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

We use two dictionaries because one is describing JSON values while the other is describing the dictionary returned in JavaScript.

One benefit of WebIDL is that we can re-use a single definition for both.

They do need to be different, both due to semantics as well as difference in naming conventions.

I'm not sure which semantics you mean here. And is there external pressure on the spec to keep separate naming conventions for field names for single values? If I'm looking for a given name and I have to remember whether this is the case where I have to use obj['given_name'] or obj['givenName'], I would find that experience bad.

I'm not sure what problem you are trying to solve with this suggestion.

Polluting the window namespace.

Regarding why the IDP iframe can't just use the token: the FedCM call is done in the RP, not the IDP iframe.

Sure, but presumably the RP can pass the token to the IDP iframe?

As such, the token should not allow querying updated account information on its own: this is power the IDP should not grant to the RP automatically

I thought there was a responsibility by the IDP to make the most recent identifiers available. Restricting updates to chrome UI rather than including RPs seems like a tradeoff.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

One benefit of WebIDL is that we can re-use a single definition for both.

I'm not sure which semantics you mean here. And is there external pressure on the spec to keep separate naming conventions for field names for single values? If I'm looking for a given name and I have to remember whether this is the case where I have to use obj['given_name'] or obj['givenName'], I would find that experience bad.

I guess semantics are fairly similar so that would be fine. But the naming conventions are at https://www.w3.org/TR/design-principles/#casing-rules. I think they should be respected?

Polluting the window namespace.

Your suggestion is to add the method to IdentityCredential instead of IdentityProvider. Either way the method is not polluting the namespace. What am I missing?

Sure, but presumably the RP can pass the token to the IDP iframe?

This assumes the IDP wants to have control over the storage space in the RP. It would need to do so to store some token that it can then pass to its iframe later on. This seems undesirable: the IDP should not need to touch RP storage!

I thought there was a responsibility by the IDP to make the most recent identifiers available. Restricting updates to chrome UI rather than including RPs seems like a tradeoff.

Sure, the IDP needs to make the most recent information available, whenever that information needs to be used (FedCM or some other form of federated login or authentication). That does not in any way imply that the IDP should permit the RP unrestricted queries into the accounts that the user has because the user once used FedCM on that RP.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I hadn't been thinking about this in the context of agl's proposal... However if that is adopted, there is a very big breaking change happening that allows other things to move around. We could have a partial interface IdentityCredential { FedCMCredentialUtilityFunctions fedcm } that holds this function.

Going through the existing experimental uses in Chrome[1]:

  • this proposal
  • sign-in status API that Sam proposed moving to navigator
  • IDP registration (abandoned?)
  • A new one to me! but that looks like it would be just as at home on IdentityCredential

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 not all are great examples. But one that I think is a good one is IdentityProvider.close(). We want the IDP to call this for the IDP SignIn Status API, to signal to the browser that the user successfully signed in to the IDP. I think it would not make sense to have it in IdentityCredential?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah, that doesn't quite make sense. That means that there isn't a great place for this to go. We could add an inline issue that we should evaluate where this goes long-term.

Copy link
Collaborator

Choose a reason for hiding this comment

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

With an inline issue, I'd call this LGTM.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Added inline issue

## The IdentityProvider Interface ## {#browser-api-identity-provider-interface}
<!-- ============================================================ -->

This specification introduces the {{IdentityUserInfo}} dictionary as well as the
{{IdentityProvider}} interface:

<pre class="idl">
dictionary IdentityUserInfo {
USVString email;
USVString name;
USVString givenName;
USVString picture;
};

[Exposed=Window, SecureContext] interface IdentityProvider {
static Promise&lt;sequence&lt;IdentityUserInfo&gt;&gt; getUserInfo(IdentityProviderConfig config);
};
</pre>

Issue: [Decide](https://github.com/fedidcg/FedCM/issues/476) whether {{IdentityProvider}} is the
correct location for the {{IdentityProvider/getUserInfo()}} method.

An {{IdentityUserInfo}} represents user account information from a user. This information is exposed
to the [=IDP=] once the user has already used the FedCM API to login in the [=RP=]. That is, it is
exposed when there exists an account |account| such that the [=connected accounts set=] [=list/contains=]
the triple ([=RP=], [=IDP=], |account|). The information matches what is received from the
<a>accounts list endpoint</a>. The [=IDP=] can obtain this information by invoking the
{{IdentityProvider/getUserInfo()}} static method from an iframe matching the [=/origin=] of its
{{IdentityProviderConfig/configURL}}.

<div class="example">
```js
const userInfo = await IdentityProvider.getUserInfo({
configUrl: "https://idp.example/fedcm.json",
clientId: "client1234"
});

if (userInfo.length > 0) {
// It's up to the IDP regarding how to display the returned accounts.
const name = userInfo[0].name;
const givenName = userInfo[0].givenName;
const displayName = givenName ? givenName : name;
const picture = userInfo[0].picture;
const email = userInfo[0].email;
}
```
</div>

<div algorithm="getUserInfo">
When invoking the {{IdentityProvider/getUserInfo()}} method given an {{IdentityProviderConfig}}
|provider|, 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 there does not exist an account |account| such that [=compute the connection status=] of
|provider|, |account|, and |globalObject| returns
[=compute the connection status/connected=], then throw a new "{{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 |configUrl| be the result of running [=parse url=] with |provider|'s
{{IdentityProviderConfig/configURL}} and |globalObject|.
1. If |configUrl| is failure, throw an "{{InvalidStateError}}" {{DOMException}}.
1. If |document|'s [=Document/origin=] is not [=same origin=] as |configUrl|'s [=url/origin=],
throw 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, throw a new "{{NetworkError}}" {{DOMException}}.
1. If |globalObject|'s [=Window/navigable=] is a [=/top-level traversable=], throw a new
"{{NetworkError}}" {{DOMException}}.
1. If the user has disabled the FedCM API on the |globalObject|'s [=Window/navigable=]'s
[=navigable/top-level traversable=], throw a new "{{NetworkError}}" {{DOMException}}.
1. Let |promise| be a new {{Promise}}.
1. Perform the following steps [=in parallel=]:
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 new "{{NetworkError}}"
{{DOMException}}.
1. Let |accountsList| be the result of [=fetch the accounts list=] with |config|, |provider|,
and |globalObject|.
1. Let |hasReturningAccount| be false.
1. For each |account| in |accountsList|:
1. If |account|["{{IdentityProviderAccount/approved_clients}}"] is not empty and it does not
[=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.

1. [=Compute the connection status=] of |provider|, |account|, and |globalObject|. If the
result is [=compute the connection status/connected=], set |hasReturningAccount| to
true.
1. If |hasReturningAccount| is false, [=reject=] |promise| with a new "{{NetworkError}}"
{{DOMException}}.
1. Let |userInfoList| be a new [=list=].
1. For each |account| in |accountsList|:
1. [=list/Append=] an {{IdentityUserInfo}} to |userInfoList| with the following values:

: {{IdentityUserInfo/email}}
:: |account|["{{IdentityProviderAccount/email}}"]
: {{IdentityUserInfo/name}}
:: |account|["{{IdentityProviderAccount/name}}"]
: {{IdentityUserInfo/givenName}}
:: |account|["{{IdentityProviderAccount/given_name}}"]
: {{IdentityUserInfo/picture}}
:: |account|["{{IdentityProviderAccount/picture}}"]
1. [=Resolve=] a new {{Promise}} with |userInfoList|.
</div>

<!-- ============================================================ -->
# Identity Provider HTTP API # {#idp-api}
<!-- ============================================================ -->
Expand Down Expand Up @@ -1309,6 +1444,10 @@ Every {{IdentityProviderAccount}} is expected to have members with the following
:: A list of [=RP=]s (that gets matched against the requesting {{IdentityProviderConfig/clientId}}) this account is already registered with.
Used in the [=request permission to sign-up=] to allow the [=IDP=] to control whether to show
the Privacy Policy and the Terms of Service.
: <dfn>login_hints</dfn>
:: 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
matching a given value is shown to the user.
</dl>

For example:
Expand All @@ -1322,14 +1461,16 @@ For example:
"name": "John Doe",
"email": "john_doe@idp.example",
"picture": "https://idp.example/profile/123",
"approved_clients": ["123", "456", "789"]
"approved_clients": ["123", "456", "789"],
"login_hints": ["john_doe"]
}, {
"id": "5678",
"given_name": "Johnny",
"name": "Johnny",
"email": "johnny@idp.example",
"picture": "https://idp.example/profile/456"
"approved_clients": ["abc", "def", "ghi"]
"picture": "https://idp.example/profile/456",
"approved_clients": ["abc", "def", "ghi"],
"login_hints": ["email=johhny@idp.example", "id=5678"]
}]
}
```
Expand Down Expand Up @@ -2111,8 +2252,7 @@ path: img/mock5.svg

Secondary use is the use of collected information about an individual without the individual's
perimssion for a purpose different from that for which the information was collected. This attack
happens when [=IDP=]s misuse the the information collected to enable sign-in for other
purposes.
happens when [=IDP=]s misuse the information collected to enable sign-in for other purposes.

Existing federation protocols require that the [=IDP=] know which service is requesting a token
in order to allow identity federation. Identity providers can use this fact to build profiles of
Expand Down