From c84ae939eddcad92af563e02c9609d9de09db05e Mon Sep 17 00:00:00 2001
From: Brad Jorsch <bjorsch@wikimedia.org>
Date: Tue, 30 Oct 2018 13:56:02 -0400
Subject: [PATCH] Require reauthentication for proposing or managing consumers

By default, creating an owner-only consumer requires reauthentication
within the past 2 hours, while proposing or managing other consumers
requires reauthentication every 48 hours.

Bug: T197156
Bug: T208008
Change-Id: Iba1777196ae9da3423487084070be84175580aa7
---
 backend/MWOAuth.hooks.php                     |  8 ++++
 backend/MWOAuthConsumer.php                   |  8 ++--
 backend/MWOAuthConsumerAcceptance.php         |  6 +--
 control/MWOAuthSubmitControl.php              |  2 +-
 .../SpecialMWOAuthConsumerRegistration.php    | 37 +++++++++++++++++++
 .../SpecialMWOAuthManageConsumers.php         | 25 +++++++++++++
 6 files changed, 78 insertions(+), 8 deletions(-)

diff --git a/backend/MWOAuth.hooks.php b/backend/MWOAuth.hooks.php
index f06a26a..63c0541 100644
--- a/backend/MWOAuth.hooks.php
+++ b/backend/MWOAuth.hooks.php
@@ -11,6 +11,14 @@ use MediaWiki\Storage\NameTableAccessException;
 class MWOAuthHooks {
 
 	public static function onExtensionFunctions() {
+		global $wgReauthenticateTime;
+
+		// TODO: Maybe make it possible to put this in extension.json
+		$wgReauthenticateTime += [
+			'OAuthManage' => 172800,
+			'OAuthProposeOwnerOnly' => 7200,
+		];
+
 		\MediaWiki\Extensions\OAuth\MWOAuthUISetup::conditionalSetup();
 	}
 
diff --git a/backend/MWOAuthConsumer.php b/backend/MWOAuthConsumer.php
index 9ef778d..9e47656 100644
--- a/backend/MWOAuthConsumer.php
+++ b/backend/MWOAuthConsumer.php
@@ -337,7 +337,7 @@ class MWOAuthConsumer extends MWOAuthDAO {
 		}
 	}
 
-	protected function userCanSee( $name, \RequestContext $context ) {
+	protected function userCanSee( $name, \IContextSource $context ) {
 		if ( $this->get( 'deleted' )
 			&& !$context->getUser()->isAllowed( 'mwoauthviewsuppressed' ) ) {
 			return $context->msg( 'mwoauth-field-hidden' );
@@ -346,7 +346,7 @@ class MWOAuthConsumer extends MWOAuthDAO {
 		}
 	}
 
-	protected function userCanSeePrivate( $name, \RequestContext $context ) {
+	protected function userCanSeePrivate( $name, \IContextSource $context ) {
 		if ( !$context->getUser()->isAllowed( 'mwoauthviewprivate' ) ) {
 			return $context->msg( 'mwoauth-field-private' );
 		} else {
@@ -354,7 +354,7 @@ class MWOAuthConsumer extends MWOAuthDAO {
 		}
 	}
 
-	protected function userCanSeeEmail( $name, \RequestContext $context ) {
+	protected function userCanSeeEmail( $name, \IContextSource $context ) {
 		if ( !$context->getUser()->isAllowed( 'mwoauthmanageconsumer' ) ) {
 			return $context->msg( 'mwoauth-field-private' );
 		} else {
@@ -362,7 +362,7 @@ class MWOAuthConsumer extends MWOAuthDAO {
 		}
 	}
 
-	protected function userCanSeeSecret( $name, \RequestContext $context ) {
+	protected function userCanSeeSecret( $name, \IContextSource $context ) {
 		return $context->msg( 'mwoauth-field-private' );
 	}
 }
diff --git a/backend/MWOAuthConsumerAcceptance.php b/backend/MWOAuthConsumerAcceptance.php
index d0ec980..bc0c66b 100644
--- a/backend/MWOAuthConsumerAcceptance.php
+++ b/backend/MWOAuthConsumerAcceptance.php
@@ -162,7 +162,7 @@ class MWOAuthConsumerAcceptance extends MWOAuthDAO {
 		return $row;
 	}
 
-	protected function userCanSee( $name, \RequestContext $context ) {
+	protected function userCanSee( $name, \IContextSource $context ) {
 		$centralUserId = MWOAuthUtils::getCentralIdFromLocalUser( $context->getUser() );
 		if ( $this->userId != $centralUserId
 			&& !$context->getUser()->isAllowed( 'mwoauthviewprivate' )
@@ -173,7 +173,7 @@ class MWOAuthConsumerAcceptance extends MWOAuthDAO {
 		}
 	}
 
-	protected function userCanSeePrivate( $name, \RequestContext $context ) {
+	protected function userCanSeePrivate( $name, \IContextSource $context ) {
 		if ( !$context->getUser()->isAllowed( 'mwoauthviewprivate' ) ) {
 			return $context->msg( 'mwoauth-field-private' );
 		} else {
@@ -181,7 +181,7 @@ class MWOAuthConsumerAcceptance extends MWOAuthDAO {
 		}
 	}
 
-	protected function userCanSeeSecret( $name, \RequestContext $context ) {
+	protected function userCanSeeSecret( $name, \IContextSource $context ) {
 		return $context->msg( 'mwoauth-field-private' );
 	}
 }
diff --git a/control/MWOAuthSubmitControl.php b/control/MWOAuthSubmitControl.php
index 20fc59b..2ef120f 100644
--- a/control/MWOAuthSubmitControl.php
+++ b/control/MWOAuthSubmitControl.php
@@ -25,7 +25,7 @@ namespace MediaWiki\Extensions\OAuth;
  * Handle the logic of submitting a client request
  */
 abstract class MWOAuthSubmitControl extends \ContextSource {
-	/** @var \RequestContext */
+	/** @var \IContextSource */
 	protected $context;
 	/** @var Array (field name => value) */
 	protected $vals;
diff --git a/frontend/specialpages/SpecialMWOAuthConsumerRegistration.php b/frontend/specialpages/SpecialMWOAuthConsumerRegistration.php
index 4aba1df..73ea7f1 100644
--- a/frontend/specialpages/SpecialMWOAuthConsumerRegistration.php
+++ b/frontend/specialpages/SpecialMWOAuthConsumerRegistration.php
@@ -28,10 +28,28 @@ use User;
  * Page that has registration request form and consumer update form
  */
 class SpecialMWOAuthConsumerRegistration extends \SpecialPage {
+
+	/**
+	 * @var array|null POST data preserved across re-authentication
+	 */
+	protected $reauthPostData = null;
+
 	public function __construct() {
 		parent::__construct( 'OAuthConsumerRegistration' );
 	}
 
+	protected function setReauthPostData( array $data ) {
+		$this->reauthPostData = $data;
+
+		$context = $this->getContext();
+		$context = new \DerivativeContext( $context );
+		$oldRequest = $this->getRequest();
+		$context->setRequest( new \DerivativeRequest(
+			$oldRequest, $data + $oldRequest->getQueryValues(), true
+		) );
+		$this->setContext( $context );
+	}
+
 	public function doesWrites() {
 		return true;
 	}
@@ -49,6 +67,10 @@ class SpecialMWOAuthConsumerRegistration extends \SpecialPage {
 
 		$this->checkPermissions();
 
+		if ( !$this->checkLoginSecurityLevel( 'OAuthManage' ) ) {
+			return;
+		}
+
 		$request = $this->getRequest();
 		$user = $this->getUser();
 		$lang = $this->getLanguage();
@@ -224,6 +246,15 @@ class SpecialMWOAuthConsumerRegistration extends \SpecialPage {
 			);
 			$form->setSubmitCallback(
 				function ( array $data, \IContextSource $context ) use ( $control ) {
+					if ( $this->reauthPostData !== null ) {
+						// Don't process the submission on the fake post in
+						// case of some crazy kind of CSRF.
+						return false;
+					}
+					if ( $data['ownerOnly'] && !$this->checkLoginSecurityLevel( 'OAuthProposeOwnerOnly' ) ) {
+						return false;
+					}
+
 					$data['grants'] = \FormatJson::encode( // adapt form to controller
 						preg_replace( '/^grant-/', '', $data['grants'] ) );
 					// 'callbackUrl' must be present,
@@ -333,6 +364,12 @@ class SpecialMWOAuthConsumerRegistration extends \SpecialPage {
 			);
 			$form->setSubmitCallback(
 				function ( array $data, \IContextSource $context ) use ( $control ) {
+					if ( $this->reauthPostData !== null ) {
+						// Don't process the submission on the fake post in
+						// case of some crazy kind of CSRF.
+						return false;
+					}
+
 					$control->setInputParameters( $data );
 					return $control->submit();
 				}
diff --git a/frontend/specialpages/SpecialMWOAuthManageConsumers.php b/frontend/specialpages/SpecialMWOAuthManageConsumers.php
index bc45500..e21d57c 100644
--- a/frontend/specialpages/SpecialMWOAuthManageConsumers.php
+++ b/frontend/specialpages/SpecialMWOAuthManageConsumers.php
@@ -34,6 +34,9 @@ class SpecialMWOAuthManageConsumers extends \SpecialPage {
 	/** @var string A stage key from MWOAuthConsumer::$stageNames */
 	protected $stageKey;
 
+	/** @var array|null POST data preserved across re-authentication */
+	protected $reauthPostData = null;
+
 	/**
 	 * Stages which are shown in a queue (they are in an actionable state and can form a backlog)
 	 * @var array
@@ -52,6 +55,18 @@ class SpecialMWOAuthManageConsumers extends \SpecialPage {
 		parent::__construct( 'OAuthManageConsumers', 'mwoauthmanageconsumer' );
 	}
 
+	protected function setReauthPostData( array $data ) {
+		$this->reauthPostData = $data;
+
+		$context = $this->getContext();
+		$context = new \DerivativeContext( $context );
+		$oldRequest = $this->getRequest();
+		$context->setRequest( new \DerivativeRequest(
+			$oldRequest, $data + $oldRequest->getQueryValues(), true
+		) );
+		$this->setContext( $context );
+	}
+
 	public function doesWrites() {
 		return true;
 	}
@@ -75,6 +90,10 @@ class SpecialMWOAuthManageConsumers extends \SpecialPage {
 			throw new \ErrorPageError( 'mwoauth-error', 'mwoauth-db-readonly' );
 		}
 
+		if ( !$this->checkLoginSecurityLevel( 'OAuthManage' ) ) {
+			return;
+		}
+
 		// Format is Special:OAuthManageConsumers[/<stage>|/<consumer key>]
 		// B/C format is Special:OAuthManageConsumers/<stage>/<consumer key>
 		$stageKey = $consumerKey = null;
@@ -321,6 +340,12 @@ class SpecialMWOAuthManageConsumers extends \SpecialPage {
 		);
 		$form->setSubmitCallback(
 			function ( array $data, \IContextSource $context ) use ( $control ) {
+				if ( $this->reauthPostData ) {
+					// Don't process the submission on the fake post in
+					// case of some crazy kind of CSRF.
+					return false;
+				}
+
 				$data['suppress'] = 0;
 				if ( $data['action'] === 'dsuppress' ) {
 					$data = [ 'action' => 'disable', 'suppress' => 1 ] + $data;
-- 
2.19.1

