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

Skip the .well-known check for Registered IdPs #613

Open
anderspitman opened this issue Jun 9, 2024 · 34 comments
Open

Skip the .well-known check for Registered IdPs #613

anderspitman opened this issue Jun 9, 2024 · 34 comments

Comments

@anderspitman
Copy link

anderspitman commented Jun 9, 2024

Sorry if this is already covered somewhere, I did a search and read through several of the existing issues and I'm still uncertain.

I'm working on a feature for LastLogin that would allow users to bring their own domain and use it as an IdP on our servers. This works fine for OIDC/IndieAuth, but according to the FedCM docs, the /.well-known/web-identity file has to be served at eTLD+1 of a domain.

I read through #230 and I'm pretty sure I understand the gist of the reasoning for this requirement. However, this would prohibit my users from using a subdomain, ie idp.example.com. It's very likely they wouldn't want to commit an entire domain to their IdP, so this creates a rather large problem for me. Is there any way around this currently?

If not, since this feature would only be applicable for IdP registration (#240) cases anyway, could the act of registering an IdP be considered sufficient consent from the user to create an exception to the .well-known rule and allow hosting on a subdomain?

I suspect at least @zicklag, @erlend-sh, and @sebadob would also be interested in this functionality.

@aaronpk
Copy link

aaronpk commented Jun 9, 2024

I filed #580 as a suggestion for an alternative way to tie the provider to the eTLD+1

I'm curious about whether the IdP registration would be enough to avoid the tracking concerns that this requirement prevents though!

@samuelgoto
Copy link
Collaborator

We are aware it is a pretty unsatisfying constraint. We have a few options that aren't perfect, but could relax this some.

First, there is #552 allows multiple configURLs if you can have all of the IdPs share the same accounts endpoint. Would that help?

Second, @bvandersloot-mozilla lightweight credentials would also allow us to skip the .well-known file at least for the accounts endpoint.

Can you expand a bit more on your use case and give us a concrete sense of what your requirements are?

@sebadob
Copy link

sebadob commented Jun 10, 2024

I don't have huge issues about the eTLD+1. I just found it a bit confusing to build, because the web-identity is defined differently than in OIDC / Oauth, where you provide the endpoints not just with a relative path, but rather with a full URI. This would make it more flexible, because then you could host your web-identity on some host but point to another one for endpoints.
For instance, even Google itself does it with its openid-configuration: https://accounts.google.com/.well-known/openid-configuration

Edit:

Sorry I mixed up the web-identity and the config file with this. Just forget about it in this place, this would be another issue.

@anderspitman wouldn't this be solved in your case if users just host their own web-identity file on their domain, but refer to your IdP's URL?

@anderspitman
Copy link
Author

anderspitman commented Jun 10, 2024

@samuelgoto

First, there is #552 allows multiple configURLs if you can have all of the IdPs share the same accounts endpoint. Would that help?

I don't think this solves it. The core problem is that anything other than eTLD+1 isn't allowed at all. I need eTLD+2+ to work.

Can you expand a bit more on your use case and give us a concrete sense of what your requirements are?

For sure. I've now deployed this feature to the LastLogin staging server: https://lastlogin.net/login.

If you go there and make sure you have at least one identity, then click the "Domains" button to try this out.

If you add an eTLD+1 on that page, you should get full OIDC, IndieAuth, and FedCM+IndieAuth functionality on your domain. If you add an eTLD+2+, you only get OIDC and IndieAuth, and FedCM doesn't work, I'm assuming because of the well-known eTLD+1 rule.

@anderspitman
Copy link
Author

anderspitman commented Jun 10, 2024

@sebadob

@anderspitman wouldn't this be solved in your case if users just host their own web-identity file on their domain, but refer to your IdP's URL?

I believe this work technically work, but it's exactly the UX situation I'm trying to avoid. My target users may have no idea how to host a web page, let alone FedCM metadata. They may not even know how to set DNS records. By integrating LastLogin with my other service TakingNames.io, I can implement a system that lets users easily register a domain, and set up their own decentralized IdP, all without ever needing to understand DNS, TLS, HTTP, HTML, CSS, JavaScript, IPs, hosts, FedCM, etc, etc. But this will only be feasible if they can delegate a single subdomain to LastLogin. If they're required to use an eTLD+1, now every user has to buy at least 2 domains, assuming they want to delegate to any other services (and I hope they will).

@npm1
Copy link
Collaborator

npm1 commented Jun 10, 2024

To explain why this is not possible, I can recap the attack which is explained elsewhere. If we allowed well-known to be hosted in an arbitrary subdomain, then an IDP can track the user:

  • Set the well-known/configs to idp.domain.rp, a subdomain of idp.domain
  • In the rp-specific config file, set accounts endpoint to accounts.rp
  • When accounts fetch is performed, the fetch goes to accounts.rp, thus the IDP learns both the RP and the identity of the user before the user has gone through the FedCM flow. Game over.

I think lightweight credentials could be useful here (though I am not familiar with the latest status), or in general some account caching such that you do not need a credentialed fetch to show the FedCM UI.

@anderspitman
Copy link
Author

anderspitman commented Jun 10, 2024

@npm1 I agree this is important and needs to be mitigated. My question is whether #240 already mitigates this. Currently in order for a user to enable IdP registration for a domain, they have to click a button or similar and trigger a consent popup that says "login.anderspitman.com wants to use your accounts to login to websites". But it won't work because it's not an eTLD+1.

I'm wondering if clicking Allow on that popup could be considered sufficient interaction to bypass the eTLD+1 rule specifically for domains the user has consented to use for login. All other domains would still be subject to the current rules.

@anderspitman
Copy link
Author

There could even be an additional disclaimer in this case that says "This will allow login.anderspitman.com to know which apps you're logging in to".

@npm1
Copy link
Collaborator

npm1 commented Jun 10, 2024

My opinion is that IdP registration is not sufficient to mitigate this. A user agreeing to use an IdP should not imply that that IdP can know all sites the user visits, even those they do not log in to with that IdP.

@samuelgoto
Copy link
Collaborator

samuelgoto commented Jun 10, 2024

I filed #580 as a suggestion for an alternative way to tie the provider to the eTLD+1

As was noted earlier, and:

But this will only be feasible if they can delegate a single subdomain to LastLogin

If they can delegate a domain, your users must know how to set up CNAME DNS records.

If they can set up DNS records, would #580 suffice to you?

@samuelgoto
Copy link
Collaborator

If you go there and make sure you have at least one identity, then click the "Domains" button to try this out.

I'm going to give this a try!

If you add an eTLD+1 on that page, you should get full OIDC, IndieAuth, and FedCM+IndieAuth functionality on your domain. If you add an eTLD+2+, you only get OIDC and IndieAuth, and FedCM doesn't work, I'm assuming because of the well-known eTLD+1 rule.

This doesn't seem right: I have a FedCM+IndieAuth server running on https://sso.sgo.to, which is not a etld+1 (another example here: https://sso.mfoster.social).

The FedCM endpoints have to be same-site with the .well-known file, but they (as opposed to the .well-known file) don't have to be in the eltd+1.

Could you allow users of lastlogin to specify a subdomain (e.g. lastlogin.sgo.to)?

What's necessary is for the .well-known file to be in the eltd+1 (and for it to point to the endpoints), but the lastlogin endpoints don't have to be.

See examples here:

https://sso.sgo.to/test/fedcm.json
https://sgo.to/.well-known/web-identity

@anderspitman
Copy link
Author

anderspitman commented Jun 10, 2024

@npm1

even those they do not log in to with that IdP

Wait, how would non-logged-in sites be exposed?

@anderspitman
Copy link
Author

anderspitman commented Jun 10, 2024

@samuelgoto

If they can delegate a domain, your users must know how to set up CNAME DNS records.

Not necessarily! I don't think users should have to understand how any of this works. They should be able to do a quick OAuth flow to delegate control of a [sub]domain to a third party. See DomainConnect for an example of a protocol that can be used for this. Unfortunately that one ended up pretty big-tech focused. I developed my own protocol called NameDrop, which is implemented by TakingNames.io. It's much more friendly for indie hosters. See here for a demo video.

That said, maybe LastLogin can use the NameDrop access token to set up the proper DNS records as proposed in #580. It's still unclear to me whether that would be sufficient. I'll describe an example below and maybe you can help me decide.

The FedCM endpoints have to be same-site with the .well-known file, but they (as opposed to the .well-known file) don't have to be in the eltd+1.

This is still too constrained for what I'm trying to do. Here's an example:

  1. User has no experience web hosting, and wants to start a blog. They've heard that ghost.org is a good platform for this.
  2. They sign up for a Ghost account, and Ghost has a button that says "Add a custom domain with TakingNames.io".
  3. They click the button and are redirected using the NameDrop protocol (OAuth2-based) to TakingNames.io.
  4. They purchase example.com and are redirected back to ghost.org.
  5. The OAuth2 flow is completed to provide ghost.org with a NameDrop access token
  6. ghost.org uses the access token to set A and AAAA records for example.com pointing to ghost.org's servers
  7. User's website is now running at example.com

Sometime later, the user hears about how they can run their own IdP on LastLogin.

  1. They visit LastLogin, which provides a button that says "Add a custom domain with TakingNames.io"
  2. The redirect to TakingNames.io. They're already using example.com to host their blog, so they select login.example.com and redirect back
  3. LastLogin uses the access token to set up login.example.com.
  4. login.example.com works for OIDC and IndieAuth, but not FedCM.

The problem here is that example.com is completely managed by ghost.org. LastLogin has no control over what is hosted there. Potentially #580 could fix this if the user grants control to login.example.com and example.com for LastLogin, but now you have 2 third parties that could potentially interfere with each others' DNS configurations. This might be mitigated by only granting LastLogin control over a single TXT record for example.com, but now the UX becomes more cluttered and the implementation more complicated.

@samuelgoto
Copy link
Collaborator

samuelgoto commented Jun 10, 2024

Potentially #580 could fix this if the user grants control to login.example.com and example.com for LastLogin, but now you have 2 third parties that could potentially interfere with each others' DNS configurations. This might be mitigated by only granting LastLogin control over a single TXT record for example.com, but now the UX becomes more cluttered and the implementation more complicated.

Yeah, that's what I was imagining: ghost.com could grant to LastLogin the ability to change TXT (and even, specifically, a web-identity TXT record). I agree that the UX becomes more cluttered and the implementation more complicated, but we are running out of better ideas here.

We can't allow .well-known files under subdomains because, as you already figured out in #230, it can cause a tracking vector.

@bvandersloot-mozilla 's https://github.com/fedidcg/CrossSiteCookieAccessCredential is also another option that I think is worth exploring to solving this problem.

I'm curious about whether the IdP registration would be enough to avoid the tracking concerns that this requirement prevents though!

As @npm1 pointed out, this is likely not going to hold up, because it becomes a 1-prompt global permission (as opposed to for every RP/IdP pair) cross-site tracking vector: by accepting a single permission, the user is granting the ability for a website to track you across every site on the web (remember, we have to deal with abusive worse case scenarios, not well meaning IdPs). Game theoretically, every ad network is going to ask for the user's permission here, which pushes browsers to make the permission sufficiently scary in a race to the bottom, so we never explored this option much further.

@anderspitman
Copy link
Author

I agree that the UX becomes more cluttered and the implementation more complicated, but we are running out of better ideas here.

🫤

@bvandersloot-mozilla 's https://github.com/fedidcg/CrossSiteCookieAccessCredential is also another option that I think is worth exploring to solving this problem.

Thanks, I'll take a look.

As @npm1 pointed out, this is likely not going to hold up, because it becomes a 1-prompt global permission (as opposed to for every RP/IdP pair) cross-site tracking vector: by accepting a single permission, the user is granting the ability for a website to track you across every site on the web (remember, we have to deal with abusive worse case scenarios, not well meaning IdPs). Game theoretically, every ad network is going to ask for the user's permission here, which pushes browsers to make the permission sufficiently scary in a race to the bottom, so we never explored this option much further.

Want to make sure I understand the attack here:

  1. User visits ads.com
  2. ads.com generates a unique subdomain abcd.ads.com for the user and redirects to it
  3. User is prompted for FedCM registration and accepts
  4. User visits badapp.com, which makes a FedCM credentials.get request, which hits abcd.ads.com
  5. ads.com now knows user abcd visited badapp.com

Am I missing anything?

@npm1
Copy link
Collaborator

npm1 commented Jun 10, 2024

Wait, how would non-logged-in sites be exposed?

It is in my comment #613 (comment), the third step. To be more precise, it will learn about any site the user visits which invokes FedCM with that particular IdP type. This is all that is needed for us to have to send the credentialed fetch, which lets the IdP know about the user.

Want to make sure I understand the attack here:

  1. User visits ads.com
  2. ads.com generates a unique subdomain abcd.ads.com for the user and redirects to it
  3. User is prompted for FedCM registration and accepts
  4. User visits badapp.com, which makes a FedCM credentials.get request, which hits abcd.ads.com
  5. ads.com now knows user abcd visited badapp.com

Am I missing anything?

That's not the attack. I'm not sure how you go from (4) to (5), as the IdP would know the user ID abcd but would not know the RP badapp.com. The accounts fetch intentionally does not expose the RP. As a side note, the RP is not necessarily bad.

@anderspitman
Copy link
Author

You're right, my proposed attack makes no sense.

The attack you describe requires a separate subdomain for each RP, right? Wouldn't the user have to visit every subdomain for every RP and register FedCM in order to be tracked at each of those RPs?

@anderspitman
Copy link
Author

anderspitman commented Jun 10, 2024

If I'm wrong about that, would you mind giving a somewhat more detailed run through of the steps for this attack, in the context of the new type parameter and #240?

Sorry I'm not getting this. For some reason I'm finding it very difficult to reason about.

@samuelgoto
Copy link
Collaborator

samuelgoto commented Jun 10, 2024

If I'm wrong about that, would you mind giving a somewhat more detailed run through of the steps for this attack

Oh, wow, I think you are actually right! I was trying to write down the steps for this attack, but I think you may actually be right: there isn't any!

The IdP has to register the configURL ahead of time, without it knowing (because it is, as I said, ahead of time) which RP is going to request it, so it can't contain any information about the RP. And the RP, when it requests, uses any (which is also a value that is RP-agnostic), which gets expanded into the registered configURL.

So, the browser, while expanding the any request knows that the configURL doesn't have RP entropy in it, so it could entirely skip the .well-known file fetch and check.

This is quite exciting to me, because maybe it could turn obsolete #580 and #589, for registered IdPs.

Do you mind if I rename this issue "Skip .well-known for registered IdPs"? Else, I could kick another issue off to track this more generally for all registered IdPs (rather than, specifically, for multi-tenant situations).

in the context of the new type parameter and #240?

The type parameter added in #585 does add some complications, but I chatted briefly with @npm1 now and it seemed to us that, as long as we keep the type parameter away from the accounts_endpoint, there wouldn't be any problems.

Sorry I'm not getting this. For some reason I'm finding it very difficult to reason about.

There is a chance you are having a hard time convincing yourself because we are actually wrong :) Thanks for asking this thought provoking question and keep asking for clarification questions: my intuition at the moment is that you are right.

@npm1
Copy link
Collaborator

npm1 commented Jun 10, 2024

Yea apologies, I wrote the general attack but it does not apply to IdP registration since you only register one configURL at a time, so there is no way to encode the RP there ahead of time. I think I agree that skipping the well-known check seems feasible for registered IdPs. Let us know if this would be useful/solve your issue here

@aaronpk
Copy link

aaronpk commented Jun 10, 2024

Yes that would be excellent! I have the same plans to offer this as a service, and it would vastly simplify the setup to avoid the well-known!

It would also simplify the enterprise use case I am working on since we are often in a similar position where the company's marketing team or external agency runs the .com site and it's not always easy to get a static file hosted there much less with a specific content type header.

@npm1
Copy link
Collaborator

npm1 commented Jun 10, 2024

We were starting to party but @johannhof pointed out that the fact that the IdP can encode the user ID as per #613 (comment) could indeed be an issue. Then the client metadata fetch becomes essentially 'credentialed' (since the URL can encode the user ID) so it leaks both RP and user ID to the IdP in the same fetch (game over). Even if we got rid of this fetch (since the RP may want to provide their own PP/TOS when using registered IdPs), having the configURL be effectively credentialed makes our privacy much weaker. For instance, right now we do not need to show UI if the config fetch fails, but we would in this scenario (it would introduce a timing attack from the config fetch itself!). Therefore, we might need to go back to the drawing board...

@samuelgoto
Copy link
Collaborator

Oh, wow, I think you are actually right! I was trying to write down the steps for this attack, but I think you may actually be right: there isn't any!

Ok, @johannhof found a possible complication, which I think is avoidable, but a complication nonetheless: it turns every FedCM request into, effectively, a credentialed request, because the IdP can annotate the user id in it:

// Registers as a "foobar" type of provider
IdentityProvider.register("https://idp.example?user_id=1234")

So that, then, a colluding RP, could call:

navigator.credentials.get({
  identity: {
    providers: [{
      type: "foobar"
    }]
  }
});

Which would, at some point, lead to a request to the configURL (which has the user's ID).

If you look at the list of requests FedCM makes, the client_metadata_endpoint stands out as the only one that reveals to the IdP's server who is the RP prior to the user's explicit selection.

So, here is one attack:

  1. A tracker calls IdentityProvider.register("https://evil.example?user_id=1234") ahead of time
  2. A colluding RP calls navigator.credentials.get({identity: {providers: [{type: "foobar"}]}});
  3. The browser fetches https://evil.example?user_id=1234
  4. The tracker returns a list of endpoints, including the client_metadata_endpoint pointing to https://evil.example/client_metadata?user_id=1234
  5. The browser fetches the client metadata endpoint at https://evil.example/client_metadata?user_id=1234 and also passes the RP's origin
  6. The tracker now has gotten both the user's id and the RP's origin without the user acknowledgement

This specific attack could be addressed if we required something like #581.

But, more generally, it would make every single FedCM that is currently uncredentialed into a credentialed one, from a privacy thread model perspective.

Because of timing attacks, I think this complicates things a bit.

I think it is still plausible, but not as trivial as I hoped for.

@samuelgoto
Copy link
Collaborator

Ok, just thinking out loud here online ...

Because of timing attacks, I think this complicates things a bit.
I think it is still plausible, but not as trivial as I hoped for.

There are two options that occur to me:

(a) make the IdP register the entire payload of the configURL, so that we don't make any credentialed fetch or
(b) guarantee there is no invisible timing attack

The former would look like something like this:

IdentityProvider.register({
  accounts_endpoint: "...",
  id_assertion_endpoint: "...",
  types: ["foobar"],
  branding: { "..." }
});

This could work but would be unsatisfying in the sense that, e.g., it would require another call to add more types or changing branding, etc.

The latter is also plausible, but requires more thought.

@anderspitman
Copy link
Author

This turned into quite a roller coaster! But it sounds like there's still hope which is awesome. I'm glad you guys understand all the nuances a lot better than me.

As far as client metadata specifically goes, that never interested me and I'm glad it's optional, since otherwise I would have to fake it for LastLogin, since unregistered clients are a core feature.

For the general case, there's no way we can prevent IdPs from encoding a user ID while allowing subdomains, because even if browsers were to reject path/query in the URI, it could still be encoded in a subdomain part, right?

@anderspitman
Copy link
Author

anderspitman commented Jun 10, 2024

@samuelgoto

(a) make the IdP register the entire payload of the configURL, so that we don't make any credentialed fetch or
(b) guarantee there is no invisible timing attack

I believe (a) would be sufficient for me. Seems like a small price to pay for subdomain support for my case at least.

Also one less round trip, and you know how I feel about round trips.

@anderspitman
Copy link
Author

@samuelgoto

Do you mind if I rename this issue "Skip .well-known for registered IdPs"

Sorry forgot to answer this. Feel free to change the title to whatever you prefer.

@samuelgoto samuelgoto changed the title .well-known for multi-tenant situations Jun 11, 2024
@samuelgoto
Copy link
Collaborator

As far as client metadata specifically goes, that never interested me and I'm glad it's optional, since otherwise I would have to fake it for LastLogin, since unregistered clients are a core feature.

So, if skipping the client metadata (and the privacy policy and terms of service links) is cool with you, then I think we have a good solution here, because we can handle the silent timing attack.

To sum up, the proposal here is to:

  1. Skip the .well-known file checks when the RP asks for an IdP that has called IdentityProvider.register() ahead of time.
  2. Require that the configURL passed in the IdentityProvider.register() call doesn't contain a client_metadata_endpoint (possibly on IdentityProvider.register() and on get()) so that there is no endpoint that has both (a) the IdP's user ids and (b) the RP's origin
  3. Threat configURL fetches as credentialed in the worst case (e.g. by showing error UIs when it fails to load so that we don't run into a silent timing attack), since the IdP may have included their user's id in the IdentityProvider.register() call.

And with that, I think we would get:

  • No need to host a .well-known file for Registered IdPs
  • No fetching of the client_metadata_endpoint

The downsides are:

  • IdPs that register themselves in this structure wouldn't be able to be reused while called by configURL (because they don't expose a .well-known file).
@npm1
Copy link
Collaborator

npm1 commented Jun 11, 2024

Sounds mostly correct to me.

The downsides are:

  • IdPs that register themselves in this structure wouldn't be able to be reused while called by configURL (because they don't expose a .well-known file).

Hmm why is that? The IdP could still itself have well-known / client_metadata files if it wanted to, they just would not be used at all if the IdP is used as a registered IdP.

@anderspitman
Copy link
Author

This is fantastic. I believe it would cover everything I need. Thank you!

@npm1
Copy link
Collaborator

npm1 commented Jun 11, 2024

My only concern with bypassing the client metadata was that we had been thinking about using it to allow the IdP to decline showing UI on certain RPs. But maybe this feature is not needed for registered IdPs?

@anderspitman
Copy link
Author

Interesting use case. What's an example where an IdP would want to do that? Kind of feels like the IdP interfering with the RP UX.

@npm1
Copy link
Collaborator

npm1 commented Jun 12, 2024

If the IdP does not want it associated with... certain sites, they could choose to tell the browser that so that no UI is shown and the request will be rejected. I was trying to find the issue where this was requested but failed. But I promise I didn't just make it up :)

@anderspitman
Copy link
Author

Ah that makes sense. The IdP could still reject the login at a later stage, but the user might be left with the impression that the IdP is affiliated with the RP.

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