From b7371d83d4a7f401513aa9cd83d7428c2af61a76 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 ++++
 .../SpecialMWOAuthConsumerRegistration.php    | 37 +++++++++++++++++++
 .../SpecialMWOAuthManageConsumers.php         | 25 +++++++++++++
 3 files changed, 70 insertions(+)

diff --git a/backend/MWOAuth.hooks.php b/backend/MWOAuth.hooks.php
index f06a26a..0f54668 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' => 60,
+		];
+
 		\MediaWiki\Extensions\OAuth\MWOAuthUISetup::conditionalSetup();
 	}
 
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

