mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/OATHAuth
synced 2024-11-24 08:14:15 +00:00
b112cc407c
Tokens are one time passwords. There's no strong reason to mask them and listing it as a password field screws up many password managers. Change-Id: Iaf5446d80ec61ddec2403554b527781ab26493b3
411 lines
9.6 KiB
PHP
411 lines
9.6 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Class representing a user from OATH's perspective
|
|
*
|
|
* @file
|
|
* @ingroup Extensions
|
|
*/
|
|
|
|
class OATHUser {
|
|
|
|
private $id, $secret, $secretReset, $scratchTokens, $scratchTokensReset, $account, $isEnabled, $isValidated;
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* @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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @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 ) {
|
|
if ( $reset ) {
|
|
$secret = $this->secretReset;
|
|
} else {
|
|
$secret = $this->secret;
|
|
}
|
|
$results = HOTP::generateByTimeWindow( Base32::decode( $secret ), 30, -4, 4 );
|
|
# Check to see if the user's given token is in the list of tokens generated
|
|
# for the time window.
|
|
foreach ( $results as $result ) {
|
|
if ( $result->toHOTP( 6 ) === $token ) {
|
|
return true;
|
|
}
|
|
}
|
|
# See if the user is using a scratch token
|
|
for ( $i = 0; $i < count( $this->scratchTokens ); $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
|
|
return $this->updateScratchTokens();
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @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
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param $username string
|
|
* @return OATHUser|null
|
|
*/
|
|
public static function newFromUsername( $username ) {
|
|
$user = User::newFromName( $username, true );
|
|
return OATHUser::newFromUser( $user );
|
|
}
|
|
|
|
/**
|
|
* @return bool
|
|
*/
|
|
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__
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param $template UserloginTemplate
|
|
* @return bool
|
|
*/
|
|
static function ModifyUITemplate( &$template ) {
|
|
$input = '<div><label for="wpOATHToken">'
|
|
. wfMsgHtml( 'oathauth-token' )
|
|
. '</label>'
|
|
. Html::input( 'wpOATHToken', null, 'text', array(
|
|
'class' => 'loginText', 'id' => 'wpOATHToken', 'tabindex' => '3', 'size' => '20'
|
|
) ) . '</div>';
|
|
$template->set( 'extrafields', $input );
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param $extraFields array
|
|
* @return bool
|
|
*/
|
|
static function ChangePasswordForm( &$extraFields ) {
|
|
$tokenField = array( 'wpOATHToken', 'oathauth-token', 'password', '' );
|
|
array_push( $extraFields, $tokenField );
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param $user User
|
|
* @param $password string
|
|
* @param $newpassword string
|
|
* @param &$errorMsg string
|
|
* @return bool
|
|
*/
|
|
static function AbortChangePassword( $user, $password, $newpassword, &$errorMsg ) {
|
|
$result = self::authenticate( $user );
|
|
if ( $result ) {
|
|
return true;
|
|
} else {
|
|
$errorMsg = 'oathauth-abortlogin';
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param $user User
|
|
* @param $password string
|
|
* @param &$abort int
|
|
* @param &$errorMsg string
|
|
* @return bool
|
|
*/
|
|
static function AbortLogin( $user, $password, &$abort, &$errorMsg ) {
|
|
$result = self::authenticate( $user );
|
|
if ( $result ) {
|
|
return true;
|
|
} else {
|
|
$abort = LoginForm::ABORTED;
|
|
$errorMsg = 'oathauth-abortlogin';
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param $user User
|
|
* @return bool
|
|
*/
|
|
static function authenticate( $user ) {
|
|
global $wgRequest;
|
|
$token = $wgRequest->getText( 'wpOATHToken' );
|
|
$oathuser = OATHUser::newFromUser( $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 );
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
static function TwoFactorIsEnabled( &$isEnabled ) {
|
|
global $wgUser;
|
|
|
|
$user = OATHUser::newFromUser( $wgUser );
|
|
if ( $user && $user->isEnabled() && $user->isValidated() ) {
|
|
$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;
|
|
}
|
|
}
|
|
|
|
public static function manageOATH( User $user, array &$preferences ) {
|
|
$oathUser = OATHUser::newFromUser( $user );
|
|
|
|
$title = SpecialPage::getTitleFor( 'OATH' );
|
|
if ( $oathUser->isEnabled() && $oathUser->isValidated() ) {
|
|
$preferences['oath-disable'] = array(
|
|
'type' => 'info',
|
|
'raw' => 'true',
|
|
'default' => Linker::link( $title, wfMsgHtml( 'oathauth-disable' ), array(), array( 'action' => 'disable', 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText() ) ),
|
|
'label-message' => 'oathauth-prefs-label',
|
|
'section' => 'personal/info',
|
|
);
|
|
$preferences['oath-reset'] = array(
|
|
'type' => 'info',
|
|
'raw' => 'true',
|
|
'default' => Linker::link( $title, wfMsgHtml( 'oathauth-reset' ), array(), array( 'action' => 'reset', 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText() ) ),
|
|
'section' => 'personal/info',
|
|
);
|
|
} else {
|
|
$preferences['oath-enable'] = array(
|
|
'type' => 'info',
|
|
'raw' => 'true',
|
|
'default' => Linker::link( $title, wfMsgHtml( 'oathauth-enable' ), array(), array( 'action' => 'enable', 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText() ) ),
|
|
'label-message' => 'oathauth-prefs-label',
|
|
'section' => 'personal/info',
|
|
);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
}
|