diff options
author | Yaco <franco@reevo.org> | 2020-06-04 11:01:00 -0300 |
---|---|---|
committer | Yaco <franco@reevo.org> | 2020-06-04 11:01:00 -0300 |
commit | fc7369835258467bf97eb64f184b93691f9a9fd5 (patch) | |
tree | daabd60089d2dd76d9f5fb416b005fbe159c799d /www/wiki/extensions/OATHAuth/includes |
first commit
Diffstat (limited to 'www/wiki/extensions/OATHAuth/includes')
15 files changed, 2022 insertions, 0 deletions
diff --git a/www/wiki/extensions/OATHAuth/includes/OATHAuthHooks.php b/www/wiki/extensions/OATHAuth/includes/OATHAuthHooks.php new file mode 100644 index 00000000..8842b16a --- /dev/null +++ b/www/wiki/extensions/OATHAuth/includes/OATHAuthHooks.php @@ -0,0 +1,221 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +use MediaWiki\Auth\AuthenticationRequest; +use MediaWiki\MediaWikiServices; +use Wikimedia\Rdbms\IDatabase; + +/** + * Hooks for Extension:OATHAuth + * + * @ingroup Extensions + */ +class OATHAuthHooks { + /** + * Get the singleton OATH user repository + * + * @return OATHUserRepository + */ + public static function getOATHUserRepository() { + global $wgOATHAuthDatabase; + + static $service = null; + + if ( $service == null ) { + $factory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory(); + $service = new OATHUserRepository( + $factory->getMainLB( $wgOATHAuthDatabase ), + new HashBagOStuff( + [ + 'maxKeys' => 5, + ] + ) + ); + } + + return $service; + } + + /** + * @param AuthenticationRequest[] $requests + * @param array $fieldInfo Field information array (union of the + * AuthenticationRequest::getFieldInfo() responses). + * @param array &$formDescriptor HTMLForm descriptor. The special key 'weight' can be set + * to change the order of the fields. + * @param string $action One of the AuthManager::ACTION_* constants. + * @return bool + */ + public static function onAuthChangeFormFields( + array $requests, array $fieldInfo, array &$formDescriptor, $action + ) { + if ( isset( $fieldInfo['OATHToken'] ) ) { + $formDescriptor['OATHToken'] += [ + 'cssClass' => 'loginText', + 'id' => 'wpOATHToken', + 'size' => 20, + 'autofocus' => true, + 'persistent' => false, + 'autocomplete' => false, + 'spellcheck' => false, + ]; + } + return true; + } + + /** + * Determine if two-factor authentication is enabled for $wgUser + * + * This isn't the preferred mechanism for controlling access to sensitive features + * (see AuthManager::securitySensitiveOperationStatus() for that) but there is no harm in + * keeping it. + * + * @param bool &$isEnabled Will be set to true if enabled, false otherwise + * @return bool False if enabled, true otherwise + */ + public static function onTwoFactorIsEnabled( &$isEnabled ) { + global $wgUser; + + $user = self::getOATHUserRepository()->findByUser( $wgUser ); + if ( $user && $user->getKey() !== null ) { + $isEnabled = true; + # This two-factor extension is enabled by the user, + # we don't need to check others. + return false; + } else { + $isEnabled = false; + # This two-factor extension isn't enabled by the user, + # but others may be. + return true; + } + } + + /** + * Add the necessary user preferences for OATHAuth + * + * @param User $user + * @param array &$preferences + * @return bool + */ + public static function onGetPreferences( User $user, array &$preferences ) { + $oathUser = self::getOATHUserRepository()->findByUser( $user ); + + // If there is no existing key, and the user is not allowed to enable it, + // we have nothing to show. ( + if ( $oathUser->getKey() === null && !$user->isAllowed( 'oathauth-enable' ) ) { + return true; + } + + $title = SpecialPage::getTitleFor( 'OATH' ); + $msg = $oathUser->getKey() !== null ? 'oathauth-disable' : 'oathauth-enable'; + + $preferences[$msg] = [ + 'type' => 'info', + 'raw' => 'true', + 'default' => Linker::link( + $title, + wfMessage( $msg )->escaped(), + [], + [ 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText() ] + ), + 'label-message' => 'oathauth-prefs-label', + 'section' => 'personal/info', ]; + + return true; + } + + /** + * @param DatabaseUpdater $updater + * @return bool + */ + public static function onLoadExtensionSchemaUpdates( $updater ) { + $base = dirname( __DIR__ ); + switch ( $updater->getDB()->getType() ) { + case 'mysql': + case 'sqlite': + $updater->addExtensionTable( 'oathauth_users', "$base/sql/mysql/tables.sql" ); + $updater->addExtensionUpdate( [ [ __CLASS__, 'schemaUpdateOldUsersFromInstaller' ] ] ); + $updater->dropExtensionField( + 'oathauth_users', + 'secret_reset', + "$base/sql/mysql/patch-remove_reset.sql" + ); + break; + + case 'oracle': + $updater->addExtensionTable( 'oathauth_users', "$base/sql/oracle/tables.sql" ); + break; + + case 'postgres': + $updater->addExtensionTable( 'oathauth_users', "$base/sql/postgres/tables.sql" ); + break; + } + + return true; + } + + /** + * Helper function for converting old users to the new schema + * @see OATHAuthHooks::OATHAuthSchemaUpdates + * + * @param DatabaseUpdater $updater + * + * @return bool + */ + public static function schemaUpdateOldUsersFromInstaller( DatabaseUpdater $updater ) { + return self::schemaUpdateOldUsers( $updater->getDB() ); + } + + /** + * Helper function for converting old users to the new schema + * @see OATHAuthHooks::OATHAuthSchemaUpdates + * + * @param IDatabase $db + * @return bool + */ + public static function schemaUpdateOldUsers( IDatabase $db ) { + if ( !$db->fieldExists( 'oathauth_users', 'secret_reset' ) ) { + return true; + } + + $res = $db->select( + 'oathauth_users', + [ 'id', 'scratch_tokens' ], + [ 'is_validated != 0' ], + __METHOD__ + ); + + foreach ( $res as $row ) { + Wikimedia\suppressWarnings(); + $scratchTokens = unserialize( base64_decode( $row->scratch_tokens ) ); + Wikimedia\restoreWarnings(); + if ( $scratchTokens ) { + $db->update( + 'oathauth_users', + [ 'scratch_tokens' => implode( ',', $scratchTokens ) ], + [ 'id' => $row->id ], + __METHOD__ + ); + } + } + + // Remove rows from the table where user never completed the setup process + $db->delete( 'oathauth_users', [ 'is_validated' => 0 ], __METHOD__ ); + + return true; + } +} diff --git a/www/wiki/extensions/OATHAuth/includes/OATHAuthKey.php b/www/wiki/extensions/OATHAuth/includes/OATHAuthKey.php new file mode 100644 index 00000000..2e178803 --- /dev/null +++ b/www/wiki/extensions/OATHAuth/includes/OATHAuthKey.php @@ -0,0 +1,187 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +/** + * Class representing a two-factor key + * + * Keys can be tied to OATHUsers + * + * @ingroup Extensions + */ +class OATHAuthKey { + /** + * Represents that a token corresponds to the main secret + * @see verifyToken + */ + const MAIN_TOKEN = 1; + + /** + * Represents that a token corresponds to a scratch token + * @see verifyToken + */ + const SCRATCH_TOKEN = -1; + + /** @var array Two factor binary secret */ + private $secret; + + /** @var string[] List of scratch tokens */ + private $scratchTokens; + + /** + * Make a new key from random values + * + * @return OATHAuthKey + */ + public static function newFromRandom() { + $object = new self( + Base32::encode( MWCryptRand::generate( 10, true ) ), + [] + ); + + $object->regenerateScratchTokens(); + + return $object; + } + + /** + * @param string $secret + * @param array $scratchTokens + */ + public function __construct( $secret, array $scratchTokens ) { + // Currently harcoded values; might be used in future + $this->secret = [ + 'mode' => 'hotp', + 'secret' => $secret, + 'period' => 30, + 'algorithm' => 'SHA1', + ]; + $this->scratchTokens = $scratchTokens; + } + + /** + * @return string + */ + public function getSecret() { + return $this->secret['secret']; + } + + /** + * @return array + */ + public function getScratchTokens() { + return $this->scratchTokens; + } + + /** + * Verify a token against the secret or scratch tokens + * + * @param string $token Token to verify + * @param OATHUser $user + * + * @return int|false Returns a constant represent what type of token was matched, + * or false for no match + */ + public function verifyToken( $token, OATHUser $user ) { + global $wgOATHAuthWindowRadius; + + if ( $this->secret['mode'] !== 'hotp' ) { + throw new \DomainException( 'OATHAuth extension does not support non-HOTP tokens' ); + } + + // Prevent replay attacks + $memc = ObjectCache::newAnything( [] ); + $uid = CentralIdLookup::factory()->centralIdFromLocalUser( $user->getUser() ); + $memcKey = wfMemcKey( 'oathauth', 'usedtokens', $uid ); + $lastWindow = (int)$memc->get( $memcKey ); + + $retval = false; + $results = HOTP::generateByTimeWindow( + Base32::decode( $this->secret['secret'] ), + $this->secret['period'], -$wgOATHAuthWindowRadius, $wgOATHAuthWindowRadius + ); + + // Remove any whitespace from the received token, which can be an intended group seperator + // or trimmeable whitespace + $token = preg_replace( '/\s+/', '', $token ); + + // Check to see if the user's given token is in the list of tokens generated + // for the time window. + foreach ( $results as $window => $result ) { + if ( $window > $lastWindow && $result->toHOTP( 6 ) === $token ) { + $lastWindow = $window; + $retval = self::MAIN_TOKEN; + break; + } + } + + // See if the user is using a scratch token + if ( !$retval ) { + $length = count( $this->scratchTokens ); + // Detect condition where all scratch tokens have been used + if ( $length == 1 && "" === $this->scratchTokens[0] ) { + $retval = false; + } else { + for ( $i = 0; $i < $length; $i++ ) { + if ( $token === $this->scratchTokens[$i] ) { + // If there is a scratch token, remove it from the scratch token list + unset( $this->scratchTokens[$i] ); + $oathrepo = OATHAuthHooks::getOATHUserRepository(); + $user->setKey( $this ); + $oathrepo->persist( $user ); + // Only return true if we removed it from the database + $retval = self::SCRATCH_TOKEN; + break; + } + } + } + } + + if ( $retval ) { + $memc->set( + $memcKey, + $lastWindow, + $this->secret['period'] * ( 1 + 2 * $wgOATHAuthWindowRadius ) + ); + } else { + // Increase rate limit counter for failed request + $user->getUser()->pingLimiter( 'badoath' ); + } + + return $retval; + } + + public function regenerateScratchTokens() { + $scratchTokens = []; + for ( $i = 0; $i < 5; $i++ ) { + array_push( $scratchTokens, Base32::encode( MWCryptRand::generate( 10, true ) ) ); + } + $this->scratchTokens = $scratchTokens; + } + + /** + * Check if a token is one of the scratch tokens for this two factor key. + * + * @param string $token Token to verify + * + * @return bool true if this is a scratch token. + */ + public function isScratchToken( $token ) { + $token = preg_replace( '/\s+/', '', $token ); + return in_array( $token, $this->scratchTokens, true ); + } +} diff --git a/www/wiki/extensions/OATHAuth/includes/OATHAuthUtils.php b/www/wiki/extensions/OATHAuth/includes/OATHAuthUtils.php new file mode 100644 index 00000000..2afd3bf8 --- /dev/null +++ b/www/wiki/extensions/OATHAuth/includes/OATHAuthUtils.php @@ -0,0 +1,141 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +/** + * Utility class for various OATH functions + * + * @ingroup Extensions + */ +class OATHAuthUtils { + /** + * Check whether OATH two-factor authentication is enabled for a given user. + * This is a stable method that does not change and can be used in other extensions. + * @param User $user + * @return bool + */ + public static function isEnabledFor( User $user ) { + $oathUser = OATHAuthHooks::getOATHUserRepository()->findByUser( $user ); + return $oathUser && $oathUser->getKey(); + } + + /** + * Encrypt an aray of variables to put into the user's session. We use this + * when storing the user's password in their session. We can use json as the + * serialization format because $plaintextVars is an array of strings. + * @param array $plaintextVars array of user input strings + * @param int $userId passed to key derivation functions so each user uses + * distinct encryption and hmac keys + * @return string encrypted data packet + */ + public static function encryptSessionData( array $plaintextVars, $userId ) { + $keyMaterial = self::getKeyMaterials(); + $keys = self::getUserKeys( $keyMaterial, $userId ); + return self::seal( json_encode( $plaintextVars ), $keys['encrypt'], $keys['hmac'] ); + } + + /** + * Decrypt an encrypted packet, generated with encryptSessionData + * @param string $ciphertext Encrypted data packet + * @param string|int $userId + * @return array of strings + */ + public static function decryptSessionData( $ciphertext, $userId ) { + $keyMaterial = self::getKeyMaterials(); + $keys = self::getUserKeys( $keyMaterial, $userId ); + return json_decode( self::unseal( $ciphertext, $keys['encrypt'], $keys['hmac'] ), true ); + } + + /** + * Get the base secret for this wiki, used to derive all of the encryption + * keys. When $wgOATHAuthSecret is rotated, users who are part way through the + * two-step login will get an exception, and have to re-start the login. + * @return string + */ + private static function getKeyMaterials() { + global $wgOATHAuthSecret, $wgSecretKey; + return $wgOATHAuthSecret ?: $wgSecretKey; + } + + /** + * Generate encryption and hmac keys, unique to this user, based on a single + * wiki secret. Use a moderate pbkdf2 work factor in case we ever leak keys. + * @param string $secret + * @param string|int $userid + * @return array including key for encryption and integrity checking + */ + private static function getUserKeys( $secret, $userid ) { + $keymats = hash_pbkdf2( 'sha256', $secret, "oath-$userid", 10001, 64, true ); + return [ + 'encrypt' => substr( $keymats, 0, 32 ), + 'hmac' => substr( $keymats, 32, 32 ), + ]; + } + + /** + * Actually encrypt the data, using a new random IV, and prepend the hmac + * of the encrypted data + IV, using a separate hmac key. + * @param string $data + * @param string $encKey + * @param string $hmacKey + * @return string $hmac.$iv.$ciphertext, each component b64 encoded + */ + private static function seal( $data, $encKey, $hmacKey ) { + $iv = MWCryptRand::generate( 16, true ); + $ciphertext = openssl_encrypt( + $data, + 'aes-256-ctr', + $encKey, + OPENSSL_RAW_DATA, + $iv + ); + $sealed = base64_encode( $iv ) . '.' . base64_encode( $ciphertext ); + $hmac = hash_hmac( 'sha256', $sealed, $hmacKey, true ); + return base64_encode( $hmac ) . '.' . $sealed; + } + + /** + * Decrypt data sealed using seal(). First checks the hmac to prevent various + * attacks. + * @param string $encrypted + * @param string $encKey + * @param string $hmacKey + * @return string plaintext + * @throws Exception + */ + private static function unseal( $encrypted, $encKey, $hmacKey ) { + $pieces = explode( '.', $encrypted ); + if ( count( $pieces ) !== 3 ) { + throw new InvalidArgumentException( 'Invalid sealed-secret format' ); + } + + list( $hmac, $iv, $ciphertext ) = $pieces; + $integCalc = hash_hmac( 'sha256', $iv . '.' . $ciphertext, $hmacKey, true ); + if ( !hash_equals( $integCalc, base64_decode( $hmac ) ) ) { + throw new Exception( 'Sealed secret has been tampered with, aborting.' ); + } + + return openssl_decrypt( + base64_decode( $ciphertext ), + 'aes-256-ctr', + $encKey, + OPENSSL_RAW_DATA, + base64_decode( $iv ) + ); + } + +} diff --git a/www/wiki/extensions/OATHAuth/includes/OATHUser.php b/www/wiki/extensions/OATHAuth/includes/OATHUser.php new file mode 100644 index 00000000..ed1b4a16 --- /dev/null +++ b/www/wiki/extensions/OATHAuth/includes/OATHUser.php @@ -0,0 +1,83 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +/** + * Class representing a user from OATH's perspective + * + * @ingroup Extensions + */ +class OATHUser { + /** @var User */ + private $user; + + /** @var OATHAuthKey|null */ + private $key; + + /** + * Constructor. Can't be called directly. Use OATHUserRepository::findByUser instead. + * @param User $user + * @param OATHAuthKey|null $key + */ + public function __construct( User $user, OATHAuthKey $key = null ) { + $this->user = $user; + $this->key = $key; + } + + /** + * @return User + */ + public function getUser() { + return $this->user; + } + + /** + * @return String + */ + public function getIssuer() { + global $wgSitename, $wgOATHAuthAccountPrefix; + if ( $wgOATHAuthAccountPrefix !== false ) { + return $wgOATHAuthAccountPrefix; + } + return $wgSitename; + } + + /** + * @return String + */ + public function getAccount() { + return $this->user->getName(); + } + + /** + * Get the key associated with this user. + * + * @return null|OATHAuthKey + */ + public function getKey() { + return $this->key; + } + + /** + * Set the key associated with this user. + * + * @param OATHAuthKey|null $key + */ + public function setKey( OATHAuthKey $key = null ) { + $this->key = $key; + } +} diff --git a/www/wiki/extensions/OATHAuth/includes/OATHUserRepository.php b/www/wiki/extensions/OATHAuth/includes/OATHUserRepository.php new file mode 100644 index 00000000..698ca49b --- /dev/null +++ b/www/wiki/extensions/OATHAuth/includes/OATHUserRepository.php @@ -0,0 +1,103 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +use Wikimedia\Rdbms\LoadBalancer; +use Wikimedia\Rdbms\DBConnRef; + +class OATHUserRepository { + /** @var LoadBalancer */ + protected $lb; + + /** @var BagOStuff */ + protected $cache; + + /** + * OATHUserRepository constructor. + * @param LoadBalancer $lb + * @param BagOStuff $cache + */ + public function __construct( LoadBalancer $lb, BagOStuff $cache ) { + $this->lb = $lb; + $this->cache = $cache; + } + + /** + * @param User $user + * @return OATHUser + */ + public function findByUser( User $user ) { + $oathUser = $this->cache->get( $user->getName() ); + if ( !$oathUser ) { + $oathUser = new OATHUser( $user, null ); + + $uid = CentralIdLookup::factory()->centralIdFromLocalUser( $user ); + $res = $this->getDB( DB_REPLICA )->selectRow( + 'oathauth_users', + '*', + [ 'id' => $uid ], + __METHOD__ + ); + if ( $res ) { + $key = new OATHAuthKey( $res->secret, explode( ',', $res->scratch_tokens ) ); + $oathUser->setKey( $key ); + } + + $this->cache->set( $user->getName(), $oathUser ); + } + return $oathUser; + } + + /** + * @param OATHUser $user + */ + public function persist( OATHUser $user ) { + $this->getDB( DB_MASTER )->replace( + 'oathauth_users', + [ 'id' ], + [ + 'id' => CentralIdLookup::factory()->centralIdFromLocalUser( $user->getUser() ), + 'secret' => $user->getKey()->getSecret(), + 'scratch_tokens' => implode( ',', $user->getKey()->getScratchTokens() ), + ], + __METHOD__ + ); + $this->cache->set( $user->getUser()->getName(), $user ); + } + + /** + * @param OATHUser $user + */ + public function remove( OATHUser $user ) { + $this->getDB( DB_MASTER )->delete( + 'oathauth_users', + [ 'id' => CentralIdLookup::factory()->centralIdFromLocalUser( $user->getUser() ) ], + __METHOD__ + ); + $this->cache->delete( $user->getUser()->getName() ); + } + + /** + * @param integer $index DB_MASTER/DB_REPLICA + * @return DBConnRef + */ + private function getDB( $index ) { + global $wgOATHAuthDatabase; + + return $this->lb->getConnectionRef( $index, [], $wgOATHAuthDatabase ); + } +} diff --git a/www/wiki/extensions/OATHAuth/includes/api/ApiOATHValidate.php b/www/wiki/extensions/OATHAuth/includes/api/ApiOATHValidate.php new file mode 100644 index 00000000..14f46414 --- /dev/null +++ b/www/wiki/extensions/OATHAuth/includes/api/ApiOATHValidate.php @@ -0,0 +1,101 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +/** + * Validate an OATH token. + * + * @ingroup API + * @ingroup Extensions + */ +class ApiOATHValidate extends ApiBase { + public function execute() { + // Be extra paranoid about the data that is sent + $this->requirePostedParameters( [ 'totp', 'token' ] ); + + $params = $this->extractRequestParams(); + if ( $params['user'] === null ) { + $params['user'] = $this->getUser()->getName(); + } + + $this->checkUserRightsAny( 'oathauth-api-all' ); + + $user = User::newFromName( $params['user'] ); + if ( $user === false ) { + $this->dieWithError( 'noname' ); + } + + // Don't increase pingLimiter, just check for limit exceeded. + if ( $user->pingLimiter( 'badoath', 0 ) ) { + $this->dieWithError( 'apierror-ratelimited' ); + } + + $result = [ + ApiResult::META_BC_BOOLS => [ 'enabled', 'valid' ], + 'enabled' => false, + 'valid' => false, + ]; + + if ( !$user->isAnon() ) { + $oathUser = OATHAuthHooks::getOATHUserRepository() + ->findByUser( $user ); + if ( $oathUser ) { + $key = $oathUser->getKey(); + if ( $key !== null ) { + $result['enabled'] = true; + $result['valid'] = $key->verifyToken( + $params['totp'], $oathUser ) !== false; + } + } + } + + $this->getResult()->addValue( null, $this->getModuleName(), $result ); + } + + public function getCacheMode( $params ) { + return 'private'; + } + + public function isInternal() { + return true; + } + + public function needsToken() { + return 'csrf'; + } + + public function getAllowedParams() { + return [ + 'user' => [ + ApiBase::PARAM_TYPE => 'user', + ], + 'totp' => [ + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_REQUIRED => true, + ], + ]; + } + + protected function getExamplesMessages() { + return [ + 'action=oathvalidate&totp=123456&token=123ABC' + => 'apihelp-oathvalidate-example-1', + 'action=oathvalidate&user=Example&totp=123456&token=123ABC' + => 'apihelp-oathvalidate-example-2', + ]; + } +} diff --git a/www/wiki/extensions/OATHAuth/includes/api/ApiQueryOATH.php b/www/wiki/extensions/OATHAuth/includes/api/ApiQueryOATH.php new file mode 100644 index 00000000..e131f193 --- /dev/null +++ b/www/wiki/extensions/OATHAuth/includes/api/ApiQueryOATH.php @@ -0,0 +1,90 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +/** + * Query module to check if a user has OATH authentication enabled. + * + * Usage requires the 'oathauth-api-all' grant which is not given to any group + * by default. Use of this API is security sensitive and should not be granted + * lightly. Configuring a special 'oathauth' user group is recommended. + * + * @ingroup API + * @ingroup Extensions + */ +class ApiQueryOATH extends ApiQueryBase { + public function __construct( $query, $moduleName ) { + parent::__construct( $query, $moduleName, 'oath' ); + } + + public function execute() { + $params = $this->extractRequestParams(); + if ( $params['user'] === null ) { + $params['user'] = $this->getUser()->getName(); + } + + $this->checkUserRightsAny( 'oathauth-api-all' ); + + $user = User::newFromName( $params['user'] ); + if ( $user === false ) { + $this->dieWithError( 'noname' ); + } + + $result = $this->getResult(); + $data = [ + ApiResult::META_BC_BOOLS => [ 'enabled' ], + 'enabled' => false, + ]; + + if ( !$user->isAnon() ) { + $oathUser = OATHAuthHooks::getOATHUserRepository() + ->findByUser( $user ); + $data['enabled'] = $oathUser && $oathUser->getKey() !== null; + } + $result->addValue( 'query', $this->getModuleName(), $data ); + } + + /** + * @param array $params + * + * @return string + */ + public function getCacheMode( $params ) { + return 'private'; + } + + public function isInternal() { + return true; + } + + public function getAllowedParams() { + return [ + 'user' => [ + ApiBase::PARAM_TYPE => 'user', + ], + ]; + } + + protected function getExamplesMessages() { + return [ + 'action=query&meta=oath' + => 'apihelp-query+oath-example-1', + 'action=query&meta=oath&oathuser=Example' + => 'apihelp-query+oath-example-2', + ]; + } +} diff --git a/www/wiki/extensions/OATHAuth/includes/auth/TOTPAuthenticationRequest.php b/www/wiki/extensions/OATHAuth/includes/auth/TOTPAuthenticationRequest.php new file mode 100644 index 00000000..c10f5e2f --- /dev/null +++ b/www/wiki/extensions/OATHAuth/includes/auth/TOTPAuthenticationRequest.php @@ -0,0 +1,43 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +use MediaWiki\Auth\AuthenticationRequest; + +/** + * AuthManager value object for the TOTP second factor of an authentication: + * a pseudorandom token that is generated from the current time independently + * by the server and the client. + */ +class TOTPAuthenticationRequest extends AuthenticationRequest { + public $OATHToken; + + public function describeCredentials() { + return [ + 'provider' => wfMessage( 'oathauth-describe-provider' ), + 'account' => new \RawMessage( '$1', [ $this->username ] ), + ] + parent::describeCredentials(); + } + + public function getFieldInfo() { + return [ + 'OATHToken' => [ + 'type' => 'string', + 'label' => wfMessage( 'oathauth-auth-token-label' ), + 'help' => wfMessage( 'oathauth-auth-token-help' ), ], ]; + } +} diff --git a/www/wiki/extensions/OATHAuth/includes/auth/TOTPSecondaryAuthenticationProvider.php b/www/wiki/extensions/OATHAuth/includes/auth/TOTPSecondaryAuthenticationProvider.php new file mode 100644 index 00000000..90d69cc3 --- /dev/null +++ b/www/wiki/extensions/OATHAuth/includes/auth/TOTPSecondaryAuthenticationProvider.php @@ -0,0 +1,121 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +use MediaWiki\Auth\AbstractSecondaryAuthenticationProvider; +use MediaWiki\Auth\AuthenticationRequest; +use MediaWiki\Auth\AuthenticationResponse; +use MediaWiki\Auth\AuthManager; + +/** + * AuthManager secondary authentication provider for TOTP second-factor authentication. + * + * After a successful primary authentication, requests a time-based one-time password + * (typically generated by a mobile app such as Google Authenticator) from the user. + * + * @see AuthManager + * @see https://en.wikipedia.org/wiki/Time-based_One-time_Password_Algorithm + */ +class TOTPSecondaryAuthenticationProvider extends AbstractSecondaryAuthenticationProvider { + + /** + * @param string $action + * @param array $options + * + * @return array + */ + public function getAuthenticationRequests( $action, array $options ) { + switch ( $action ) { + case AuthManager::ACTION_LOGIN: + // don't ask for anything initially so the second factor is on a separate screen + return []; + default: + return []; + } + } + + /** + * If the user has enabled two-factor authentication, request a second factor. + * + * @param User $user + * @param array $reqs + * + * @return AuthenticationResponse + */ + public function beginSecondaryAuthentication( $user, array $reqs ) { + $oathuser = OATHAuthHooks::getOATHUserRepository()->findByUser( $user ); + + if ( $oathuser->getKey() === null ) { + return AuthenticationResponse::newAbstain(); + } else { + return AuthenticationResponse::newUI( [ new TOTPAuthenticationRequest() ], + wfMessage( 'oathauth-auth-ui' ), 'warning' ); + } + } + + /** + * Verify the second factor. + * @inheritDoc + */ + public function continueSecondaryAuthentication( $user, array $reqs ) { + /** @var TOTPAuthenticationRequest $request */ + $request = AuthenticationRequest::getRequestByClass( $reqs, TOTPAuthenticationRequest::class ); + if ( !$request ) { + return AuthenticationResponse::newUI( [ new TOTPAuthenticationRequest() ], + wfMessage( 'oathauth-login-failed' ), 'error' ); + } + + $oathuser = OATHAuthHooks::getOATHUserRepository()->findByUser( $user ); + /** @suppress PhanUndeclaredProperty */ + $token = $request->OATHToken; + + if ( $oathuser->getKey() === null ) { + $this->logger->warning( 'Two-factor authentication was disabled mid-authentication for ' + . $user->getName() ); + return AuthenticationResponse::newAbstain(); + } + + // Don't increase pingLimiter, just check for limit exceeded. + if ( $user->pingLimiter( 'badoath', 0 ) ) { + return AuthenticationResponse::newUI( + [ new TOTPAuthenticationRequest() ], + new Message( + 'oathauth-throttled', + // Arbitrary duration given here + [ Message::durationParam( 60 ) ] + ), 'error' ); + } + + if ( $oathuser->getKey()->verifyToken( $token, $oathuser ) ) { + return AuthenticationResponse::newPass(); + } else { + return AuthenticationResponse::newUI( [ new TOTPAuthenticationRequest() ], + wfMessage( 'oathauth-login-failed' ), 'error' ); + } + } + + /** + * @param User $user + * @param User $creator + * @param array $reqs + * + * @return AuthenticationResponse + */ + public function beginSecondaryAccountCreation( $user, $creator, array $reqs ) { + return AuthenticationResponse::newAbstain(); + } +} diff --git a/www/wiki/extensions/OATHAuth/includes/lib/base32.php b/www/wiki/extensions/OATHAuth/includes/lib/base32.php new file mode 100644 index 00000000..d4ca1dff --- /dev/null +++ b/www/wiki/extensions/OATHAuth/includes/lib/base32.php @@ -0,0 +1,105 @@ +<?php +/** + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * PHP Google two-factor authentication module. + * + * See http://www.idontplaydarts.com/2011/07/google-totp-two-factor-authentication-for-php/ + * for more details + * + * @author Phil + **/ + +class Base32 { + + private static $lut = array( + "A" => 0, "B" => 1, + "C" => 2, "D" => 3, + "E" => 4, "F" => 5, + "G" => 6, "H" => 7, + "I" => 8, "J" => 9, + "K" => 10, "L" => 11, + "M" => 12, "N" => 13, + "O" => 14, "P" => 15, + "Q" => 16, "R" => 17, + "S" => 18, "T" => 19, + "U" => 20, "V" => 21, + "W" => 22, "X" => 23, + "Y" => 24, "Z" => 25, + "2" => 26, "3" => 27, + "4" => 28, "5" => 29, + "6" => 30, "7" => 31 + ); + + /** + * Decodes a base32 string into a binary string according to RFC 4648. + **/ + public static function decode($b32) { + + $b32 = strtoupper($b32); + + if (!preg_match('/^[ABCDEFGHIJKLMNOPQRSTUVWXYZ234567]+$/', $b32, $match)) + throw new Exception('Invalid characters in the base32 string.'); + + $l = strlen($b32); + $n = 0; + $j = 0; + $binary = ""; + + for ($i = 0; $i < $l; $i++) { + // Move buffer left by 5 to make room + $n = $n << 5; + // Add value into buffer + $n = $n + self::$lut[$b32[$i]]; + // Keep track of number of bits in buffer + $j = $j + 5; + + if ($j >= 8) { + $j = $j - 8; + $binary .= chr(($n & (0xFF << $j)) >> $j); + } + } + + return $binary; + } + + /** + * Encodes a binary string into a base32 string according to RFC 4648 (no padding). + **/ + public static function encode($string) { + + if (empty($string)) + throw new Exception('Empty string.'); + + $b32 = ""; + $binary = ""; + + $bytes = str_split($string); + $length = count( $bytes ); + for ($i = 0; $i < $length; $i++) { + $bits = base_convert(ord($bytes[$i]), 10, 2); + $binary .= str_pad($bits, 8, '0', STR_PAD_LEFT); + } + + $map = array_keys(self::$lut); + $fivebits = str_split($binary, 5); + $length = count( $fivebits ); + for ($i = 0; $i < $length; $i++) { + $dec = base_convert(str_pad($fivebits[$i], 5, '0'), 2, 10); + $b32 .= $map[$dec]; + } + + return $b32; + } +} diff --git a/www/wiki/extensions/OATHAuth/includes/lib/hotp.php b/www/wiki/extensions/OATHAuth/includes/lib/hotp.php new file mode 100644 index 00000000..8fd3d94b --- /dev/null +++ b/www/wiki/extensions/OATHAuth/includes/lib/hotp.php @@ -0,0 +1,179 @@ +<?php +/** + * HOTP Class + * Based on the work of OAuth, and the sample implementation of HMAC OTP + * http://tools.ietf.org/html/draft-mraihi-oath-hmac-otp-04#appendix-D + * @author Jakob Heuser (firstname)@felocity.com + * @copyright 2011 + * @license BSD-3-Clause + * @version 1.0 + */ +class HOTP { + /** + * Generate a HOTP key based on a counter value (event based HOTP) + * @param string $key the key to use for hashing + * @param int $counter the number of attempts represented in this hashing + * @return HOTPResult a HOTP Result which can be truncated or output + */ + public static function generateByCounter( $key, $counter ) { + // the counter value can be more than one byte long, + // so we need to pack it down properly. + $cur_counter = array( 0, 0, 0, 0, 0, 0, 0, 0 ); + for ( $i = 7; $i >= 0; $i-- ) { + $cur_counter[$i] = pack( 'C*', $counter ); + $counter = $counter >> 8; + } + + $bin_counter = implode( $cur_counter ); + + // Pad to 8 chars + if ( strlen( $bin_counter ) < 8 ) { + $bin_counter = str_repeat( "\0", 8 - strlen( $bin_counter ) ) . $bin_counter; + } + + // HMAC + $hash = hash_hmac( 'sha1', $bin_counter, $key ); + + return new HOTPResult( $hash ); + } + + /** + * Generate a HOTP key based on a timestamp and window size + * + * @param string $key the key to use for hashing + * @param int $window the size of the window a key is valid for in seconds + * @param int|bool $timestamp a timestamp to calculate for, defaults to time() + * + * @return HOTPResult a HOTP Result which can be truncated or output + */ + public static function generateByTime( $key, $window, $timestamp = false ) { + if ( !$timestamp && $timestamp !== 0 ) { + $timestamp = HOTP::getTime(); + } + + $counter = (int)( $timestamp / $window ); + + return HOTP::generateByCounter( $key, $counter ); + } + + /** + * Generate a HOTP key collection based on a timestamp and window size + * all keys that could exist between a start and end time will be included + * in the returned array + * + * @param string $key the key to use for hashing + * @param int $window the size of the window a key is valid for in seconds + * @param int $min the minimum window to accept before $timestamp + * @param int $max the maximum window to accept after $timestamp + * @param int|bool $timestamp a timestamp to calculate for, defaults to time() + * + * @return HOTPResult[] + */ + public static function generateByTimeWindow( $key, $window, $min = -1, + $max = 1, $timestamp = false + ) { + if ( !$timestamp && $timestamp !== 0 ) { + $timestamp = HOTP::getTime(); + } + + $counter = (int)( $timestamp / $window ); + $window = range( $min, $max ); + + $out = array(); + $length = count( $window ); + for ( $i = 0; $i < $length; $i++ ) { + $shift_counter = $counter + $window[$i]; + $out[$shift_counter] = HOTP::generateByCounter($key, $shift_counter); + } + + return $out; + } + + /** + * Gets the current time + * Ensures we are operating in UTC for the entire framework + * Restores the timezone on exit. + * @return int the current time + */ + public static function getTime() { + return time(); // PHP's time is always UTC + } +} + +/** + * The HOTPResult Class converts an HOTP item to various forms + * Supported formats include hex, decimal, string, and HOTP + * @author Jakob Heuser (firstname)@felocity.com + */ +class HOTPResult { + protected $hash; + protected $binary; + protected $decimal; + protected $hex; + + /** + * Build an HOTP Result + * @param string $value the value to construct with + */ + public function __construct( $value ) { + // store raw + $this->hash = $value; + + // store calculate decimal + $hmac_result = array(); + + // Convert to decimal + foreach ( str_split( $this->hash, 2 ) as $hex ) { + $hmac_result[] = hexdec($hex); + } + + $offset = $hmac_result[19] & 0xf; + + $this->decimal = ( + ( ( $hmac_result[$offset+0] & 0x7f ) << 24 ) | + ( ( $hmac_result[$offset+1] & 0xff ) << 16 ) | + ( ( $hmac_result[$offset+2] & 0xff ) << 8 ) | + ( $hmac_result[$offset+3] & 0xff ) + ); + + // calculate hex + $this->hex = dechex( $this->decimal ); + } + + /** + * Returns the string version of the HOTP + * @return string + */ + public function toString() { + return $this->hash; + } + + /** + * Returns the hex version of the HOTP + * @return string + */ + public function toHex() { + return $this->hex; + } + + /** + * Returns the decimal version of the HOTP + * @return int + */ + public function toDec() { + return $this->decimal; + } + + /** + * Returns the truncated decimal form of the HOTP + * @param int $length the length of the HOTP to return + * @return string + */ + public function toHOTP( $length ) { + $str = str_pad( $this->toDec(), $length, "0", STR_PAD_LEFT ); + $str = substr( $str, ( -1 * $length ) ); + + return $str; + } + +} diff --git a/www/wiki/extensions/OATHAuth/includes/special/ProxySpecialPage.php b/www/wiki/extensions/OATHAuth/includes/special/ProxySpecialPage.php new file mode 100644 index 00000000..bdb808f0 --- /dev/null +++ b/www/wiki/extensions/OATHAuth/includes/special/ProxySpecialPage.php @@ -0,0 +1,227 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +/** + * A proxy class that routes a special page to other special pages based on + * request parameters + */ +abstract class ProxySpecialPage extends SpecialPage { + /** + * @var SpecialPage|null Target page to execute + */ + private $target = null; + + /** + * Instantiate a SpecialPage based on request parameters + * + * The page returned by this function will be cached and used as + * the target page for this proxy object. + * + * @return SpecialPage + */ + abstract protected function getTargetPage(); + + /** + * Helper function that initializes the target SpecialPage object + */ + private function init() { + if ( $this->target === null ) { + $this->target = $this->getTargetPage(); + } + } + + /** + * Magic function that proxies function calls to the target object + * + * @param string $method Method name being called + * @param array $args Array of arguments + * + * @return mixed + */ + public function __call( $method, $args ) { + $this->init(); + return call_user_func_array( [ $this->target, $method ], $args ); + } + + /** + * @return string + */ + function getName() { + $this->init(); + return $this->target->getName(); + } + + /** + * @param string|bool $subpage + * @return Title + */ + function getPageTitle( $subpage = false ) { + $this->init(); + return $this->target->getPageTitle( $subpage ); + } + + /** + * @return string + */ + function getLocalName() { + $this->init(); + return $this->target->getLocalName(); + } + + /** + * @return string + */ + function getRestriction() { + $this->init(); + return $this->target->getRestriction(); + } + + /** + * @return bool + */ + function isListed() { + $this->init(); + return $this->target->isListed(); + } + + /** + * @param bool $listed + * @return bool + */ + function setListed( $listed ) { + $this->init(); + return $this->target->setListed( $listed ); + } + + /** + * @param bool $x + * @return bool + */ + function listed( $x = null ) { + $this->init(); + return $this->target->listed( $x ); + } + + /** + * @return bool + */ + public function isIncludable() { + $this->init(); + return $this->target->isIncludable(); + } + + /** + * @param bool $x + * @return bool + */ + function including( $x = null ) { + $this->init(); + return $this->target->including( $x ); + } + + /** + * @return bool + */ + public function isRestricted() { + $this->init(); + return $this->target->isRestricted(); + } + + /** + * @param User $user + * @return bool + */ + public function userCanExecute( User $user ) { + $this->init(); + return $this->target->userCanExecute( $user ); + } + + /** + * @throws PermissionsError + */ + function displayRestrictionError() { + $this->init(); + $this->target->displayRestrictionError(); + } + + /** + * @return void + * @throws PermissionsError + */ + public function checkPermissions() { + $this->init(); + $this->target->checkPermissions(); + } + + /** + * @param string|null $subPage + */ + protected function beforeExecute( $subPage ) { + $this->init(); + $this->target->beforeExecute( $subPage ); + } + + /** + * @param string|null $subPage + */ + protected function afterExecute( $subPage ) { + $this->init(); + $this->target->afterExecute( $subPage ); + } + + /** + * @param string|null $subPage + */ + public function execute( $subPage ) { + $this->init(); + $this->target->execute( $subPage ); + } + + /** + * @return string + */ + function getDescription() { + $this->init(); + return $this->target->getDescription(); + } + + /** + * @param IContextSource $context + */ + public function setContext( $context ) { + $this->init(); + $this->target->setContext( $context ); + parent::setContext( $context ); + } + + /** + * @return string + */ + protected function getRobotPolicy() { + $this->init(); + return $this->target->getRobotPolicy(); + } + + /** + * @return string + */ + protected function getGroupName() { + $this->init(); + return $this->target->getGroupName(); + } +} diff --git a/www/wiki/extensions/OATHAuth/includes/special/SpecialOATH.php b/www/wiki/extensions/OATHAuth/includes/special/SpecialOATH.php new file mode 100644 index 00000000..21a854c2 --- /dev/null +++ b/www/wiki/extensions/OATHAuth/includes/special/SpecialOATH.php @@ -0,0 +1,44 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +/** + * Proxy page that redirects to the proper OATH special page + */ +class SpecialOATH extends ProxySpecialPage { + /** + * If the user already has OATH enabled, show them a page to disable + * If the user has OATH disabled, show them a page to enable + * + * @return SpecialOATHDisable|SpecialOATHEnable + */ + protected function getTargetPage() { + $repo = OATHAuthHooks::getOATHUserRepository(); + + $user = $repo->findByUser( $this->getUser() ); + + if ( $user->getKey() === null ) { + return new SpecialOATHEnable( $repo, $user ); + } else { + return new SpecialOATHDisable( $repo, $user ); + } + } + + protected function getGroupName() { + return 'oath'; + } +} diff --git a/www/wiki/extensions/OATHAuth/includes/special/SpecialOATHDisable.php b/www/wiki/extensions/OATHAuth/includes/special/SpecialOATHDisable.php new file mode 100644 index 00000000..7e5758b9 --- /dev/null +++ b/www/wiki/extensions/OATHAuth/includes/special/SpecialOATHDisable.php @@ -0,0 +1,136 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +/** + * Special page to display key information to the user + * + * @ingroup Extensions + */ +class SpecialOATHDisable extends FormSpecialPage { + /** @var OATHUserRepository */ + private $OATHRepository; + + /** @var OATHUser */ + private $OATHUser; + + /** + * Initialize the OATH user based on the current local User object in the context + * + * @param OATHUserRepository $repository + * @param OATHUser $user + */ + public function __construct( OATHUserRepository $repository, OATHUser $user ) { + parent::__construct( 'OATH', '', false ); + $this->OATHRepository = $repository; + $this->OATHUser = $user; + } + + public function doesWrites() { + return true; + } + + /** + * Set the page title and add JavaScript RL modules + * + * @param HTMLForm $form + */ + public function alterForm( HTMLForm $form ) { + $form->setMessagePrefix( 'oathauth' ); + $form->setWrapperLegend( false ); + $form->getOutput()->setPageTitle( $this->msg( 'oathauth-disable' ) ); + } + + /** + * @return string + */ + protected function getDisplayFormat() { + return 'ooui'; + } + + /** + * @return bool + */ + public function requiresUnblock() { + return false; + } + + /** + * Require users to be logged in + * + * @param User $user + * + * @return bool|void + */ + protected function checkExecutePermissions( User $user ) { + parent::checkExecutePermissions( $user ); + + $this->requireLogin(); + } + + /** + * @return array[] + */ + protected function getFormFields() { + return [ + 'token' => [ + 'type' => 'text', + 'label-message' => 'oathauth-entertoken', + 'name' => 'token', + 'required' => true, + 'autofocus' => true, + ], + 'returnto' => [ + 'type' => 'hidden', + 'default' => $this->getRequest()->getVal( 'returnto' ), + 'name' => 'returnto', + ], + 'returntoquery' => [ + 'type' => 'hidden', + 'default' => $this->getRequest()->getVal( 'returntoquery' ), + 'name' => 'returntoquery', + ] + ]; + } + + /** + * @param array $formData + * + * @return array|bool + */ + public function onSubmit( array $formData ) { + // Don't increase pingLimiter, just check for limit exceeded. + if ( $this->OATHUser->getUser()->pingLimiter( 'badoath', 0 ) ) { + // Arbitrary duration given here + return [ 'oathauth-throttled', Message::durationParam( 60 ) ]; + } + + if ( !$this->OATHUser->getKey()->verifyToken( $formData['token'], $this->OATHUser ) ) { + return [ 'oathauth-failedtovalidateoath' ]; + } + + $this->OATHUser->setKey( null ); + $this->OATHRepository->remove( $this->OATHUser ); + + return true; + } + + public function onSuccess() { + $this->getOutput()->addWikiMsg( 'oathauth-disabledoath' ); + $this->getOutput()->returnToMain(); + } +} diff --git a/www/wiki/extensions/OATHAuth/includes/special/SpecialOATHEnable.php b/www/wiki/extensions/OATHAuth/includes/special/SpecialOATHEnable.php new file mode 100644 index 00000000..bf4b62e5 --- /dev/null +++ b/www/wiki/extensions/OATHAuth/includes/special/SpecialOATHEnable.php @@ -0,0 +1,241 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +/** + * Special page to display key information to the user + * + * @ingroup Extensions + */ +class SpecialOATHEnable extends FormSpecialPage { + /** @var OATHUserRepository */ + private $OATHRepository; + + /** @var OATHUser */ + private $OATHUser; + + /** + * Initialize the OATH user based on the current local User object in the context + * + * @param OATHUserRepository $repository + * @param OATHUser $user + */ + public function __construct( OATHUserRepository $repository, OATHUser $user ) { + parent::__construct( 'OATH', 'oathauth-enable', false ); + + $this->OATHRepository = $repository; + $this->OATHUser = $user; + } + + public function doesWrites() { + return true; + } + + /** + * Set the page title and add JavaScript RL modules + * + * @param HTMLForm $form + */ + public function alterForm( HTMLForm $form ) { + $form->setMessagePrefix( 'oathauth' ); + $form->setWrapperLegend( false ); + $form->getOutput()->setPageTitle( $this->msg( 'oathauth-enable' ) ); + $form->getOutput()->addModules( 'ext.oath.showqrcode' ); + $form->getOutput()->addModuleStyles( 'ext.oath.showqrcode.styles' ); + } + + /** + * @return string + */ + protected function getDisplayFormat() { + return 'ooui'; + } + + /** + * @return bool + */ + public function requiresUnblock() { + return false; + } + + /** + * Require users to be logged in + * + * @param User $user + * + * @return bool|void + */ + protected function checkExecutePermissions( User $user ) { + parent::checkExecutePermissions( $user ); + + $this->requireLogin(); + } + + /** + * @return array[] + */ + protected function getFormFields() { + $key = $this->getRequest()->getSessionData( 'oathauth_key' ); + + if ( $key === null ) { + $key = OATHAuthKey::newFromRandom(); + $this->getRequest()->setSessionData( 'oathauth_key', $key ); + } + + $secret = $key->getSecret(); + $label = "{$this->OATHUser->getIssuer()}:{$this->OATHUser->getAccount()}"; + $qrcodeUrl = "otpauth://totp/" + . rawurlencode( $label ) + . "?secret=" + . rawurlencode( $secret ) + . "&issuer=" + . rawurlencode( $this->OATHUser->getIssuer() ); + + $qrcodeElement = Html::element( 'div', [ + 'data-mw-qrcode-url' => $qrcodeUrl, + 'class' => 'mw-display-qrcode', + // Include width/height, so js won't re-arrange layout + // And non-js users will have this hidden with CSS + 'style' => 'width: 256px; height: 256px;' + ] ); + + return [ + 'app' => [ + 'type' => 'info', + 'default' => $this->msg( 'oathauth-step1-test' )->escaped(), + 'raw' => true, + 'section' => 'step1', + ], + 'qrcode' => [ + 'type' => 'info', + 'default' => $qrcodeElement, + 'raw' => true, + 'section' => 'step2', + ], + 'manual' => [ + 'type' => 'info', + 'label-message' => 'oathauth-step2alt', + 'default' => + '<strong>' . $this->msg( 'oathauth-account' )->escaped() . '</strong><br/>' + . $this->OATHUser->getAccount() . '<br/><br/>' + . '<strong>' . $this->msg( 'oathauth-secret' )->escaped() . '</strong><br/>' + . '<kbd>' . $this->getSecretForDisplay( $key ) . '</kbd><br/>', + 'raw' => true, + 'section' => 'step2', + ], + 'scratchtokens' => [ + 'type' => 'info', + 'default' => + $this->msg( 'oathauth-scratchtokens' ) + . $this->createResourceList( $this->getScratchTokensForDisplay( $key ) ), + 'raw' => true, + 'section' => 'step3', + ], + 'token' => [ + 'type' => 'text', + 'default' => '', + 'label-message' => 'oathauth-entertoken', + 'name' => 'token', + 'section' => 'step4', + ], + 'returnto' => [ + 'type' => 'hidden', + 'default' => $this->getRequest()->getVal( 'returnto' ), + 'name' => 'returnto', + ], + 'returntoquery' => [ + 'type' => 'hidden', + 'default' => $this->getRequest()->getVal( 'returntoquery' ), + 'name' => 'returntoquery', ] + ]; + } + + /** + * @param array $formData + * + * @return array|bool + */ + public function onSubmit( array $formData ) { + /** @var OATHAuthKey $key */ + $key = $this->getRequest()->getSessionData( 'oathauth_key' ); + + if ( $key->isScratchToken( $formData['token'] ) ) { + // A scratch token is not allowed for enrollement + return [ 'oathauth-noscratchforvalidation' ]; + } + if ( !$key->verifyToken( $formData['token'], $this->OATHUser ) ) { + return [ 'oathauth-failedtovalidateoath' ]; + } + + $this->getRequest()->setSessionData( 'oathauth_key', null ); + $this->OATHUser->setKey( $key ); + $this->OATHRepository->persist( $this->OATHUser ); + + return true; + } + + public function onSuccess() { + $this->getOutput()->addWikiMsg( 'oathauth-validatedoath' ); + $this->getOutput()->returnToMain(); + } + + /** + * @param $resources array + * @return string + */ + private function createResourceList( $resources ) { + $resourceList = ''; + foreach ( $resources as $resource ) { + $resourceList .= Html::rawElement( 'li', [], Html::rawElement( 'kbd', [], $resource ) ); + } + return Html::rawElement( 'ul', [], $resourceList ); + } + + /** + * Retrieve the current secret for display purposes + * + * The characters of the token are split in groups of 4 + * + * @param OATHAuthKey $key + * @return String + */ + protected function getSecretForDisplay( OATHAuthKey $key ) { + return $this->tokenFormatterFunction( $key->getSecret() ); + } + + /** + * Retrieve current scratch tokens for display purposes + * + * The characters of the token are split in groups of 4 + * + * @param OATHAuthKey $key + * @return string[] + */ + protected function getScratchTokensForDisplay( OATHAuthKey $key ) { + return array_map( [ $this, 'tokenFormatterFunction' ], $key->getScratchTokens() ); + } + + /** + * Formats a key or scratch token by creating groups of 4 seperated by space characters + * + * @param string $token Token to format + * @return string The token formatted for display + */ + private function tokenFormatterFunction( $token ) { + return implode( ' ', str_split( $token, 4 ) ); + } +} |