Page MenuHomePhabricator

D9202.id21848.diff
No OneTemporary

D9202.id21848.diff

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,151 @@
+<?php
+
+/**
+ * Authentication adapter for MediaWiki OAuth1.
+ */
+final class PhutilAuthAdapterOAuthMediaWiki extends PhutilAuthAdapterOAuth1 {
+
+ private $userinfo;
+
+ 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 'http://en.wikipedia.beta.wmflabs.org/w/index.php?title=User:'.$name;
+ }
+ return null;
+ }
+
+ public function getCallbackURI() {
+ return 'oob';
+ }
+
+ 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 'mediawiki.org';
+ }
+
+ protected function getRequestTokenURI() {
+ return 'http://en.wikipedia.beta.wmflabs.org/w/index.php?title=Special:OAuth/initiate';
+ }
+
+ protected function getAuthorizeTokenURI() {
+ return 'http://en.wikipedia.beta.wmflabs.org/w/index.php?title=Special:OAuth/authorize';
+ }
+
+ 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 'http://en.wikipedia.beta.wmflabs.org/w/index.php?title=Special:OAuth/token';
+ }
+
+
+ private function getUserInfo() {
+ if ($this->userinfo === null) {
+ $uri = new PhutilURI('http://en.wikipedia.beta.wmflabs.org/w/index.php?title=Special:OAuth/identify&format=json');
+ $nonce = Filesystem::readRandomCharacters(32); // We gen this so we can check for replay below
+ 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 = 'http://en.wikipedia.beta.wmflabs.org';
+ $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
Tue, Mar 4, 9:52 AM (12 h, 25 m)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
7221364
Default Alt Text
D9202.id21848.diff (4 KB)

Event Timeline