mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/OATHAuth
synced 2024-09-23 10:23:23 +00:00
Refactor extension key storage
This takes out the actual key information from OATHUser and puts it into an OATHKey class, which OATHUser depends on. This allows easily swapping keys in/out from a user. Change-Id: Ife5f1bae4ad65b66c5e20017cc43c0576b4aba19
This commit is contained in:
parent
6a0bba4579
commit
89455cdfb2
|
@ -76,14 +76,17 @@ class OATHAuthHooks {
|
|||
global $wgRequest;
|
||||
|
||||
$token = $wgRequest->getText( 'wpOATHToken' );
|
||||
$oathuser = OATHUser::newFromUser( $user );
|
||||
$oathrepo = new OATHUserRepository( wfGetLB() );
|
||||
$oathuser = $oathrepo->findByUser( $user );
|
||||
# Though it's weird to default to true, we only want to deny
|
||||
# users who have two-factor enabled and have validated their
|
||||
# token.
|
||||
$result = true;
|
||||
if ( $oathuser && $oathuser->isEnabled() && $oathuser->isValidated() ) {
|
||||
$result = $oathuser->verifyToken( $token );
|
||||
|
||||
if ( $oathuser->getKey() !== null ) {
|
||||
$result = $oathuser->getKey()->verifyToken( $token, $oathuser );
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
@ -97,8 +100,9 @@ class OATHAuthHooks {
|
|||
static function TwoFactorIsEnabled( &$isEnabled ) {
|
||||
global $wgUser;
|
||||
|
||||
$user = OATHUser::newFromUser( $wgUser );
|
||||
if ( $user && $user->isEnabled() && $user->isValidated() ) {
|
||||
$oathrepo = new OATHUserRepository( wfGetLB() );
|
||||
$user = $oathrepo->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.
|
||||
|
@ -120,10 +124,11 @@ class OATHAuthHooks {
|
|||
* @return bool
|
||||
*/
|
||||
public static function manageOATH( User $user, array &$preferences ) {
|
||||
$oathUser = OATHUser::newFromUser( $user );
|
||||
$oathrepo = new OATHUserRepository( wfGetLB() );
|
||||
$oathUser = $oathrepo->findByUser( $user );
|
||||
|
||||
$title = SpecialPage::getTitleFor( 'OATH' );
|
||||
if ( $oathUser->isEnabled() && $oathUser->isValidated() ) {
|
||||
if ( $oathUser->getKey() !== null ) {
|
||||
$preferences['oath-disable'] = array(
|
||||
'type' => 'info',
|
||||
'raw' => 'true',
|
||||
|
@ -139,20 +144,6 @@ class OATHAuthHooks {
|
|||
'label-message' => 'oathauth-prefs-label',
|
||||
'section' => 'personal/info',
|
||||
);
|
||||
$preferences['oath-reset'] = array(
|
||||
'type' => 'info',
|
||||
'raw' => 'true',
|
||||
'default' => Linker::link(
|
||||
$title,
|
||||
wfMessage( 'oathauth-reset' )->escaped(),
|
||||
array(),
|
||||
array(
|
||||
'action' => 'reset',
|
||||
'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText()
|
||||
)
|
||||
),
|
||||
'section' => 'personal/info',
|
||||
);
|
||||
} else {
|
||||
$preferences['oath-enable'] = array(
|
||||
'type' => 'info',
|
||||
|
@ -183,9 +174,55 @@ class OATHAuthHooks {
|
|||
switch ( $updater->getDB()->getType() ) {
|
||||
case 'mysql':
|
||||
case 'sqlite':
|
||||
$updater->addExtensionTable( 'oathauth_users', "$base/oathauth.sql" );
|
||||
$updater->addExtensionTable( 'oathauth_users', "$base/sql/mysql/tables.sql" );
|
||||
$updater->addExtensionUpdate( array( array( __CLASS__, 'schemaUpdateOldUsersFromInstaller' ) ) );
|
||||
$updater->dropExtensionField( 'oathauth_users', 'secret_reset',
|
||||
"$base/sql/mysql/patch-remove_reset.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 DatabaseUpdater $updater
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function schemaUpdateOldUsers( DatabaseBase $db ) {
|
||||
if ( !$db->fieldExists( 'oathauth_users', 'secret_reset' ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$res = $db->select( 'oathauth_users', array( 'id', 'scratch_tokens' ), '', __METHOD__ );
|
||||
|
||||
foreach ( $res as $row ) {
|
||||
$scratchTokens = unserialize( base64_decode( $row->scratch_tokens ) );
|
||||
if ( $scratchTokens ) {
|
||||
$db->update(
|
||||
'oathauth_users',
|
||||
array( 'scratch_tokens' => implode( ',', $scratchTokens ) ),
|
||||
array( 'id' => $row->id ),
|
||||
__METHOD__
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
131
OATHAuthKey.php
Normal file
131
OATHAuthKey.php
Normal file
|
@ -0,0 +1,131 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Class representing a two-factor key
|
||||
*
|
||||
* Keys can be tied to OAUTHUsers
|
||||
*/
|
||||
class OATHAuthKey {
|
||||
/** @var string 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 ) ),
|
||||
array()
|
||||
);
|
||||
|
||||
$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 = array(
|
||||
'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 bool True on match, false otherwise
|
||||
*/
|
||||
public function verifyToken( $token, $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( array() );
|
||||
$memcKey = wfMemcKey( 'oauthauth', 'usedtokens', $user->getUser()->getId() );
|
||||
$lastWindow = (int)$memc->get( $memcKey );
|
||||
|
||||
$retval = false;
|
||||
$results = HOTP::generateByTimeWindow(
|
||||
Base32::decode( $this->secret['secret'] ),
|
||||
$this->secret['period'], -$wgOATHAuthWindowRadius, $wgOATHAuthWindowRadius );
|
||||
// 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 = true;
|
||||
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 = new OATHUserRepository( wfGetLB() );
|
||||
$user->setKey( $this );
|
||||
$oathrepo->persist( $user );
|
||||
// Only return true if we removed it from the database
|
||||
$retval = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( $retval ) {
|
||||
$memc->set( $memcKey, $lastWindow,
|
||||
$this->secret['period'] * (1 + 2 * $wgOATHAuthWindowRadius) );
|
||||
}
|
||||
|
||||
return $retval;
|
||||
}
|
||||
|
||||
public function regenerateScratchTokens() {
|
||||
$scratchTokens = array();
|
||||
for ( $i = 0; $i < 5; $i++ ) {
|
||||
array_push( $scratchTokens, Base32::encode( MWCryptRand::generate( 10, true ) ) );
|
||||
}
|
||||
$this->scratchTokens = $scratchTokens;
|
||||
}
|
||||
}
|
313
OATHUser.php
313
OATHUser.php
|
@ -7,319 +7,50 @@
|
|||
* @ingroup Extensions
|
||||
*/
|
||||
class OATHUser {
|
||||
/** @var int User ID */
|
||||
private $id;
|
||||
/** @var User */
|
||||
private $user;
|
||||
|
||||
/** @var string Two factor binary secret */
|
||||
private $secret;
|
||||
|
||||
/** @var string New two factor secret when resetting */
|
||||
private $secretReset;
|
||||
|
||||
/** @var string[] List of scratch tokens */
|
||||
private $scratchTokens;
|
||||
|
||||
/** @var string[] New scratch tokens when resetting */
|
||||
private $scratchTokensReset;
|
||||
|
||||
/** @var string Name for the two-factor account */
|
||||
private $account;
|
||||
|
||||
/** @var bool Whether two-factor is enabled */
|
||||
private $isEnabled;
|
||||
|
||||
/** @var bool Whether two-factor is validated */
|
||||
private $isValidated;
|
||||
/** @var OATHAuthKey|null */
|
||||
private $key;
|
||||
|
||||
/**
|
||||
* Constructor. Can't be called directly. Call one of the static NewFrom* methods
|
||||
* @param $id Int Database id for the group
|
||||
* @param $account
|
||||
* @param $secret
|
||||
* @param $secretReset
|
||||
* @param $scratchTokens
|
||||
* @param $scratchTokensReset
|
||||
* @param bool $isValidated bool
|
||||
* @todo Get rid of telescoping constructor anti-pattern
|
||||
* @param User $user
|
||||
* @param OATHAuthKey $key
|
||||
*/
|
||||
public function __construct( $id, $account, $secret = null, $secretReset = null,
|
||||
$scratchTokens = null, $scratchTokensReset = null, $isValidated = false
|
||||
) {
|
||||
$this->id = $id;
|
||||
$this->account = $account;
|
||||
$this->isEnabled = true;
|
||||
if ( $secret ) {
|
||||
$this->secret = $secret;
|
||||
} else {
|
||||
$this->secret = Base32::encode( MWCryptRand::generate( 10, true ) );
|
||||
$this->isEnabled = false;
|
||||
}
|
||||
if ( $secretReset ) {
|
||||
$this->secretReset = $secretReset;
|
||||
} else {
|
||||
$this->secretReset = Base32::encode( MWCryptRand::generate( 10, true ) );
|
||||
}
|
||||
if ( $scratchTokens ) {
|
||||
$this->scratchTokens = $scratchTokens;
|
||||
} else {
|
||||
$this->regenerateScratchTokens( false );
|
||||
$this->isEnabled = false;
|
||||
}
|
||||
if ( $scratchTokensReset ) {
|
||||
$this->scratchTokensReset = $scratchTokensReset;
|
||||
} else {
|
||||
$this->regenerateScratchTokens( true );
|
||||
}
|
||||
$this->isValidated = $isValidated;
|
||||
public function __construct( User $user, OATHAuthKey $key = null ) {
|
||||
$this->user = $user;
|
||||
$this->key = $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $reset bool
|
||||
*/
|
||||
public function regenerateScratchTokens( $reset ) {
|
||||
$scratchTokens = array();
|
||||
for ( $i = 0; $i < 5; $i++ ) {
|
||||
array_push( $scratchTokens, Base32::encode( MWCryptRand::generate( 10, true ) ) );
|
||||
}
|
||||
if ( $reset ) {
|
||||
$this->scratchTokensReset = $scratchTokens;
|
||||
} else {
|
||||
$this->scratchTokens = $scratchTokens;
|
||||
}
|
||||
public function getUser() {
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return String
|
||||
*/
|
||||
public function getAccount() {
|
||||
return $this->account;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return String
|
||||
*/
|
||||
public function getSecret() {
|
||||
return $this->secret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return String
|
||||
*/
|
||||
public function getSecretReset() {
|
||||
return $this->secretReset;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Array
|
||||
*/
|
||||
public function getScratchTokens() {
|
||||
return $this->scratchTokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Array
|
||||
*/
|
||||
public function getScratchTokensReset() {
|
||||
return $this->scratchTokensReset;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Boolean
|
||||
*/
|
||||
public function isEnabled() {
|
||||
return $this->isEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Boolean
|
||||
*/
|
||||
public function isValidated() {
|
||||
return $this->isValidated;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $token
|
||||
* @param $reset bool
|
||||
* @return Boolean
|
||||
*/
|
||||
public function verifyToken( $token, $reset = false ) {
|
||||
global $wgOATHAuthWindowRadius;
|
||||
|
||||
$memc = ObjectCache::newAnything( array() );
|
||||
|
||||
// Prevent replay attacks
|
||||
$memcKey = wfMemcKey( 'oauthauth', 'usedtokens', $reset ? 'reset' : null, $this->getAccount() );
|
||||
$lastWindow = (int)$memc->get( $memcKey );
|
||||
|
||||
$retval = false;
|
||||
$secret = $reset ? $this->secretReset : $this->secret;
|
||||
$results = HOTP::generateByTimeWindow(
|
||||
Base32::decode( $secret ),
|
||||
30, -$wgOATHAuthWindowRadius, $wgOATHAuthWindowRadius );
|
||||
// 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 = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// See if the user is using a scratch token
|
||||
$length = count( $this->scratchTokens );
|
||||
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] );
|
||||
// Only return true if we removed it from the database
|
||||
$retval = $this->updateScratchTokens();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $retval ) {
|
||||
$memc->set( $memcKey, $lastWindow, 30 * (1 + 2 * $wgOATHAuthWindowRadius) );
|
||||
}
|
||||
|
||||
return $retval;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $user User
|
||||
* @return OATHUser|null
|
||||
*/
|
||||
public static function newFromUser( $user ) {
|
||||
global $wgSitename;
|
||||
|
||||
$id = $user->getId();
|
||||
if ( $id === 0 ) {
|
||||
return null;
|
||||
}
|
||||
$dbr = wfGetDB( DB_SLAVE );
|
||||
$row = $dbr->selectRow(
|
||||
'oathauth_users',
|
||||
array( 'id',
|
||||
'secret',
|
||||
'secret_reset',
|
||||
'scratch_tokens',
|
||||
'scratch_tokens_reset',
|
||||
'is_validated' ),
|
||||
array( 'id' => $id ),
|
||||
__METHOD__ );
|
||||
|
||||
if ( $row ) {
|
||||
return new OATHUser(
|
||||
$id,
|
||||
urlencode( $user->getName() ) . '@' . $wgSitename,
|
||||
$row->secret,
|
||||
$row->secret_reset,
|
||||
unserialize( base64_decode( $row->scratch_tokens ) ),
|
||||
unserialize( base64_decode( $row->scratch_tokens_reset ) ),
|
||||
$row->is_validated
|
||||
);
|
||||
} else {
|
||||
return new OATHUser(
|
||||
$id,
|
||||
urlencode( $user->getName() ) . '@' . $wgSitename
|
||||
);
|
||||
}
|
||||
return "$wgSitename:{$this->user->getName()}";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $username string
|
||||
* @return OATHUser|null
|
||||
* Get the key associated with this user.
|
||||
*
|
||||
* @return null|OATHAuthKey
|
||||
*/
|
||||
public static function newFromUsername( $username ) {
|
||||
$user = User::newFromName( $username, true );
|
||||
return OATHUser::newFromUser( $user );
|
||||
public function getKey() {
|
||||
return $this->key;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
* Set the key associated with this user.
|
||||
*
|
||||
* @param OATHAuthKey|null $key
|
||||
*/
|
||||
public function enable() {
|
||||
$dbw = wfGetDB( DB_MASTER );
|
||||
return $dbw->insert(
|
||||
'oathauth_users',
|
||||
array( 'id' => $this->id,
|
||||
'secret' => $this->secret,
|
||||
'scratch_tokens' => base64_encode( serialize( $this->scratchTokens ) ),
|
||||
'is_validated' => false,
|
||||
),
|
||||
__METHOD__
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function setReset() {
|
||||
$dbw = wfGetDB( DB_MASTER );
|
||||
return $dbw->update(
|
||||
'oathauth_users',
|
||||
array(
|
||||
'secret_reset' => $this->secretReset,
|
||||
'scratch_tokens_reset' => base64_encode( serialize( $this->scratchTokensReset ) )
|
||||
),
|
||||
array( 'id' => $this->id ),
|
||||
__METHOD__
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function reset() {
|
||||
$dbw = wfGetDB( DB_MASTER );
|
||||
return $dbw->update(
|
||||
'oathauth_users',
|
||||
array(
|
||||
'secret' => $this->secretReset,
|
||||
'secret_reset' => null,
|
||||
'scratch_tokens' => base64_encode( serialize( $this->scratchTokensReset ) ),
|
||||
'scratch_tokens_reset' => null,
|
||||
),
|
||||
array( 'id' => $this->id ),
|
||||
__METHOD__
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function validate() {
|
||||
$dbw = wfGetDB( DB_MASTER );
|
||||
return $dbw->update(
|
||||
'oathauth_users',
|
||||
array( 'is_validated' => true ),
|
||||
array( 'id' => $this->id ),
|
||||
__METHOD__
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function updateScratchTokens() {
|
||||
$dbw = wfGetDB( DB_MASTER );
|
||||
return $dbw->update(
|
||||
'oathauth_users',
|
||||
array( 'scratch_tokens' => base64_encode( serialize( $this->scratchTokens ) ) ),
|
||||
array( 'id' => $this->id ),
|
||||
__METHOD__
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function disable() {
|
||||
$dbw = wfGetDB( DB_MASTER );
|
||||
return $dbw->delete(
|
||||
'oathauth_users',
|
||||
array( 'id' => $this->id ),
|
||||
__METHOD__
|
||||
);
|
||||
public function setKey( OATHAuthKey $key = null ) {
|
||||
$this->key = $key;
|
||||
}
|
||||
}
|
||||
|
|
45
OATHUserRepository.php
Normal file
45
OATHUserRepository.php
Normal file
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
class OATHUserRepository {
|
||||
private $dbr;
|
||||
|
||||
private $dbw;
|
||||
|
||||
public function __construct( LoadBalancer $lb ) {
|
||||
$this->dbr = $lb->getConnection( DB_SLAVE );
|
||||
$this->dbw = $lb->getConnection( DB_MASTER );
|
||||
}
|
||||
|
||||
public function findByUser( User $user ) {
|
||||
$oathUser = new OATHUser( $user, null );
|
||||
|
||||
$res = $this->dbr->selectRow( 'oathauth_users', '*', array( 'id' => $user->getId() ), __METHOD__ );
|
||||
if ($res) {
|
||||
$key = new OATHAuthKey( $res->secret, explode( ',', $res->scratch_tokens ) );
|
||||
$oathUser->setKey( $key );
|
||||
}
|
||||
|
||||
return $oathUser;
|
||||
}
|
||||
|
||||
public function persist( OATHUser $user ) {
|
||||
$this->dbw->replace(
|
||||
'oathauth_users',
|
||||
array( 'id' ),
|
||||
array(
|
||||
'id' => $user->getUser()->getId(),
|
||||
'secret' => $user->getKey()->getSecret(),
|
||||
'scratch_tokens' => implode( ',', $user->getKey()->getScratchTokens() ),
|
||||
),
|
||||
__METHOD__
|
||||
);
|
||||
}
|
||||
|
||||
public function remove( OATHUser $user ) {
|
||||
$this->dbw->delete(
|
||||
'oathauth_users',
|
||||
array( 'id' => $user->getUser()->getId() ),
|
||||
__METHOD__
|
||||
);
|
||||
}
|
||||
}
|
|
@ -7,6 +7,8 @@
|
|||
"type": "other",
|
||||
"AutoloadClasses": {
|
||||
"OATHAuthHooks": "OATHAuth.hooks.php",
|
||||
"OATHAuthKey": "OATHAuthKey.php",
|
||||
"OATHUserRepository": "OATHUserRepository.php",
|
||||
"HOTP": "lib/hotp.php",
|
||||
"HOTPResult": "lib/hotp.php",
|
||||
"Base32": "lib/base32.php",
|
||||
|
|
|
@ -27,7 +27,7 @@ class HOTP {
|
|||
$bin_counter = implode( $cur_counter );
|
||||
|
||||
// Pad to 8 chars
|
||||
if ( strlen( $bin_counter ) < 8) {
|
||||
if ( strlen( $bin_counter ) < 8 ) {
|
||||
$bin_counter = str_repeat( "\0", 8 - strlen( $bin_counter ) ) . $bin_counter;
|
||||
}
|
||||
|
||||
|
|
54
maintenance/update_scratch_token_format.php
Normal file
54
maintenance/update_scratch_token_format.php
Normal file
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
/**
|
||||
* Update scratch_token column format
|
||||
*
|
||||
* Usage: php update_scratch_token_format.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
|
||||
*
|
||||
* @file
|
||||
* @author Darian Anthony Patrick
|
||||
* @ingroup Maintenance
|
||||
*/
|
||||
|
||||
|
||||
if ( getenv( 'MW_INSTALL_PATH' ) ) {
|
||||
$IP = getenv( 'MW_INSTALL_PATH' );
|
||||
} else {
|
||||
$IP = dirname( __FILE__ ) . '/../../..';
|
||||
}
|
||||
require_once( "$IP/maintenance/Maintenance.php" );
|
||||
|
||||
class UpdateScratchTokenFormat extends Maintenance {
|
||||
|
||||
private $mPurgeDays = null;
|
||||
|
||||
function __construct() {
|
||||
parent::__construct();
|
||||
$this->mDescription = 'Script to update scratch_token column format';
|
||||
}
|
||||
|
||||
public function execute() {
|
||||
$dbr = wfGetDB( DB_SLAVE );
|
||||
if ( !OATHAuthHooks::schemaUpdateOldUsers( $dbr ) ) {
|
||||
$this->error( "Failed to update scratch_token rows.\n", 1);
|
||||
}
|
||||
$this->output( "Done.\n" );
|
||||
}
|
||||
}
|
||||
|
||||
$maintClass = "UpdateScratchTokenFormat";
|
||||
require_once( RUN_MAINTENANCE_IF_MAIN );
|
20
oathauth.sql
20
oathauth.sql
|
@ -1,20 +0,0 @@
|
|||
CREATE TABLE /*_*/oathauth_users (
|
||||
-- User ID
|
||||
id int not null primary key,
|
||||
|
||||
-- Secret key
|
||||
secret varchar(255) binary not null,
|
||||
|
||||
-- Secret key used for resets
|
||||
secret_reset varchar(255) binary,
|
||||
|
||||
-- List of tokens
|
||||
scratch_tokens varchar(512) binary not null,
|
||||
|
||||
-- List of tokens used for resets
|
||||
scratch_tokens_reset varchar(512) binary not null,
|
||||
|
||||
-- Whether the user has validated their token
|
||||
is_validated boolean not null
|
||||
|
||||
) /*$wgDBTableOptions*/;
|
|
@ -8,7 +8,6 @@
|
|||
*/
|
||||
|
||||
class SpecialOATH extends UnlistedSpecialPage {
|
||||
|
||||
/** @var OATHUser|null */
|
||||
private $OATHUser;
|
||||
|
||||
|
@ -18,7 +17,8 @@ class SpecialOATH extends UnlistedSpecialPage {
|
|||
public function __construct() {
|
||||
parent::__construct( 'OATH' );
|
||||
|
||||
$this->OATHUser = OATHUser::newFromUser( $this->getUser() );
|
||||
$this->OATHRepository = new OATHUserRepository( wfGetLB() );
|
||||
$this->OATHUser = $this->OATHRepository->findByUser( $this->getUser() );
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -33,13 +33,10 @@ class SpecialOATH extends UnlistedSpecialPage {
|
|||
$this->getOutput()->addWikiMsg( 'oathauth-mustbeloggedin' );
|
||||
return;
|
||||
}
|
||||
|
||||
$action = $this->getRequest()->getVal( 'action' );
|
||||
if ( $action == "enable" ) {
|
||||
$this->enable();
|
||||
} elseif ( $action == "validate" ) {
|
||||
$this->validate();
|
||||
} elseif ( $action == "reset" ) {
|
||||
$this->reset();
|
||||
} elseif ( $action == "disable" ) {
|
||||
$this->disable();
|
||||
}
|
||||
|
@ -53,16 +50,16 @@ class SpecialOATH extends UnlistedSpecialPage {
|
|||
$this->getOutput()->setPagetitle( $this->msg( 'oathauth-enable' ) );
|
||||
$returnto = $this->getRequest()->getVal( 'returnto' );
|
||||
|
||||
if ( !$this->OATHUser->isEnabled() ) {
|
||||
$result = $this->OATHUser->enable();
|
||||
if ( !$result ) {
|
||||
$this->getOutput()->addWikiMsg( 'oathauth-failedtoenableoauth' );
|
||||
return true;
|
||||
}
|
||||
} elseif ( $this->OATHUser->isEnabled() && $this->OATHUser->isValidated() ) {
|
||||
if ( $this->OATHUser->getKey() ) {
|
||||
$this->getOutput()->addWikiMsg( 'oathauth-alreadyenabled' );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if ( null === $this->getRequest()->getSessionData( 'oathauth_key' ) ) {
|
||||
$this->getRequest()->setSessionData( 'oathauth_key', OATHAuthKey::newFromRandom() );
|
||||
}
|
||||
|
||||
$info['token'] = array(
|
||||
'type' => 'text',
|
||||
'default' => '',
|
||||
|
@ -81,7 +78,7 @@ class SpecialOATH extends UnlistedSpecialPage {
|
|||
);
|
||||
$info['action'] = array(
|
||||
'type' => 'hidden',
|
||||
'default' => 'validate',
|
||||
'default' => 'enable',
|
||||
'name' => 'action',
|
||||
);
|
||||
$form = new HTMLForm(
|
||||
|
@ -91,23 +88,20 @@ class SpecialOATH extends UnlistedSpecialPage {
|
|||
);
|
||||
$form->setSubmitID( 'oathauth-validate-submit' );
|
||||
$form->setSubmitCallback( array( $this, 'tryValidateSubmit' ) );
|
||||
$form->show();
|
||||
|
||||
$this->displaySecret();
|
||||
if ( !$form->show() ) {
|
||||
$this->displaySecret();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $reset bool
|
||||
*/
|
||||
private function displaySecret( $reset = false ) {
|
||||
private function displaySecret() {
|
||||
$this->getOutput()->addModules( 'ext.oathauth' );
|
||||
if ( $reset ) {
|
||||
$secret = $this->OATHUser->getSecretReset();
|
||||
} else {
|
||||
$secret = $this->OATHUser->getSecret();
|
||||
}
|
||||
|
||||
/** @var OATHAuthKey $key */
|
||||
$key = $this->getRequest()->getSessionData( 'oathauth_key' );
|
||||
$secret = $key->getSecret();
|
||||
|
||||
$out = '<strong>' . $this->msg( 'oathauth-account' )->escaped() . '</strong> '
|
||||
. $this->OATHUser->getAccount() . '<br/>'
|
||||
. '<strong>' . $this->msg( 'oathauth-secret' )->escaped() . '</strong> '
|
||||
|
@ -130,89 +124,8 @@ class SpecialOATH extends UnlistedSpecialPage {
|
|||
|
||||
$this->getOutput()->addHTML( $out );
|
||||
$this->getOutput()->addWikiMsg( 'openstackmanager-scratchtokens' );
|
||||
if ( $reset ) {
|
||||
$this->getOutput()->addHTML(
|
||||
$this->createResourceList( $this->OATHUser->getScratchTokensReset() ) );
|
||||
} else {
|
||||
$this->getOutput()->addHTML(
|
||||
$this->createResourceList( $this->OATHUser->getScratchTokens() ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
private function validate() {
|
||||
$this->setHeaders();
|
||||
$this->getOutput()->setPagetitle( $this->msg( 'oathauth-enable' ) );
|
||||
$mode = $this->getRequest()->getVal( 'mode' );
|
||||
$returnto = $this->getRequest()->getVal( 'returnto' );
|
||||
|
||||
$info['token'] = array(
|
||||
'type' => 'text',
|
||||
'default' => '',
|
||||
'label-message' => 'oathauth-token',
|
||||
'name' => 'token',
|
||||
);
|
||||
$info['mode'] = array(
|
||||
'type' => 'hidden',
|
||||
'default' => $mode,
|
||||
'name' => 'mode',
|
||||
);
|
||||
$info['returnto'] = array(
|
||||
'type' => 'hidden',
|
||||
'default' => $returnto,
|
||||
'name' => 'returnto',
|
||||
);
|
||||
$info['action'] = array(
|
||||
'type' => 'hidden',
|
||||
'default' => 'validate',
|
||||
'name' => 'action',
|
||||
);
|
||||
$form = new HTMLForm(
|
||||
$info,
|
||||
$this->getContext(),
|
||||
'oathauth-verify'
|
||||
);
|
||||
$form->setSubmitID( 'oathauth-validate-submit' );
|
||||
$form->setSubmitCallback( array( $this, 'tryValidateSubmit' ) );
|
||||
$form->show();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
private function reset() {
|
||||
$this->setHeaders();
|
||||
$this->getOutput()->setPagetitle( $this->msg( 'oathauth-reset' ) );
|
||||
$returnto = $this->getRequest()->getVal( 'returnto' );
|
||||
|
||||
$info['token'] = array(
|
||||
'type' => 'text',
|
||||
'label-message' => 'oathauth-currenttoken',
|
||||
'name' => 'token',
|
||||
);
|
||||
$info['returnto'] = array(
|
||||
'type' => 'hidden',
|
||||
'default' => $returnto,
|
||||
'name' => 'returnto',
|
||||
);
|
||||
$info['action'] = array(
|
||||
'type' => 'hidden',
|
||||
'default' => 'reset',
|
||||
'name' => 'action',
|
||||
);
|
||||
$form = new HTMLForm(
|
||||
$info,
|
||||
$this->getContext(),
|
||||
'oathauth-reset'
|
||||
);
|
||||
$form->setSubmitID( 'oauth-form-disablesubmit' );
|
||||
$form->setSubmitCallback( array( $this, 'tryResetSubmit' ) );
|
||||
$form->show();
|
||||
return true;
|
||||
$this->getOutput()->addHTML(
|
||||
$this->createResourceList( $key->getScratchTokens() ) );
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -266,26 +179,16 @@ class SpecialOATH extends UnlistedSpecialPage {
|
|||
* @return bool
|
||||
*/
|
||||
public function tryValidateSubmit( $formData ) {
|
||||
$mode = $formData['mode'];
|
||||
if ( $mode == "reset" ) {
|
||||
$reset = true;
|
||||
} else {
|
||||
$reset = false;
|
||||
}
|
||||
|
||||
$verify = $this->OATHUser->verifyToken( $formData['token'], $reset );
|
||||
if ( $verify ) {
|
||||
if ( $reset ) {
|
||||
$result = $this->OATHUser->reset();
|
||||
} else {
|
||||
$result = $this->OATHUser->validate();
|
||||
}
|
||||
} else {
|
||||
$result = false;
|
||||
}
|
||||
/** @var OATHAuthKey $key */
|
||||
$key = $this->getRequest()->getSessionData( 'oathauth_key' );
|
||||
|
||||
$verify = $key->verifyToken( $formData['token'], $this->OATHUser );
|
||||
$out = '';
|
||||
if ( $result ) {
|
||||
if ( $verify ) {
|
||||
$this->OATHUser->setKey( $key );
|
||||
$this->OATHRepository->persist( $this->OATHUser );
|
||||
$this->getRequest()->setSessionData( 'oathauth_key', null );
|
||||
|
||||
$this->getOutput()->addWikiMsg( 'oathauth-validatedoath' );
|
||||
if ( $formData['returnto'] ) {
|
||||
$out = '<br />';
|
||||
|
@ -296,28 +199,15 @@ class SpecialOATH extends UnlistedSpecialPage {
|
|||
$this->getOutput()->addWikiMsg( 'oathauth-failedtovalidateoauth' );
|
||||
$out = '<br />';
|
||||
|
||||
if ( $reset ) {
|
||||
$out .= Linker::link(
|
||||
$this->getPageTitle(),
|
||||
$this->msg( 'oathauth-reattemptreset' )->escaped(),
|
||||
array(),
|
||||
array(
|
||||
'action' => 'enable',
|
||||
'mode' => 'reset',
|
||||
'returnto' => $formData['returnto']
|
||||
)
|
||||
);
|
||||
} else {
|
||||
$out .= Linker::link(
|
||||
$this->getPageTitle(),
|
||||
$this->msg( 'oathauth-reattemptenable' )->escaped(),
|
||||
array(),
|
||||
array(
|
||||
'action' => 'enable',
|
||||
'returnto' => $formData['returnto']
|
||||
)
|
||||
);
|
||||
}
|
||||
$out .= Linker::link(
|
||||
$this->getPageTitle(),
|
||||
$this->msg( 'oathauth-reattemptenable' )->escaped(),
|
||||
array(),
|
||||
array(
|
||||
'action' => 'enable',
|
||||
'returnto' => $formData['returnto']
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$this->getOutput()->addHTML( $out );
|
||||
|
@ -330,7 +220,7 @@ class SpecialOATH extends UnlistedSpecialPage {
|
|||
* @return bool
|
||||
*/
|
||||
public function tryDisableSubmit( $formData ) {
|
||||
$verify = $this->OATHUser->verifyToken( $formData['token'] );
|
||||
$verify = $this->OATHUser->getKey()->verifyToken( $formData['token'], $this->OATHUser );
|
||||
if ( !$verify ) {
|
||||
$this->getOutput()->addWikiMsg( 'oathauth-failedtovalidateoauth' );
|
||||
$out = '<br />';
|
||||
|
@ -344,107 +234,13 @@ class SpecialOATH extends UnlistedSpecialPage {
|
|||
return true;
|
||||
}
|
||||
|
||||
$result = $this->OATHUser->disable();
|
||||
if ( $result ) {
|
||||
$this->getOutput()->addWikiMsg( 'oathauth-disabledoath' );
|
||||
if ( $formData['returnto'] ) {
|
||||
$out = '<br />';
|
||||
$title = Title::newFromText( $formData['returnto'] );
|
||||
$out .= Linker::link( $title, $this->msg( 'oathauth-backtopreferences' )->escaped() );
|
||||
$this->getOutput()->addHTML( $out );
|
||||
}
|
||||
} else {
|
||||
$this->getOutput()->addWikiMsg( 'oathauth-failedtodisableoauth' );
|
||||
$this->OATHRepository->remove( $this->OATHUser );
|
||||
|
||||
$this->getOutput()->addWikiMsg( 'oathauth-disabledoath' );
|
||||
if ( $formData['returnto'] ) {
|
||||
$out = '<br />';
|
||||
|
||||
$out .= Linker::link(
|
||||
$this->getPageTitle(),
|
||||
$this->msg( 'oathauth-reattemptdisable' )->escaped(),
|
||||
array(
|
||||
'action' => 'disable',
|
||||
'returnto' => $formData['returnto'],
|
||||
)
|
||||
);
|
||||
$this->getOutput()->addHTML( $out );
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $formData array
|
||||
* @return bool
|
||||
*/
|
||||
public function tryResetSubmit( $formData ) {
|
||||
$verify = $this->OATHUser->verifyToken( $formData['token'] );
|
||||
if ( !$verify ) {
|
||||
$this->getOutput()->addWikiMsg( 'oathauth-failedtovalidateoauth' );
|
||||
$out = '<br />';
|
||||
$out .= Linker::link(
|
||||
$this->getPageTitle(),
|
||||
$this->msg( 'oathauth-reattemptreset' )->escaped(),
|
||||
array(),
|
||||
array(
|
||||
'action' => 'reset',
|
||||
'returnto' => $formData['returnto']
|
||||
)
|
||||
);
|
||||
|
||||
$this->getOutput()->addHTML( $out );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$this->getOutput()->addWikiMsg( 'oathauth-donotdeleteoldsecret' );
|
||||
$info['token'] = array(
|
||||
'type' => 'text',
|
||||
'default' => '',
|
||||
'label-message' => 'oathauth-newtoken',
|
||||
'name' => 'token',
|
||||
);
|
||||
$info['mode'] = array(
|
||||
'type' => 'hidden',
|
||||
'default' => 'reset',
|
||||
'name' => 'mode',
|
||||
);
|
||||
$info['returnto'] = array(
|
||||
'type' => 'hidden',
|
||||
'default' => $formData['returnto'],
|
||||
'name' => 'returnto',
|
||||
);
|
||||
$info['action'] = array(
|
||||
'type' => 'hidden',
|
||||
'default' => 'validate',
|
||||
'name' => 'action',
|
||||
);
|
||||
$myContext = new DerivativeContext( $this->getContext() );
|
||||
$myRequest = new DerivativeRequest( $this->getRequest(),
|
||||
array(
|
||||
'action' => 'validate',
|
||||
'mode' => 'reset',
|
||||
'token' => '',
|
||||
'returnto' => $formData['returnto']
|
||||
), false );
|
||||
$myContext->setRequest( $myRequest );
|
||||
$form = new HTMLForm( $info, $myContext );
|
||||
$form->setSubmitID( 'oathauth-validate-submit' );
|
||||
$form->setSubmitCallback( array( $this, 'tryValidateSubmit' ) );
|
||||
$form->show();
|
||||
|
||||
$result = $this->OATHUser->setReset();
|
||||
if ( $result ) {
|
||||
$this->displaySecret( true );
|
||||
} else {
|
||||
$this->getOutput()->addWikiMsg( 'oathauth-failedtoresetoath' );
|
||||
$out = '<br />';
|
||||
$out .= Linker::link(
|
||||
$this->getPageTitle(),
|
||||
$this->msg( 'oathauth-reattemptreset' )->escaped(),
|
||||
array(),
|
||||
array(
|
||||
'action' => 'reset',
|
||||
'returnto' => $formData['returnto']
|
||||
)
|
||||
);
|
||||
$title = Title::newFromText( $formData['returnto'] );
|
||||
$out .= Linker::link( $title, $this->msg( 'oathauth-backtopreferences' )->escaped() );
|
||||
$this->getOutput()->addHTML( $out );
|
||||
}
|
||||
|
||||
|
|
8
sql/mysql/patch-remove_reset.sql
Normal file
8
sql/mysql/patch-remove_reset.sql
Normal file
|
@ -0,0 +1,8 @@
|
|||
ALTER TABLE /*_*/oathauth_users
|
||||
DROP COLUMN secret_reset;
|
||||
|
||||
ALTER TABLE /*_*/oathauth_users
|
||||
DROP COLUMN scratch_tokens_reset;
|
||||
|
||||
ALTER TABLE /*_*/oathauth_users
|
||||
DROP COLUMN is_validated;
|
11
sql/mysql/tables.sql
Normal file
11
sql/mysql/tables.sql
Normal file
|
@ -0,0 +1,11 @@
|
|||
CREATE TABLE /*_*/oathauth_users (
|
||||
-- User ID
|
||||
id int not null primary key,
|
||||
|
||||
-- Secret key
|
||||
secret varbinary(255) null,
|
||||
|
||||
-- Scratch tokens
|
||||
scratch_tokens varbinary(511) null
|
||||
|
||||
) /*$wgDBTableOptions*/;
|
Loading…
Reference in a new issue