Gergő Tisza has uploaded a new change for review.
https://gerrit.wikimedia.org/r/253300
Change subject: [WIP] Update for AuthManager
......................................................................
[WIP] Update for AuthManager
FIXME:
* core needs to be fixed not to readd its own fields when there is
no password form
* do we need throttling for the second-factor guesses?
* expand data change handling in core so changes to TOTP secret
can be done that way. If that does not work, at least make the
special page call securitySensitiveOperationStatus.
Bug: T110457
Change-Id: Ic492b8f2477c475f8414b61505139e9a1df2ba5b
---
A OATHAuth.hooks.legacy.php
M OATHAuth.hooks.php
M OATHAuth.php
A auth/TOTPAuthenticationRequest.php
A auth/TOTPSecondaryAuthenticationProvider.php
M i18n/en.json
M i18n/qqq.json
7 files changed, 397 insertions(+), 92 deletions(-)
git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/OATHAuth
refs/changes/00/253300/1
diff --git a/OATHAuth.hooks.legacy.php b/OATHAuth.hooks.legacy.php
new file mode 100644
index 0000000..f04c2aa
--- /dev/null
+++ b/OATHAuth.hooks.legacy.php
@@ -0,0 +1,177 @@
+<?php
+
+/**
+ * Hooks for Extension:OATHAuth
+ * @deprecated B/C class for compatibility with pre-AuthManager core
+ */
+class OATHAuthLegacyHooks {
+ /**
+ * @param $template UserloginTemplate
+ * @return bool
+ */
+ static function ModifyUITemplate( &$template ) {
+ $input = '<div><label for="wpOATHToken">'
+ . wfMessage( 'oathauth-token' )->escaped()
+ . '</label>'
+ . Html::input( 'wpOATHToken', null, 'text', array(
+ 'class' => 'loginText', 'id' =>
'wpOATHToken', 'tabindex' => '3', 'size' => '20'
+ ) ) . '</div>';
+
+ $template->set( 'extrafields', $template->get( '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;
+ }
+
+ /**
+ * Determine if two-factor authentication is enabled for $wgUser
+ *
+ * @param bool &$isEnabled Will be set to true if enabled, false
otherwise
+ *
+ * @return bool False if enabled, true otherwise
+ */
+ 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;
+ }
+ }
+
+ /**
+ * Add the necessary user preferences for OATHAuth
+ *
+ * @param User $user
+ * @param array $preferences
+ *
+ * @return bool
+ */
+ 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,
+ wfMessage( 'oathauth-disable'
)->escaped(),
+ 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,
+ wfMessage( 'oathauth-reset'
)->escaped(),
+ 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,
+ wfMessage( 'oathauth-enable'
)->escaped(),
+ array(),
+ array(
+ 'action' => 'enable',
+ 'returnto' =>
SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText()
+ )
+ ),
+ 'label-message' => 'oathauth-prefs-label',
+ 'section' => 'personal/info',
+ );
+ }
+
+ return true;
+ }
+}
diff --git a/OATHAuth.hooks.php b/OATHAuth.hooks.php
index 4aa8a67..ee8803a 100644
--- a/OATHAuth.hooks.php
+++ b/OATHAuth.hooks.php
@@ -1,100 +1,45 @@
<?php
+use MediaWiki\Auth\AuthManager;
/**
* Hooks for Extension:OATHAuth
*/
class OATHAuthHooks {
/**
- * @param $template UserloginTemplate
+ * @param string $fieldName Name of the field
+ * @param array $singleFieldInfo Field information array for a single
field (an item from
+ * AuthenticationRequest::getFieldInfo()).
+ * @param array $fieldDescriptor HTMLFormField 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 ModifyUITemplate( &$template ) {
- $input = '<div><label for="wpOATHToken">'
- . wfMessage( 'oathauth-token' )->escaped()
- . '</label>'
- . Html::input( 'wpOATHToken', null, 'text', array(
- 'class' => 'loginText', 'id' =>
'wpOATHToken', 'tabindex' => '3', 'size' => '20'
- ) ) . '</div>';
-
- $template->set( 'extrafields', $template->get( 'extrafields',
'' ) . $input );
-
+ public static function onAuthChangeFormField(
+ $fieldName, $singleFieldInfo, &$fieldDescriptor, $action
+ ) {
+ if ( $fieldName === 'OATHToken' ) {
+ $fieldDescriptor = array(
+ 'type' => $action ===
AuthManager::ACTION_CHANGE ? 'password' : 'text',
+ 'label' => 'oathauth-auth-token-label',
+ 'cssClass' => 'loginText',
+ 'id' => 'wpOATHToken',
+ 'size' => 20,
+ );
+ }
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;
}
/**
* 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 = OATHUser::newFromUser( $wgUser );
@@ -116,10 +61,9 @@
*
* @param User $user
* @param array $preferences
- *
* @return bool
*/
- public static function manageOATH( User $user, array &$preferences ) {
+ public static function onGetPreferences( User $user, array
&$preferences ) {
$oathUser = OATHUser::newFromUser( $user );
$title = SpecialPage::getTitleFor( 'OATH' );
diff --git a/OATHAuth.php b/OATHAuth.php
index b29911e..d84cae4 100644
--- a/OATHAuth.php
+++ b/OATHAuth.php
@@ -47,12 +47,15 @@
$wgExtensionMessagesFiles['OATHAuth'] = $dir . 'OATHAuth.i18n.php';
$wgExtensionMessagesFiles['OATHAuthAlias'] = $dir . 'OATHAuth.alias.php';
$wgAutoloadClasses['OATHAuthHooks'] = $dir . 'OATHAuth.hooks.php';
+$wgAutoloadClasses['OATHAuthLegacyHooks'] = $dir . 'OATHAuth.hooks.legacy.php';
$wgAutoloadClasses['HOTP'] = $dir . 'lib/hotp.php';
$wgAutoloadClasses['HOTPResult'] = $dir . 'lib/hotp.php';
$wgAutoloadClasses['Base32'] = $dir . 'lib/base32.php';
$wgAutoloadClasses['OATHUser'] = $dir . 'OATHUser.php';
$wgAutoloadClasses['SpecialOATH'] = $dir . 'special/SpecialOATH.php';
-$wgSpecialPages['OATH'] = 'SpecialOATH';
+$wgAutoloadClasses['TOTPAuthenticationRequest'] = $dir .
'auth/TOTPAuthenticationRequest.php';
+$wgAutoloadClasses['TOTPSecondaryAuthenticationProvider'] =
+ $dir . 'auth/TOTPSecondaryAuthenticationProvider.php';
$wgResourceModules['ext.oathauth'] = array(
'scripts' => array(
@@ -64,12 +67,15 @@
'remoteExtPath' => 'OATHAuth',
);
-$wgHooks['AbortChangePassword'][] = 'OATHAuthHooks::AbortChangePassword';
-$wgHooks['AbortLogin'][] = 'OATHAuthHooks::AbortLogin';
-$wgHooks['UserLoginForm'][] = 'OATHAuthHooks::ModifyUITemplate';
-$wgHooks['ChangePasswordForm'][] = 'OATHAuthHooks::ChangePasswordForm';
-$wgHooks['TwoFactorIsEnabled'][] = 'OATHAuthHooks::TwoFactorIsEnabled';
+$wgSpecialPages['OATH'] = 'SpecialOATH';
$wgHooks['LoadExtensionSchemaUpdates'][] =
'OATHAuthHooks::OATHAuthSchemaUpdates';
-$wgHooks['GetPreferences'][] = 'OATHAuthHooks::manageOATH';
-
-
+if ( class_exists( 'MediaWiki\Auth\AuthManager' ) ) {
+ $wgHooks['AuthChangeFormField'][] =
'OATHAuthHooks::onAuthChangeFormField';
+} else {
+ $wgHooks['AbortChangePassword'][] =
'OATHAuthHooks::AbortChangePassword';
+ $wgHooks['AbortLogin'][] = 'OATHAuthHooks::AbortLogin';
+ $wgHooks['UserLoginForm'][] = 'OATHAuthHooks::ModifyUITemplate';
+ $wgHooks['ChangePasswordForm'][] = 'OATHAuthHooks::ChangePasswordForm';
+}
+$wgHooks['TwoFactorIsEnabled'][] = 'OATHAuthHooks::onTwoFactorIsEnabled';
+$wgHooks['GetPreferences'][] = 'OATHAuthHooks::onGetPreferences';
diff --git a/auth/TOTPAuthenticationRequest.php
b/auth/TOTPAuthenticationRequest.php
new file mode 100644
index 0000000..07bfd17
--- /dev/null
+++ b/auth/TOTPAuthenticationRequest.php
@@ -0,0 +1,21 @@
+<?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 static function getFieldInfo() {
+ return array(
+ 'OATHToken' => array(
+ 'type' => 'string',
+ 'label' => 'oathauth-auth-token-label',
+ 'help' => 'oathauth-auth-token-help',
+ ),
+ );
+ }
+}
diff --git a/auth/TOTPSecondaryAuthenticationProvider.php
b/auth/TOTPSecondaryAuthenticationProvider.php
new file mode 100644
index 0000000..f6e976b
--- /dev/null
+++ b/auth/TOTPSecondaryAuthenticationProvider.php
@@ -0,0 +1,149 @@
+<?php
+
+use MediaWiki\Auth\AbstractSecondaryAuthenticationProvider;
+use MediaWiki\Auth\AuthenticationRequest;
+use MediaWiki\Auth\AuthenticationResponse;
+use MediaWiki\Auth\AuthManager;
+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 {
+
+ /**
+ * Return the applicable list of AuthenticationRequests
+ *
+ * @see AuthManager::getAuthenticationRequestTypes()
+ * @param string $action One of the AuthManager::ACTION_* constants
+ * @return string[] AuthenticationRequest class names
+ */
+ public function getAuthenticationRequestTypes( $action ) {
+ switch ( $action ) {
+ case AuthManager::ACTION_LOGIN:
+ // don't ask for anything initially so the
second factor is a separate screen
+ return array();
+ case AuthManager::ACTION_LOGIN_CONTINUE:
+ case AuthManager::ACTION_ALL:
+ return array( 'TOTPAuthenticationRequest' );
+ default:
+ return array();
+ }
+ }
+
+ /**
+ * If the user has enabled two-factor authentication, request a second
factor.
+ *
+ * @param User $user User being authenticated. This may become a
+ * "UserValue" in the future, or User may be refactored into such.
+ * @param AuthenticationRequest[] $reqs Keys are class names
+ * @return AuthenticationResponse Expected responses:
+ * - PASS: The user is authenticated. Additional secondary providers
may run.
+ * - FAIL: The user is not authenticated. Fail the authentication
process.
+ * - ABSTAIN: Additional secondary providers may run.
+ * - UI: Additional AuthenticationRequests are needed to complete the
process.
+ * - REDIRECT: Redirection to a third party is needed to complete the
process.
+ */
+ public function beginSecondaryAuthentication( $user, array $reqs ) {
+ $oathuser = OATHUser::newFromUser( $user );
+
+ if ( !$oathuser || !$oathuser->isEnabled() ||
!$oathuser->isValidated() ) {
+ return AuthenticationResponse::newAbstain();
+ } else {
+ return AuthenticationResponse::newUI( array(
'TOTPAuthenticationRequest' ),
+ wfMessage( 'oathauth-auth-ui' ) );
+ }
+ }
+
+
+ /**
+ * Continue an authentication flow
+ * @param User $user User being authenticated. This may become a
+ * "UserValue" in the future, or User may be refactored into such.
+ * @param AuthenticationRequest[] $reqs Keys are class names
+ * @return AuthenticationResponse Expected responses:
+ * - PASS: The user is authenticated. Additional secondary providers
may run.
+ * - FAIL: The user is not authenticated. Fail the authentication
process.
+ * - ABSTAIN: Additional secondary providers may run.
+ * - UI: Additional AuthenticationRequests are needed to complete the
process.
+ * - REDIRECT: Redirection to a third party is needed to complete the
process.
+ */
+ public function continueSecondaryAuthentication( $user, array $reqs ) {
+ if ( !isset( $reqs['TOTPAuthenticationRequest'] ) ) {
+ throw new LogicException( 'TOTPAuthenticationRequest
required' );
+ }
+
+ $oathuser = OATHUser::newFromUser( $user );
+ $token = $reqs['TOTPAuthenticationRequest']->OATHToken;
+
+ if ( !$oathuser || !$oathuser->isEnabled() ||
!$oathuser->isValidated() ) {
+ $this->logger->warning( 'Two-factor authentication was
disabled mid-authentication for '
+ . $user->getName() );
+ return AuthenticationResponse::newAbstain();
+ }
+
+ if ( $oathuser->verifyToken( $token ) ) {
+ return AuthenticationResponse::newPass();
+ } else {
+ // FIXME how does this get throttled? (is that needed
at all?)
+ return AuthenticationResponse::newUI( array(
'TOTPAuthenticationRequest' ),
+ wfMessage( 'oathauth-abortlogin' ) );
+ }
+ }
+
+
+ /**
+ * Start an account creation flow
+ * @param User $user User being created (has been added to the
database).
+ * This may become a "UserValue" in the future, or User may be
refactored
+ * into such.
+ * @param AuthenticationRequest[] $reqs Keys are class names
+ * @return AuthenticationResponse Expected responses:
+ * - PASS: The user creation is ok. Additional secondary providers may
run.
+ * - ABSTAIN: Additional secondary providers may run.
+ * - UI: Additional AuthenticationRequests are needed to complete the
process.
+ * - REDIRECT: Redirection to a third party is needed to complete the
process.
+ */
+ public function beginSecondaryAccountCreation( $user, array $reqs ) {
+ return AuthenticationResponse::newAbstain();
+ }
+
+ /**
+ * Validate a change of authentication data (e.g. passwords)
+ * @param AuthenticationRequest $req
+ * @return StatusValue
+ */
+ public function providerAllowsAuthenticationDataChange(
AuthenticationRequest $req ) {
+ if ( !isset( $reqs['PasswordAuthenticationRequest'] ) ) {
+ return StatusValue::newGood();
+ }
+
+ $user = SessionManager::getGlobalSession()->getUser();
+ $oathuser = OATHUser::newFromUser( $user );
+
+ if ( !$oathuser || !$oathuser->isEnabled() ||
!$oathuser->isValidated() ) {
+ return StatusValue::newGood();
+ }
+
+ // this is a password change and the user enabled two-factor;
second factor is required
+
+ if ( !isset( $reqs['TOTPAuthenticationRequest'] ) ) {
+ return StatusValue::newFatal( 'oathauth-required' );
+ }
+
+ $token = $reqs['TOTPAuthenticationRequest'];
+ if ( $oathuser->verifyToken( $token ) ) {
+ return StatusValue::newGood();
+ } else {
+ return StatusValue::newFatal( 'oathauth-abortlogin' );
+ }
+ }
+
+
+}
diff --git a/i18n/en.json b/i18n/en.json
index 5514f1f..14f1dfe 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -33,5 +33,9 @@
"oathauth-notloggedin": "Login required",
"oathauth-mustbeloggedin": "You must be logged in to perform this
action.",
"oathauth-prefs-label": "Two-factor authentication:",
- "oathauth-abortlogin": "The two-factor authentication token provided
was invalid."
+ "oathauth-abortlogin": "The two-factor authentication token provided
was invalid.",
+ "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",
+ "oathauth-auth-required": "A verification code is required"
}
diff --git a/i18n/qqq.json b/i18n/qqq.json
index a45e378..9e742bb 100644
--- a/i18n/qqq.json
+++ b/i18n/qqq.json
@@ -37,5 +37,9 @@
"oathauth-notloggedin": "Page title seen on Special:OATH when a user is
not logged in.\n{{Identical|Login required}}",
"oathauth-mustbeloggedin": "Plain text seen on Special:OATH when a user
is not logged in.",
"oathauth-prefs-label": "Plain text label seen on
Special:Preferences\n\nSee
[https://en.wikipedia.org/wiki/Two_factor_authentication two factor
authentication]",
- "oathauth-abortlogin": "Error message shown on login and password
change pages when authentication is aborted.\n\nSee
[https://en.wikipedia.org/wiki/Two_factor_authentication two factor
authentication]"
+ "oathauth-abortlogin": "Error message shown on login and password
change pages when authentication is aborted.\n\nSee
[https://en.wikipedia.org/wiki/Two_factor_authentication two factor
authentication]",
+ "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-auth-required": "Error message used when a password change
would require a verification code but none is given"
}
--
To view, visit https://gerrit.wikimedia.org/r/253300
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: Ic492b8f2477c475f8414b61505139e9a1df2ba5b
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/OATHAuth
Gerrit-Branch: master
Gerrit-Owner: Gergő Tisza <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits