Page MenuHomePhabricator

D9202.id23377.diff
No OneTemporary

D9202.id23377.diff

diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -104,6 +104,7 @@
'PhutilAuthAdapterOAuthGitHub' => 'auth/PhutilAuthAdapterOAuthGitHub.php',
'PhutilAuthAdapterOAuthGoogle' => 'auth/PhutilAuthAdapterOAuthGoogle.php',
'PhutilAuthAdapterOAuthJIRA' => 'auth/PhutilAuthAdapterOAuthJIRA.php',
+ 'PhutilAuthAdapterOAuthMediaWiki' => 'auth/PhutilAuthAdapterOAuthMediaWiki.php',
'PhutilAuthAdapterOAuthTwitch' => 'auth/PhutilAuthAdapterOAuthTwitch.php',
'PhutilAuthAdapterOAuthTwitter' => 'auth/PhutilAuthAdapterOAuthTwitter.php',
'PhutilAuthAdapterOAuthWordPress' => 'auth/PhutilAuthAdapterOAuthWordPress.php',
@@ -533,6 +534,7 @@
'PhutilAuthAdapterOAuthGitHub' => 'PhutilAuthAdapterOAuth',
'PhutilAuthAdapterOAuthGoogle' => 'PhutilAuthAdapterOAuth',
'PhutilAuthAdapterOAuthJIRA' => 'PhutilAuthAdapterOAuth1',
+ 'PhutilAuthAdapterOAuthMediaWiki' => 'PhutilAuthAdapterOAuth1',
'PhutilAuthAdapterOAuthTwitch' => 'PhutilAuthAdapterOAuth',
'PhutilAuthAdapterOAuthTwitter' => 'PhutilAuthAdapterOAuth1',
'PhutilAuthAdapterOAuthWordPress' => 'PhutilAuthAdapterOAuth',
diff --git a/src/auth/PhutilAuthAdapterOAuthMediaWiki.php b/src/auth/PhutilAuthAdapterOAuthMediaWiki.php
new file mode 100644
--- /dev/null
+++ b/src/auth/PhutilAuthAdapterOAuthMediaWiki.php
@@ -0,0 +1,172 @@
+<?php
+
+/**
+ * Authentication adapter for MediaWiki OAuth1.
+ */
+final class PhutilAuthAdapterOAuthMediaWiki
+ extends PhutilAuthAdapterOAuth1 {
+
+ private $userinfo;
+ private $domain = 'mediawiki.org';
+ private $mediaWikiBaseURI = 'https://www.mediawiki.org';
+
+ public function getAccountID() {
+ $this->getHandshakeData();
+ return idx($this->getUserInfo(), 'userid');
+ }
+
+ public function getAccountName() {
+ return idx($this->getUserInfo(), 'username');
+ }
+
+ public function getAccountURI() {
+ $name = $this->getAccountName();
+ if (strlen($name)) {
+ return $this->mediaWikiBaseURI.'/wiki/User:'.$name;
+ }
+ return null;
+ }
+
+ public function getAccountImageURI() {
+ $info = $this->getUserInfo();
+ return idx($info, 'profile_image_url');
+ }
+
+ public function getAccountRealName() {
+ $info = $this->getUserInfo();
+ return idx($info, 'name');
+ }
+
+ public function getAdapterType() {
+ return 'mediawiki';
+ }
+
+ public function getAdapterDomain() {
+ return $this->domain;
+ }
+
+ /* mediawiki oauth needs the callback uri to be "oob"
+ (out of band callback) */
+ public function getCallbackURI() {
+ return 'oob';
+ }
+
+ protected function getRequestTokenURI() {
+ return $this->mediaWikiBaseURI.'/wiki/Special:OAuth/initiate';
+ }
+
+ protected function getAuthorizeTokenURI() {
+ return $this->mediaWikiBaseURI.'/wiki/Special:OAuth/authorize';
+ }
+
+ public function setAdapterDomain($domain) {
+ $this->domain = $domain;
+ return $this;
+ }
+
+ public function setMediaWikiBaseURI($uri) {
+ $this->mediaWikiBaseURI = $uri;
+ return $this;
+ }
+
+ public function getClientRedirectURI() {
+ $p = parent::getClientRedirectURI();
+ $token = $this->getToken();
+ $token_secret = $this->getTokenSecret();
+
+ // MediaWiki requires the call to /authorize be signed with the temp
+ // secret. This also forces the connected app to prevent CSRF.
+ setcookie('mwoauth', "$token:$token_secret");
+ return $p."&oauth_consumer_key={$this->getConsumerKey()}";
+ }
+
+ protected function getValidateTokenURI() {
+ return $this->mediaWikiBaseURI.'/wiki/Special:OAuth/token';
+ }
+
+ private function getUserInfo() {
+ if ($this->userinfo === null) {
+ $uri = new PhutilURI($this->mediaWikiBaseURI.
+ '/wiki/Special:OAuth/identify&format=json');
+ // We gen this so we can check for replay below:
+ $nonce = Filesystem::readRandomCharacters(32);
+ list($body) = $this->newOAuth1Future($uri)
+ ->setMethod('GET')
+ ->setNonce($nonce)
+ ->resolvex();
+ $this->userinfo = $this->decodeAndVerifyJWT($body, $nonce);
+ }
+ return $this->userinfo;
+ }
+
+ /**
+ * MediaWiki uses a signed JWT to assert the user's identity
+ */
+ private function decodeAndVerifyJWT($jwt, $nonce) {
+ $userinfo = array();
+ $identity = $this->decodeJWT($jwt);
+ $expected_connonical_server = $this->mediaWikiBaseURI;
+ $now = time();
+
+ if ($identity->iss !== $expected_connonical_server) {
+ throw new Exception(
+ "OAuth JWT iss didn't match expected server name");
+ }
+ if ($identity->aud !== $this->getConsumerKey()) {
+ throw new Exception(
+ "OAuth JWT aud didn't match expected consumer key");
+ }
+ if ($identity->iat > $now || $identity->exp < $now) {
+ throw new Exception(
+ "OAuth JWT wasn't valid at this time");
+ }
+ if ($identity->nonce !== $nonce) {
+ throw new Exception(
+ "OAuth JWT nonce didn't match what we sent. MITM?");
+ }
+ $userinfo['userid'] = $identity->sub;
+ $userinfo['username'] = $identity->username;
+ $userinfo['groups'] = $identity->groups;
+ $userinfo['blocked'] = $identity->blocked;
+ $userinfo['editcount'] = $identity->editcount;
+ return $userinfo;
+ }
+
+ private function decodeJWT($jwt) {
+ list($headb64, $bodyb64, $sigb64) = explode('.', $jwt);
+
+ $header = json_decode($this->urlsafeB64Decode($headb64));
+ $payload = json_decode($this->urlsafeB64Decode($bodyb64));
+ $sig = $this->urlsafeB64Decode($sigb64);
+
+ $expect_sig = hash_hmac(
+ 'sha256',
+ "$headb64.$bodyb64",
+ $this->getConsumerSecret()->openEnvelope(),
+ true);
+
+ if ($header->alg !== 'HS256' ||
+ !$this->compareHash($sig, $expect_sig)) {
+ throw new Exception('Invalid JWT signature from /identify.');
+ }
+ return $payload;
+ }
+
+ private function urlsafeB64Decode($input) {
+ $remainder = strlen($input) % 4;
+ if ($remainder) {
+ $padlen = 4 - $remainder;
+ $input .= str_repeat('=', $padlen);
+ }
+ return base64_decode(strtr($input, '-_', '+/'));
+ }
+
+ private function compareHash($hash1, $hash2) {
+ $result = strlen($hash1) ^ strlen($hash2);
+ $len = min(strlen($hash1), strlen($hash2)) - 1;
+ for ($i = 0; $i < $len; $i++) {
+ $result |= ord($hash1{$i}) ^ ord($hash2{$i});
+ }
+ return $result == 0;
+ }
+}

File Metadata

Mime Type
text/plain
Expires
Sun, Mar 16, 5:36 PM (1 w, 2 d ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
7707420
Default Alt Text
D9202.id23377.diff (6 KB)

Event Timeline