Update for AuthManager

Handling enabling/disabling via AuthManager is left to a separate
patch.

Bug: T110457
Change-Id: Ic492b8f2477c475f8414b61505139e9a1df2ba5b
This commit is contained in:
Gergő Tisza 2015-11-15 18:52:23 -08:00 committed by Gergő Tisza
parent 6f809fef27
commit 563796a98c
8 changed files with 286 additions and 88 deletions

79
OATHAuth.hooks.legacy.php Normal file
View file

@ -0,0 +1,79 @@
<?php
/**
* Hooks for Extension:OATHAuth
* @deprecated B/C class for compatibility with pre-AuthManager core
*/
class OATHAuthLegacyHooks {
/**
* @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 ) {
global $wgRequest;
$token = $wgRequest->getText( 'wpOATHToken' );
$oathrepo = OATHAuthHooks::getOATHUserRepository();
$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->getKey() !== null ) {
$result = $oathuser->getKey()->verifyToken( $token, $oathuser );
}
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 ) {
$context = RequestContext::getMain();
$request = $context->getRequest();
$output = $context->getOutput();
$oathrepo = OATHAuthHooks::getOATHUserRepository();
$oathuser = $oathrepo->findByUser( $user );
$uid = CentralIdLookup::factory()->centralIdFromLocalUser( $user );
if ( $oathuser->getKey() !== null && !$request->getCheck( 'token' ) ) {
$encData = OATHAuthUtils::encryptSessionData(
$request->getValues(),
$uid
);
$request->setSessionData( 'oath_login', $encData );
$request->setSessionData( 'oath_uid', $uid );
$output->redirect( SpecialPage::getTitleFor( 'OATH' )->getFullURL( '', false, PROTO_CURRENT ) );
return false;
} else {
return true;
}
}
}

View file

@ -1,5 +1,10 @@
<?php <?php
use MediaWiki\Auth\AuthenticationRequest;
use MediaWiki\Auth\AuthManager;
use MediaWiki\Auth\ConfirmLinkSecondaryAuthenticationProvider;
use MediaWiki\Auth\EmailNotificationSecondaryAuthenticationProvider;
/** /**
* Hooks for Extension:OATHAuth * Hooks for Extension:OATHAuth
* *
@ -24,85 +29,63 @@ class OATHAuthHooks {
} }
/** /**
* @param $extraFields array * Register hooks which depend on MediaWiki core version
*/
public static function onRegistration() {
global $wgDisableAuthManager, $wgAuthManagerAutoConfig;
if ( !$wgDisableAuthManager && class_exists( AuthManager::class ) ) {
$wgAuthManagerAutoConfig['secondaryauth'] += [
TOTPSecondaryAuthenticationProvider::class => [
'class' => TOTPSecondaryAuthenticationProvider::class,
// after non-interactive prroviders but before the ones that run after a
// succesful authentication
'sort' => 50,
]
];
Hooks::register( 'AuthChangeFormFields', 'OATHAuthHooks::onAuthChangeFormFields' );
} else {
Hooks::register( 'AbortChangePassword', 'OATHAuthLegacyHooks::AbortChangePassword' );
Hooks::register( 'AbortLogin', 'OATHAuthLegacyHooks::AbortLogin' );
Hooks::register( 'ChangePasswordForm', 'OATHAuthLegacyHooks::ChangePasswordForm' );
}
}
/**
* @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 * @return bool
*/ */
static function ChangePasswordForm( &$extraFields ) { public static function onAuthChangeFormFields(
$tokenField = array( 'wpOATHToken', 'oathauth-token', 'password', '' ); array $requests, array $fieldInfo, array &$formDescriptor, $action
array_push( $extraFields, $tokenField ); ) {
if ( isset( $fieldInfo['OATHToken'] ) ) {
$formDescriptor['OATHToken'] += [
'cssClass' => 'loginText',
'id' => 'wpOATHToken',
'size' => 20,
'autofocus' => true,
'persistent' => false,
];
}
return true; return true;
} }
/**
* @param $user User
* @param $password string
* @param $newpassword string
* @param &$errorMsg string
* @return bool
*/
static function AbortChangePassword( $user, $password, $newpassword, &$errorMsg ) {
global $wgRequest;
$token = $wgRequest->getText( 'wpOATHToken' );
$oathrepo = self::getOATHUserRepository();
$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->getKey() !== null ) {
$result = $oathuser->getKey()->verifyToken( $token, $oathuser );
}
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 ) {
$context = RequestContext::getMain();
$request = $context->getRequest();
$output = $context->getOutput();
$oathrepo = self::getOATHUserRepository();
$oathuser = $oathrepo->findByUser( $user );
$uid = CentralIdLookup::factory()->centralIdFromLocalUser( $user );
if ( $oathuser->getKey() !== null && !$request->getCheck( 'token' ) ) {
$encData = OATHAuthUtils::encryptSessionData(
$request->getValues(),
$uid
);
$request->setSessionData( 'oath_login', $encData );
$request->setSessionData( 'oath_uid', $uid );
$output->redirect( SpecialPage::getTitleFor( 'OATH' )->getFullURL( '', false, PROTO_CURRENT ) );
return false;
} else {
return true;
}
}
/** /**
* Determine if two-factor authentication is enabled for $wgUser * Determine if two-factor authentication is enabled for $wgUser
* *
* @param bool &$isEnabled Will be set to true if enabled, false otherwise * 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 * @return bool False if enabled, true otherwise
*/ */
static function TwoFactorIsEnabled( &$isEnabled ) { public static function onTwoFactorIsEnabled( &$isEnabled ) {
global $wgUser; global $wgUser;
$user = self::getOATHUserRepository()->findByUser( $wgUser ); $user = self::getOATHUserRepository()->findByUser( $wgUser );
@ -124,10 +107,9 @@ class OATHAuthHooks {
* *
* @param User $user * @param User $user
* @param array $preferences * @param array $preferences
*
* @return bool * @return bool
*/ */
public static function manageOATH( User $user, array &$preferences ) { public static function onGetPreferences( User $user, array &$preferences ) {
if ( !$user->isAllowed( 'oathauth-enable' ) ) { if ( !$user->isAllowed( 'oathauth-enable' ) ) {
return true; return true;
} }
@ -157,7 +139,7 @@ class OATHAuthHooks {
* @param DatabaseUpdater $updater * @param DatabaseUpdater $updater
* @return bool * @return bool
*/ */
public static function OATHAuthSchemaUpdates( $updater ) { public static function onLoadExtensionSchemaUpdates( $updater ) {
$base = dirname( __FILE__ ); $base = dirname( __FILE__ );
switch ( $updater->getDB()->getType() ) { switch ( $updater->getDB()->getType() ) {
case 'mysql': case 'mysql':
@ -188,8 +170,7 @@ class OATHAuthHooks {
* Helper function for converting old users to the new schema * Helper function for converting old users to the new schema
* @see OATHAuthHooks::OATHAuthSchemaUpdates * @see OATHAuthHooks::OATHAuthSchemaUpdates
* *
* @param DatabaseUpdater $updater * @param DatabaseBase $db
*
* @return bool * @return bool
*/ */
public static function schemaUpdateOldUsers( DatabaseBase $db ) { public static function schemaUpdateOldUsers( DatabaseBase $db ) {

View file

@ -0,0 +1,28 @@
<?php
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 indepdendently 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 array(
'OATHToken' => array(
'type' => 'string',
'label' => wfMessage( 'oathauth-auth-token-label' ),
'help' => wfMessage( 'oathauth-auth-token-help' ),
),
);
}
}

View file

@ -0,0 +1,86 @@
<?php
use MediaWiki\Auth\AbstractSecondaryAuthenticationProvider;
use MediaWiki\Auth\AuthenticationRequest;
use MediaWiki\Auth\AuthenticationResponse;
use MediaWiki\Auth\AuthManager;
use MediaWiki\Auth\Throttler;
use MediaWiki\Session\SessionManager;
/**
* 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 {
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.
* @inheritdoc
*/
public function beginSecondaryAuthentication( $user, array $reqs ) {
$oathuser = OATHAuthHooks::getOATHUserRepository()->findByUser( $user );
if ( $oathuser->getKey() === null ) {
return AuthenticationResponse::newAbstain();
} else {
return AuthenticationResponse::newUI( array( new TOTPAuthenticationRequest() ),
wfMessage( 'oathauth-auth-ui' ) );
}
}
/**
* 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( array( new TOTPAuthenticationRequest() ),
wfMessage( 'oathauth-login-failed' ) );
}
$throttler = new Throttler( null, [ 'type' => 'TOTP' ] );
$result = $throttler->increase( $user->getName(), null, __METHOD__ );
if ( $result ) {
return AuthenticationResponse::newUI( array( new TOTPAuthenticationRequest() ),
new Message( 'oathauth-throttled', [ Message::durationParam( $result['wait'] ) ] ) );
}
$oathuser = OATHAuthHooks::getOATHUserRepository()->findByUser( $user );
$token = $request->OATHToken;
if ( $oathuser->getKey() === null ) {
$this->logger->warning( 'Two-factor authentication was disabled mid-authentication for '
. $user->getName() );
return AuthenticationResponse::newAbstain();
}
if ( $oathuser->getKey()->verifyToken( $token, $oathuser ) ) {
$throttler->clear( $user->getName(), null );
return AuthenticationResponse::newPass();
} else {
return AuthenticationResponse::newUI( array( new TOTPAuthenticationRequest() ),
wfMessage( 'oathauth-login-failed' ) );
}
}
public function beginSecondaryAccountCreation( $user, $creator, array $reqs ) {
return AuthenticationResponse::newAbstain();
}
}

View file

@ -7,6 +7,7 @@
"type": "other", "type": "other",
"AutoloadClasses": { "AutoloadClasses": {
"OATHAuthHooks": "OATHAuth.hooks.php", "OATHAuthHooks": "OATHAuth.hooks.php",
"OATHAuthLegacyHooks": "OATHAuth.hooks.legacy.php",
"OATHAuthKey": "OATHAuthKey.php", "OATHAuthKey": "OATHAuthKey.php",
"OATHAuthUtils": "OATHAuthUtils.php", "OATHAuthUtils": "OATHAuthUtils.php",
"OATHUserRepository": "OATHUserRepository.php", "OATHUserRepository": "OATHUserRepository.php",
@ -18,29 +19,23 @@
"SpecialOATHEnable": "special/SpecialOATHEnable.php", "SpecialOATHEnable": "special/SpecialOATHEnable.php",
"SpecialOATHDisable": "special/SpecialOATHDisable.php", "SpecialOATHDisable": "special/SpecialOATHDisable.php",
"SpecialOATHLogin": "special/SpecialOATHLogin.php", "SpecialOATHLogin": "special/SpecialOATHLogin.php",
"ProxySpecialPage": "special/ProxySpecialPage.php" "ProxySpecialPage": "special/ProxySpecialPage.php",
"TOTPAuthenticationRequest": "auth/TOTPAuthenticationRequest.php",
"TOTPSecondaryAuthenticationProvider": "auth/TOTPSecondaryAuthenticationProvider.php"
}, },
"ExtensionMessagesFiles": { "ExtensionMessagesFiles": {
"OATHAuthAlias": "OATHAuth.alias.php" "OATHAuthAlias": "OATHAuth.alias.php"
}, },
"callback": "OATHAuthHooks::onRegistration",
"Hooks": { "Hooks": {
"AbortChangePassword": [
"OATHAuthHooks::AbortChangePassword"
],
"AbortLogin": [
"OATHAuthHooks::AbortLogin"
],
"ChangePasswordForm": [
"OATHAuthHooks::ChangePasswordForm"
],
"TwoFactorIsEnabled": [ "TwoFactorIsEnabled": [
"OATHAuthHooks::TwoFactorIsEnabled" "OATHAuthHooks::onTwoFactorIsEnabled"
], ],
"LoadExtensionSchemaUpdates": [ "LoadExtensionSchemaUpdates": [
"OATHAuthHooks::OATHAuthSchemaUpdates" "OATHAuthHooks::onLoadExtensionSchemaUpdates"
], ],
"GetPreferences": [ "GetPreferences": [
"OATHAuthHooks::manageOATH" "OATHAuthHooks::onGetPreferences"
] ]
}, },
"MessagesDirs": { "MessagesDirs": {

View file

@ -45,5 +45,11 @@
"oathauth-step4": "Step 4: Verification", "oathauth-step4": "Step 4: Verification",
"oathauth-entertoken": "Enter a code from your mobile app to verify:", "oathauth-entertoken": "Enter a code from your mobile app to verify:",
"right-oathauth-enable": "Enable two-factor authentication", "right-oathauth-enable": "Enable two-factor authentication",
"action-oathauth-enable": "enable two-factor authentication" "action-oathauth-enable": "enable two-factor authentication",
"oathauth-auth-token-label": "Token",
"oathauth-auth-token-help": "The one-time password used as the second factor of two-factor authentication.",
"oathauth-auth-ui": "Please enter verification code from your mobile app",
"oathauth-throttled": "Too many verification attempts! Please wait $1.",
"oathauth-login-failed": "Verification failed.",
"oathauth-describe-provider": "Two-factor authentication (OATH)."
} }

View file

@ -49,5 +49,11 @@
"oathauth-step4": "Label for step 4 information on Special:OATH", "oathauth-step4": "Label for step 4 information on Special:OATH",
"oathauth-entertoken": "Label on input field on Special:OATH asking user to enter token", "oathauth-entertoken": "Label on input field on Special:OATH asking user to enter token",
"right-oathauth-enable": "{{doc-right|oathauth-enable}}", "right-oathauth-enable": "{{doc-right|oathauth-enable}}",
"action-oathauth-enable": "{{doc-action|oathauth-enable}}" "action-oathauth-enable": "{{doc-action|oathauth-enable}}",
"oathauth-auth-token-label": "Label of the second-factor field on special pages and in the API",
"oathauth-auth-token-help": "Extended help message for the second factor field in the API.",
"oathauth-auth-ui": "Shown on top of the login form when second factor is required",
"oathauth-throttled": "Error message when throttling limit is hit.\n\nParameters:\n* $1 - throttle block duration",
"oathauth-login-failed": "Error message when verifying the second factor failed.",
"oathauth-describe-provider": "Description of the extension as an authentication provider."
} }

View file

@ -0,0 +1,17 @@
<?php
use MediaWiki\Auth\AuthenticationRequestTestCase;
class TOTPAuthenticationRequestTest extends AuthenticationRequestTestCase {
protected function getInstance( array $args = [] ) {
return new TOTPAuthenticationRequest();
}
public function provideLoadFromSubmission() {
return [
[ [], [], false ],
[ [], [ 'OATHToken' => '123456' ], [ 'OATHToken' => '123456' ] ],
];
}
}