w3c-fedid / idp-registration

A proposal to extend FedCM to allow RPs to accept "any" registered IdP
4 stars 0 forks source link

Allow IdP registration and RPs to match on a "type" #1

Open aaronpk opened 6 months ago

aaronpk commented 6 months ago

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:

IdentityProvider.register({configURL: 'https://authorization-server.com/fedcm/config.php', type: ['indieauth']});

Then RPs could ask for IdPs with a matching type:

    const identityCredential = await navigator.credentials.get({
      identity: {
        context: "signin",
        providers: [
          {
            type: "indieauth",
            clientId: window.location.origin+"/"
          },
        ],
        // mode: "button"
      },
    }).catch(e => {
      console.log("Error", e.message);
    });

This would avoid IdPs showing up in the list when they would be unable to complete an exchange with an RP.

npm1 commented 6 months ago

Would there be some list of types to consider, or would type be more of an arbitrary string? Ideally the IDP registration allows almost any IDP to show up in an RP that supports them, but with this proposal we may cause some unneeded fragmentation where each IDP could have their own 'type' which makes registration work closer to the existing FedCM flow where you need to know the IDP ahead of time.

aaronpk commented 6 months ago

I would not hardcode the list, since you don't want to maintain a registry of these and really it's in the spirit of being open to just use an arbitrary string. Maybe it's more like "protocol" than "type"? Some other flavors of OAuth that come to mind off the top of my head:

In addition to there being slight differences in the actual protocols between these, there are also very different user expectations about what is possible when logging in with an IdP of these types.

For example, there are tools and services you can add to your Home Assistant installation, which only make sense in the Home Assistant context. So I'd like websites to be able to make a button like this which asks the browser for the user's Home Assistant installation:

image

It's more about avoiding a dead end user experience, since if I click "add to home assistant" and then log in with my Fediverse account, the site won't be able to do anything with the Fediverse account if the login even succeeds at all.

There's another version of this which is in the enterprise space. If I visit a SaaS app, they often already have an option to sign in as an individual user, but also to use company SSO. Right now the user experience for that is pretty bad, either having the user enter their work email and doing discovery on the domain, or asking the user to enter their enterprise org subdomain. Instead, I'd like to be able to provide a "SSO" button which asks the browser for their "enterprise" IdP, which sounds a lot like another one of these types.

image

(This is slightly different than the "open world" version since RPs and IdPs do have pre-established relationships in this context, but the list of supported IdPs at any given SaaS app is too big to put into the FedCM API call, not to mention is usually private information.)

snarfed commented 6 months ago

This is great! Love it. Thank you!

I'm sympathetic to the type registry question, on both sides. I'll defer that to people who know these ecosystems better, but I'm glad it's being discussed.

samuelgoto commented 6 months ago

If we made the type be a URL (e.g. "https://indieauth.net", "https://atproto.com/", "https://datatracker.ietf.org/doc/html/rfc6749", "https://openid.net/specs/openid-connect-core-1_0.html", "https://seamlessaccess.org"), maybe we can leave the registry to be DNS?

snarfed commented 6 months ago

Right, lots of prior art with that too! XML namespaces, JSON-LD contexts, NSIDs, etc.

My main question isn't what the namespace is, though, it's how do we coordinate it. Ie is the fediverse type fediverse, activitypub, social-web, SocialWeb, etc. DNS vs plain text doesn't address that.

Regardless, I know there's a ton of experience and prior art on managing this kind of taxonomy namespace, whether with or without registry, plain text or DNS or other, etc, so I'd definitely hope to lean on existing best practices and knowledge.

ThisIsMissEm commented 6 months ago

@aaronpk just a heads up, there's a TONNE of changes coming to Mastodon's OAuth 2 IdP setup, and I'm working to support standardised OAuth 2 dynamic client registration (currently POST /api/v1/apps is non-standard), we've recently landed support for RFC 8414 for discovering OAuth 2 Authorization Server Metadata too.

You can see everything I've been working on related to this here: https://github.com/mastodon/mastodon/pulls?q=is%3Apr+author%3AThisIsMissEm+sort%3Aupdated-desc

aaronpk commented 6 months ago

Using a URL for the type would be fine. We'd still need to get RPs/IdPs in these clusters to agree on the URL. But that is a good candidate for being defined in a FedCM profile. For example I could easily see adding a FedCM section to the IndieAuth spec that defines the string to use here, then anyone reading that spec would know what to use. And when it's not a spec, but something like Home Assistant, they could just define it in their API docs.

anderspitman commented 6 months ago

I fully agree with the premise here. I would just like to note that I've implemented various OAuth2 protocols from scratch, including OIDC, and OIDC is simply a joy to work with because it specifies so many details. If you create an OIDC OP or RP, you know it will work with other software. Plain OAuth2 does not enjoy this level of compatibility, since it's not really a protocol but a "protocol framework".

So, all that to say, I love the idea of implementers being able to use whatever string they want for the type, but I think maybe there should be a small number of specified types that can be expected to work with a wide variety of implementations.

aaronpk commented 6 months ago

This isn't just about the protocol, it's also about the list of acceptable IdPs and RPs.

For example, even though two IdPs might support OIDC in the exact same way, the RP might only be able to actually do anything with only one of them. Going back to my original example, let's say hypothetically that both Mastodon and Home Assistant supported the exact same feature set of OIDC. We'd still need a way to have an RP ask for a Home Assistant IdP, and the Mastodon IdP should not show up in the list, because the RP is expecting to be able to do things with Home Assistant that Mastodon doesn't support.

Similarly, an enterprise IdP and a university IdP might support the exact same feature set of OIDC, but an RP might only actually work with a university IdP.

So this is talking me out of calling the property protocol since it's actually not just about the protocol.

samuelgoto commented 6 months ago

So this is talking me out of calling the property protocol since it's actually not just about the protocol.

Would federation work better than protocol or type? You used the word bubble before, which seems to allude to a set of agreed upon clusters.

aaronpk commented 6 months ago

I think "federation" is too narrowly scoped. It works well for the Research+Education and Open Banking use cases. But the IndieAuth/Mastodon/Home Assistant use cases work with no pre-existing relationship between RPs and IdPs, and no common trust anchors like you would have in a federation. The only thing in common they have is the protocol and what the user expects to get out of it. I also think the relationship between SaaS app and enterprise IdP would not be described as a federation.

anderspitman commented 6 months ago

For example, even though two IdPs might support OIDC in the exact same way, the RP might only be able to actually do anything with only one of them. Going back to my original example, let's say hypothetically that both Mastodon and Home Assistant supported the exact same feature set of OIDC. We'd still need a way to have an RP ask for a Home Assistant IdP, and the Mastodon IdP should not show up in the list, because the RP is expecting to be able to do things with Home Assistant that Mastodon doesn't support.

Not sure I'm understanding correctly. Are you talking about Home Assistant features like APIs for doing Home Assistant things? It would be cool for that to be discoverable, but isn't it outside the scope of authentication? Please correct me if I'm misinterpreting.

In terms of how to actually implement this, would it make sense to have it be a list of features that need to be supported? That way you can compose them into the features required by your RP, and any IdPs that support all the features you need would be returned. Feels somewhat analogous to OAuth2 scope.

samuelgoto commented 6 months ago

@npm1 started putting together a prototype (https://chromium-review.googlesource.com/c/chromium/src/+/5546318) of this proposal and we were debating this specific part of the proposal:

IdentityProvider.register({
  configURL: 'https://authorization-server.com/fedcm/config.php', 
  type: ['indieauth']
});

What occurred to me while reviewing the code was that if we follow this, the IdP wouldn't be able to change the type that it is part of dynamically (e.g. say, add/remove itself from the bubbles), but rather at registration time. That would mean that, for every change it would like to make to the "protocols" it understands, it would have to re-request the user's permission.

My suggestion to @npm1 was that we should move, instead, the type to the configURL, which gets loaded dynamically at run time when the RP requests it. That way, after loading a fresh file, the browser can look for the most recent types that this IdP represents and can still use them to filter things out.

So, instead of the snippet below, the registration API remains:

IdentityProvider.register({
  configURL: 'https://authorization-server.com/fedcm/config.php'});

And then we move the types to the configURL, e.g.:

{
  "accounts_endpoint": "/accounts",
  // .. other endpoints ...

  // types of protocols this IdP speaks
  "type": ["indieauth"]
}

WDYT?

aaronpk commented 6 months ago

Oh that's a great point, it would be convenient if the IdP could change that without requiring the user confirm it. I think this works fine with the type in the config file instead.

aaronpk commented 6 months ago

Not sure I'm understanding correctly. Are you talking about Home Assistant features like APIs for doing Home Assistant things? It would be cool for that to be discoverable, but isn't it outside the scope of authentication? Please correct me if I'm misinterpreting.

Yes and no. The problem is if I can't do an OAuth flow because the browser is blocking redirects, then I have to first use FedCM before an OAuth flow will even work. If I have to first use FedCM anyway, then this provides a huge opportunity to smooth over a lot of the UX problems that exist today. Without this proposal, the RP would need to first ask the user to enter their Home Assistant URL (as they currently do today), and then start the FedCM call with the configURL based on what the user entered. I would argue the resulting user experience would be worse than it is today without FedCM.

anderspitman commented 6 months ago

The problem is if I can't do an OAuth flow because the browser is blocking redirects

Can you clarify what you mean by this? I'm not sure I've ever been in this scenario.

aaronpk commented 6 months ago

That's the premise of the whole thing. Eventually the browser's goal is to prevent cross site tracking including redirect-based tracking, not just third-party cookies. The unfortunate coincidence is that federated login flows like OAuth look a lot like cross-site tracking, so those will eventually get blocked too.

https://developers.google.com/privacy-sandbox/3pcd/fedcm#why_do_we_need_fedcm

Unfortunately, the mechanisms that identity federation has relied on (iframes, redirects and cookies) are actively being abused to track users across the web. As the user agent isn't able to differentiate between identity federation and tracking, the mitigations for the various types of abuse make the deployment of identity federation more difficult.

ThisIsMissEm commented 6 months ago

@aaronpk is their intent to block all cross domain redirects (whether via 30x Location redirects or via window.location in JavaScript) ?

If so that may impact the "interaction required" concept we talked about on Friday, since that would need to do a redirect.

aaronpk commented 6 months ago

The "interaction required" proposal (#590) wouldn't be affected because it would happen after the user clicks a button confirming they are trying to sign in, so the browser can un-block the redirects.

anderspitman commented 6 months ago

Eventually the browser's goal is to prevent cross site tracking including redirect-based tracking

Interesting; somehow I missed that redirects were also on the block

samuelgoto commented 6 months ago

So this is talking me out of calling the property protocol since it's actually not just about the protocol.

@aaronpk what about profile?

aaronpk commented 6 months ago

I can live with profile, but it sounds a bit like "profile of a protocol", but I think as long as we refer to it as "profile of FedCM" in docs and such that should make sense.

samuelgoto commented 6 months ago

Trying to paraphrase and capture Elf's point in the CG call

Elf: what happens when the RP supports multiple "profiles" and so does the IdP? How do they choose one?

npm1 commented 6 months ago

For what it's worth, profile seems confusing to me. Perhaps we can use type in the initial prototype while we bikeshed the naming.

samuelgoto commented 6 months ago

Perhaps we can use type in the initial prototype while we bikeshed the naming.

As long as we allow ourselves time to bikeshed and change the name before I2S, SGTM.

npm1 commented 6 months ago

Hmm I was going to use type in the configURL but it's certainly too generic. Anybody else have alternative ideas for the naming? Would love to start with a name I don't hate even if temporary :)

npm1 commented 6 months ago

AI generated ideas:

From these, any preferences? I propose going with class for now.

aaronpk commented 6 months ago

Throwing out some ideas in no particular order, and not even necessarily because I like all the options:

aaronpk commented 6 months ago

class is fine with me!

anderspitman commented 6 months ago

Trying to paraphrase and capture Elf's point in the CG call

Elf: what happens when the RP supports multiple "profiles" and so does the IdP? How do they choose one?

I think this is an important point. Let's say a user has registered two IdPs: one that supports IndieWeb and one that supports OIDC ID tokens like LastLogin. Then they navigate to an RP that has implemented both of those. How does the RP request to present the user with both options? Does it need to be a list of supported variants?

samuelgoto commented 6 months ago

class is fine with me!

I worry about class colliding with HTML's CSS class element attribute, because it is one of the first few things that a web developer learns.

aaronpk commented 6 months ago

I worry about class colliding with HTML's CSS class element attribute

Good point.

How does the RP request to present the user with both options? Does it need to be a list of supported variants?

Yeah I think a list is reasonable. I suspect there will be some overlapping types/classes/whatevers.

Hey variant is not a bad term for this...

npm1 commented 6 months ago

How is it colliding? The attribute would be in the configURL JSON file and in the JavaScript get() call. I guess you mean it could be confusing?

I'm fine with variant!

aaronpk commented 6 months ago

colliding in brain space

anderspitman commented 6 months ago

Might be because I do bioinformatics for my day job, but variant stuck out to me as well. So are we looking at something like this?

navigator.credentials.get({
  identity: {
    context: "signin",
    providers: [
      {
        variants: [
          "indieauth",
          "oidc-id-token",
        ],
        clientId: window.location.origin+"/"
      },
    ],
  },
})

Feels a bit weird having the list of providers be length 1 and then having a list of variants that imply a mapping to multiple possible providers...

samuelgoto commented 6 months ago

A few more options I heard from past discussions, so that we can look at all together:

samuelgoto commented 6 months ago

Might be because I do bioinformatics for my day job, but variant stuck out to me as well. So are we looking at something like this?

Yeah, that's the right exercise: to see how it would feel calling it from the RP.

Another example:

const credential = await navigator.credentials.get({
  identity: {
    context: "signin",
    providers: [{
        protocol: ["indieauth",  "oidc-id-token"],
        clientId: window.location.origin
      },
    ],
  },
})
aaronpk commented 6 months ago

Just for the sake of argument, oidc-id-token isn't actually a very likely value to use here, because that doesn't tell you why you'd trust this provider. Probably more likely in your case would be ["indieauth","solid-oidc"] since those both talk about why the RP would trust the provider.

samuelgoto commented 6 months ago

As a side node to the side note, maybe the reason we are struggling to agree on a name is because we don't know what role it plays yet. Maybe, once we have 3-4 instances of values it will take it will become clearer?

anderspitman commented 6 months ago

Just for the sake of argument, oidc-id-token isn't actually a very likely value to use here, because that doesn't tell you why you'd trust this provider. Probably more likely in your case would be ["indieauth","solid-oidc"] since those both talk about why the RP would trust the provider.

I'm certainly not married to the specific name, but I'm not sure I understand what you mean by "trust the provider" here?

aaronpk commented 6 months ago

Let's talk about some of the very wide range of different use cases we're trying to cover:

Now the error cases if we only used a generic protocol identifier like oidc-id-token, but with an RP that doesn't care what kind of IDP it talks to:

This is why this identifier is more than just a protocol identifier. It also indicates why the RP and IDP have a mutually agreeable trust relationship, even if that trust relationship is "I don't care who you are" like in the IndieAuth and solid-oidc cases.

anderspitman commented 6 months ago

Ah ok that makes sense. Thanks for the clarification. So maybe LastLogin would use something permissive sounding like any-oidc?

aaronpk commented 6 months ago

No my point is "any" isn't actually possible. If you got back literally any IDP, your request might fail because the IDP might have required client registration that you didn't do.

What I'd expect for the LastLogin use is actually something like this:

const credential = await navigator.credentials.get({
  identity: {
    context: "signin",
    providers: [
      {
        variant: ["indieauth",  "solid-oidc"],
        clientId: window.location.origin+"/id"
      },
      {
        configUrl: "https://accounts.google.com/gsi/fedcm.json",
        clientId: "your-google-client-id"
      },
      {
        configUrl: "https://github.com/fedcm.json",
        clientId: "your-github-client-id"
      }
    ],
  },
})
anderspitman commented 6 months ago

I think maybe we're in agreement but using different terminology. LastLogin may not implement solid-oidc because they will eventually require DPoP I believe, and I'm not sure the extra complexity is necessary in this case. So essentially any-oidc would represent an IdP that supports handing over a basic OIDC token, but doesn't make any other guarantees (the I don't care who you are category you mention above). So enterprise and bank IdPs wouldn't register as providing any-oidc because they have more stringent requirements. Solid servers wouldn't either because they require DPoP. IndieAuth servers wouldn't because they're completely different token formats.

aaronpk commented 6 months ago

ah gotcha, I see what you mean. So yeah the any-oidc would be another of these families/variants. Do you know of any providers that do this kind of "plain" OIDC right now, but accept any unregistered clients?

ThisIsMissEm commented 6 months ago

class is fine with me!

I'd avoid class because it's a reserved word in JavaScript

ThisIsMissEm commented 6 months ago

Would this work?


const credential = await navigator.credentials.get({
  identity: {
    context: "signin",
    providers: [{
        protocol: ["oidc-id-token"],
        clientId: window.location.origin
      },
      {
        protocol: ["indieauth"],
        clientId: window.location.origin
      }
    ],
  },
})
aaronpk commented 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 the clientId value can be the same anyway, which is definitely an edge case.

anderspitman commented 6 months ago

ah gotcha, I see what you mean. So yeah the any-oidc would be another of these families/variants.

My bad, I think I created additional confusion by not making it clear I was talking about a different variant.

Do you know of any providers that do this kind of "plain" OIDC right now, but accept any unregistered clients?

Not many other than LastLogin. It's one of the things I track in the table here. Portier and Rauthy both do, with Portier offering a hosted instance. Funny enough I originally got the idea from you.

IMO this functionality is critical for the self-hosting ecosystem. People should be able to use an identity provider to log in to their self-hosted services, without requiring every user to register a client. This is a core functionality LastLogin was designed to provide.

Note that on the protocol level, you can accomplish essentially the same thing by implementing dynamic client registration without requiring credentials. LastLogin supports this as well. Keeps the clients happy but doesn't actually store anything because it's privacy-focused and almost entirely stateless.

aaronpk commented 6 months ago

Yep totally agreed, Dynamic Client Registration works, but creates other problems and provides (almost) no value in that kind of ecosystem. If you want client instance authentication, just use DPoP instead. But this is veering off topic so we can have that discussion elsewhere 😄