Gergő Tisza has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/294861

Change subject: [WIP] Use AuthManager data change methods
......................................................................

[WIP] Use AuthManager data change methods

TODO:
* token reset
* handle returnto
* T132329
* make it look less horrible in oouiform
* cannot call HtmlForm::setMessagePrefix
* T122691

Bug: T110457
Change-Id: I2c1a9bf87ca5643cc7c52acc92955177aa4bdfad
Depends-On: I4d796a46d99daa8841fcdcbe76dddf9db46af9ed
---
A HTMLOATHField.php
M OATHAuth.hooks.php
M auth/TOTPAuthenticationRequest.php
A auth/TOTPDisableAuthenticationRequest.php
A auth/TOTPEnableAuthenticationRequest.php
M auth/TOTPSecondaryAuthenticationProvider.php
M extension.json
M i18n/en.json
M i18n/qqq.json
9 files changed, 326 insertions(+), 16 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/OATHAuth 
refs/changes/61/294861/1

diff --git a/HTMLOATHField.php b/HTMLOATHField.php
new file mode 100644
index 0000000..e727bfd
--- /dev/null
+++ b/HTMLOATHField.php
@@ -0,0 +1,98 @@
+<?php
+
+class HTMLOATHField extends HTMLFormField {
+       /** @var OATHAuthKey */
+       protected $oathKey;
+
+       /** @var OATHUser */
+       protected $oathUser;
+
+       /**
+        * Constructs a verification form field for a given OATH key
+        * - oathKey: OATHAuthKey
+        * - oathUser: OATHUser
+        * @param array $params
+        */
+       public function __construct( array $params ) {
+               parent::__construct( $params );
+               $this->oathKey = $params['oathKey'];
+               $this->oathUser = $params['oathUser'];
+       }
+
+       public function hasVisibleOutput() {
+               return false;
+       }
+
+       public function getInputHTML( $value ) {
+               $output = $this->mParent ? $this->mParent->getOutput() : 
RequestContext::getMain()->getOutput();
+               $output->addModules( 'ext.oathauth' );
+               $output->addHTML( ResourceLoader::makeInlineScript( 
Xml::encodeJsCall( 'mw.loader.using', array(
+                       array( 'ext.oathauth' ),
+                       new XmlJsCode( 'function () {' . 
'$("#qrcode").qrcode("otpauth://totp/' .
+                               rawurlencode( $this->oathUser->getAccount() ) . 
'?secret='
+                               . $this->oathKey->getSecret() . '");' . '}' )
+               ) ) ) );
+               return '';
+       }
+
+       public static function getSubFields( OATHAuthKey $oathKey, OATHUser 
$oathUser ) {
+               return array(
+                       'OATHModule' => array(
+                               'class' => self::class,
+                               'oathKey' => $oathKey,
+                               'oathUser' => $oathUser,
+                       ),
+                       'OATHAppInfo' => array(
+                               'type' => 'info',
+                               'default' => wfMessage( 'oathauth-step1-test' 
)->escaped(),
+                               'raw' => true,
+                               'section' => 'step1',
+                       ),
+                       'OATHQRCode' => array(
+                               'type' => 'info',
+                               'default' => '<div id="qrcode"></div>',
+                               'raw' => true,
+                               'section' => 'step2',
+                       ),
+                       'OATHScratchCodes' => array(
+                               'type' => 'info',
+                               'label-message' => 'oathauth-step2alt',
+                               'default' =>
+                                       '<strong>' . wfMessage( 
'oathauth-account' )->escaped() . '</strong><br/>'
+                                       . $oathUser->getAccount() . '<br/><br/>'
+                                       . '<strong>' . wfMessage( 
'oathauth-secret' )->escaped() . '</strong><br/>'
+                                       . $oathKey->getSecret() . '<br/>',
+                               'raw' => true,
+                               'section' => 'step2',
+                       ),
+                       'OATHScratchTokens' => array(
+                               'type' => 'info',
+                               'default' =>
+                                       wfMessage( 
'openstackmanager-scratchtokens' )
+                                       . self::createResourceList( 
$oathKey->getScratchTokens() ),
+                               'raw' => true,
+                               'section' => 'step3',
+                       ),
+                       'OATHToken' => array(
+                               'type' => 'text',
+                               'default' => '',
+                               'label-message' => 'oathauth-entertoken',
+                               'name' => 'token',
+                               'section' => 'step4',
+                               'persistent' => false,
+                       ),
+               );
+       }
+
+       /**
+        * @param $resources array
+        * @return string
+        */
+       protected static function createResourceList( $resources ) {
+               $resourceList = '';
+               foreach ( $resources as $resource ) {
+                       $resourceList .= Html::rawElement( 'li', array(), 
$resource );
+               }
+               return Html::rawElement( 'ul', array(), $resourceList );
+       }
+}
diff --git a/OATHAuth.hooks.php b/OATHAuth.hooks.php
index 66fbfd4..7d24410 100644
--- a/OATHAuth.hooks.php
+++ b/OATHAuth.hooks.php
@@ -65,16 +65,32 @@
        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,
-                       ];
+               if ( !isset( $fieldInfo['OATHToken'] ) ) {
+                       return;
+               };
+
+               // handle enable request
+               $req = AuthenticationRequest::getRequestByClass( $requests,
+                       TOTPEnableAuthenticationRequest::class );
+               if ( $req ) {
+                       // info fields are for the API, HTMLOATHField already 
contains the same information
+                       unset( $formDescriptor['OATHSecret'], 
$formDescriptor['OATHScratchCodes'],
+                               $formDescriptor['OATHToken'] );
+
+                       $repo = OATHAuthHooks::getOATHUserRepository();
+                       $oathuser = $repo->findByUser( User::newFromName( 
$req->username ) );
+                       $formDescriptor += HTMLOATHField::getSubFields( 
$req->key, $oathuser );
+                       return;
                }
-               return true;
+
+               // disable and login is the same, just a field for the token
+               $formDescriptor['OATHToken'] += [
+                       'cssClass' => 'loginText',
+                       'id' => 'wpOATHToken',
+                       'size' => 20,
+                       'autofocus' => true,
+                       'persistent' => false,
+               ];
        }
 
        /**
@@ -117,9 +133,12 @@
                }
 
                $oathUser = self::getOATHUserRepository()->findByUser( $user );
+               $isEnabled = $oathUser->getKey() !== null;
 
-               $title = SpecialPage::getTitleFor( 'OATH' );
-               $msg = $oathUser->getKey() !== null ? 'oathauth-disable' : 
'oathauth-enable';
+               $title = SpecialPage::getTitleFor( 'ChangeCredentials', 
$isEnabled
+                       ? TOTPDisableAuthenticationRequest::class
+                       : TOTPEnableAuthenticationRequest::class );
+               $msg = $isEnabled ? 'oathauth-disable' : 'oathauth-enable';
 
                $preferences[$msg] = array(
                        'type' => 'info',
diff --git a/auth/TOTPAuthenticationRequest.php 
b/auth/TOTPAuthenticationRequest.php
index 99220ce..8fda60d 100644
--- a/auth/TOTPAuthenticationRequest.php
+++ b/auth/TOTPAuthenticationRequest.php
@@ -7,6 +7,7 @@
  * that is generated from the current time indepdendently by the server and 
the client.
  */
 class TOTPAuthenticationRequest extends AuthenticationRequest {
+       /** @var string */
        public $OATHToken;
 
        public function describeCredentials() {
diff --git a/auth/TOTPDisableAuthenticationRequest.php 
b/auth/TOTPDisableAuthenticationRequest.php
new file mode 100644
index 0000000..ebe437c
--- /dev/null
+++ b/auth/TOTPDisableAuthenticationRequest.php
@@ -0,0 +1,30 @@
+<?php
+
+use MediaWiki\Auth\AuthenticationRequest;
+
+/**
+ * AuthManager value object for disabling TOTP second-factor checks.
+ * There are two methods to disable: externally via ACTION_CHANGE and 
internally via ACTION_REMOVE.
+ * The former requires a successful TOTP key verification.
+ */
+class TOTPDisableAuthenticationRequest extends AuthenticationRequest {
+       /** @var string */
+       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-entertoken' ),
+                               'help' => wfMessage( 'oathauth-auth-token-help' 
),
+                       ),
+               );
+       }
+}
diff --git a/auth/TOTPEnableAuthenticationRequest.php 
b/auth/TOTPEnableAuthenticationRequest.php
new file mode 100644
index 0000000..3f158a7
--- /dev/null
+++ b/auth/TOTPEnableAuthenticationRequest.php
@@ -0,0 +1,58 @@
+<?php
+
+use MediaWiki\Auth\AuthenticationRequest;
+
+/**
+ * AuthManager value object for the TOTP secret key + scratch codes. Used to 
pass the secret key and
+ * the codes to the user and get back a one-time token as a proof that the 
user managed to configure
+ * their token generator (and enabling two-factor authentication won't lock 
them out of their
+ * account).
+ */
+class TOTPEnableAuthenticationRequest extends AuthenticationRequest {
+       /** @var string */
+       public $OATHToken;
+
+       /** @var OATHAuthKey */
+       public $key;
+
+       public function __construct( OATHAuthKey $key ) {
+               $this->key = $key;
+       }
+
+       public function describeCredentials() {
+               return [
+                       'provider' => wfMessage( 'oathauth-describe-provider' ),
+                       'account' => new \RawMessage( '$1', [ $this->username ] 
),
+               ] + parent::describeCredentials();
+       }
+
+       public function getFieldInfo() {
+               return array(
+                       'OATHSecret' => array(
+                               'type' => 'null',
+                               'value' => new RawMessage( '$1', array( 
$this->key->getSecret() ) ),
+                               'label' => wfMessage( 'oathauth-secret-label' ),
+                               'help' => wfMessage( 'oathauth-secret-help' ),
+                       ),
+                       'OATHScratchCodes' => array(
+                               'type' => 'null',
+                               'value' => new RawMessage( '$1', array( 
implode( wfMessage( 'comma-separator' ),
+                                       $this->key->getScratchTokens() ) ) ),
+                               'label' => wfMessage( 
'oathauth-scratch-codes-label' ),
+                               'help' => wfMessage( 
'oathauth-scratch-codes-help' ),
+                       ),
+                       'OATHToken' => array(
+                               'type' => 'string',
+                               'label' => wfMessage( 
'oathauth-auth-token-label' ),
+                               'help' => wfMessage( 'oathauth-auth-token-help' 
),
+                       ),
+               );
+       }
+
+       public function getMetadata() {
+               return [
+                       'OATHSecret' => $this->key->getSecret(),
+                       'OATHScratchCodes' => $this->key->getScratchTokens(),
+               ];
+       }
+}
diff --git a/auth/TOTPSecondaryAuthenticationProvider.php 
b/auth/TOTPSecondaryAuthenticationProvider.php
index 2ca930a..981d102 100644
--- a/auth/TOTPSecondaryAuthenticationProvider.php
+++ b/auth/TOTPSecondaryAuthenticationProvider.php
@@ -23,6 +23,26 @@
                        case AuthManager::ACTION_LOGIN:
                                // don't ask for anything initially so the 
second factor is on a separate screen
                                return [];
+                       case AuthManager::ACTION_CHANGE:
+                       case AuthManager::ACTION_REMOVE:
+                               $user = User::newFromName( $options['username'] 
);
+                               if ( !$user->getId() ) {
+                                       return [];
+                               }
+
+                               $repo = OATHAuthHooks::getOATHUserRepository();
+                               $oathuser = $repo->findByUser( $user );
+                               $key = $oathuser->getKey();
+
+                               if ( !$key && $action === 
AuthManager::ACTION_CHANGE ) {
+                                       // add a key
+                                       return [ new 
TOTPEnableAuthenticationRequest( $this->getPendingKey() ) ];
+                               } elseif ( $key ) {
+                                       // remove the key - on REMOVE it is 
unconditional and only allowed internally,
+                                       // on CHANGE it requires a verification 
token
+                                       return [ new 
TOTPDisableAuthenticationRequest() ];
+                               }
+                               return [];
                        default:
                                return [];
                }
@@ -83,4 +103,72 @@
        public function beginSecondaryAccountCreation( $user, $creator, array 
$reqs ) {
                return AuthenticationResponse::newAbstain();
        }
+
+       public function providerAllowsAuthenticationDataChange(
+               AuthenticationRequest $req, $checkData = true
+       ) {
+               if ( $req instanceof TOTPEnableAuthenticationRequest ) {
+                       $repo = OATHAuthHooks::getOATHUserRepository();
+                       $oathuser = $repo->findByUser( User::newFromName( 
$req->username ) );
+                       $key = $this->getPendingKey();
+
+                       if ( $oathuser->getKey() ) {
+                               return StatusValue::newFatal( 
'oathauth-alreadyenabled' );
+                       } elseif ( !$key || $key != $req->key ) {
+                               // sanity check, should not be possible
+                               return StatusValue::newFatal( 
'oathauth-failedtovalidateoauth' );
+                       } elseif ( $checkData && !$key->verifyToken( 
$req->OATHToken, $oathuser ) ) {
+                               return StatusValue::newFatal( 
'oathauth-failedtovalidateoauth' );
+                       }
+
+                       return StatusValue::newGood();
+               } elseif ( $req instanceof TOTPDisableAuthenticationRequest ) {
+                       $repo = OATHAuthHooks::getOATHUserRepository();
+                       $oathuser = $repo->findByUser( User::newFromName( 
$req->username ) );
+                       $key = $oathuser->getKey();
+
+                       if ( !$key ) {
+                               return StatusValue::newFatal( 
'oathauth-notenabled' );
+                       } elseif ( $checkData && 
!$oathuser->getKey()->verifyToken( $req->OATHToken, $oathuser ) ) {
+                               return StatusValue::newFatal( 
'oathauth-failedtovalidateoauth' );
+                       }
+
+                       return StatusValue::newGood();
+               }
+               return StatusValue::newGood( 'ignored' );
+       }
+
+       public function providerChangeAuthenticationData( AuthenticationRequest 
$req ) {
+               if ( $req instanceof TOTPEnableAuthenticationRequest ) {
+                       $repo = OATHAuthHooks::getOATHUserRepository();
+                       $oathuser = $repo->findByUser( User::newFromName( 
$req->username ) );
+
+                       $oathuser->setKey( $req->key );
+                       $repo->persist( $oathuser );
+                       $this->manager->getRequest()->getSession()->remove( 
'oathauth-key' );
+                       // T132329 - should return 'oathauth-validatedoath'
+               } elseif ( $req instanceof TOTPDisableAuthenticationRequest ) {
+                       $repo = OATHAuthHooks::getOATHUserRepository();
+                       $oathuser = $repo->findByUser( User::newFromName( 
$req->username ) );
+
+                       $oathuser->setKey( null );
+                       $repo->remove( $oathuser );
+                       // T132329 - should return 'oathauth-disabledoath'
+               }
+       }
+
+       /**
+        * Fetches (and if necessary creates and stores) the pending TOTP 
secret key that the user is
+        * about to enable but has not confirmed yet.
+        * @return OATHAuthKey
+        */
+       protected function getPendingKey() {
+               $session = $this->manager->getRequest()->getSession();
+               $key = $session->getSecret( 'oathauth-key' );
+               if ( $key === null ) {
+                       $key = OATHAuthKey::newFromRandom();
+                       $session->setSecret( 'oathauth-key', $key );
+               }
+               return $key;
+       }
 }
diff --git a/extension.json b/extension.json
index f4081c9..bb85c32 100644
--- a/extension.json
+++ b/extension.json
@@ -21,7 +21,10 @@
                "SpecialOATHLogin": "special/SpecialOATHLogin.php",
                "ProxySpecialPage": "special/ProxySpecialPage.php",
                "TOTPAuthenticationRequest": 
"auth/TOTPAuthenticationRequest.php",
-               "TOTPSecondaryAuthenticationProvider": 
"auth/TOTPSecondaryAuthenticationProvider.php"
+               "TOTPEnableAuthenticationRequest": 
"auth/TOTPEnableAuthenticationRequest.php",
+               "TOTPDisableAuthenticationRequest": 
"auth/TOTPDisableAuthenticationRequest.php",
+               "TOTPSecondaryAuthenticationProvider": 
"auth/TOTPSecondaryAuthenticationProvider.php",
+               "HTMLOATHField": "HTMLOATHField.php"
        },
        "ExtensionMessagesFiles": {
                "OATHAuthAlias": "OATHAuth.alias.php"
@@ -72,5 +75,8 @@
                        "oathauth-enable": true
                }
        },
+       "RemoveCredentialsBlacklist": [
+               "TOTPDisableAuthenticationRequest"
+       ],
        "manifest_version": 1
 }
diff --git a/i18n/en.json b/i18n/en.json
index 3a8e123..8c15c40 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -14,6 +14,7 @@
        "oathauth-enable": "Enable two-factor authentication",
        "oathauth-failedtoenableoauth": "Failed to enable two-factor 
authentication.",
        "oathauth-alreadyenabled": "Two-factor authentication is already 
enabled.",
+       "oathauth-notenabled": "Two-factor authentication is not enabled.",
        "oathauth-verify": "Verify two-factor token",
        "openstackmanager-scratchtokens": "The following list is a list of 
one-time use scratch tokens. These tokens can only be used once, and are for 
emergency use. Please write these down and keep them in a secure location. If 
you lose your phone, these tokens are the only way to rescue your account. 
These tokens will never be shown again.",
        "oathauth-reset": "Reset two-factor credentials",
@@ -51,5 +52,9 @@
        "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)."
+       "oathauth-describe-provider": "Two-factor authentication (OATH)",
+       "oathauth-secret-label": "Secret key",
+       "oathauth-secret-help": "Two-factor secret key used to generate the 
one-time passwords.",
+       "oathauth-scratch-codes-label": "Scratch codes",
+       "oathauth-scratch-codes-help": "A list of one-time passwords in case 
the secret key gets lost."
 }
diff --git a/i18n/qqq.json b/i18n/qqq.json
index 3a28752..e85e20b 100644
--- a/i18n/qqq.json
+++ b/i18n/qqq.json
@@ -17,8 +17,9 @@
        "oathauth-legend": "Label for the legend on Special:OATH",
        "oathauth-secret": "Plain text found on Special:OATH while enabling 
OATH\n\nSee [https://en.wikipedia.org/wiki/Two_factor_authentication two factor 
authentication]",
        "oathauth-enable": "Page title on Special:OATH, when enabling 
OATH.\n\nSee [https://en.wikipedia.org/wiki/Two_factor_authentication two 
factor authentication]",
-       "oathauth-failedtoenableoauth": "Plain text, found on Special:OATH when 
failing to enable OATH.\n\nSee 
[https://en.wikipedia.org/wiki/Two_factor_authentication two factor 
authentication]",
-       "oathauth-alreadyenabled": "Plain text, found on Special:OATH when 
failing to enable OATH.\n\nSee 
[https://en.wikipedia.org/wiki/Two_factor_authentication two factor 
authentication]",
+       "oathauth-failedtoenableoauth": "Plain text, shown when failing to 
enable OATH.\n\nSee [https://en.wikipedia.org/wiki/Two_factor_authentication 
two factor authentication]",
+       "oathauth-alreadyenabled": "Plain text, shown when failing to disable 
OATH.\n\nSee [https://en.wikipedia.org/wiki/Two_factor_authentication two 
factor authentication]",
+       "oathauth-notenabled": "Plain text Two-factor authentication is not 
enabled.",
        "oathauth-verify": "Link, found on Special:OATH when no parameters are 
passed.\n\nSee [https://en.wikipedia.org/wiki/Two_factor_authentication two 
factor authentication]",
        "openstackmanager-scratchtokens": "Plain text, found on Special:OATH 
while enabling OATH.",
        "oathauth-reset": "Page title for Special:OATH, when resetting 
OATH.\n\nSee [https://en.wikipedia.org/wiki/Two_factor_authentication two 
factor authentication]",
@@ -56,5 +57,9 @@
        "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."
+       "oathauth-describe-provider": "Description of the extension as an 
authentication provider.",
+       "oathauth-secret-label": "Field label for the OATH secret key field.",
+       "oathauth-secret-help": "API help message for the OATH secret key 
field.",
+       "oathauth-scratch-codes-label": "Field label for the OATH scratch code 
field.",
+       "oathauth-scratch-codes-help": "API help message for the OATH scratch 
code field."
 }

-- 
To view, visit https://gerrit.wikimedia.org/r/294861
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I2c1a9bf87ca5643cc7c52acc92955177aa4bdfad
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/OATHAuth
Gerrit-Branch: master
Gerrit-Owner: Gergő Tisza <gti...@wikimedia.org>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to