From d75e35b75e9d70e511bee2c9fc830825363a4fd6 Mon Sep 17 00:00:00 2001
From: Joas Schilling <coding@schilljs.com>
Date: Mon, 19 Sep 2016 15:33:30 +0200
Subject: [PATCH] Introduce the UI for password confirmation

Signed-off-by: Joas Schilling <coding@schilljs.com>
---
 core/Controller/LoginController.php     | 37 ++++++++++++++
 core/Controller/OCJSController.php      | 10 ++--
 core/css/styles.css                     | 31 ++++++++++++
 core/js/js.js                           | 64 +++++++++++++++++++++++++
 core/routes.php                         |  1 +
 core/templates/layout.user.php          |  7 +++
 lib/private/Template/JSConfigHelper.php | 20 +++++++-
 lib/private/TemplateLayout.php          |  1 +
 8 files changed, 167 insertions(+), 4 deletions(-)

diff --git a/core/Controller/LoginController.php b/core/Controller/LoginController.php
index 71478470ffe..b1542de5d3c 100644
--- a/core/Controller/LoginController.php
+++ b/core/Controller/LoginController.php
@@ -1,5 +1,6 @@
 <?php
 /**
+ * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
  * @copyright Copyright (c) 2016, ownCloud, Inc.
  *
  * @author Christoph Wurst <christoph@owncloud.com>
@@ -31,6 +32,8 @@ use OC\User\Session;
 use OC_App;
 use OC_Util;
 use OCP\AppFramework\Controller;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\DataResponse;
 use OCP\AppFramework\Http\RedirectResponse;
 use OCP\AppFramework\Http\TemplateResponse;
 use OCP\Authentication\TwoFactorAuth\IProvider;
@@ -242,6 +245,8 @@ class LoginController extends Controller {
 		// User has successfully logged in, now remove the password reset link, when it is available
 		$this->config->deleteUserValue($loginResult->getUID(), 'core', 'lostpassword');
 
+		$this->session->set('last-password-confirm', $loginResult->getLastLogin());
+
 		if ($this->twoFactorManager->isTwoFactorAuthenticated($loginResult)) {
 			$this->twoFactorManager->prepareTwoFactorLogin($loginResult, $remember_login);
 
@@ -273,4 +278,36 @@ class LoginController extends Controller {
 		return $this->generateRedirect($redirect_url);
 	}
 
+	/**
+	 * @NoAdminRequired
+	 * @UseSession
+	 *
+	 * @license GNU AGPL version 3 or any later version
+	 *
+	 * @param string $password
+	 * @return DataResponse
+	 */
+	public function confirmPassword($password) {
+		$currentDelay = $this->throttler->getDelay($this->request->getRemoteAddress());
+		$this->throttler->sleepDelay($this->request->getRemoteAddress());
+
+		$user = $this->userSession->getUser();
+		if (!$user instanceof IUser) {
+			return new DataResponse([], Http::STATUS_UNAUTHORIZED);
+		}
+
+		$loginResult = $this->userManager->checkPassword($user->getUID(), $password);
+		if ($loginResult === false) {
+			$this->throttler->registerAttempt('sudo', $this->request->getRemoteAddress(), ['user' => $user->getUID()]);
+			if ($currentDelay === 0) {
+				$this->throttler->sleepDelay($this->request->getRemoteAddress());
+			}
+
+			return new DataResponse([], Http::STATUS_FORBIDDEN);
+		}
+
+		$confirmTimestamp = time();
+		$this->session->set('last-password-confirm', $confirmTimestamp);
+		return new DataResponse(['lastLogin' => $confirmTimestamp], Http::STATUS_OK);
+	}
 }
diff --git a/core/Controller/OCJSController.php b/core/Controller/OCJSController.php
index b1c2208377e..c2292a6733e 100644
--- a/core/Controller/OCJSController.php
+++ b/core/Controller/OCJSController.php
@@ -32,6 +32,7 @@ use OCP\IConfig;
 use OCP\IGroupManager;
 use OCP\IL10N;
 use OCP\IRequest;
+use OCP\ISession;
 use OCP\IURLGenerator;
 use OCP\IUserSession;
 
@@ -48,7 +49,8 @@ class OCJSController extends Controller {
 	 * @param IL10N $l
 	 * @param \OC_Defaults $defaults
 	 * @param IAppManager $appManager
-	 * @param IUserSession $session
+	 * @param ISession $session
+	 * @param IUserSession $userSession
 	 * @param IConfig $config
 	 * @param IGroupManager $groupManager
 	 * @param IniGetWrapper $iniWrapper
@@ -59,7 +61,8 @@ class OCJSController extends Controller {
 								IL10N $l,
 								\OC_Defaults $defaults,
 								IAppManager $appManager,
-								IUserSession $session,
+								ISession $session,
+								IUserSession $userSession,
 								IConfig $config,
 								IGroupManager $groupManager,
 								IniGetWrapper $iniWrapper,
@@ -70,7 +73,8 @@ class OCJSController extends Controller {
 			$l,
 			$defaults,
 			$appManager,
-			$session->getUser(),
+			$session,
+			$userSession->getUser(),
 			$config,
 			$groupManager,
 			$iniWrapper,
diff --git a/core/css/styles.css b/core/css/styles.css
index f0c4c4f33ff..7b2be87f610 100644
--- a/core/css/styles.css
+++ b/core/css/styles.css
@@ -983,3 +983,34 @@ fieldset.warning legend + p, fieldset.update legend + p {
    opacity: 0;
    -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";
 }
+
+#sudo-login-background {
+	position: absolute;
+	width: 100%;
+	height: 100%;
+	background-color: #1d2d44;
+	opacity:0.4;
+	z-index: 999;
+}
+
+#sudo-login-form {
+	position: absolute;
+	top: 45%;
+	left: 40%;
+	border: 1px solid #e9322d;
+	z-index: 1000;
+	background: #e4b9c0;
+	border-radius: 10px;
+	opacity:1;
+	padding: 25px;
+}
+
+#sudo-login-form .question {
+	width: 250px;
+}
+
+#sudo-login-form .confirm {
+	position: absolute;
+	top: 25px;
+	right: 25px;
+}
diff --git a/core/js/js.js b/core/js/js.js
index 54b103a7b7d..bc99e1c77da 100644
--- a/core/js/js.js
+++ b/core/js/js.js
@@ -1512,8 +1512,72 @@ function initCore() {
 			$(this).text(OC.Util.relativeModifiedDate(parseInt($(this).attr('data-timestamp'), 10)));
 		});
 	}, 30 * 1000);
+
+	OC.PasswordConfirmation.init();
 }
 
+OC.PasswordConfirmation = {
+	$form: null,
+	$background: null,
+	$input: null,
+	$submit: null,
+
+	init: function() {
+		this.$form = $('#sudo-login-form');
+		this.$background = $('#sudo-login-background');
+		this.$input = this.$form.find('.question');
+		this.$submit = this.$form.find('.confirm');
+
+		this.$background.on('click', _.bind(this._fadeOut, this));
+		$('.password-confirm-required').on('click', _.bind(this.requirePasswordConfirmation, this));
+		this.$submit.on('click', _.bind(this._submitPasswordConfirmation, this));
+	},
+
+	requirePasswordConfirmation: function() {
+		var timeSinceLogin = moment.now() - nc_lastLogin * 1000;
+		if (timeSinceLogin > 30 * 60 * 1000) { // 30 minutes
+			this.$form.removeClass('hidden');
+			this.$background.removeClass('hidden');
+
+			// Hack against firefox ignoring autocomplete="off"
+			if (this.$input.val() === ' ') {
+				this.$input.val('');
+			}
+		}
+	},
+
+	_submitPasswordConfirmation: function() {
+		var self = this;
+
+		self.$submit.removeClass('icon-confirm').addClass('icon-loading-small');
+
+		$.ajax({
+			url: OC.generateUrl('/login/confirm'),
+			data: {
+				password: this.$input.val()
+			},
+			type: 'POST',
+			success: function(response) {
+				nc_lastLogin = response.lastLogin;
+				self.$submit.addClass('icon-confirm').removeClass('icon-loading-small');
+
+				self.$form.addClass('hidden');
+				self.$background.addClass('hidden');
+			},
+			error: function() {
+				OC.Notification.showTemporary(t('core', 'Failed to authenticate, try again'));
+				self.$submit.addClass('icon-confirm').removeClass('icon-loading-small');
+			}
+		});
+	},
+
+	_fadeOut: function() {
+		this.$form.addClass('hidden');
+		this.$background.addClass('hidden');
+		this.$input.value = '';
+	}
+};
+
 $(document).ready(initCore);
 
 /**
diff --git a/core/routes.php b/core/routes.php
index 2ddd77c1445..e5636ff6c00 100644
--- a/core/routes.php
+++ b/core/routes.php
@@ -46,6 +46,7 @@ $application->registerRoutes($this, [
 		['name' => 'avatar#getTmpAvatar', 'url' => '/avatar/tmp', 'verb' => 'GET'],
 		['name' => 'avatar#postAvatar', 'url' => '/avatar/', 'verb' => 'POST'],
 		['name' => 'login#tryLogin', 'url' => '/login', 'verb' => 'POST'],
+		['name' => 'login#confirmPassword', 'url' => '/login/confirm', 'verb' => 'POST'],
 		['name' => 'login#showLoginForm', 'url' => '/login', 'verb' => 'GET'],
 		['name' => 'login#logout', 'url' => '/logout', 'verb' => 'GET'],
 		['name' => 'TwoFactorChallenge#selectChallenge', 'url' => '/login/selectchallenge', 'verb' => 'GET'],
diff --git a/core/templates/layout.user.php b/core/templates/layout.user.php
index bc8edf085d0..1b4f0bc0030 100644
--- a/core/templates/layout.user.php
+++ b/core/templates/layout.user.php
@@ -146,6 +146,13 @@
 			</div>
 		</div></nav>
 
+		<div id="sudo-login-background" class="hidden"></div>
+		<div id="sudo-login-form" class="hidden">
+			<input type="password" class="question" autocomplete="off" name="question" value=" "
+				placeholder="<?php p($l->t('Confirm your password')); ?>" />
+			<input class="confirm icon-confirm" title="<?php p($l->t('Confirm')); ?>" value="" type="submit">
+		</div>
+
 		<div id="content-wrapper">
 			<div id="content" class="app-<?php p($_['appid']) ?>" role="main">
 				<?php print_unescaped($_['content']); ?>
diff --git a/lib/private/Template/JSConfigHelper.php b/lib/private/Template/JSConfigHelper.php
index a7f8c251cee..eceaed0c380 100644
--- a/lib/private/Template/JSConfigHelper.php
+++ b/lib/private/Template/JSConfigHelper.php
@@ -27,6 +27,7 @@ use OCP\App\IAppManager;
 use OCP\IConfig;
 use OCP\IGroupManager;
 use OCP\IL10N;
+use OCP\ISession;
 use OCP\IURLGenerator;
 use OCP\IUser;
 
@@ -41,7 +42,10 @@ class JSConfigHelper {
 	/** @var IAppManager */
 	private $appManager;
 
-	/** @var IUser */
+	/** @var ISession */
+	private $session;
+
+	/** @var IUser|null */
 	private $currentUser;
 
 	/** @var IConfig */
@@ -60,6 +64,7 @@ class JSConfigHelper {
 	 * @param IL10N $l
 	 * @param \OC_Defaults $defaults
 	 * @param IAppManager $appManager
+	 * @param ISession $session
 	 * @param IUser|null $currentUser
 	 * @param IConfig $config
 	 * @param IGroupManager $groupManager
@@ -69,6 +74,7 @@ class JSConfigHelper {
 	public function __construct(IL10N $l,
 								\OC_Defaults $defaults,
 								IAppManager $appManager,
+								ISession $session,
 								$currentUser,
 								IConfig $config,
 								IGroupManager $groupManager,
@@ -77,6 +83,7 @@ class JSConfigHelper {
 		$this->l = $l;
 		$this->defaults = $defaults;
 		$this->appManager = $appManager;
+		$this->session = $session;
 		$this->currentUser = $currentUser;
 		$this->config = $config;
 		$this->groupManager = $groupManager;
@@ -119,6 +126,16 @@ class JSConfigHelper {
 			$dataLocation = false;
 		}
 
+		if ($this->currentUser instanceof IUser) {
+			$lastConfirmTimestamp = $this->currentUser->getLastLogin();
+			$sessionTime = $this->session->get('last-password-confirm');
+			if (is_int($sessionTime)) {
+				$lastConfirmTimestamp = $sessionTime;
+			}
+		} else {
+			$lastConfirmTimestamp = 0;
+		}
+
 		$array = [
 			"oc_debug" => $this->config->getSystemValue('debug', false) ? 'true' : 'false',
 			"oc_isadmin" => $this->groupManager->isAdmin($uid) ? 'true' : 'false',
@@ -126,6 +143,7 @@ class JSConfigHelper {
 			"oc_webroot" => "\"".\OC::$WEBROOT."\"",
 			"oc_appswebroots" =>  str_replace('\\/', '/', json_encode($apps_paths)), // Ugly unescape slashes waiting for better solution
 			"datepickerFormatDate" => json_encode($this->l->l('jsdate', null)),
+			'nc_lastLogin' => $lastConfirmTimestamp,
 			"dayNames" =>  json_encode([
 				(string)$this->l->t('Sunday'),
 				(string)$this->l->t('Monday'),
diff --git a/lib/private/TemplateLayout.php b/lib/private/TemplateLayout.php
index 7878737bdef..8919f14216e 100644
--- a/lib/private/TemplateLayout.php
+++ b/lib/private/TemplateLayout.php
@@ -148,6 +148,7 @@ class TemplateLayout extends \OC_Template {
 					\OC::$server->getL10N('core'),
 					\OC::$server->getThemingDefaults(),
 					\OC::$server->getAppManager(),
+					\OC::$server->getSession(),
 					\OC::$server->getUserSession()->getUser(),
 					\OC::$server->getConfig(),
 					\OC::$server->getGroupManager(),
-- 
GitLab