Page MenuHomePhabricator

Review CORS strategy for WikimediaApiPortalOAuth extension
Closed, ResolvedPublic

Description

Problem statement

Wikimedia API Portal wiki is intended to serve as a central knowledge hub for developers using Wikimedia APIs. Additionally, the portal would provide the ability for developers to manage credentials for their registered OAuth consumers. The consumer management is being implemented in WikimediaApiPortalOAuth extension.

Architecture overview

WikimediaApiPortalOAuth extension will be enabled on api.wikimedia.org. The extension provides client-side interface to manipulate OAuth consumers. REST API is being added to the OAuth extension, exposing CRUD operations for OAuth consumers. This capability will be only enabled on the OAuth central wiki (meta.wikimedia.org) since on the database level the consumers are only stored on central wiki, and cross-tenant writes is not well supported in MW - e.g. it would be extremely hard to expose the oauth consumer CRUD API on any wiki other then central wiki, since we lack a bunch of necessary abstractions in core to safely execute the code in the context of another wiki.

In the patch linked above it's implemented by exposing proxy api on the portal wiki, and copying the user's cookie over, and calling central wiki over HTTP from the app server. This is done to avoid a CORS request from portal wiki to the central wiki. I do not think the reasons why this is pretty horrific need to be listed.

Proposal

Instead, we intend to make authenticated CORS requests from api.wikimedia.org to meta.wikimedia.org.
The plan:

  • On meta.wikimedia.org, ONLY for the required oath consumer CRUD endpoints, set Access-Control-Allow-Credentials: true. According to the spec, for CORS requests with credentials, Access-Control-Allow-Origin must be an exact match, so we would 'hardcode' it to api.wikimedia.org to restrict usage of consumer CRUD API to a single use-case.
  • The fronted code from api.wikimedia.org will set withCredentials on the Ajax request to meta.wikimedia.org, and will include the central auth cookies.

This theoretically should just work (TM), however I'm interested in any potential security concerns with this approach.

Event Timeline

Would it be possible to make Wikimedia API Portal an OAuth consumer itself? Basically that extension would use OAuth to authenticate into Meta and manage the developer's own OAuth apps?

Errr.. I mean use OAuth in order to manage the authorization with requests to Meta. I know it doesn't do the authentication. :)

It is conceptually possible, but AFAIK MediaWiki doesn't support being an OAuth client. There is an extension but it seems to be in a very early stage of development.

Properly supporting MW being an OAuth client of itself wold be
a) significantly harder then the proposed solution
b) IMHO very confusing for the users since the OAuth login flow is very different from normal MW login flow.

In general, are there downsides to CORS request with credentials that you see and what wold the benefit of making MW and oauth consumer of itself be?

It is conceptually possible, but AFAIK MediaWiki doesn't support being an OAuth client. There is an extension but it seems to be in a very early stage of development.

Properly supporting MW being an OAuth client of itself wold be
a) significantly harder then the proposed solution
b) IMHO very confusing for the users since the OAuth login flow is very different from normal MW login flow.

I wouldn't think MediaWiki itself needs to be an OAuth client, just need to go through the OAuth flow from within your extension (i.e. wherever you were going to use it). Seems no different than a tool on toolforge?

In general, are there downsides to CORS request with credentials that you see and what wold the benefit of making MW and oauth consumer of itself be?

Based on T232176#6400218, authenticated cross-origin requests will not be supported by the REST API. That includes between wikis on our wiki farm. If you need to have authenticated cross-origin requests, I would use OAuth.

Another thought I had... would it be possible to deploy your extension onto Meta and use it there instead of a new wiki? Alternatively, would it be possible to move the OAuth central wiki to this new wiki?

And another idea! You could do something similar to MediaWiki-extensions-CentralAuth and access another database in the cluster directly (I'm assuming the wiki is in the production cluster).

Another thought I had... would it be possible to deploy your extension onto Meta and use it there instead of a new wiki?

This is a no-go, API Portal wiki will have a different default skin, and VERY different permission settings. Plus, it being it's own portal is one of the product requirements.

Alternatively, would it be possible to move the OAuth central wiki to this new wiki?

This would not only be A LOT of unnecessary work, but will mix the concepts - api documentation portal is not a central wiki, might require moving CentralAuth central wikis over as well, will bring a lot of confusion and migration costs for community power users.

You could do something similar to MediaWiki-extensions-CentralAuth and access another database in the cluster directly (I'm assuming the wiki is in the production cluster)

We have studies this option extensively and I completely agree this would be the best solution in the perfect world. However, CentralAuth does cross-tenant reads, and it's possible to support that, but here we also need cross-tenant writes. Cross-tenant access abstractions in core are so poor, that it would take way too much work to support cross-tenant writes as well. We can't even properly get a config of another wiki.

Welp, given all that, I would either use OAuth (when you need to make authenticated requests, ask the user to authenticate to Meta) or see if T232176 can be resolved in a different direction. I don't have an opinion on cross-origin cookie requests, other than it should be limited to the existing allowlist. It does present some performance concerns (i.e. Vary: Origin) that I don't have a very good solution for.

Welp, given all that, I would either use OAuth (when you need to make authenticated requests, ask the user to authenticate to Meta) or see if T232176 can be resolved in a different direction. I don't have an opinion on cross-origin cookie requests, other than it should be limited to the existing allowlist. It does present some performance concerns (i.e. Vary: Origin) that I don't have a very good solution for.

So, the direction you're implementing could be the default. We can allow handlers to opt-in for a policy allowing CORS requests with cookie. Thus, we get the best of two worlds - do not loose a ton of performance for the majority of requests, only for the select few when we need. I do not see anything preventing us from having different policies for different routes

Thus, we get the best of two worlds - do not loose a ton of performance for the majority of requests, only for the select few when we need. I do not see anything preventing us from having different policies for different routes

I suppose that is true. You might also need to handle the OPTIONS request yourself for the same route. I think there is a security risk that developers wouldn't implement it correctly (it's not secure by design). But on the other hand, it would be limited in scope to the routes they themselves define.

To be honest, I don't see a problem with forcing OAuth on cross-origin clients (even trusted clients). It makes it explicit that the user is making a cross-origin authenticated request that the user can opt-out of. It makes it explicit what the client is attempting to access and how. Though one could argue that if it's a trusted origin, then the user should also trust that origin.

Let's take another example, should I be able to use the Content Translation tool on English without getting an OAuth token to the wiki I'm translating too? Or should I blindly be able to edit the other wiki with the cookie I have?

You might also need to handle the OPTIONS request yourself for the same route. I think there is a security risk that developers wouldn't implement it correctly (it's not secure by design). But on the other hand, it would be limited in scope to the routes they themselves define

Oh, what I have in mind is

protected Handler::needsCrossDomainAuthenticated() { return false; }

with developers overriding this. All the actual policy should be handled by the REST framework. Perhaps we'd want to the override slightly more elaborate to be able to limit the hosts beyond what the default allsowlist limits, TBD. The point is that developers will not be allowed to assign random CORS headers.

Let's take another example, should I be able to use the Content Translation tool on English without getting an OAuth token to the wiki I'm translating too?

I'm not an expert in this, but it seems like right now you are allowed to just publish your translation?

Oh, what I have in mind is

protected Handler::needsCrossDomainAuthenticated() { return false; }

with developers overriding this. All the actual policy should be handled by the REST framework. Perhaps we'd want to the override slightly more elaborate to be able to limit the hosts beyond what the default allsowlist limits, TBD. The point is that developers will not be allowed to assign random CORS headers.

I see. That makes more sense.

I'm not an expert in this, but it seems like right now you are allowed to just publish your translation?

Right now it does because it uses the Action API which allows cross-origin credential'd requests (from trusted origins). I'm not sure that is a good thing though.

@Pchelolo -

Reviewing the initial proposal within the task description and not any of the various thought experiments above:

On meta.wikimedia.org, ONLY for the required oath consumer CRUD endpoints, set Access-Control-Allow-Credentials: true. According to the spec, for CORS requests with credentials, Access-Control-Allow-Origin must be an exact match, so we would 'hardcode' it to api.wikimedia.org to restrict usage of consumer CRUD API to a single use-case.

So just the two in experimentalRoutes.json for now? And by 'hardcode' I assume you mean a global or similar config. Otherwise this seems minimally effective enough to enable secure CORS requests from api to meta. Though I'm also assuming TLS, sufficient protection for any non-Ajax requests, etc. which should all be happening. If all of this is true, I'd rate this as a low risk right now.

The fronted code from api.wikimedia.org will set withCredentials on the Ajax request to meta.wikimedia.org, and will include the central auth cookies.

Yes, this is how it should work, assuming all access will be through Ajax requests (as you imply).

One last note - I often have a look at tasks I'm subscribed to, but do not necessarily respond unless specifically asked to do so. The best way to make sure any requests get the visibility they deserve is to add the Security-Team project to the task. Worst-case scenario is that such tasks are triaged weekly.

sbassett triaged this task as Medium priority.Aug 31 2020, 6:41 PM
sbassett added a project: Security-Team.
sbassett moved this task from Incoming to Our Part Is Done on the Security-Team board.

So just the two in experimentalRoutes.json for now?

The endpoints I'm talking about are implemented in this CR, specifically, these 3 endpoints

And by 'hardcode' I assume you mean a global or similar config.

Yes, definitely a config variable, something like $wgOAuthConsumerAPICORSAllowList.. I am still working out in my head how would support for this look like in the REST Framework, but ultimately the 'hardcoded' origin will be coming from a config.

So just the two in experimentalRoutes.json for now?

The endpoints I'm talking about are implemented in this CR, specifically, these 3 endpoints

Yes, those are the ones I was referring to (I was treating /oauth2/client as one endpoint with two http verbs).

And by 'hardcode' I assume you mean a global or similar config.

Yes, definitely a config variable, something like $wgOAuthConsumerAPICORSAllowList.. I am still working out in my head how would support for this look like in the REST Framework, but ultimately the 'hardcoded' origin will be coming from a config.

Ok. Again, I'm basing my review just on what's been discussed in this task with the limited endpoints, restricting the Access-Control-Allow-Origin to api.w.o, etc. If there are potential, larger implications for other parts MediaWiki including the REST api, we'd want to consider the point @dbarratt made above:

Based on T232176#6400218, authenticated cross-origin requests will not be supported by the REST API. That includes between wikis on our wiki farm. If you need to have authenticated cross-origin requests, I would use OAuth.

Change 623610 had a related patch set uploaded (by Ppchelko; owner: Ppchelko):
[mediawiki/extensions/OAuth@master] Configurably allow CORS with credentials for /oauth/client routes.

https://gerrit.wikimedia.org/r/623610

Change 623610 abandoned by Ppchelko:
[mediawiki/extensions/OAuth@master] Configurably allow CORS with credentials for /oauth/client routes.

Reason:
We took a different approach

https://gerrit.wikimedia.org/r/623610

Change 630947 had a related patch set uploaded (by Cicalese; owner: Cicalese):
[operations/mediawiki-config@master] Add beta config for API Portal/OAuth communications

https://gerrit.wikimedia.org/r/630947

Change 630947 merged by jenkins-bot:
[operations/mediawiki-config@master] Add beta config for API Portal/OAuth communications

https://gerrit.wikimedia.org/r/630947