mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/OATHAuth
synced 2024-11-23 15:56:59 +00:00
Update for AuthManager
Handling enabling/disabling via AuthManager is left to a separate patch. Bug: T110457 Change-Id: Ic492b8f2477c475f8414b61505139e9a1df2ba5b
This commit is contained in:
parent
6f809fef27
commit
563796a98c
79
OATHAuth.hooks.legacy.php
Normal file
79
OATHAuth.hooks.legacy.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,10 @@
|
|||
<?php
|
||||
|
||||
use MediaWiki\Auth\AuthenticationRequest;
|
||||
use MediaWiki\Auth\AuthManager;
|
||||
use MediaWiki\Auth\ConfirmLinkSecondaryAuthenticationProvider;
|
||||
use MediaWiki\Auth\EmailNotificationSecondaryAuthenticationProvider;
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
static function ChangePasswordForm( &$extraFields ) {
|
||||
$tokenField = array( 'wpOATHToken', 'oathauth-token', 'password', '' );
|
||||
array_push( $extraFields, $tokenField );
|
||||
|
||||
public static function onAuthChangeFormFields(
|
||||
array $requests, array $fieldInfo, array &$formDescriptor, $action
|
||||
) {
|
||||
if ( isset( $fieldInfo['OATHToken'] ) ) {
|
||||
$formDescriptor['OATHToken'] += [
|
||||
'cssClass' => 'loginText',
|
||||
'id' => 'wpOATHToken',
|
||||
'size' => 20,
|
||||
'autofocus' => true,
|
||||
'persistent' => false,
|
||||
];
|
||||
}
|
||||
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
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
static function TwoFactorIsEnabled( &$isEnabled ) {
|
||||
public static function onTwoFactorIsEnabled( &$isEnabled ) {
|
||||
global $wgUser;
|
||||
|
||||
$user = self::getOATHUserRepository()->findByUser( $wgUser );
|
||||
|
@ -124,10 +107,9 @@ class OATHAuthHooks {
|
|||
*
|
||||
* @param User $user
|
||||
* @param array $preferences
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function manageOATH( User $user, array &$preferences ) {
|
||||
public static function onGetPreferences( User $user, array &$preferences ) {
|
||||
if ( !$user->isAllowed( 'oathauth-enable' ) ) {
|
||||
return true;
|
||||
}
|
||||
|
@ -157,7 +139,7 @@ class OATHAuthHooks {
|
|||
* @param DatabaseUpdater $updater
|
||||
* @return bool
|
||||
*/
|
||||
public static function OATHAuthSchemaUpdates( $updater ) {
|
||||
public static function onLoadExtensionSchemaUpdates( $updater ) {
|
||||
$base = dirname( __FILE__ );
|
||||
switch ( $updater->getDB()->getType() ) {
|
||||
case 'mysql':
|
||||
|
@ -188,8 +170,7 @@ class OATHAuthHooks {
|
|||
* Helper function for converting old users to the new schema
|
||||
* @see OATHAuthHooks::OATHAuthSchemaUpdates
|
||||
*
|
||||
* @param DatabaseUpdater $updater
|
||||
*
|
||||
* @param DatabaseBase $db
|
||||
* @return bool
|
||||
*/
|
||||
public static function schemaUpdateOldUsers( DatabaseBase $db ) {
|
||||
|
|
28
auth/TOTPAuthenticationRequest.php
Normal file
28
auth/TOTPAuthenticationRequest.php
Normal 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' ),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
86
auth/TOTPSecondaryAuthenticationProvider.php
Normal file
86
auth/TOTPSecondaryAuthenticationProvider.php
Normal 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();
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@
|
|||
"type": "other",
|
||||
"AutoloadClasses": {
|
||||
"OATHAuthHooks": "OATHAuth.hooks.php",
|
||||
"OATHAuthLegacyHooks": "OATHAuth.hooks.legacy.php",
|
||||
"OATHAuthKey": "OATHAuthKey.php",
|
||||
"OATHAuthUtils": "OATHAuthUtils.php",
|
||||
"OATHUserRepository": "OATHUserRepository.php",
|
||||
|
@ -18,29 +19,23 @@
|
|||
"SpecialOATHEnable": "special/SpecialOATHEnable.php",
|
||||
"SpecialOATHDisable": "special/SpecialOATHDisable.php",
|
||||
"SpecialOATHLogin": "special/SpecialOATHLogin.php",
|
||||
"ProxySpecialPage": "special/ProxySpecialPage.php"
|
||||
"ProxySpecialPage": "special/ProxySpecialPage.php",
|
||||
"TOTPAuthenticationRequest": "auth/TOTPAuthenticationRequest.php",
|
||||
"TOTPSecondaryAuthenticationProvider": "auth/TOTPSecondaryAuthenticationProvider.php"
|
||||
},
|
||||
"ExtensionMessagesFiles": {
|
||||
"OATHAuthAlias": "OATHAuth.alias.php"
|
||||
},
|
||||
"callback": "OATHAuthHooks::onRegistration",
|
||||
"Hooks": {
|
||||
"AbortChangePassword": [
|
||||
"OATHAuthHooks::AbortChangePassword"
|
||||
],
|
||||
"AbortLogin": [
|
||||
"OATHAuthHooks::AbortLogin"
|
||||
],
|
||||
"ChangePasswordForm": [
|
||||
"OATHAuthHooks::ChangePasswordForm"
|
||||
],
|
||||
"TwoFactorIsEnabled": [
|
||||
"OATHAuthHooks::TwoFactorIsEnabled"
|
||||
"OATHAuthHooks::onTwoFactorIsEnabled"
|
||||
],
|
||||
"LoadExtensionSchemaUpdates": [
|
||||
"OATHAuthHooks::OATHAuthSchemaUpdates"
|
||||
"OATHAuthHooks::onLoadExtensionSchemaUpdates"
|
||||
],
|
||||
"GetPreferences": [
|
||||
"OATHAuthHooks::manageOATH"
|
||||
"OATHAuthHooks::onGetPreferences"
|
||||
]
|
||||
},
|
||||
"MessagesDirs": {
|
||||
|
|
|
@ -45,5 +45,11 @@
|
|||
"oathauth-step4": "Step 4: Verification",
|
||||
"oathauth-entertoken": "Enter a code from your mobile app to verify:",
|
||||
"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)."
|
||||
}
|
||||
|
|
|
@ -49,5 +49,11 @@
|
|||
"oathauth-step4": "Label for step 4 information on Special:OATH",
|
||||
"oathauth-entertoken": "Label on input field on Special:OATH asking user to enter token",
|
||||
"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."
|
||||
}
|
||||
|
|
17
tests/phpunit/TOTPAuthenticationRequestTest.php
Normal file
17
tests/phpunit/TOTPAuthenticationRequestTest.php
Normal 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' ] ],
|
||||
];
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue