2014-05-19 00:05:59 +00:00
|
|
|
<?php
|
2018-04-11 01:29:26 +00:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
2014-05-19 00:05:59 +00:00
|
|
|
|
2018-12-17 23:56:47 +00:00
|
|
|
use Psr\Log\LoggerInterface;
|
|
|
|
use \MediaWiki\Logger\LoggerFactory;
|
|
|
|
|
2014-05-19 00:05:59 +00:00
|
|
|
/**
|
|
|
|
* Class representing a two-factor key
|
|
|
|
*
|
2016-11-24 10:00:50 +00:00
|
|
|
* Keys can be tied to OATHUsers
|
2014-05-22 07:33:40 +00:00
|
|
|
*
|
|
|
|
* @ingroup Extensions
|
2014-05-19 00:05:59 +00:00
|
|
|
*/
|
|
|
|
class OATHAuthKey {
|
2014-05-22 07:33:40 +00:00
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
|
2018-04-05 11:27:13 +00:00
|
|
|
/** @var array Two factor binary secret */
|
2014-05-19 00:05:59 +00:00
|
|
|
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(
|
2018-07-17 07:20:57 +00:00
|
|
|
Base32::encode( random_bytes( 10 ) ),
|
2016-09-16 23:18:35 +00:00
|
|
|
[]
|
2014-05-19 00:05:59 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
$object->regenerateScratchTokens();
|
|
|
|
|
|
|
|
return $object;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param string $secret
|
|
|
|
* @param array $scratchTokens
|
|
|
|
*/
|
|
|
|
public function __construct( $secret, array $scratchTokens ) {
|
|
|
|
// Currently harcoded values; might be used in future
|
2016-09-16 23:18:35 +00:00
|
|
|
$this->secret = [
|
2014-05-19 00:05:59 +00:00
|
|
|
'mode' => 'hotp',
|
|
|
|
'secret' => $secret,
|
|
|
|
'period' => 30,
|
|
|
|
'algorithm' => 'SHA1',
|
2016-09-16 23:18:35 +00:00
|
|
|
];
|
2014-05-19 00:05:59 +00:00
|
|
|
$this->scratchTokens = $scratchTokens;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-04-05 11:27:13 +00:00
|
|
|
* @return string
|
2014-05-19 00:05:59 +00:00
|
|
|
*/
|
|
|
|
public function getSecret() {
|
|
|
|
return $this->secret['secret'];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2016-09-16 23:18:35 +00:00
|
|
|
* @return array
|
2014-05-19 00:05:59 +00:00
|
|
|
*/
|
|
|
|
public function getScratchTokens() {
|
|
|
|
return $this->scratchTokens;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Verify a token against the secret or scratch tokens
|
|
|
|
*
|
|
|
|
* @param string $token Token to verify
|
|
|
|
* @param OATHUser $user
|
|
|
|
*
|
2014-05-22 07:33:40 +00:00
|
|
|
* @return int|false Returns a constant represent what type of token was matched,
|
|
|
|
* or false for no match
|
2014-05-19 00:05:59 +00:00
|
|
|
*/
|
2016-10-03 04:32:30 +00:00
|
|
|
public function verifyToken( $token, OATHUser $user ) {
|
2014-05-19 00:05:59 +00:00
|
|
|
global $wgOATHAuthWindowRadius;
|
|
|
|
|
2016-09-16 23:18:35 +00:00
|
|
|
if ( $this->secret['mode'] !== 'hotp' ) {
|
2014-05-19 00:05:59 +00:00
|
|
|
throw new \DomainException( 'OATHAuth extension does not support non-HOTP tokens' );
|
|
|
|
}
|
|
|
|
|
|
|
|
// Prevent replay attacks
|
2016-09-16 23:18:35 +00:00
|
|
|
$memc = ObjectCache::newAnything( [] );
|
2016-04-01 20:45:40 +00:00
|
|
|
$uid = CentralIdLookup::factory()->centralIdFromLocalUser( $user->getUser() );
|
2016-11-24 10:07:02 +00:00
|
|
|
$memcKey = wfMemcKey( 'oathauth', 'usedtokens', $uid );
|
2014-05-19 00:05:59 +00:00
|
|
|
$lastWindow = (int)$memc->get( $memcKey );
|
|
|
|
|
|
|
|
$retval = false;
|
|
|
|
$results = HOTP::generateByTimeWindow(
|
|
|
|
Base32::decode( $this->secret['secret'] ),
|
2016-09-16 23:18:35 +00:00
|
|
|
$this->secret['period'], -$wgOATHAuthWindowRadius, $wgOATHAuthWindowRadius
|
|
|
|
);
|
2016-11-16 22:25:17 +00:00
|
|
|
|
|
|
|
// Remove any whitespace from the received token, which can be an intended group seperator
|
|
|
|
// or trimmeable whitespace
|
|
|
|
$token = preg_replace( '/\s+/', '', $token );
|
|
|
|
|
2018-12-17 23:56:47 +00:00
|
|
|
$clientIP = $user->getUser()->getRequest()->getIP();
|
|
|
|
|
|
|
|
$logger = $this->getLogger();
|
|
|
|
|
2014-05-19 00:05:59 +00:00
|
|
|
// 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;
|
2014-05-22 07:33:40 +00:00
|
|
|
$retval = self::MAIN_TOKEN;
|
2018-12-17 23:56:47 +00:00
|
|
|
|
|
|
|
$logger->info( 'OATHAuth user {user} entered a valid OTP from {clientip}', [
|
|
|
|
'user' => $user->getAccount(),
|
|
|
|
'clientip' => $clientIP,
|
|
|
|
] );
|
2014-05-19 00:05:59 +00:00
|
|
|
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
|
2018-12-17 23:56:47 +00:00
|
|
|
if ( $length === 1 && $this->scratchTokens[0] === "" ) {
|
2014-05-19 00:05:59 +00:00
|
|
|
$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] );
|
2018-12-17 23:56:47 +00:00
|
|
|
|
|
|
|
$logger->info( 'OATHAuth user {user} used a scratch token from {clientip}', [
|
|
|
|
'user' => $user->getAccount(),
|
|
|
|
'clientip' => $clientIP,
|
|
|
|
] );
|
|
|
|
|
2015-05-25 23:47:03 +00:00
|
|
|
$oathrepo = OATHAuthHooks::getOATHUserRepository();
|
2014-05-19 00:05:59 +00:00
|
|
|
$user->setKey( $this );
|
2018-12-17 23:56:47 +00:00
|
|
|
$oathrepo->persist( $user, $clientIP );
|
2014-05-19 00:05:59 +00:00
|
|
|
// Only return true if we removed it from the database
|
2014-05-22 07:33:40 +00:00
|
|
|
$retval = self::SCRATCH_TOKEN;
|
2014-05-19 00:05:59 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( $retval ) {
|
2016-09-16 23:18:35 +00:00
|
|
|
$memc->set(
|
|
|
|
$memcKey,
|
|
|
|
$lastWindow,
|
|
|
|
$this->secret['period'] * ( 1 + 2 * $wgOATHAuthWindowRadius )
|
|
|
|
);
|
2016-10-03 04:32:30 +00:00
|
|
|
} else {
|
2018-12-17 23:56:47 +00:00
|
|
|
|
|
|
|
$logger->info( 'OATHAuth user {user} failed OTP/scratch token from {clientip}', [
|
|
|
|
'user' => $user->getAccount(),
|
|
|
|
'clientip' => $clientIP,
|
|
|
|
] );
|
|
|
|
|
2016-10-03 04:32:30 +00:00
|
|
|
// Increase rate limit counter for failed request
|
|
|
|
$user->getUser()->pingLimiter( 'badoath' );
|
2014-05-19 00:05:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return $retval;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function regenerateScratchTokens() {
|
2016-09-16 23:18:35 +00:00
|
|
|
$scratchTokens = [];
|
2018-12-12 23:35:58 +00:00
|
|
|
for ( $i = 0; $i < 10; $i++ ) {
|
2018-07-17 07:20:57 +00:00
|
|
|
$scratchTokens[] = Base32::encode( random_bytes( 10 ) );
|
2014-05-19 00:05:59 +00:00
|
|
|
}
|
|
|
|
$this->scratchTokens = $scratchTokens;
|
|
|
|
}
|
2016-11-16 23:03:24 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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 );
|
|
|
|
}
|
2018-12-17 23:56:47 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @return LoggerInterface
|
|
|
|
*/
|
|
|
|
private function getLogger() {
|
|
|
|
return LoggerFactory::getInstance( 'authentication' );
|
|
|
|
}
|
2014-05-19 00:05:59 +00:00
|
|
|
}
|