Page MenuHomePhabricator

Choosing client credentials grant for OAuth 2 results in an access token (JWT) with the 'sub' field empty
Closed, ResolvedPublicBUG REPORT

Description

Steps to replicate:

The access token will work (an invalid access token would result in an API error) but you will be the anonymous user.

Might or might not be related to T407655: Document how access tokens with an oaac_id of 0 are used.

Related Objects

Mentioned In
T420297: Allow OAuth 2 apps using client credentials flow to perform actions as the app's owner (i.e. be logged in)
T418957: Add client-side logging for non-MediaWiki action API errors (HTTP 429)
T419921: TypeError: MediaWiki\Extension\OAuth\ResourceServer::getUser(): Return value must be of type MediaWiki\User\User, false returned
T414338: FY25-26 WE5.4.12: Identify the provenance of image requests
T419107: The PHPUnit config override does not appear to be auto-generated
T418991: JWT tokens issued with empty subject
T407987: Define best practice for single-user apps which need a high MediaWiki API rate limit
T417839: Editing using OAuth 2 doesn’t work
T417820: TypeError: MediaWiki\Extension\OAuth\Entity\AccessTokenEntity::setUserIdentifier(): Argument #1 ($identifier) must be of type string, null given, called in AccessTokenEntity.php
T407655: Document how access tokens with an oaac_id of 0 are used
Mentioned Here
T420297: Allow OAuth 2 apps using client credentials flow to perform actions as the app's owner (i.e. be logged in)
T414338: FY25-26 WE5.4.12: Identify the provenance of image requests
T418957: Add client-side logging for non-MediaWiki action API errors (HTTP 429)
T419921: TypeError: MediaWiki\Extension\OAuth\ResourceServer::getUser(): Return value must be of type MediaWiki\User\User, false returned
T419107: The PHPUnit config override does not appear to be auto-generated
T407987: Define best practice for single-user apps which need a high MediaWiki API rate limit
T412214: Ensure a good experience for apps which want to use OAuth credentials for a long time (refresh token grace period)
T407655: Document how access tokens with an oaac_id of 0 are used

Event Timeline

Restricted Application added a subscriber: Aklapper. · View Herald Transcript

The relevant call chain is

  • ClientCredentialsGrant->respondToAccessTokenRequest() calls ClientCredentialsGrant->issueAccessToken() with userIdentity hardcoded as null (this is third-party code)
  • that calls AccessTokenRepository->getNewToken()
  • that constructs an AccessTokenEntity

Somewhere in that chain, we need to look up the actual user ID. Probably in the AccessTokenEntity constructor we need to end up on the same code path as owner-only consumers.

(Should probably review where else client credentials and owner-only behavior needs to be brought in sync.)

Change #1240048 had a related patch set uploaded (by Gergő Tisza; author: Gergő Tisza):

[mediawiki/extensions/OAuth@master] Fix 'sub' field in access tokens obtained via client credentials

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

This blocks T412214: Ensure a good experience for apps which want to use OAuth credentials for a long time (refresh token grace period) if we want to solve it with client credentials (which we probably do).

The patch fixes the user identity issue but the access token still doesn't work because there is no ConsumerAcceptance record. There are two ways to fix that:

  • automatically create such a record when the consumer is created, like for owner-only apps
  • do not require a ConsumerAcceptance record if the app has the client credentials grant and the user is the owner (along the lines of the current exception for user ID = 0 access tokens in SessionProvider)

The first is cleaner; the problem with it (and to some extent with both) is that the same app can have both the client credentials grant and the authorization code grant. If we create a ConsumerAcceptance whenever the app is created, that will interact with the authorization code flow in weird and confusing ways (e.g. make it hard to test such consumers). Maybe we should disallow having both the client credentials and authorization code grants? We'd still have to deal with existing apps, though. I wonder how other sites handle this.

Spent some time testing this today, and below are my observations after creating an OAuth2 consumer with the "client credentials" grant:

Ran the following cURL command or via the RestSandbox special page:

curl -X 'POST' \
  'http://metawiki.mediawiki.local.wmftest.net:8082/w/rest.php/oauth2/access_token' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'grant_type=client_credentials&client_id=c2dd9fc82d9ed22c6013136a75d9d75a&client_secret=d5fc300e0491a6cd17ab206465bf279bc557e4ac&redirect_uri=http%3A%2F%2Flocalhost%3A3000&scope=&code=&refresh_token=&code_verifier='

Before the patch and consumer is not yet authorized
I ran into the error as reported:

{
  "error": "server_error",
  "error_description": "The authorization server encountered an unexpected condition which prevented it from fulfilling the request: MediaWiki\\Extension\\OAuth\\Entity\\AccessTokenEntity::setUserIdentifier(): Argument #1 ($identifier) must be of type string, null given, called in /var/www/html/w/extensions/OAuth/src/Entity/AccessTokenEntity.php on line 79"
}

After the patch and consumer is not yet authorized

{
  "token_type": "Bearer",
  "expires_in": 3600,
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJjMmRkOWZjODJkOWVkMjJjNjAxMzEzNmE3NWQ5ZDc1YSIsImp0aSI6IjhiMTg5OTM0MjRmYzhmNWYxMDg3OWUyNTk4MzQxZTIzNjkzODZjNmYwYTg4YTVmNzVhOGQ4NmM4MDgxMmQ5ZWJlMzQ4YTY2MzA1YzJlYzc0IiwiaWF0IjoxNzcxNDA4MDc1LjM2NDk3NSwibmJmIjoxNzcxNDA4MDc1LjM2NDk4NiwiZXhwIjoxNzcxNDExNjc1LjM1MDM1Miwic3ViIjoiNSIsInNjb3BlcyI6WyJiYXNpYyJdfQ.dJGTlNaR4y_1Ax0R768pYbt_2zRkwZtMDYw-eXBsK-X0pxest-Ypzccz4i3OaW1_9hpoGbmcTsIUXcA3Wz-cXY56vsGYU9Wo-BTe5PE2NMPTxGQka4XmfclmA1SdenohWXpuqnjzGTATzK_SioUUyHfILovAPtwGvP3eM2ZEu0C77DG0LChhF2QUMlP09LUszGiG01O-Wd4WYkMazHuqTustyi6MNo7aS18-bQND-7jPaFtUbreCGQ5FEAs66WnCZw0l3DcuCgzeV60F94MdC-1uXG72myn3potJaFQiaXvkAWYwuRCmTXWcQdA5Y7rKnUIqK7a9FexMjkzq0hLVfQ"
}
curl -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJjMmRkOWZjODJkOWVkMjJjNjAxMzEzNmE3NWQ5ZDc1YSIsImp0aSI6IjhiMTg5OTM0MjRmYzhmNWYxMDg3OWUyNTk4MzQxZTIzNjkzODZjNmYwYTg4YTVmNzVhOGQ4NmM4MDgxMmQ5ZWJlMzQ4YTY2MzA1YzJlYzc0IiwiaWF0IjoxNzcxNDA4MDc1LjM2NDk3NSwibmJmIjoxNzcxNDA4MDc1LjM2NDk4NiwiZXhwIjoxNzcxNDExNjc1LjM1MDM1Miwic3ViIjoiNSIsInNjb3BlcyI6WyJiYXNpYyJdfQ.dJGTlNaR4y_1Ax0R768pYbt_2zRkwZtMDYw-eXBsK-X0pxest-Ypzccz4i3OaW1_9hpoGbmcTsIUXcA3Wz-cXY56vsGYU9Wo-BTe5PE2NMPTxGQka4XmfclmA1SdenohWXpuqnjzGTATzK_SioUUyHfILovAPtwGvP3eM2ZEu0C77DG0LChhF2QUMlP09LUszGiG01O-Wd4WYkMazHuqTustyi6MNo7aS18-bQND-7jPaFtUbreCGQ5FEAs66WnCZw0l3DcuCgzeV60F94MdC-1uXG72myn3potJaFQiaXvkAWYwuRCmTXWcQdA5Y7rKnUIqK7a9FexMjkzq0hLVfQ' 'http://metawiki.mediawiki.local.wmftest.net:8082/w/api.php?format=json&action=query&meta=userinfo'

// output
{"error":{"code":"mwoauth-invalid-authorization","info":"The authorization headers in your request are not valid: Cannot create access token, user did not approve issuing this access token","*":"See http://metawiki.mediawiki.local.wmftest.net:8082/w/api.php for API usage. Subscribe to the mediawiki-api-announce mailing list at <https://lists.wikimedia.org/postorius/lists/mediawiki-api-announce.lists.wikimedia.org/> for notice of API deprecations and breaking changes."},"servedby":"12c7b9adee94"}

I paused briefly and authorized the consumer (which I would have already done). Is this step necessary @Tgr? I think it is necessary.

After the patch with the consumer authorized
After authorizing the OAuth2 application, I ran the following command (again) to get a new access_token:

curl -X 'POST' \
  'http://metawiki.mediawiki.local.wmftest.net:8082/w/rest.php/oauth2/access_token' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'grant_type=client_credentials&client_id=c2dd9fc82d9ed22c6013136a75d9d75a&client_secret=d5fc300e0491a6cd17ab206465bf279bc557e4ac&redirect_uri=http%3A%2F%2Flocalhost%3A3000&scope=&code=&refresh_token=&code_verifier='

With the new access token in hand, I re-ran the following:

curl -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJjMmRkOWZjODJkOWVkMjJjNjAxMzEzNmE3NWQ5ZDc1YSIsImp0aSI6IjI4MjcxNDU1ZTJlYjkzZjBkYWQzZjQ4ZGM1MDlmZmYwN2Y2MzFhMzI2YjQ1NGU3MGNmOTI1ZmEyYTE0YTI0ZGQ0NjNjOTc5ZGE2N2QwOTQ0IiwiaWF0IjoxNzcxNDA4NjUwLjE3MDYyNSwibmJmIjoxNzcxNDA4NjUwLjE3MDYzMiwiZXhwIjoxNzcxNDEyMjUwLjE2MDYzNCwic3ViIjoiNSIsInNjb3BlcyI6WyJiYXNpYyJdfQ.uuUpybYDmppD-UFnDVhz5SOkC-iW1zo3mohdVx8xe5c9taqqD3-RlxBs07BVsx6z6-aXwZeN9uQKG-CdNyAfgGDbgZpgxPtUn7dqok0wlIwVl9AoYYTZNTKwVsfoIFCSAPhk2yi8klgdlUyheit_Zs299QwgAcgTsYbjhcCl_yes47TDArQ6OkYgTrLs11G7ajv-isrQTONFPxginkZGd3ddp662VvHqr-gG1Tak9w-RL9h86dBLkdXbM7g-i9VrXxK37_g26ycPSxlZ-b2B5MoC5ztoLWID2Jby_09pfDN6eVAXVmt4MtDELk-QaDW-BG0ZAKCxAT93p23qN9QP-Q' 'http://metawiki.mediawiki.local.wmftest.net:8082/w/api.php?format=json&action=query&meta=userinfo'

// output
{"batchcomplete":"","query":{"userinfo":{"id":3,"name":"Admin"}}}

Conclusion

  • The type error that was thrown is no longer happening since the identifier (with the patch applied) is no longer NULL, but the user ID of the user associated with the access token.
  • The output of the last cURL request resulted in valid user information returned by the API rather than an anonymous user, as noted in the task description. Is there something I'm missing in my testing? Could this be related to the consumer authorization step above?

I paused briefly and authorized the consumer (which I would have already done). Is this step necessary @Tgr? I think it is necessary.

It's not supposed to be, the whole idea of the client credentials flow is that you don't need any user interaction to authorize it. But that's a somewhat different problem, and I'm not sure what's the best way to solve it, so I'm fixing it in a separate patch.

I may be late here and/or misunderstanding, but… why is it a problem that it results in an anonymous user?

From what I've read about the client credentials grant, it's intended that it is not associated with any user, because it's instead associated with the client (aka the app).

In our environment this doesn't permit you to do anything that you couldn't do by just accessing the wiki completely unauthenticated, so it's a bit pointless. I'm not sure why we support it.

After looking around other tasks, I am guessing that the idea here is to use the client credentials grant as a modern replacement for bot passwords and/or owner-only consumers?

If so, I think this may be surprising for developers. I wouldn't immediately think that using the client credentials grant makes the app use its owner's account's permissions. I only did a tiny bit of research, but I haven't seen it used that way elsewhere.

If we want to use it that way, then I would agree with T417278#11625679 that we should disallow having both the client credentials and authorization code grants (and for existing apps, the the client credentials grant should continue doing nothing), and clearly document that's how we use it.

From what I've read about the client credentials grant, it's intended that it is not associated with any user, because it's instead associated with the client (aka the app).

The RFC says

The client can request an access token using only its client credentials (or other supported means of authentication) when the client is requesting access to the protected resources under its control, or those of another resource owner that have been previously arranged with the authorization server (the method of which is beyond the scope of this specification).

OAuth is an authorization protocol, not an authentication one, so it doesn't say anything about whether the client should be associated with a user, but I think "requesting access to the protected resources ... of another resource owner that have been previously arranged with the authorization server" matches our owner-only use case.

You are certainly right that this not how other sites typically use it, though, and it would be confusing. At a minimum we would have to rethink the UI.
Let's put this on hold until @JTweed-WMF is back and then have a discussion about it.

In our environment this doesn't permit you to do anything that you couldn't do by just accessing the wiki completely unauthenticated, so it's a bit pointless. I'm not sure why we support it.

OAuthRateLimiter can control throttling in the old API gateway, so maybe the plan was to use it for high-rate reads. (All the actual clients that make use of the OAuthRateLimiter mechanism are owner-only, though.)

There are only five apps which have the client credentials grant but no other one, FWIW. But the wider context here is that we need some solution for bots who need high Envoy rate limits (T407987: Define best practice for single-user apps which need a high MediaWiki API rate limit), and client credentials seemed like the way to go, but for that you do need a JWT tied to the user.

(…) I think "requesting access to the protected resources ... of another resource owner that have been previously arranged with the authorization server" matches our owner-only use case.

Yeah, you're right, it seems like an acceptable way of arranging access.

We will have to make it clear in the client registration form that this is what we mean by "client credentials".

I wonder if we should make client credentials and authorization code mutually exclusive (for new clients)? On the one hand, I could imagine a web-based tool making edits both on behalf of other users and on behalf of itself (for example: imagine a tool that lets you move categories, performing this action under your name, and then transfer the category members, performing those edits using a bot account). On the other hand, it could lead to mistakes where you would accidentally let others edit on behalf of yourself.

I think if we go this way, we'd want them mutually exclusive because client-credentials apps are conceptually like owner-only apps, so human review could be relaxed. Also we might want a more dedicated registration UI. If someone needs both client credentials and authorization code flow, it's not too much hassle to register two separate consumers.

Two aspects to this:

  • the request will be anonymous, which is probably a bad thing but changing it without warning for existing clients might have unwanted effects (unlikely that anyone would do a logged action with an anonymous client but we should still check first)
  • the sub field will be empty, so all such clients get bucketed together for rate limiting - this is clearly bad and fixing it would be harmless

So if we can easily fill the sub field without actually turning these clients non-anonymous, we should do that now as a quick fix.

Change #1248530 had a related patch set uploaded (by Gergő Tisza; author: Gergő Tisza):

[mediawiki/extensions/OAuth@master] Set 'sub' JWT field in client credentials access tokens

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

Fixing this (fully, not just for the sub field) is straightforward, but we need to consider what to do with the existing clients. Your client suddenly switching from being anonymous to using your user account is surprising. Probably no one uses client credentials to do edits (because why would you authenticate just to do anonymous edits?) but we might want to double-check usage nevertheless.

Change #1248530 merged by jenkins-bot:

[mediawiki/extensions/OAuth@master] Set 'sub' JWT field in client credentials access tokens

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

Change #1251087 had a related patch set uploaded (by Gergő Tisza; author: Gergő Tisza):

[mediawiki/extensions/OAuth@wmf/1.46.0-wmf.18] Set 'sub' JWT field in client credentials access tokens

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

Change #1251088 had a related patch set uploaded (by Gergő Tisza; author: Gergő Tisza):

[mediawiki/extensions/OAuth@wmf/1.46.0-wmf.19] Set 'sub' JWT field in client credentials access tokens

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

Change #1251087 merged by jenkins-bot:

[mediawiki/extensions/OAuth@wmf/1.46.0-wmf.18] Set 'sub' JWT field in client credentials access tokens

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

Change #1251088 merged by jenkins-bot:

[mediawiki/extensions/OAuth@wmf/1.46.0-wmf.19] Set 'sub' JWT field in client credentials access tokens

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

Mentioned in SAL (#wikimedia-operations) [2026-03-12T20:35:55Z] <tgr@deploy2002> Started scap sync-world: Backport for [[gerrit:1251087|Set 'sub' JWT field in client credentials access tokens (T417278)]], [[gerrit:1251088|Set 'sub' JWT field in client credentials access tokens (T417278)]], [[gerrit:1251106|phpunit: Avoid unnecessary writes in generatePHPUnitConfig.php (T419107)]]

Mentioned in SAL (#wikimedia-operations) [2026-03-12T20:37:42Z] <tgr@deploy2002> tgr, daimona: Backport for [[gerrit:1251087|Set 'sub' JWT field in client credentials access tokens (T417278)]], [[gerrit:1251088|Set 'sub' JWT field in client credentials access tokens (T417278)]], [[gerrit:1251106|phpunit: Avoid unnecessary writes in generatePHPUnitConfig.php (T419107)]] synced to the testservers (see https://wikitech.wikimedia.org/wiki/Mwdebug). Changes can now be verified there.

Mentioned in SAL (#wikimedia-operations) [2026-03-12T20:43:32Z] <tgr@deploy2002> Finished scap sync-world: Backport for [[gerrit:1251087|Set 'sub' JWT field in client credentials access tokens (T417278)]], [[gerrit:1251088|Set 'sub' JWT field in client credentials access tokens (T417278)]], [[gerrit:1251106|phpunit: Avoid unnecessary writes in generatePHPUnitConfig.php (T419107)]] (duration: 07m 37s)

Change #1251987 had a related patch set uploaded (by Gergő Tisza; author: Gergő Tisza):

[mediawiki/extensions/OAuth@master] Fix client credentials access tokens

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

Thanks for looking into this, everyone! There's a user on the API Portal who is reporting that the token they retrieved using their client credentials is no longer working. See their talk page post here. I see there's a recent patch up with a fix, so I'll keep an eye on that so I can update them.

Change #1251987 merged by jenkins-bot:

[mediawiki/extensions/OAuth@master] Fix client credentials access tokens

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

Change #1253623 had a related patch set uploaded (by Bartosz Dziewoński; author: Gergő Tisza):

[mediawiki/extensions/OAuth@wmf/1.46.0-wmf.19] Fix client credentials access tokens

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

Change #1253623 merged by jenkins-bot:

[mediawiki/extensions/OAuth@wmf/1.46.0-wmf.19] Fix client credentials access tokens

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

Mentioned in SAL (#wikimedia-operations) [2026-03-16T20:57:26Z] <catrope@deploy2002> Started scap sync-world: Backport for [[gerrit:1253623|Fix client credentials access tokens (T417278 T419921)]], [[gerrit:1253625|Enable $wgTrackMediaRequestProvenance on testwikis and beta cluster (T414338)]], [[gerrit:1253626|Configure $wgApiClientErrorSampleRate (T418957)]]

Mentioned in SAL (#wikimedia-operations) [2026-03-16T20:59:17Z] <catrope@deploy2002> matmarex, catrope: Backport for [[gerrit:1253623|Fix client credentials access tokens (T417278 T419921)]], [[gerrit:1253625|Enable $wgTrackMediaRequestProvenance on testwikis and beta cluster (T414338)]], [[gerrit:1253626|Configure $wgApiClientErrorSampleRate (T418957)]] synced to the testservers (see https://wikitech.wikimedia.org/wiki/Mwdebug). Changes can now be verified there.

Mentioned in SAL (#wikimedia-operations) [2026-03-16T21:05:37Z] <catrope@deploy2002> Finished scap sync-world: Backport for [[gerrit:1253623|Fix client credentials access tokens (T417278 T419921)]], [[gerrit:1253625|Enable $wgTrackMediaRequestProvenance on testwikis and beta cluster (T414338)]], [[gerrit:1253626|Configure $wgApiClientErrorSampleRate (T418957)]] (duration: 08m 06s)

Thanks for the note @apaskulin. The recent problem should be fixed now, I'll follow that thread and see if the user confirms that it works.

As for this task overall, it got a bit messy, so I'll re-scope it to cover just the work that was done so far (making the 'sub' JWT field in client credentials access tokens non-empty, and fixing the bug that was revealed by that), and file a new task for actually allowing apps using the client credentials flow to be treated as non-anonymous.

matmarex renamed this task from Choosing client credentials grant for OAuth 2 results in an anonymous access token to Choosing client credentials grant for OAuth 2 results in an access token (JWT) with the 'sub' field empty.Mar 17 2026, 1:06 AM

Change #1240048 abandoned by Bartosz Dziewoński:

[mediawiki/extensions/OAuth@master] Fix 'sub' field in access tokens obtained via client credentials

Reason:

We'll need a new patch

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