Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F14449853
D9202.id25120.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
7 KB
Referenced Files
None
Subscribers
None
D9202.id25120.diff
View Options
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
@@ -212,6 +212,7 @@
'PhutilLunarPhaseTestCase' => 'utils/__tests__/PhutilLunarPhaseTestCase.php',
'PhutilMarkupEngine' => 'markup/PhutilMarkupEngine.php',
'PhutilMarkupTestCase' => 'markup/__tests__/PhutilMarkupTestCase.php',
+ 'PhutilMediaWikiAuthAdapter' => 'auth/PhutilMediaWikiAuthAdapter.php',
'PhutilMemcacheKeyValueCache' => 'cache/PhutilMemcacheKeyValueCache.php',
'PhutilMethodNotImplementedException' => 'error/PhutilMethodNotImplementedException.php',
'PhutilMetricsChannel' => 'channel/PhutilMetricsChannel.php',
@@ -620,6 +621,7 @@
'PhutilLogfileChannel' => 'PhutilChannelChannel',
'PhutilLunarPhaseTestCase' => 'PhutilTestCase',
'PhutilMarkupTestCase' => 'PhutilTestCase',
+ 'PhutilMediaWikiAuthAdapter' => 'PhutilOAuth1AuthAdapter',
'PhutilMemcacheKeyValueCache' => 'PhutilKeyValueCache',
'PhutilMethodNotImplementedException' => 'Exception',
'PhutilMetricsChannel' => 'PhutilChannelChannel',
diff --git a/src/auth/PhutilMediaWikiAuthAdapter.php b/src/auth/PhutilMediaWikiAuthAdapter.php
new file mode 100644
--- /dev/null
+++ b/src/auth/PhutilMediaWikiAuthAdapter.php
@@ -0,0 +1,198 @@
+<?php
+
+/**
+ * Authentication adapter for MediaWiki OAuth1.
+ */
+final class PhutilMediaWikiAuthAdapter
+ extends PhutilOAuth1AuthAdapter {
+
+ private $userinfo;
+ private $domain = '';
+ private $mediaWikiBaseURI = '';
+
+ public function getWikiPageURI($title, $query_params = null) {
+ $uri = $this->mediaWikiBaseURI;
+ if (substr($uri, -1) != '/') {
+ $uri .= '/';
+ }
+ if (!is_array($query_params)) {
+ $query_params = array();
+ }
+ $query_params['title'] = $title;
+ return $uri.'index.php?'.
+ http_build_query(
+ $query_params,
+ '',
+ '&');
+ }
+
+ 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->getWikiPageURI('User:'.urlencode($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';
+ }
+
+ public function shouldAddCSRFTokenToCallbackURI() {
+ return false;
+ }
+
+ protected function getRequestTokenURI() {
+ return $this->getWikiPageURI('Special:OAuth/initiate',
+ array('oauth_callback' => 'oob'));
+ }
+
+ protected function getAuthorizeTokenURI() {
+ return $this->getWikiPageURI('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();
+ return $p."&oauth_consumer_key={$this->getConsumerKey()}";
+ }
+
+ protected function getValidateTokenURI() {
+ return $this->getWikiPageURI('Special:OAuth/token');
+ }
+
+ private function getUserInfo() {
+ if ($this->userinfo === null) {
+ $uri = new PhutilURI(
+ $this->getWikiPageURI('Special:OAuth/identify',
+ array('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
+ * here we verify the identity, not just the jwt signature.
+ */
+ private function decodeAndVerifyJWT($jwt, $nonce) {
+ $userinfo = array();
+ $identity = $this->decodeJWT($jwt);
+ $iss_uri = new PhutilURI($identity->iss);
+ $expected_uri = new PhutilURI($this->mediaWikiBaseURI);
+
+ $now = time();
+
+ if ($iss_uri->getDomain() !== $expected_uri->getDomain()) {
+ 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;
+ }
+
+ /** decode a JWT and verify the signature is valid */
+ private function decodeJWT($jwt) {
+ list($headb64, $bodyb64, $sigb64) = explode('.', $jwt);
+
+ $header = json_decode($this->urlsafeB64Decode($headb64));
+ $body = json_decode($this->urlsafeB64Decode($bodyb64));
+ $sig = $this->urlsafeB64Decode($sigb64);
+
+ $expect_sig = hash_hmac(
+ 'sha256',
+ "$headb64.$bodyb64",
+ $this->getConsumerSecret()->openEnvelope(),
+ true);
+
+ // MediaWiki will only use sha256 hmac (HS256) for now.
+ // This checks that an attacker doesn't return invalid JWT signature type.
+ if ($header->alg !== 'HS256' ||
+ !$this->compareHash($sig, $expect_sig)) {
+ throw new Exception('Invalid JWT signature from /identify.');
+ }
+
+ return $body;
+ }
+
+ private function urlsafeB64Decode($input) {
+ $remainder = strlen($input) % 4;
+ if ($remainder) {
+ $padlen = 4 - $remainder;
+ $input .= str_repeat('=', $padlen);
+ }
+ return base64_decode(strtr($input, '-_', '+/'));
+ }
+
+ /** return true if hash1 has the same value as hash2 */
+ private function compareHash($hash1, $hash2) {
+ $result = strlen($hash1) ^ strlen($hash2);
+ $len = min(strlen($hash1), strlen($hash2));
+ for ($i = 0; $i < $len; $i++) {
+ $result |= ord($hash1{$i}) ^ ord($hash2{$i});
+ }
+ // this is just a constant time compare of the two hash strings
+ return $result == 0;
+ }
+}
diff --git a/src/auth/PhutilOAuth1AuthAdapter.php b/src/auth/PhutilOAuth1AuthAdapter.php
--- a/src/auth/PhutilOAuth1AuthAdapter.php
+++ b/src/auth/PhutilOAuth1AuthAdapter.php
@@ -198,4 +198,13 @@
return;
}
+ /**
+ * This tells the authentication provider whether CSRF token should be
+ * added to callback uri and verified after the callback completes.
+ * A subclass can return false to disable the extra CSRF check.
+ */
+ public function shouldAddCSRFTokenToCallbackURI() {
+ return true;
+ }
+
}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sat, Dec 28, 12:30 AM (8 h, 20 m)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
6933662
Default Alt Text
D9202.id25120.diff (7 KB)
Attached To
Mode
D9202: MediaWiki oauth1 adaptor for phabricator
Attached
Detach File
Event Timeline
Log In to Comment