diff --git a/core/Controller/CSRFTokenController.php b/core/Controller/CSRFTokenController.php new file mode 100644 index 0000000000..24888e2179 --- /dev/null +++ b/core/Controller/CSRFTokenController.php @@ -0,0 +1,63 @@ + + * + * @author 2017 Christoph Wurst + * + * @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 . + * + */ + +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 3c6ababf76..26dbbdb6e6 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 616e7509f7..6766fc2789 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 d357fd45f9..eb6db1046f 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 0000000000..a97f2eabc2 --- /dev/null +++ b/tests/Core/Controller/CSRFTokenControllerTest.php @@ -0,0 +1,71 @@ + + * + * @author 2017 Christoph Wurst + * + * @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 . + * + */ + +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()); + } + +}