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

Information in the IdentityCredential on output #42

Closed
ekovac opened this issue Sep 11, 2024 · 9 comments · Fixed by #45
Closed

Information in the IdentityCredential on output #42

ekovac opened this issue Sep 11, 2024 · 9 comments · Fixed by #45

Comments

@ekovac
Copy link
Contributor

ekovac commented Sep 11, 2024

After w3c-fedid/FedCM#39 is resolved and if w3c-fedid/FedCM#41 is merged, there isn't much left for the RP to do with the IdentityCredential it gets back from navigator.credentials.get(). This is a bit of a bummer. After some discussion with @samuelgoto and @johannhof we've identified a couple of possibilities for what the RP could do with this (aside from the implicit Storage-Access grant that is currently assumed by the explainer.)

Alternatives presented in order of "simplest" to "most complicated:"

  • (A) We just accept that the returned IdentityCredential only conveys 1) that the user selected an account and 2) which IdP the user's account selection is from. From there the RP can know which IdP's embedded frame to communicate with to request storage access and work from there.

    • Pros
      • No changes to current explainer or existing implementations.
    • Cons
      • Not much that can be done with this information. RPs can't do anything cool in a few straightforward lines.
      • Using this requires an entirely different codepath for the RP to implement that can't be reused for full FedCM.
  • (B) On output, we populate a dictionary on the IdentityCredential with the UI hint information provided by the IdP at .store() time.

    • Pros
      • Extremely easy to leverage on the RP side to perform basic customization, displaying the
        user's name and profile picture. You can do something useful in a few lines.
    • Cons
      • Using this requires an entirely different codepath for the RP to implement that can't be reused for full FedCM.
  • (C) On output, we could include a token field that contains an unsigned JWT with assertions corresponding to the UI hints that the IdP provided when .store() was called.

    • Pros
      • The same codepaths on the RP's site or identity SDK could be used when either "full" FedCM or Lightweight FedCM is selected by the user.
    • Cons
      • Encouraging developers to ever do anything with an unsigned JWT aside from throw it in the
        garbage seems like a bad idea.
  • (D) After get() is called and the user makes a selection from the account picker, we can optionally proceed with the "get the assertion" part of the full FedCM process.

    • Pros
      • This starts to look way more like a subset of full FedCM rather than a weird parallel mode.
      • RP winds up with a signed JWT they can trust to contain real assertions of the user's
        identity insofar as they trust the IdP.
      • Code on the RP side can be reused for full FedCM; the resulting IdentityCredential would be
        basically indistinguishable.
    • Cons
      • Additional complexity for the IdP; this can be mitigated by making it optional and falling
        back to option (A) or (B), but then that creates the same codepath bifurcation problem.
      • For this option to make sense this would need to adopt a lot of the configuration from full
        FedCM; identifying what subset would apply here is left as an exercise to the reader.
      • Undoes some of the latency wins we were hoping for with Lightweight FedCM.
@ekovac
Copy link
Contributor Author

ekovac commented Sep 11, 2024

I think option (D) is the most interesting here, but that might quickly devolve into something too complex to achieve the goals of Lightweight FedCM. Option (A) and (B) both "feel" fine to me.

@bvandersloot-mozilla
Copy link
Collaborator

bvandersloot-mozilla commented Sep 13, 2024

I think option (D) is the most interesting here, but that might quickly devolve into something too complex to achieve the goals of Lightweight FedCM. Option (A) and (B) both "feel" fine to me.

I agree with this take. The thing that I think might make D work is that putting the token endpoint URL inside of the argument to get() is possible, since it is fetched after the UI. We may even be able to have it auto-refetch on each collection from the credential store too, which would make it very FedCM-token-like. Whether we do that or just give the same token back that was stored probably is up to the details of the OAuth profile for FedCM.

@samuelgoto
Copy link
Contributor

samuelgoto commented Sep 13, 2024

Just a couple of comments that may add more clarity here:

  1. token can typically either be an id_token, an code, or a token in OIDC (not an OIDC expert here, probably worth sanity checking), so it is not always a JWT (e.g. it can be an access token). SAML would return something entirely different. So, (C) feels a bit off.
  2. token is overly (semantically) and unnecessarily (privacy/security wise) restrictive, and there is a proposal to allow the IdP to return some "arbitrary" data here, so that the browser doesn't have to form an opinion on what gets sent back to RPs. I don't know if that makes it more or less like (d).

I think option (D) is the most interesting here, but that might quickly devolve into something too complex to achieve the goals of Lightweight FedCM. Option (A) and (B) both "feel" fine to me.

Yeah, I tend to agree that (D) seems to be the most "conceptually consistent" option: the "output" of the IdentityCredential is something that gets created "by the IdP". As you noted, In "full FedCM", that's something that gets returned in the id_assertion_endpoint.

Just curious: in "light FedCM", would it be possible to execute a Javascript callback (maybe using service workers?) that the IdP gets to control to generate a message back to the RP? I have no idea if this would be possible, but I think this might be "complex on browser engines" but "light on developers".

For reference, Mozilla Persona had a "callback" that the Identity Provider would get here. I'm not sure how that worked, but seems worth looking.

Update: seems like it accomplished some of that by this step (ironically, using well-known files):

The user-agent checks https://eyedee.me/.well-known/browserid and determines that eyedee.me supports BrowserID. From this configuration file it determines the provisioning and authentication URLs.
The user-agent loads, in an invisible IFRAME, the provisioning URL https://eyedee.me/browserid/provision.html

@ekovac
Copy link
Contributor Author

ekovac commented Sep 13, 2024

  1. token can typically either be an id_token, an code, or a token in OIDC (not an OIDC expert here, probably worth sanity checking), so it is not always a JWT (e.g. it can be an access token). SAML would return something entirely different. So, (C) feels a bit off.

Ah, thanks for clarifying. And yes, C was definitely not my favorite choice, I included it for completeness :)

  1. token is overly (semantically) and unnecessarily (privacy/security wise) restrictive, and there is a proposal to allow the IdP to return some "arbitrary" data here, so that the browser doesn't have to form an opinion on what gets sent back to RPs. I don't know if that makes it more or less like (d).

I think that makes it, in practice, less like (d), especially if the implication is that the IdP would call n.c.store() with the data object defined on the IdentityCredential it is passing in.

Yeah, I tend to agree that (D) seems to be the most "conceptually consistent" option: the "output" of the IdentityCredential is something that gets created "by the IdP". As you noted, In "full FedCM", that's something that gets returned in the id_assertion_endpoint.

I think @samuelgoto you've been mentioning the possibility of folding some of the lightweight FedCM design ideas into the main FedCM specification. Is there any reason we can't just make all the fields of the IdentityProviderAPIConfig optional?

Here's a quick explanation of how this might work for the "user has an account with the IdP and has visited the IdP already" case:

User visits the IdP at some point, and it stores that they have an account with the IdP, and any UI hint they also want to provide via n.c.store(), as per the existing Lightweight FedCM explainer.

Relying party call to .get() from the IdP is done as in the full FedCM specification.

let cred = await navigator.credentials.get(
  {identity: {providers: {configURL: "https://idp.example/config.json"});

User agent fetches the config.json and .well_known/webidentity as normal without any cookies etc. The .well_known/webidentity validates that the config.json URL is sound.

The config.json (minimally) looks like this:

{
  "id_assertion_endpoint": "/assert",
}

Since there's no accounts_endpoint defined, we check our stored accounts/UI hints from earlier; if we have them, great, the user agent doesn't need to make any more calls before the user selects an account, so we've preserved the new privacy property we hoped to create. If the user agent doesn't have UI hints from before, and a loginUrl was provided in the config, then we could do something with that (I'd have to think through all the mediation mode and multi-IdP considerations to know exactly what.)

The user agent displays the account selector using the UI hints stored earlier, the user selects an account, and then the assertion endpoint is called as per the existing FedCM spec. (If a clientId wasn't provided in the .get() call, then the IdP may choose to reject the request, but in an open-federation usecase it would not.)

The IdP doesn't even have to implement an assertion endpoint if it doesn't want to. If the only goal is to get a nicer UX than a basic Storage Access prompt, they needn't define anything at all (except maybe a loginUrl for a redirect if the user hasn't visited the IdP on this browser yet.) The IdentityCredential that the RP gets back in this case won't be terribly useful, as mentioned in the original post, but it's a perfectly valid point along a continuum of functionality, instead of being the only option, and that makes this seem a lot more appealing.

This adds a lot more branches to the already complicated FedCM specification, but might well address a wide continuum of usecases and make piecemeal implementation of FedCM integration feasible for IdPs. It also gives a couple knobs for implementations to adjust (some browsers may decide the inconvenience of falling back to the loginURL is acceptable in contrast to the privacy impact of the up-front accounts endpoint call when no pre-stored accounts are available.)

@samuelgoto
Copy link
Contributor

samuelgoto commented Sep 13, 2024

I think @samuelgoto you've been mentioning the possibility of folding some of the lightweight FedCM design ideas into the main FedCM specification. Is there any reason we can't just make all the fields of the IdentityProviderAPIConfig optional?

Yeah, I think that matches my intuition.

Just as a concrete example of another variation, here is another case:

The IdP doesn't even have to implement an assertion endpoint if it doesn't want to.

They could also choose to implement the accounts_endpoint (to have the accounts "pulled" rather than "push") but not the id_assertion_endpoint:

{
  "accounts_endpoint": "/accounts",
}

This tells the browser: pull accounts from the accounts_endpoint, but don't hit the id_assertion_endpoint: the IdP will take it from there with the Storage Access API auto-grant.

This adds a lot more branches to the already complicated FedCM specification, but might well address a wide continuum of usecases and make piecemeal implementation of FedCM integration feasible for IdPs.

Yeah, I agree that this can get complex quick, but maybe we don't need to think about every single permutation until we hear from developers that they need a specific one?

@bvandersloot-mozilla
Copy link
Collaborator

I think it would also be interesting to have the token endpoint provided as an argument to the n.c.get call rather than via the site-level well-known. Even having a general "config" argument could be useful, if the IDP already opted into being used in this way via storing a credential. In fact, the argument could be on the n.c.store.

The accounts endpoint would be useful as well, however it would have to be fetched after an IDP-entry in the credential chooser, to prevent attacks on privacy.

@ekovac
Copy link
Contributor Author

ekovac commented Sep 17, 2024

I think it would also be interesting to have the token endpoint provided as an argument to the n.c.get call rather than via the site-level well-known. Even having a general "config" argument could be useful, if the IDP already opted into being used in this way via storing a credential. In fact, the argument could be on the n.c.store.

I think there's value in trying to minimize the amount of divergence between FedCM and Lightweight FedCM here and leverage the existing configuration. What benefit does supplying the config URL in the n.c.store() call have over just relying on the existing FedCM configuration mechanism?

It's true that if we take it as a given that the user will be picking the IdP from a list before any requests (credential-less or otherwise) are issued, there's probably no harm in doing this. But it seems like supplying this at .store() time is an unnecessary difference.

The accounts endpoint would be useful as well, however it would have to be fetched after an IDP-entry in the credential chooser, to prevent attacks on privacy.

Agreed, I think this fits together nicely in this model.

@bvandersloot-mozilla
Copy link
Collaborator

I think there's value in trying to minimize the amount of divergence between FedCM and Lightweight FedCM here and leverage the existing configuration.

I agree, which is why I would use the fetch description of the Token endpoint from FedCM.

What benefit does supplying the config URL in the n.c.store() call have over just relying on the existing FedCM configuration mechanism?

If an IDP wants to provide the token, I don't want to require adopting the site-level well-known resource since it is a challenge for some deployments. This is also why the loginURL is in the n.c.store arguments. FedCM had to fetch IDP config from a well-known resource because it had no prior guarantee of interaction with the IDP. We have a convenient place for it!

@samuelgoto
Copy link
Contributor

samuelgoto commented Sep 20, 2024

Just an idea that occurred to me after thinking about this a bit more on (C) ...

(C) On output, we could include a token field that contains an unsigned JWT with assertions corresponding to the UI hints that the IdP provided when .store() was called.

So, something that could be very interesting, but may be something that we'd want to layer on top of the other options, is to use a signed SD-JWT and the the Three Party Model with the browser (or a wallet or a password manager) as the holder: have the navigator.credentials.store() store an SD-JWT and the browser to derive a selective disclosure presentation at navigator.credentials.get() (without ever phone-homing the IdP).

This would be clearly not retrofittable into the current system, requiring the RP to redeploy, so not a great starting point, but seems like a good long term design: something that allows us a gradual upgrade path from pull (heavyweight) > push (lighweight) > issue (delegated) for IdPs.

It is not clear to me whether that's a FedCM concern or a Digital Credentials concern and where to draw the line between the two (maybe the former is more "high level" and the latter is more "low level"?), but just wanted to throw it out there as a possible "long term design" that can be built on top of lightweight.

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

Successfully merging a pull request may close this issue.

3 participants