mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/OATHAuth
synced 2024-11-27 17:50:15 +00:00
Make Key objects aware of their database IDs
Bug: T242031 Depends-On: I1db9b04a42783b8b64ed69f1f950c794c8659209 Change-Id: I0d8d0a42ce627387949dbbbb32fc318088b3538e
This commit is contained in:
parent
18d7b47eb8
commit
c09ec34213
|
@ -14,8 +14,8 @@ require_once "$IP/maintenance/Maintenance.php";
|
||||||
class DisableOATHAuthForUser extends Maintenance {
|
class DisableOATHAuthForUser extends Maintenance {
|
||||||
public function __construct() {
|
public function __construct() {
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
$this->addDescription( 'Remove OATHAuth from a specific user' );
|
$this->addDescription( 'Remove all two-factor authentication devices from a specific user' );
|
||||||
$this->addArg( 'user', 'The username to remove OATHAuth from.' );
|
$this->addArg( 'user', 'The username to remove 2FA devices from.' );
|
||||||
$this->requireExtension( 'OATHAuth' );
|
$this->requireExtension( 'OATHAuth' );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,17 +31,17 @@ class DisableOATHAuthForUser extends Maintenance {
|
||||||
$repo = OATHAuthServices::getInstance()->getUserRepository();
|
$repo = OATHAuthServices::getInstance()->getUserRepository();
|
||||||
$oathUser = $repo->findByUser( $user );
|
$oathUser = $repo->findByUser( $user );
|
||||||
if ( !$oathUser->isTwoFactorAuthEnabled() ) {
|
if ( !$oathUser->isTwoFactorAuthEnabled() ) {
|
||||||
$this->fatalError( "User $username doesn't have OATHAuth enabled!" );
|
$this->fatalError( "User $username does not have two-factor authentication enabled!" );
|
||||||
}
|
}
|
||||||
|
|
||||||
$repo->remove( $oathUser, 'Maintenance script', false );
|
$repo->removeAll( $oathUser, 'Maintenance script', false );
|
||||||
// Kill all existing sessions.
|
// Kill all existing sessions.
|
||||||
// If this request to disable 2FA was social-engineered by an attacker,
|
// If this request to disable 2FA was social-engineered by an attacker,
|
||||||
// the legitimate user will hopefully log in again to the wiki, and notice that the second factor
|
// the legitimate user will hopefully log in again to the wiki, and notice that the second factor
|
||||||
// is missing or different, and alert the operators.
|
// is missing or different, and alert the operators.
|
||||||
SessionManager::singleton()->invalidateSessionsForUser( $user );
|
SessionManager::singleton()->invalidateSessionsForUser( $user );
|
||||||
|
|
||||||
$this->output( "OATHAuth disabled for $username.\n" );
|
$this->output( "Two-factor authentication disabled for $username.\n" );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -71,7 +71,7 @@ class TOTPDisableForm extends OATHAuthOOUIHTMLForm {
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->oathUser->setKeys();
|
$this->oathUser->setKeys();
|
||||||
$this->oathRepo->remove( $this->oathUser, $this->getRequest()->getIP(), true );
|
$this->oathRepo->removeAll( $this->oathUser, $this->getRequest()->getIP(), true );
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,11 +7,15 @@ use stdClass;
|
||||||
|
|
||||||
interface IAuthKey extends JsonSerializable {
|
interface IAuthKey extends JsonSerializable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return int|null the ID of this key in the oathauth_devices table, or null if this key has not been saved yet
|
||||||
|
*/
|
||||||
|
public function getId(): ?int;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array|stdClass $data
|
* @param array|stdClass $data
|
||||||
* @param OATHUser $user
|
* @param OATHUser $user
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function verify( $data, OATHUser $user );
|
public function verify( $data, OATHUser $user );
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,10 @@ use Psr\Log\LoggerInterface;
|
||||||
* @ingroup Extensions
|
* @ingroup Extensions
|
||||||
*/
|
*/
|
||||||
class TOTPKey implements IAuthKey {
|
class TOTPKey implements IAuthKey {
|
||||||
/** @var array Two-factor binary secret */
|
/** @var int|null */
|
||||||
|
private ?int $id;
|
||||||
|
|
||||||
|
/** @var array Two factor binary secret */
|
||||||
private $secret;
|
private $secret;
|
||||||
|
|
||||||
/** @var string[] List of recovery codes */
|
/** @var string[] List of recovery codes */
|
||||||
|
@ -53,6 +56,7 @@ class TOTPKey implements IAuthKey {
|
||||||
*/
|
*/
|
||||||
public static function newFromRandom() {
|
public static function newFromRandom() {
|
||||||
$object = new self(
|
$object = new self(
|
||||||
|
null,
|
||||||
Base32::encode( random_bytes( 10 ) ),
|
Base32::encode( random_bytes( 10 ) ),
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
@ -70,14 +74,17 @@ class TOTPKey implements IAuthKey {
|
||||||
if ( !isset( $data['secret'] ) || !isset( $data['scratch_tokens'] ) ) {
|
if ( !isset( $data['secret'] ) || !isset( $data['scratch_tokens'] ) ) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return new static( $data['secret'], $data['scratch_tokens'] );
|
return new static( $data['id'] ?? null, $data['secret'], $data['scratch_tokens'] );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @param int|null $id the database id of this key
|
||||||
* @param string $secret
|
* @param string $secret
|
||||||
* @param array $recoveryCodes
|
* @param array $recoveryCodes
|
||||||
*/
|
*/
|
||||||
public function __construct( $secret, array $recoveryCodes ) {
|
public function __construct( ?int $id, $secret, array $recoveryCodes ) {
|
||||||
|
$this->id = $id;
|
||||||
|
|
||||||
// Currently hardcoded values; might be used in the future
|
// Currently hardcoded values; might be used in the future
|
||||||
$this->secret = [
|
$this->secret = [
|
||||||
'mode' => 'hotp',
|
'mode' => 'hotp',
|
||||||
|
@ -88,6 +95,13 @@ class TOTPKey implements IAuthKey {
|
||||||
$this->recoveryCodes = array_values( $recoveryCodes );
|
$this->recoveryCodes = array_values( $recoveryCodes );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return int|null
|
||||||
|
*/
|
||||||
|
public function getId(): ?int {
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -76,41 +76,7 @@ class OATHUserRepository implements LoggerAwareInterface {
|
||||||
$uid = $this->centralIdLookupFactory->getLookup()
|
$uid = $this->centralIdLookupFactory->getLookup()
|
||||||
->centralIdFromLocalUser( $user );
|
->centralIdFromLocalUser( $user );
|
||||||
$oathUser = new OATHUser( $user, $uid );
|
$oathUser = new OATHUser( $user, $uid );
|
||||||
|
$this->loadKeysFromDatabase( $oathUser );
|
||||||
$res = $this->dbProvider
|
|
||||||
->getReplicaDatabase( 'virtual-oathauth' )
|
|
||||||
->newSelectQueryBuilder()
|
|
||||||
->select( [
|
|
||||||
'oad_data',
|
|
||||||
'oat_name',
|
|
||||||
] )
|
|
||||||
->from( 'oathauth_devices' )
|
|
||||||
->join( 'oathauth_types', null, [ 'oat_id = oad_type' ] )
|
|
||||||
->where( [ 'oad_user' => $uid ] )
|
|
||||||
->caller( __METHOD__ )
|
|
||||||
->fetchResultSet();
|
|
||||||
|
|
||||||
$module = null;
|
|
||||||
|
|
||||||
foreach ( $res as $row ) {
|
|
||||||
if ( $module && $row->oat_name !== $module->getName() ) {
|
|
||||||
// Not supported by current application-layer code.
|
|
||||||
throw new RuntimeException( "user {$uid} has multiple different oathauth modules defined" );
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( !$module ) {
|
|
||||||
$module = $this->moduleRegistry->getModuleByKey( $row->oat_name );
|
|
||||||
$oathUser->setModule( $module );
|
|
||||||
|
|
||||||
if ( !$module ) {
|
|
||||||
throw new MWException( 'oathauth-module-invalid' );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$keyData = FormatJson::decode( $row->oad_data, true );
|
|
||||||
$oathUser->addKey( $module->newKey( $keyData ) );
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->cache->set( $user->getName(), $oathUser );
|
$this->cache->set( $user->getName(), $oathUser );
|
||||||
}
|
}
|
||||||
return $oathUser;
|
return $oathUser;
|
||||||
|
@ -128,7 +94,6 @@ class OATHUserRepository implements LoggerAwareInterface {
|
||||||
}
|
}
|
||||||
$prevUser = $this->findByUser( $user->getUser() );
|
$prevUser = $this->findByUser( $user->getUser() );
|
||||||
$userId = $this->centralIdLookupFactory->getLookup()->centralIdFromLocalUser( $user->getUser() );
|
$userId = $this->centralIdLookupFactory->getLookup()->centralIdFromLocalUser( $user->getUser() );
|
||||||
|
|
||||||
$moduleId = $this->moduleRegistry->getModuleId( $user->getModule()->getName() );
|
$moduleId = $this->moduleRegistry->getModuleId( $user->getModule()->getName() );
|
||||||
|
|
||||||
$dbw = $this->dbProvider->getPrimaryDatabase( 'virtual-oathauth' );
|
$dbw = $this->dbProvider->getPrimaryDatabase( 'virtual-oathauth' );
|
||||||
|
@ -141,20 +106,22 @@ class OATHUserRepository implements LoggerAwareInterface {
|
||||||
->caller( __METHOD__ )
|
->caller( __METHOD__ )
|
||||||
->execute();
|
->execute();
|
||||||
|
|
||||||
$insert = $dbw->newInsertQueryBuilder()
|
|
||||||
->insertInto( 'oathauth_devices' )
|
|
||||||
->caller( __METHOD__ );
|
|
||||||
foreach ( $user->getKeys() as $key ) {
|
foreach ( $user->getKeys() as $key ) {
|
||||||
$insert->row( [
|
$dbw->newInsertQueryBuilder()
|
||||||
'oad_user' => $userId,
|
->insertInto( 'oathauth_devices' )
|
||||||
'oad_type' => $moduleId,
|
->row( [
|
||||||
'oad_data' => FormatJson::encode( $key->jsonSerialize() )
|
'oad_user' => $userId,
|
||||||
] );
|
'oad_type' => $moduleId,
|
||||||
|
'oad_data' => FormatJson::encode( $key->jsonSerialize() )
|
||||||
|
] )
|
||||||
|
->caller( __METHOD__ )
|
||||||
|
->execute();
|
||||||
}
|
}
|
||||||
$insert->execute();
|
|
||||||
|
|
||||||
$dbw->endAtomic( __METHOD__ );
|
$dbw->endAtomic( __METHOD__ );
|
||||||
|
|
||||||
|
$this->loadKeysFromDatabase( $user );
|
||||||
|
|
||||||
$userName = $user->getUser()->getName();
|
$userName = $user->getUser()->getName();
|
||||||
$this->cache->set( $userName, $user );
|
$this->cache->set( $userName, $user );
|
||||||
|
|
||||||
|
@ -209,7 +176,7 @@ class OATHUserRepository implements LoggerAwareInterface {
|
||||||
|
|
||||||
$hasExistingKey = $user->isTwoFactorAuthEnabled();
|
$hasExistingKey = $user->isTwoFactorAuthEnabled();
|
||||||
|
|
||||||
$key = $module->newKey( $keyData );
|
$key = $module->newKey( $keyData + [ 'id' => $id ] );
|
||||||
$user->addKey( $key );
|
$user->addKey( $key );
|
||||||
|
|
||||||
$this->logger->info( 'OATHAuth {oathtype} key {key} added for {user} from {clientip}', [
|
$this->logger->info( 'OATHAuth {oathtype} key {key} added for {user} from {clientip}', [
|
||||||
|
@ -231,8 +198,19 @@ class OATHUserRepository implements LoggerAwareInterface {
|
||||||
* @param OATHUser $user
|
* @param OATHUser $user
|
||||||
* @param string $clientInfo
|
* @param string $clientInfo
|
||||||
* @param bool $self Whether the user disabled the 2FA themselves
|
* @param bool $self Whether the user disabled the 2FA themselves
|
||||||
|
*
|
||||||
|
* @deprecated since 1.41, use removeAll() instead
|
||||||
*/
|
*/
|
||||||
public function remove( OATHUser $user, $clientInfo, bool $self ) {
|
public function remove( OATHUser $user, $clientInfo, bool $self ) {
|
||||||
|
$this->removeAll( $user, $clientInfo, $self );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param OATHUser $user
|
||||||
|
* @param string $clientInfo
|
||||||
|
* @param bool $self Whether they disabled it themselves
|
||||||
|
*/
|
||||||
|
public function removeAll( OATHUser $user, $clientInfo, bool $self ) {
|
||||||
$userId = $this->centralIdLookupFactory->getLookup()
|
$userId = $this->centralIdLookupFactory->getLookup()
|
||||||
->centralIdFromLocalUser( $user->getUser() );
|
->centralIdFromLocalUser( $user->getUser() );
|
||||||
$this->dbProvider->getPrimaryDatabase( 'virtual-oathauth' )
|
$this->dbProvider->getPrimaryDatabase( 'virtual-oathauth' )
|
||||||
|
@ -260,4 +238,47 @@ class OATHUserRepository implements LoggerAwareInterface {
|
||||||
|
|
||||||
Manager::notifyDisabled( $user, $self );
|
Manager::notifyDisabled( $user, $self );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function loadKeysFromDatabase( OATHUser $user ): void {
|
||||||
|
$uid = $this->centralIdLookupFactory->getLookup()
|
||||||
|
->centralIdFromLocalUser( $user->getUser() );
|
||||||
|
|
||||||
|
$res = $this->dbProvider
|
||||||
|
->getReplicaDatabase( 'virtual-oathauth' )
|
||||||
|
->newSelectQueryBuilder()
|
||||||
|
->select( [
|
||||||
|
'oad_id',
|
||||||
|
'oad_data',
|
||||||
|
'oat_name',
|
||||||
|
] )
|
||||||
|
->from( 'oathauth_devices' )
|
||||||
|
->join( 'oathauth_types', null, [ 'oat_id = oad_type' ] )
|
||||||
|
->where( [ 'oad_user' => $uid ] )
|
||||||
|
->caller( __METHOD__ )
|
||||||
|
->fetchResultSet();
|
||||||
|
|
||||||
|
$module = null;
|
||||||
|
|
||||||
|
// Clear stored key list before loading keys
|
||||||
|
$user->disable();
|
||||||
|
|
||||||
|
foreach ( $res as $row ) {
|
||||||
|
if ( $module && $row->oat_name !== $module->getName() ) {
|
||||||
|
// Not supported by current application-layer code.
|
||||||
|
throw new RuntimeException( "User {$uid} has multiple different two-factor modules defined" );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( !$module ) {
|
||||||
|
$module = $this->moduleRegistry->getModuleByKey( $row->oat_name );
|
||||||
|
$user->setModule( $module );
|
||||||
|
|
||||||
|
if ( !$module ) {
|
||||||
|
throw new MWException( 'oathauth-module-invalid' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$keyData = FormatJson::decode( $row->oad_data, true );
|
||||||
|
$user->addKey( $module->newKey( $keyData + [ 'id' => (int)$row->oad_id ] ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -142,7 +142,7 @@ class DisableOATHForUser extends FormSpecialPage {
|
||||||
return [ 'oathauth-throttled', Message::durationParam( 60 ) ];
|
return [ 'oathauth-throttled', Message::durationParam( 60 ) ];
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->userRepo->remove( $oathUser, $this->getRequest()->getIP(), false );
|
$this->userRepo->removeAll( $oathUser, $this->getRequest()->getIP(), false );
|
||||||
|
|
||||||
// messages used: logentry-oath-disable-other, log-action-oath-disable-other
|
// messages used: logentry-oath-disable-other, log-action-oath-disable-other
|
||||||
$logEntry = new ManualLogEntry( 'oath', 'disable-other' );
|
$logEntry = new ManualLogEntry( 'oath', 'disable-other' );
|
||||||
|
|
Loading…
Reference in a new issue