Page MenuHomePhabricator

Unable to perform POST request to REST API using mw.ForeignApi on Wikimedia sites
Open, HighPublicBUG REPORT

Description

Steps to replicate the issue (include links if applicable):

  • Navigate to a Wikimedia wiki (e.g., enwiki) and ensure you're logged in
  • Open you browser console and run the code snippet below.
const api = new mw.ForeignApi( 'https://pl.wikipedia.org/w/api.php' );
const rest = new mw.ForeignRest( 'https://pl.wikipedia.org/w/rest.php', api );

api.getToken( 'csrf' ).then( ( token ) => {
    const payload = {
        token,
        users: {
            "~2025-40161-15": {
                "revIds": ["78290827"],
                "logIds": [],
                "lastUsedIp": true,
                "abuseLogIds": []
            }
        }
    };
    rest.post( '/checkuser/v0/batch-temporaryaccount', payload ).then( console.log );
} );

(the checked temporary account is mine, feel free to reveal its IP policy-wise ;) )

What happens?:
A CORS error is reported in the browser console and the POST request fails.

image.png (248×1 px, 72 KB)

I haven't observed a request for CentralAuth token.

What should have happened instead?:
The request should have succeeded, and the response should be displayed in the browser console.

Software version (on Special:Version page; skip for WMF-hosted wikis like Wikipedia): 1.46.0-wmf.5 (rMW1c947f3d5936)

Other information (browser name/version, screenshots, etc.):
Tested in:

  • Microsoft Edge 143.0.3650.66
  • Firefox 146.0

Event Timeline

Restricted Application added a subscriber: Aklapper. · View Herald Transcript
Tgr renamed this task from Unable to perform CORS POST request to REST API to Unable to perform CORS POST request to REST API using mw.ForeignApi on Wikimedia sites.Dec 12 2025, 4:04 PM
Tgr renamed this task from Unable to perform CORS POST request to REST API using mw.ForeignApi on Wikimedia sites to Unable to perform POST request to REST API using mw.ForeignApi on Wikimedia sites.
Tgr subscribed.

CORS is currently disallowed but ForeignApi should be falling back to centralauthtoken and that's apparently not working.

The problem seems to be that mw.ForeignRest uses mw.ForeignApi.checkForeignLogin() to check whether it needs to use a centralauthtoken, and that check does succeed since it uses the action API (for which CORS is enabled via a separate setting).

Either we need a REST endpoint for checking whether the user is logged in (the REST version of action=query&meta=userinfo) or we need to get rid of $wgRestAllowCrossOriginCookieAuth and make sure that the Action API and REST API are always in sync wrt CORS support. The latter makes more sense to me but I'm probably missing the reason why it was done this way in the first place.

Change #1218719 had a related patch set uploaded (by Tchanders; author: Tchanders):

[operations/mediawiki-config@master] Add Special:GlobalContributions to no-IP reveal pages

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

For context, I believe the reason $wgRestAllowCrossOriginCookieAuth is disabled by default and prod, is for CDN performance and (by extent) the hardware capacity for MediaWiki on misses.

Background: When browsers make cross-origin requests, they automatically append an Origin header with the sender domain. This header is something we can trust server-side because, similar to User-Agent and other fixed headers, browsers don't let JavaScript override this via XHR or Fetch. MediaWiki uses this header to decide whether to allow a caller to act with browser cookies for the receiving domain (i.e. treat them as logged-in) or to treat them as logged-out and optionally bring their own credentials. This decision is based on $wgCrossSiteAJAXdomains.

In the Action API, it works like this:

  • support for CORS is opt-in via the origin= parameter. This means the majority of API requests (those that do not send this parameter, are not cross-domain, and not logged-in) enjoy a shared CDN cache. Whether the browser sends an Origin header, or what its value is, does not change our response, and does not fragment the CDN cache.
  • There is no use case for sending this parameter from a third-party site, since those are never allowed in $wgCrossSiteAJAXdomains.
  • The small subset that specifies an origin= parameter naturally has a different URL, and so doesn't utilize that cache, but that's okay because those will generally be logged-in requests between our own domains, which aren't cachable anyway.

In the REST API, it was seemingly implemented without such parameter, relying solely on the Origin header (via wgRestAllowCrossOriginCookieAuth, disabled by default) or bringing your own credentials (i.e. OAuth, or centralauthtoken=).

  • The wgRestAllowCrossOriginCookieAuth feature thus requires emitting a Vary: Origin header to avoid mixing/sharing responses for other domains.
  • However, this also means that for the majority of logged-out calls from third-party domains, we'd be fragmenting the cache to a point where it likely isn't very effective, because the same rest.php call from domain A and domain B would not be sharing a CDN cache, since they'd have a different Origin header.

Either we need a REST endpoint for checking whether the user is logged in (the REST version of action=query&meta=userinfo) or we need to get rid of $wgRestAllowCrossOriginCookieAuth and make sure that the Action API and REST API are always in sync wrt CORS support. The latter makes more sense to me but I'm probably missing the reason why it was done this way in the first place.

There could be a rest.php version of action=query&meta=userinfo, though it's awkward to have a resource where the contents depend so heavily on the user (not just stuff being hidden due to permissions, but the content itself). There isn't an obvious trustable token/hash, to include in the route path, that represents the "(cookie) client credentials sent from origin X to site Y". You cant even put such a thing in the URL since it would unsafe and beg the question (would the browser actually send this). Without such a path component, these kind of special resources don't fit into a pure RESTful style. Maybe they could be thought of as being under fully qualified paths, but being a "mirror-like" resources, only supporting the QUERY verb, with credentials being a part of the "query" too...a bit of a slight of hand.

Alternatively, I wonder if mw.ForeignRest could just always get the centralauthtoken (or maybe also check AllowCrossOriginCookieAuth via an api exposing config, but that's a whole extra request just to catch the metawiki case). I'm not sure what happens when the token is used but there is also either of the following:

  • Cookie login via MW/SUL3
  • Authentication login via centralauthtoken
  • Authentication via OAuth bearer token

...does that cause log spam or errors? It seems ugly to send redundant credentials. The session data could be different, so "redundant" isn't even the right word. The RESTful login info approach sounds less messy.

As far as CDN caching concerns ago, I'd be a bit weary of replacing wgRestAllowCrossOriginCookieAuth with "always on" behavior. On the other hand, I see a big TODO in CoreUtils::modifyResponse:

			// @TODO Since we only Vary the response if (1) the method is OPTIONS or (2) the user is
			//       registered, it is safe to only add the Vary: Origin when those two conditions
			//       are met since a response to a logged-in user's request is not cachable.
			//       Therefore, logged out users should always get `Access-Control-Allow-Origin: *`
			//       on all non-OPTIONS request and logged-in users *may* get
			//      `Access-Control-Allow-Origin: <requested origin>`

...is that worth exploring more? It seems like it would help.

Alternatively, I wonder if mw.ForeignRest could just always get the centralauthtoken

I was also thinking that. It may not be the most efficient solution, but it will work and it's correct.

I'm not sure what happens when the token is used but there is also either of the following:

  • Cookie login via MW/SUL3

The token takes priority. Also, mw.ForeignRest has some code to avoid sending both kinds of authorization data at the same time.

  • Authentication login via centralauthtoken

I'm not sure what you mean – that's the same token?

  • Authentication via OAuth bearer token

In the REST API, both kinds of tokens go in the Authorization header, and there can be only one, so there can't be a conflict.

There could be a rest.php version of action=query&meta=userinfo, though it's awkward to have a resource where the contents depend so heavily on the user (not just stuff being hidden due to permissions, but the content itself). There isn't an obvious trustable token/hash, to include in the route path, that represents the "(cookie) client credentials sent from origin X to site Y". You cant even put such a thing in the URL since it would unsafe and beg the question (would the browser actually send this). Without such a path component, these kind of special resources don't fit into a pure RESTful style. Maybe they could be thought of as being under fully qualified paths, but being a "mirror-like" resources, only supporting the QUERY verb, with credentials being a part of the "query" too...a bit of a slight of hand.

You could just put the username in the path and it would return a 403 if the current user does not match the path user. But TBH I don't think there is much value to the "philosophical" side of RESTfulness. We have plenty of endpoints already which don't represent resources in a truly RESTful way (e.g. most of the OAuth-related endpoints), and so do all other large websites. Some are even required by specifications, like the OIDC userinfo endpoint. Pure RESTful is just not a particularly useful approach to organizing things for APIs that don't specifically deal with content. So I'd just go with /user/current or something like that.

Alternatively, I wonder if mw.ForeignRest could just always get the centralauthtoken

It could, but then you lose all the potential performance advantages of using a REST API, since you'll need to make a blocking action API request before every REST request (centralauthtokens are not reusable).

(or maybe also check AllowCrossOriginCookieAuth via an api exposing config, but that's a whole extra request just to catch the metawiki case)

Potentially a whole set of extra requests since the check itself would have to be cross-site, and might come with its own userinfo request and centralauthtoken request.

I'm not sure what happens when the token is used but there is also either of the following:

  • Cookie login via MW/SUL3
  • Authentication login via centralauthtoken
  • Authentication via OAuth bearer token

...does that cause log spam or errors?

Two session providers with the same priority cause an error. That would be the case for centralauthtoken + OAuth but I don't see how that would happen in practice as MediaWiki JS code doesn't use OAuth.
Cookies are lower priority and will just be ignored.

As far as CDN caching concerns ago, I'd be a bit weary of replacing wgRestAllowCrossOriginCookieAuth with "always on" behavior. On the other hand, I see a big TODO in CoreUtils::modifyResponse:

			// @TODO Since we only Vary the response if (1) the method is OPTIONS or (2) the user is
			//       registered, it is safe to only add the Vary: Origin when those two conditions
			//       are met since a response to a logged-in user's request is not cachable.
			//       Therefore, logged out users should always get `Access-Control-Allow-Origin: *`
			//       on all non-OPTIONS request and logged-in users *may* get
			//      `Access-Control-Allow-Origin: <requested origin>`

...is that worth exploring more? It seems like it would help.

Can you add Vary for some requests to a given URL but not others? In theory it seems fine since we vary on login status and I assume CDNs vary on the request method by default, but not sure if Varnish is smart enough to figure things out.

In the REST API, both kinds of tokens go in the Authorization header, and there can be only one, so there can't be a conflict.

I suppose there could be two Authorization headers, although it's not spec compliant. Not sure how that would even appear on the PHP side.

Change #1219942 had a related patch set uploaded (by Bartosz Dziewoński; author: Bartosz Dziewoński):

[mediawiki/extensions/CentralAuth@master] ForeignRest: Always use 'centralauthtoken' for auth'd requests

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

You could just put the username in the path and it would return a 403 if the current user does not match the path user.

Yeah, in this case, I guess we know what name to look for given CentralAuth-style SUL and based on the current wiki and the mw.* JS values.

But TBH I don't think there is much value to the "philosophical" side of RESTfulness. We have plenty of endpoints already which don't represent resources in a truly RESTful way (e.g. most of the OAuth-related endpoints), and so do all other large websites. Some are even required by specifications, like the OIDC userinfo endpoint. Pure RESTful is just not a particularly useful approach to organizing things for APIs that don't specifically deal with content. So I'd just go with /user/current or something like that.

I'm OK with this if including a username component in the URL (as above) is not adequate and CDN vary tricks won't work. It's not a deal breaker, but somethings that is nice to follow whenever possible (especially since people tend to copy existing examples).

As far as CDN caching concerns ago, I'd be a bit weary of replacing wgRestAllowCrossOriginCookieAuth with "always on" behavior. On the other hand, I see a big TODO in CoreUtils::modifyResponse:

			// @TODO Since we only Vary the response if (1) the method is OPTIONS or (2) the user is
			//       registered, it is safe to only add the Vary: Origin when those two conditions
			//       are met since a response to a logged-in user's request is not cachable.
			//       Therefore, logged out users should always get `Access-Control-Allow-Origin: *`
			//       on all non-OPTIONS request and logged-in users *may* get
			//      `Access-Control-Allow-Origin: <requested origin>`

...is that worth exploring more? It seems like it would help.

Can you add Vary for some requests to a given URL but not others? In theory it seems fine since we vary on login status and I assume CDNs vary on the request method by default, but not sure if Varnish is smart enough to figure things out.

Judging from https://github.com/varnishcache/varnish-cache/blob/9e5b0d1de7b42626966a4e0c9bd10dd33beaba2a/bin/varnishd/cache/cache_hash.c#L510 and https://github.com/varnishcache/varnish-cache/blob/9e5b0d1de7b42626966a4e0c9bd10dd33beaba2a/bin/varnishd/cache/cache_vary.c , it looks like Varnish keeps Vary header info for each object in the object hash-chain for a given (Host: value,URL). This makes sense, as the origin software might change what Vary header is emitted for a given URL even if it goes from one stable set of headers to the new stable set of headers. So, I think the TODO comment is doable. Someone like @BBlack could confirm this, however.