Page MenuHomePhabricator

OAuth 2.0 non-confidential clients cannot use refresh tokens without client secret
Closed, ResolvedPublic

Description

MediaWiki-extensions-OAuth supports non-confidential clients, which can get an access token based on only their client ID. However, those clients can’t refresh the access tokens with only their client ID – they get a refresh token, but trying to use it will yield an authentication error. Since the access token has the same TTL as for confidential clients (14400 seconds), non-confidential clients only have two options to make authenticated requests beyond that TTL: ship the client secret together with the client ID (making it not secret), or repeat the authorization flow every four hours.

This is a limitation inherited from the underlying league/oauth2-server package, where you can see the difference between auth code and refresh token handlers:

AuthCodeGrant.php
// Only validate the client if it is confidential
if ($client->isConfidential()) {
    $this->validateClient($request);
}
RefreshTokenGrant.php
// Validate request
$client = $this->validateClient($request);

It’s tracked upstream as issue #1073, but I thought it would be useful to have a task on Phabricator too. (Note that Extension:OAuth and mediawiki/vendor.git currently use a custom “9.0.0-alpha” version of the library, so even if the library implements this feature, switching to that release version might not be trivial depending on how much it diverges from the version we currently use.)

Event Timeline

Although MediaWiki still generates a secret key for non-confidential OAuth 2.0 consumers (idk why), and if you use that secret key, you can still successfully use the refresh token.

On the one hand, that means you’re using the non-confidential client exactly like a confidential one, which seems kind of pointless.

But on the other hand… I guess you could just treat the client secret of a non-confidential client as another piece of public information, and publish it together with the client ID?

There's some discussion here - the behavior of using refresh tokens with public clients is not super well-defined in the spec, but it's reasonable to expect the refresh_token grant to work without the (known to be insecure) client authentication in the case of public clients. But it's not something we'd want to fork upstream over, since clients can just use the client secret and everything will work fine, so this seems like a non-issue to me (other than maybe clarifying the documentation).

It’s tracked upstream as issue #1073

Apparently this was fixed upstream in #1420, which is included in the 9.2.0 release that was published last Saturday, so at some point we’ll hopefully get that fix pulled into mediawiki/vendor. (Though we’ll need to test it first, of course, which I haven’t done in the slightest so far.)

LucasWerkmeister renamed this task from OAuth 2.0 non-confidential clients cannot use refresh tokens to OAuth 2.0 non-confidential clients cannot use refresh tokens without client secret.Feb 28 2025, 12:46 PM
LucasWerkmeister updated the task description. (Show Details)

We're working towards enabling the native apps (Android and iOS) to use OAuth 2, and it looks like we might be blocked on this issue, since apps must necessarily be non-confidential clients, and also must be able to use refresh tokens.
Since it looks like the issue was fixed upstream, are there plans to pull in the latest upstream dependency?
(Or would you recommend we ship the client secret along with the client ID?)

(Or would you recommend we ship the client secret along with the client ID?)

As far as I understand, there’s basically no reason not to ship the client secret. You shouldn’t have to, but also it shouldn’t hurt, so you might as well do it to work around this task. (See also T323867.)

It would be nice to fix this by the time apps need production OAuth client credentials, but my impression is also that there's no harm in shipping the secret. In theory, you could require some more sensitive things to go through the server and use the secret as a proof, and publishing the secret makes that impossible; but in practice, we don't support that kind of thing anyway.

Apparently this was fixed upstream in #1420, which is included in the 9.2.0 release

We use a fork of 9.0.0.
https://github.com/thephpleague/oauth2-server/compare/9.0.0...9.2.0

So it sounds like the proper fix for this is blocked on either fixing T261462: Migrate OAuth extension back from wikimedia/oauth2-server fork to upstream or at least upgrading our fork. Which seems messy, I left some notes at T261462#11034161.

We use a fork of 9.3 now. @LucasWerkmeister can you check whether this is fixed?

Seems to work! I made this edit with the following local diff to the webapp-clientside-vite-guestbook m3api example:

diff --git i/webapp-clientside-vite-guestbook/src/session.js w/webapp-clientside-vite-guestbook/src/session.js
index 402718585c..f823b0a3f3 100644
--- i/webapp-clientside-vite-guestbook/src/session.js
+++ w/webapp-clientside-vite-guestbook/src/session.js
@@ -20,7 +20,6 @@ export function loadSession() {
 		userAgent: 'm3api-examples/webapp-clientside-vite-guestbook (https://gitlab.wikimedia.org/repos/m3api/m3api-examples)',
 		'm3api-oauth2/client': new OAuthClient(
 			'3176b30484225b3b594b2a3074b01aff',
-			'503c5a6b247016bf30169c3cf8663be9f1b001ae'
 		),
 	} );
 

Edit: scratch that, that’s not testing refresh behavior is it 🤦 I’ll see if it still works in four hours!

Well, the refresh doesn’t quite work properly, though I’m not sure it’s due to this issue. My web app makes a meta=tokens request, and the API responds with {"httpCode":401,"httpReason":"Jwt is expired"}, but the response is missing Access-Control-Allow-Origin, so my code crashes before it can even try a refresh.

IIUC, this kind of error (similar to T417839) doesn’t even come from MediaWiki, but from Wikimedia’s edge infrastructure? (This might also mean that my code wouldn’t properly refresh the token even if there was no CORS error? I’m looking for a mwoauth-invalid-authorization error to trigger the refresh; a "httpReason":"Jwt is expired" won’t do the trick.)

I can test if the same error happens when still using the client secret (which would mean it’s not related to this task). But given that access tokens take four hours to expire, and I can’t shorten that time (T374562), that won’t happen tonight :)

The error is indeed unrelated and affects both examples: ⇒ T419034: Custom OAuth 2 error from Wikimedia infrastructure breaks automatic retry of requests

So refresh is just generally broken in Wikimedia production right now AFAICT; getting a new access token from the refresh token may or may not work, but I can’t easily check it because the error that would tell my code to refresh the access token in the first place isn’t being emitted correctly.

Now (following T419034#11788469) it works! This edit was made with just the client ID (no client secret) and successfully refreshed the access token before making the edit. (The 401 response even had CORS headers, including access-control-expose-headers: Retry-After,WWW-Authenticate, so that this worked fully in the browser.)

Well, the refresh doesn’t quite work properly, though I’m not sure it’s due to this issue. My web app makes a meta=tokens request, and the API responds with {"httpCode":401,"httpReason":"Jwt is expired"}, but the response is missing Access-Control-Allow-Origin, so my code crashes before it can even try a refresh.

IIUC, this kind of error (similar to T417839) doesn’t even come from MediaWiki, but from Wikimedia’s edge infrastructure? (This might also mean that my code wouldn’t properly refresh the token even if there was no CORS error? I’m looking for a mwoauth-invalid-authorization error to trigger the refresh; a "httpReason":"Jwt is expired" won’t do the trick.)

I can test if the same error happens when still using the client secret (which would mean it’s not related to this task). But given that access tokens take four hours to expire, and I can’t shorten that time (T374562), that won’t happen tonight :)

This was fixed for Wikimedia wikis in T419034, and will be fixed for other wikis (without weird proxies) by the patch on T421778 (T421778#11767754).

• matmarex claimed this task.