mediawiki/oauthclient
PHP client library for MediaWiki OAuth 1.0a authentication. Handles the three-legged handshake, signed API requests, and token management, letting your tool act on behalf of wiki users across Wikimedia and other MediaWiki sites.
Install and register a consumer first — the code is the easy part.
composer require mediawiki/oauthclient
Before writing any PHP you need OAuth credentials from the wiki. On Wikimedia projects go to Special:OAuthConsumerRegistration/propose; on a self-hosted wiki install Extension:OAuth and use Special:OAuthConsumerRegistration. You receive a consumer key/secret and must choose OAuth 1.0a (this library does not implement OAuth 2.0). For a web app, register the exact callback URL; for CLI/bots, pick "owner-only" so you can generate a long-lived access token directly and skip the redirect dance.
The package is framework-agnostic. The three classes you will touch 95% of the time are MediaWiki\OAuthClient\Client, ClientConfig, and Consumer; Token and User are the data objects. Read src/Client.php and the README first — there are fewer than a dozen public methods.
Minimal bootstrap:
use MediaWiki\OAuthClient\{Client, ClientConfig, Consumer};
$conf = new ClientConfig('https://en.wikipedia.org/w/index.php'); // index.php, not api.php
$conf->setConsumer(new Consumer($consumerKey, $consumerSecret));
$conf->setUserAgent('MyApp/1.0 ([email protected])'); // required by Wikimedia
$conf->setLogger($logger); // optional PSR-3
$client = new Client($conf);
First use case: "Log in with Wikipedia".
// 1. Redirect out
[$authUrl, $requestToken] = $client->initiate();
session(['mw_req' => $requestToken->toString()]);
return redirect()->away($authUrl);
// 2. Callback — oauth_token + oauth_verifier arrive as query params
$stored = Token::newFromString(session('mw_req'));
if ($request->query('oauth_token') !== $stored->key) {
abort(400, 'OAuth token mismatch');
}
$accessToken = $client->complete($stored, $request->query('oauth_verifier'));
// 3. Who is this?
$user = $client->identify($accessToken);
echo $user->getName(); // global username
Persist $accessToken->toString() (encrypted) alongside your local user. That token is what you reuse for every subsequent API call.
Three-legged web login (Socialite-style). The flow above is the canonical pattern: initiate() → store request token in the session → redirect → verify oauth_token matches → complete() → identify() → link/create local user. Store the request token, not just the verifier, because the token secret is needed to sign the final leg. This is what a Laravel Socialite driver for MediaWiki looks like in practice.
Owner-only / bot automation. No redirects needed. Generate the access token once on Special:OAuthConsumerRegistration, drop it into config, and every call is signed with it:
$token = Token::newFromString(config('mediawiki.oauth_token'));
$client->setToken($token);
$res = $client->makeOAuthCall(
$token,
'https://en.wikipedia.org/w/api.php?action=query&meta=userinfo&format=json'
);
$data = json_decode((string) $res->getBody(), true);
This is the pattern for scheduled jobs, queue workers, and Artisan commands. It also avoids the callback-URL headache entirely.
Authenticated API calls, including writes. makeOAuthCall() is a thin authenticated HTTP client. For reads, hit action=query directly. For edits you still need a CSRF token — OAuth does not replace it:
$res = $client->makeOAuthCall($token,
$endpoint . '?action=query&meta=tokens&type=csrf&format=json');
$csrf = json_decode((string) $res->getBody(), true)['query']['tokens']['csrftoken'];
$client->makeOAuthCall($token, $endpoint, 'POST', [
'form_params' => [
'action' => 'edit', 'title' => $title, 'text' => $text,
'token' => $csrf, 'format' => 'json', 'maxlag' => 5,
],
]);
Prefer form_params over a raw body so Guzzle encodes consistently; the OAuth signature covers the query string and form body.
Laravel wiring. Bind the client as a singleton keyed by wiki, keep credentials in config/mediawiki.php, and wrap the library behind your own interface so you can swap a fake in tests:
$this->app->singleton(MediaWikiOAuth::class, fn () => new MediaWikiOAuth(
config('mediawiki.endpoint'),
config('mediawiki.consumer_key'),
config('mediawiki.consumer_secret'),
config('mediawiki.user_agent'),
));
Put the four OAuth routes (redirect, callback, logout/revoke) in a dedicated controller, a mediawiki_identities table holding wiki, central_id, username, access_token (encrypted cast), and a middleware or guard that resolves the current identity.
Multi-wiki. Tokens are scoped to the wiki that issued them — an enwiki token cannot sign a request to dewiki. Treat ClientConfig endpoint as part of the identity, and build one Client per wiki (a small factory is enough). Cross-wiki identity is a CentralAuth concern, not this library's.
Resilience. Wrap makeOAuthCall and identify in try/catch for MediaWiki\OAuthClient\MwOAuthException and Guzzle transport exceptions. Treat 401/403 as "token revoked, force re-auth", and 429/503 as retry-with-backoff. Log the OAuth error text — it usually tells you whether the signature, timestamp, or token is wrong.
Guzzle 6 pin and 2019 release date. The package depends on guzzlehttp/guzzle ^6 and guzzlehttp/oauth-subscriber, so it can clash with apps already on Guzzle 7. On PHP 8.1+ it generally still runs, but it is effectively unmaintained — pin versions deliberately and be prepared to fork or vendor it if your dependency graph forces Guzzle 7. Budget for that risk up front rather than discovering it in CI.
User-Agent is not optional on Wikimedia. A missing or generic UA gets you 403s from the edge or outright rejection during consumer review. Include app name, version, and a contact URL/email, and set it on ClientConfig (not just on Guzzle).
Callback URL must match exactly. Scheme, host, port, path, and trailing slash all matter unless the consumer is flagged "allow consumer to specify a callback". Development on localhost versus production is the usual failure; either register separate consumers or use a tunnel with the registered hostname.
The request token is a secret and single-use. Store it in the session or a short-lived cache between redirect and callback, keyed to the browser session, and compare oauth_token on return to block token-fixation/CSRF. Never put it in a URL you log, and never reuse it after complete().
Tokens don't expire, but they do get revoked. Users can revoke at Special:OAuthManageMyGrants. Your "connected accounts" page should call the revoke endpoint and clear the local token, and your API wrapper should degrade to re-auth on 401 instead of throwing a 500.
You still need CSRF tokens and maxlag. OAuth authenticates the user; it does not authorise edits. Fetch meta=tokens&type=csrf before any write action, and pass maxlag so the API can shed load politely. OAuth grants higher rate limits than anonymous access but not immunity — batch action=query with multiple titles and back off on 429.
Clock skew breaks signatures. oauth_timestamp errors usually mean the server clock is off. Run NTP on workers, and don't cache signed requests across a long delay.
Debugging the handshake. Attach a PSR-3 logger via ClientConfig::setLogger() and, when you need more, wrap Guzzle's handler stack or temporarily dump the signature base string. Most failures are one of: wrong endpoint (you passed api.php instead of index.php to ClientConfig), consumer key/secret swapped, callback mismatch, or identify() called before the token was actually authorised.
Don't confuse identify() with meta=userinfo. identify() returns a User object with getName(), getId(), and group/block/email accessors from the OAuth identity endpoint. For anything richer (edit count, registration date, rights in context) use a normal action=query&meta=userinfo call through makeOAuthCall.
Testing. There is no mock layer. The pragmatic approach is to define a small OAuthGateway interface around the four operations (begin, complete, identify, call) and bind a fake in tests. For an end-to-end smoke test, register a throwaway consumer on a test wiki rather than mocking signatures — signature bugs are exactly what unit tests won't catch.
Extension points are thin but sufficient. ClientConfig supports a logger; Client::setToken() lets you reuse a stored token across calls without passing it each time; subclassing Client to override makeOAuthCall() is the cleanest place to add retries, metrics, or maxlag defaults.
How can I help you explore Laravel packages today?