From b9720703e8afa26fd42d1bb7cc8fbf54ba2eeeae Mon Sep 17 00:00:00 2001
From: Christoph Wurst <christoph@winzerhof-wurst.at>
Date: Mon, 26 Feb 2018 17:13:23 +0100
Subject: [PATCH] Add CSRF token controller to retrieve the current CSRF token

Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>
---
 core/Controller/CSRFTokenController.php       | 63 ++++++++++++++++
 core/js/js.js                                 | 37 +++++-----
 core/js/tests/specs/coreSpec.js               | 12 ++--
 core/routes.php                               |  6 +-
 .../Controller/CSRFTokenControllerTest.php    | 71 +++++++++++++++++++
 5 files changed, 157 insertions(+), 32 deletions(-)
 create mode 100644 core/Controller/CSRFTokenController.php
 create mode 100644 tests/Core/Controller/CSRFTokenControllerTest.php

diff --git a/core/Controller/CSRFTokenController.php b/core/Controller/CSRFTokenController.php
new file mode 100644
index 00000000000..24888e2179f
--- /dev/null
+++ b/core/Controller/CSRFTokenController.php
@@ -0,0 +1,63 @@
+<?php
+declare(strict_types=1);
+
+/**
+ * @copyright 2017 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2017 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OC\Core\Controller;
+
+use OC\Security\CSRF\CsrfTokenManager;
+use OCP\AppFramework\Controller;
+use OCP\AppFramework\Http\JSONResponse;
+use OCP\IRequest;
+
+class CSRFTokenController extends Controller {
+
+	/** @var CsrfTokenManager */
+	private $tokenManager;
+
+	/**
+	 * @param string $appName
+	 * @param IRequest $request
+	 * @param CsrfTokenManager $tokenManager
+	 */
+	public function __construct(string $appName, IRequest $request,
+		CsrfTokenManager $tokenManager) {
+		parent::__construct($appName, $request);
+		$this->tokenManager = $tokenManager;
+	}
+
+	/**
+	 * @NoAdminRequired
+	 * @NoCSRFRequired
+	 * @PublicPage
+	 * @return JSONResponse
+	 */
+	public function index(): JSONResponse {
+		$requestToken = $this->tokenManager->getToken();
+
+		return new JSONResponse([
+			'token' => $requestToken->getEncryptedValue(),
+		]);
+	}
+
+}
diff --git a/core/js/js.js b/core/js/js.js
index 3c6ababf764..26dbbdb6e63 100644
--- a/core/js/js.js
+++ b/core/js/js.js
@@ -1366,34 +1366,29 @@ function initCore() {
 	});
 
 	/**
-	 * Calls the server periodically to ensure that session doesn't
-	 * time out
+	 * Calls the server periodically to ensure that session and CSRF
+	 * token doesn't expire
 	 */
-	function initSessionHeartBeat(){
-		// max interval in seconds set to 24 hours
-		var maxInterval = 24 * 3600;
+	function initSessionHeartBeat() {
 		// interval in seconds
 		var interval = 900;
 		if (oc_config.session_lifetime) {
 			interval = Math.floor(oc_config.session_lifetime / 2);
 		}
 		// minimum one minute
-		if (interval < 60) {
-			interval = 60;
-		}
-		if (interval > maxInterval) {
-			interval = maxInterval;
-		}
-		var url = OC.generateUrl('/heartbeat');
-		var heartBeatTimeout = null;
-		var heartBeat = function() {
-			clearInterval(heartBeatTimeout);
-			heartBeatTimeout = setInterval(function() {
-				$.post(url);
-			}, interval * 1000);
-		};
-		$(document).ajaxComplete(heartBeat);
-		heartBeat();
+		interval = Math.max(60, interval);
+		// max interval in seconds set to 24 hours
+		interval = Math.min(24 * 3600, interval);
+
+		var url = OC.generateUrl('/csrftoken');
+		setInterval(function() {
+			$.ajax(url).then(function(resp) {
+				oc_requesttoken = resp.token;
+				OC.requestToken = resp.token;
+			}).fail(function(e) {
+				console.error('session heartbeat failed', e);
+			});
+		}, interval * 1000);
 	}
 
 	// session heartbeat (defaults to enabled)
diff --git a/core/js/tests/specs/coreSpec.js b/core/js/tests/specs/coreSpec.js
index 616e7509f7c..6766fc2789c 100644
--- a/core/js/tests/specs/coreSpec.js
+++ b/core/js/tests/specs/coreSpec.js
@@ -351,14 +351,14 @@ describe('Core base tests', function() {
 		beforeEach(function() {
 			clock = sinon.useFakeTimers();
 			oldConfig = window.oc_config;
-			routeStub = sinon.stub(OC, 'generateUrl').returns('/heartbeat');
+			routeStub = sinon.stub(OC, 'generateUrl').returns('/csrftoken');
 			counter = 0;
 
 			fakeServer.autoRespond = true;
 			fakeServer.autoRespondAfter = 0;
-			fakeServer.respondWith(/\/heartbeat/, function(xhr) {
+			fakeServer.respondWith(/\/csrftoken/, function(xhr) {
 				counter++;
-				xhr.respond(200, {'Content-Type': 'application/json'}, '{}');
+				xhr.respond(200, {'Content-Type': 'application/json'}, '{"token": "pgBEsb3MzTb1ZPd2mfDZbQ6/0j3OrXHMEZrghHcOkg8=:3khw5PSa+wKQVo4f26exFD3nplud9ECjJ8/Y5zk5/k4="}');
 			});
 			$(document).off('ajaxComplete'); // ignore previously registered heartbeats
 		});
@@ -377,7 +377,7 @@ describe('Core base tests', function() {
 				session_lifetime: 300
 			};
 			window.initCore();
-			expect(routeStub.calledWith('/heartbeat')).toEqual(true);
+			expect(routeStub.calledWith('/csrftoken')).toEqual(true);
 
 			expect(counter).toEqual(0);
 
@@ -502,8 +502,8 @@ describe('Core base tests', function() {
 	});
 	describe('Generate Url', function() {
 		it('returns absolute urls', function() {
-			expect(OC.generateUrl('heartbeat')).toEqual(OC.webroot + '/index.php/heartbeat');
-			expect(OC.generateUrl('/heartbeat')).toEqual(OC.webroot + '/index.php/heartbeat');
+			expect(OC.generateUrl('csrftoken')).toEqual(OC.webroot + '/index.php/csrftoken');
+			expect(OC.generateUrl('/csrftoken')).toEqual(OC.webroot + '/index.php/csrftoken');
 		});
 		it('substitutes parameters which are escaped by default', function() {
 			expect(OC.generateUrl('apps/files/download/{file}', {file: '<">ImAnUnescapedString/!'})).toEqual(OC.webroot + '/index.php/apps/files/download/%3C%22%3EImAnUnescapedString%2F!');
diff --git a/core/routes.php b/core/routes.php
index d357fd45f96..eb6db1046fb 100644
--- a/core/routes.php
+++ b/core/routes.php
@@ -46,6 +46,7 @@ $application->registerRoutes($this, [
 		['name' => 'avatar#postCroppedAvatar', 'url' => '/avatar/cropped', 'verb' => 'POST'],
 		['name' => 'avatar#getTmpAvatar', 'url' => '/avatar/tmp', 'verb' => 'GET'],
 		['name' => 'avatar#postAvatar', 'url' => '/avatar/', 'verb' => 'POST'],
+		['name' => 'CSRFToken#index', 'url' => '/csrftoken', 'verb' => 'GET'],
 		['name' => 'login#tryLogin', 'url' => '/login', 'verb' => 'POST'],
 		['name' => 'login#confirmPassword', 'url' => '/login/confirm', 'verb' => 'POST'],
 		['name' => 'login#showLoginForm', 'url' => '/login', 'verb' => 'GET'],
@@ -148,8 +149,3 @@ $this->create('files_sharing.publicpreview.directLink', '/s/{token}/preview')->g
 		throw new \OC\HintException('App file sharing is not enabled');
 	}
 });
-
-// used for heartbeat
-$this->create('heartbeat', '/heartbeat')->action(function(){
-	// do nothing
-});
diff --git a/tests/Core/Controller/CSRFTokenControllerTest.php b/tests/Core/Controller/CSRFTokenControllerTest.php
new file mode 100644
index 00000000000..a97f2eabc27
--- /dev/null
+++ b/tests/Core/Controller/CSRFTokenControllerTest.php
@@ -0,0 +1,71 @@
+<?php
+
+/**
+ * @copyright 2017 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2017 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace Tests\Core\Controller;
+
+use OC\Core\Controller\CSRFTokenController;
+use OC\Security\CSRF\CsrfToken;
+use OC\Security\CSRF\CsrfTokenManager;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\JSONResponse;
+use OCP\IRequest;
+use PHPUnit_Framework_MockObject_MockObject;
+use Test\TestCase;
+
+class CSRFTokenControllerTest extends TestCase {
+
+	/** @var CSRFTokenController */
+	private $controller;
+
+	/** @var IRequest|PHPUnit_Framework_MockObject_MockObject */
+	private $request;
+
+	/** @var CsrfTokenManager|PHPUnit_Framework_MockObject_MockObject */
+	private $tokenManager;
+
+	protected function setUp() {
+		parent::setUp();
+
+		$this->request = $this->createMock(IRequest::class);
+		$this->tokenManager = $this->createMock(CsrfTokenManager::class);
+
+		$this->controller = new CSRFTokenController('core', $this->request,
+			$this->tokenManager);
+	}
+
+	public function testGetToken() {
+		$token = $this->createMock(CsrfToken::class);
+		$this->tokenManager->method('getToken')->willReturn($token);
+		$token->method('getEncryptedValue')->willReturn('toktok123');
+
+		$response = $this->controller->index();
+
+		$this->assertInstanceOf(JSONResponse::class, $response);
+		$this->assertSame(Http::STATUS_OK, $response->getStatus());
+		$this->assertEquals([
+			'token' => 'toktok123'
+			], $response->getData());
+	}
+
+}
-- 
GitLab