Open aaronpk opened 6 months ago
Not sure what the difference here is that you're trying to point out. I think
protocol
isn't specific enough (for the reasons listed here), but I have no problem with putting the calls to each one separately. It'll actually only work as an array if theclientId
value can be the same anyway, which is definitely an edge case.
As in, repeat client_ids but varying protocols — what would happen here?
Either of these options seem fine to me, I don't really mind the redundancy of listing out the two variants separately even if they used the same client_id
:
const credential = await navigator.credentials.get({
identity: {
context: "signin",
providers: [{
variant: "oidc-id-token",
clientId: window.location.origin
},
{
variant: "indieauth",
clientId: window.location.origin
}
],
},
})
or
const credential = await navigator.credentials.get({
identity: {
context: "signin",
providers: [{
variant: ["indieauth","solid-oidc"],
clientId: window.location.origin
}
],
},
})
I think I actually prefer listing them separately. It looks more natural to me. I'm also really hopeful that we'll get support for privacy-friendly IdPs that don't require knowledge of the RP, in which case the clientId
wouldn't be a URI. That might look something like this:
const randomClientId = generateRandomId();
const credential = await navigator.credentials.get({
identity: {
context: "signin",
providers: [{
variant: "oidc-id-token",
clientId: window.location.origin
},
{
variant: "indieauth",
clientId: window.location.origin
},
{
variant: "anonymous-oidc",
clientId: randomClientId,
}
],
},
})
Should variant
be ignored if the RP is using it in the non-registered case (eg using a specific configURL
?)
Yes I assume variant
and configURL
are mutually exclusive parameters
I like this idea, and think it would be consistent with https://github.com/fedidcg/CrossSiteCookieAccessCredential, not requiring mutual exclusivity there
Update: we landed a prototype for this on Chrome. To try it out, use Chrome Canary, chrome://version should say 127.0.6528.0 or later. We went with type
when requesting (in the get
call). In the config file, the IdP can use a types
array to specify which ones it supports. Try it out and let us know what you think!
Let's go! Excited to try this.
@npm1 when do you expect this to go live?
@npm1 when do you expect this to go live?
Hey! It depends on what you mean by 'go live'! We are hoping to finalize addressing feedback and cleaning up the outstanding issues soon. After this, we'd need someone committed to trying it out with real users in production to start an Origin Trial. We would need to work on the UX for registration, which is currently not in acceptable shape for an Origin Trial.
Hey! It depends on what you mean by 'go live'!
I think the answer @anderspitman may be looking for is "it is currently available in chrome canaries" (it gets pushed daily).
Oh ok! Yea I would expect your Chrome Canary to already be updated with this? If not you can go to chrome://settings/help and force an update.
Yeah that's what I meant. I think the problem may be that I'm on Linux, which apparently isn't officially supported?
Ah yes, we do not have Chrome Canary on Linux. The closest here is Chrome Dev, which is released about once a week (and this change has not made it there yet).
Oh I didn't even realize those were different things. I should be able to do my FedCM testing on a windows machine so not a problem.
Initial attempt at implementing this. My FedCM config is as follows:
{
"accounts_endpoint": "https://anderspitman.net/fedcm/accounts",
"id_assertion_endpoint": "https://anderspitman.net/fedcm/id-assertion",
"login_url": "https://anderspitman.net/login-fedcm-auto",
"types": [
"indieauth"
]
}
I tried the following to make sure it doesn't work:
const identityCredential = await navigator.credentials.get({
identity: {
context: "signin",
providers: [
{
type: "fake-type",
clientId: window.location.origin,
nonce: "fake-nonce",
},
]
},
})
But it does work. As near as I can tell it's returning all registered IdPs regardless of the type
value.
Note that this also seems to be a problem with the previous version of Chrome that I've been using, ie even if I leave off the configURL
parameter, it still returns all registered IdPs.
I'm confused. What is the chrome://version and are you saying registered providers are being returned without passing configURL: "any"
?
126.0.6468.2
and 128.0.6538.0
. Note that for 126.0.6468.2
it's possible that it's remembering previously valid IdPs (though I would expect that to be checked every time). I didn't try deleting the chrome cache folder for that one. But I definitely deleted it for 128.0.6538.0
and fake-type
is returning IdPs that are only registered for type indieauth
.
Please don't test older versions, even Chrome Stable. This is an API in development so testing a month+ old code can lead to issues. In particular, 126 does not have the type
change, so it would ignore that completely even if you tried to pass it. 128.0.6538.0
should work correctly, let me know if that is not the case.
Right, but 126 does have configURL: any
, and that behavior is not working correctly in that version (it's returning IdPs even if configURL: fake-idp
). The point of bringing that up is that I suspect the logic that decides which IdPs to return has been broken since before the type
functionality, which might help you narrow it down.
It's fine if that's not useful information. The main problem I have is that 128 is returning all registered IdPs with the following call, even though I haven't registered any IdPs with fake-type
:
const identityCredential = await navigator.credentials.get({
identity: {
context: "signin",
providers: [
{
type: "fake-type",
clientId: window.location.origin,
nonce: "fake-nonce",
},
]
},
})
Right, but 126 does have
configURL: any
, and that behavior is not working correctly in that version (it's returning IdPs even ifconfigURL: fake-idp
). The point of bringing that up is that I suspect the logic that decides which IdPs to return has been broken since before thetype
functionality, which might help you narrow it down.
Do you mean you are using configURL: 'fake-idp' and it returns registered providers? I would expect that to say the configURL is invalid and no UI to be shown. That would be a bug indeed but I was fairly certain we only look at register providers when we pass configURL: 'any'.
It's fine if that's not useful information. The main problem I have is that 128 is returning all registered IdPs with the following call, even though I haven't registered any IdPs with
fake-type
:const identityCredential = await navigator.credentials.get({ identity: { context: "signin", providers: [ { type: "fake-type", clientId: window.location.origin, nonce: "fake-nonce", }, ] }, })
Perhaps your JS is incomplete? Calling the code you are showing results in "TypeError: Failed to execute 'get' on 'CredentialsContainer': Missing the provider's configURL." on my end.
Perhaps your JS is incomplete? Calling the code you are showing results in "TypeError: Failed to execute 'get' on 'CredentialsContainer': Missing the provider's configURL." on my end.
You are correct. I was not running the RP code I thought I was running :man_facepalming:. Sorry!
For anyone else looking to implement this, it differs slightly from what I expected (see https://github.com/fedidcg/FedCM/issues/585#issuecomment-2125937550). You need both configURL: any
and type
, like so:
await navigator.credentials.get({
identity: {
context: "signin",
providers: [
{
configURL: "any",
type: "indieauth",
clientId: window.location.origin,
nonce: "fake-nonce",
},
]
},
})
Also, it appears credentials.get
will access either a string or array of strings for type
, but if you use an array it's limited to a single value.
No worries! Yes, you are right that both configURL
and type
are needed. The type
is meant to be a string, so yea a single type (I suppose it can handle passing an array with a single string in it). Do you feel this is a limitation? Note that you can pass multiple providers, with different types in each, so we thought this is fine (and most RPs would probably just have a specific type in mind, right?).
As another announcement, I am soon landing a requirement to enable multi IDP when using registered providers (configURL: 'any'), since using this may trigger multi IDP UI. So please enable the multi IDP flag when playing with IDP registration!
@aaronpk I'm keen to test this against webmention.io. I created a PR. No pressure, it was just an easy one.
The type is meant to be a string, so yea a single type (I suppose it can handle passing an array with a single string in it). Do you feel this is a limitation?
Nope, as implemented should be great!
@anderspitman I haven't had a chance to update mine yet but I just merged your PR to webmention.io!
@anderspitman I haven't had a chance to update mine yet but I just merged your PR to webmention.io!
Thanks!
@npm1 I have LastLogin working with webmention.io, but I've run into a problem.
My IdP can support multiple types of FedCM flows (currently IndieAuth and direct ID token return), but I don't see a way to figure out which flow a specific client is using. I think maybe the ID assertion endpoint needs a type
query parameter sent from the browser?
One way I could think of to possibly hack around this for now would be to smuggle the type information back in the account IDs return from the accounts endpoint. So for example if the account ID was 1234
, I could return:
{
"accounts": [
{
"id": "1234 - IndieAuth",
...
},
{
"id": "1234 - Solid OIDC",
...
}
]
}
Then parse it out at the ID assertion endpoint. Pretty hacky though, and exposes implementation details the user probably shouldn't need to be aware of.
EDIT: Actually, this would only be visible to the user if I put it in the name
or email
fields, which I would probably want to otherwise it would appear as two identical options to the user, which could be confusing.
@npm1 I have LastLogin working with webmention.io, but I've run into a problem.
My IdP can support multiple types of FedCM flows (currently IndieAuth and direct ID token return), but I don't see a way to figure out which flow a specific client is using. I think maybe the ID assertion endpoint needs a
type
query parameter sent from the browser?
Yea that was feedback I was kinda expecting heh :) I will work to add type
to the ID assertion request.
In the case where both the IdP and RP support multiple match types, would the browser just send the first option in the list to the IdP ID assertion endpoint?
In the case where both the IdP and RP support multiple match types, would the browser just send the first option in the list to the IdP ID assertion endpoint?
Or maybe all that match?
It is not like the user has made a specific choice in terms of which "protocol" to use, and the RP has already said that they support "one of these", so I guess the IdP could pick arbitrarily (rather than the browser picking arbitrarily?)?
As an IdP, I would like a little more control over which protocol is used. It can be a bit awkward to pass lists in query params though. Is there even a standard way to do that?
As an IdP, I would like a little more control over which protocol is used. It can be a bit awkward to pass lists in query params though. Is there even a standard way to do that?
I'm partially thinking out loud here, but let me see I understand you correctly.
Here is one example of what the RP would request:
const credential = await navigator.credentials.get({
identity: {
providers: [{
configURL: "any", // NOTE(self): maybe we can infer that configURL: any when type is provided?
type: "indieauth",
}, {
configURL: "any",
type: "solid",
}, {
configURL: "any",
type: "oauth",
}]
}
});
And then, you'd have one IdP that has been registered with the following configURL
:
{
"types": ["indieauth", "solid"]
}
In this case, this IdP could be used either as an indieauth
IdP or as an solid
IdP. Since the RP accepts both, both are valid options. But, I'm guessing, conceptually speaking, the difference between indieauth
and solid
isn't one that is explainable to users, so we shouldn't make the browser show duplicate accounts as an option to the user and have the user make a selection of which "protocol" to use, but rather deduplicate the accounts.
Does that match your intuition so far?
It seems right now the call is rejected right away when passing multiple "any" as it is considered repeated. We can either relax this to allow multiple "any" or we can change type
to be an array, similar to the IDP config. Any thoughts on this? I think this depends on whether the other parameters passed can depend on the type used. If they are all expected to be the same then we can use an array, otherwise we can allow multiple "any" providers.
@samuelgoto yeah that's basically what I'm thinking.
@npm1 my instincts tell me it would be better to keep the extra flexibility of listing multiple "any" entries. At one point (see https://github.com/fedidcg/FedCM/issues/585#issuecomment-2125929920) I would have considered this very important, but I think it's less necessary since I gave up on w3c-fedid/FedCM#595. Still, maybe there could be some other utility in being able to have different client IDs for each entry.
Ok! I think this is not currently supported, although I found crbug.com/347715555 while testing.
Also filed crbug.com/347742955 and crbug.com/347740420 based on the discussions here.
No worries! Yes, you are right that both configURL and type are needed. The type is meant to be a string, so yea a single type (I suppose it can handle passing an array with a single string in it). Do you feel this is a limitation?
I'm wondering if we should accept an undefined type
in the JS to match any IdP that also has an undefined in the configURL
, so that there is a good default that works on a "catch all" basis.
Currently, webmention.io is broken because we now require both that the JS requires a type
and that the IdP announces a type
, but it seems useful to me to allow type
to be optional.
On the other hand, it is not like the RP would actually be able to open a response without having a convention agreement with the IdP, so maybe type does need to be required.
I'm thinking that maybe we could come up with a good default for "undefined type" that serves as a lowest common denominator (e.g. sharing a JSON response with the user's attributes) between RPs and IdPs.
I'm not sure it's realistic to assume an undefined type
would do anything useful and be interoperable, since you need both sides to agree on what to do with the returned data whether that's parsing an ID token or using an authorization code.
Currently, webmention.io is broken because we now require both that the JS requires a
type
and that the IdP announces atype
webmention.io now sends type: indieauth
, but I'm not sure what you mean by it's broken.
What I meant above is that when you specify a type
you also need to specify configURL
. It is already the case that you can specify configURL: 'any'
but not a type
, in which case all registered IDPs are used. But like Aaron said, this is likely not going to be very useful to RPs.
webmention.io now sends type: indieauth, but I'm not sure what you mean by it's broken.
By "it is broken" I mean that we (chrome) made a backwards incompatible change to the API (which we reserve the right to do - but avoid - while it is behind a flag), and in that process, we accidentally broke webmention.io. What I'm asking, which you seem to be confirming, is whether the breakage (requiring type
) is "working as intended" or if we needed to degrade more gracefully when type
wasn't provided (which you seem to imply that we shouldn't).
I just now added types: ["indieauth"]
to my indieauth-fedcm service, and tested against webmention.io, so I think we have it all working again in the latest canaries.
So, all in all, I think we are all good now :)
@anderspitman there is a chance you'll have to update lastlogin too to add types
to the configURL
that lastlogin exposes.
Ok, so, as far as I'm concerned, asides from bikesheding what to call this attribute, I think the formulation we have in chrome canaries right now seems to match what we discussed in this thread in a way that satisfies this issue. We should probably keep this issue open until this isn't merged into the spec (or learn from others trying that this doesn't work), but from a "do we know how to fix this problem" perspective, I think we have something that works well.
@samuelgoto I believe LastLogin staging (https://lastlogin.net) is already good to go.
This seems similar to the OpenID4VP spec patch: https://openid.github.io/OpenID4VP/openid-4-verifiable-presentations-wg-draft.html#name-openid4vp-profile-for-the-w. They use 'protocol', but maybe both 'type' (a granular version) and protocol (SAML, OIDC/OAuth) could be used. I guess type implies protocol, but in the simplest case I'd just want to know how to decode the request.
Right now if you send type: "X"
and there is no registered IdP of that type, the JS gets an exception NetworkError: Error retrieving a token
and in the console I see
The requested IdP type did not match the registered IdP.
I don't think this case should raise an exception, since it is the same scenario in which there are no IdPs registered at all. The RP asked for IdPs of a certain type, there were none, so nothing can proceed. This is true whether or not there are IdPs of other types registered.
(Also the error message implies only a single IdP can be registered which is also misleading)
I don't think this case should raise an exception, since it is the same scenario in which there are no IdPs registered at all. The RP asked for IdPs of a certain type, there were none, so nothing can proceed. This is true whether or not there are IdPs of other types registered.
What should happen if not an exception? Currently an exception is thrown in all failure cases, like when the user cancels the request by closing the dialog, right?
(Also the error message implies only a single IdP can be registered which is also misleading)
Fair enough, we can improve the console error message
Well there's a difference between an error after something has attempted to happen and nothing being possible at all. In particular, I might want to send the user through some path if they encounter a FedCM error after for example clicking their account. But if there are no IdPs available, I want to do nothing. So I need to be able to distinguish between these different error cases. Right now it is all just a generic exception.
Hmm ok. We probably do not want to let the RP know right away whether the user has registered an IDP of a given type, for privacy reasons. If we allowed this, it may provide a fingerprinting vector to an attacker (although it would become visible the first time there is a registered provider of the types requested).
But we do allow the RP to know when there was an error after clicking the account. In this case, a different type of error is thrown. It is not yet merged in the spec but hopefully soon, see https://github.com/w3c-fedid/FedCM/pull/498
IdP registration opens up a whole new world of possibilities. However that world is very large. For the bubbles of RPs/IdPs that aren't explicit OpenID Federations, there are still bubbles defined by which protocols the RP/IdP pair can speak, even though they don't have preexisting relationships or any trust roots. For example, webmention.io expects to be able to speak IndieAuth through FedCM, and wouldn't work if you had registered a SAML IdP in the browser.
Concretely, if a user had registered a SAML provider as an IdP in the browser, it would lead to a dead end if they landed on webmention.io and the account popped up in the chooser.
The solution could be as simple as allowing arbitrary strings in a "type" property, and letting IdPs register as being able to handle that type in the register call:
Then RPs could ask for IdPs with a matching type:
This would avoid IdPs showing up in the list when they would be unable to complete an exchange with an RP.