Changeset View
Changeset View
Standalone View
Standalone View
src/auth/PhutilMediaWikiAuthAdapter.php
- This file was added.
<?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); | |||||
$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; | |||||
} | |||||
/** 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; | |||||
} | |||||
} |