Add CSRF token controller to retrieve the current CSRF token
Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>
This commit is contained in:
parent
cccf6f4d5f
commit
b9720703e8
|
@ -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(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1366,34 +1366,29 @@ function initCore() {
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls the server periodically to ensure that session doesn't
|
* Calls the server periodically to ensure that session and CSRF
|
||||||
* time out
|
* token doesn't expire
|
||||||
*/
|
*/
|
||||||
function initSessionHeartBeat(){
|
function initSessionHeartBeat() {
|
||||||
// max interval in seconds set to 24 hours
|
|
||||||
var maxInterval = 24 * 3600;
|
|
||||||
// interval in seconds
|
// interval in seconds
|
||||||
var interval = 900;
|
var interval = 900;
|
||||||
if (oc_config.session_lifetime) {
|
if (oc_config.session_lifetime) {
|
||||||
interval = Math.floor(oc_config.session_lifetime / 2);
|
interval = Math.floor(oc_config.session_lifetime / 2);
|
||||||
}
|
}
|
||||||
// minimum one minute
|
// minimum one minute
|
||||||
if (interval < 60) {
|
interval = Math.max(60, interval);
|
||||||
interval = 60;
|
// max interval in seconds set to 24 hours
|
||||||
}
|
interval = Math.min(24 * 3600, interval);
|
||||||
if (interval > maxInterval) {
|
|
||||||
interval = maxInterval;
|
var url = OC.generateUrl('/csrftoken');
|
||||||
}
|
setInterval(function() {
|
||||||
var url = OC.generateUrl('/heartbeat');
|
$.ajax(url).then(function(resp) {
|
||||||
var heartBeatTimeout = null;
|
oc_requesttoken = resp.token;
|
||||||
var heartBeat = function() {
|
OC.requestToken = resp.token;
|
||||||
clearInterval(heartBeatTimeout);
|
}).fail(function(e) {
|
||||||
heartBeatTimeout = setInterval(function() {
|
console.error('session heartbeat failed', e);
|
||||||
$.post(url);
|
});
|
||||||
}, interval * 1000);
|
}, interval * 1000);
|
||||||
};
|
|
||||||
$(document).ajaxComplete(heartBeat);
|
|
||||||
heartBeat();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// session heartbeat (defaults to enabled)
|
// session heartbeat (defaults to enabled)
|
||||||
|
|
|
@ -351,14 +351,14 @@ describe('Core base tests', function() {
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
clock = sinon.useFakeTimers();
|
clock = sinon.useFakeTimers();
|
||||||
oldConfig = window.oc_config;
|
oldConfig = window.oc_config;
|
||||||
routeStub = sinon.stub(OC, 'generateUrl').returns('/heartbeat');
|
routeStub = sinon.stub(OC, 'generateUrl').returns('/csrftoken');
|
||||||
counter = 0;
|
counter = 0;
|
||||||
|
|
||||||
fakeServer.autoRespond = true;
|
fakeServer.autoRespond = true;
|
||||||
fakeServer.autoRespondAfter = 0;
|
fakeServer.autoRespondAfter = 0;
|
||||||
fakeServer.respondWith(/\/heartbeat/, function(xhr) {
|
fakeServer.respondWith(/\/csrftoken/, function(xhr) {
|
||||||
counter++;
|
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
|
$(document).off('ajaxComplete'); // ignore previously registered heartbeats
|
||||||
});
|
});
|
||||||
|
@ -377,7 +377,7 @@ describe('Core base tests', function() {
|
||||||
session_lifetime: 300
|
session_lifetime: 300
|
||||||
};
|
};
|
||||||
window.initCore();
|
window.initCore();
|
||||||
expect(routeStub.calledWith('/heartbeat')).toEqual(true);
|
expect(routeStub.calledWith('/csrftoken')).toEqual(true);
|
||||||
|
|
||||||
expect(counter).toEqual(0);
|
expect(counter).toEqual(0);
|
||||||
|
|
||||||
|
@ -502,8 +502,8 @@ describe('Core base tests', function() {
|
||||||
});
|
});
|
||||||
describe('Generate Url', function() {
|
describe('Generate Url', function() {
|
||||||
it('returns absolute urls', function() {
|
it('returns absolute urls', function() {
|
||||||
expect(OC.generateUrl('heartbeat')).toEqual(OC.webroot + '/index.php/heartbeat');
|
expect(OC.generateUrl('csrftoken')).toEqual(OC.webroot + '/index.php/csrftoken');
|
||||||
expect(OC.generateUrl('/heartbeat')).toEqual(OC.webroot + '/index.php/heartbeat');
|
expect(OC.generateUrl('/csrftoken')).toEqual(OC.webroot + '/index.php/csrftoken');
|
||||||
});
|
});
|
||||||
it('substitutes parameters which are escaped by default', function() {
|
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!');
|
expect(OC.generateUrl('apps/files/download/{file}', {file: '<">ImAnUnescapedString/!'})).toEqual(OC.webroot + '/index.php/apps/files/download/%3C%22%3EImAnUnescapedString%2F!');
|
||||||
|
|
|
@ -46,6 +46,7 @@ $application->registerRoutes($this, [
|
||||||
['name' => 'avatar#postCroppedAvatar', 'url' => '/avatar/cropped', 'verb' => 'POST'],
|
['name' => 'avatar#postCroppedAvatar', 'url' => '/avatar/cropped', 'verb' => 'POST'],
|
||||||
['name' => 'avatar#getTmpAvatar', 'url' => '/avatar/tmp', 'verb' => 'GET'],
|
['name' => 'avatar#getTmpAvatar', 'url' => '/avatar/tmp', 'verb' => 'GET'],
|
||||||
['name' => 'avatar#postAvatar', 'url' => '/avatar/', 'verb' => 'POST'],
|
['name' => 'avatar#postAvatar', 'url' => '/avatar/', 'verb' => 'POST'],
|
||||||
|
['name' => 'CSRFToken#index', 'url' => '/csrftoken', 'verb' => 'GET'],
|
||||||
['name' => 'login#tryLogin', 'url' => '/login', 'verb' => 'POST'],
|
['name' => 'login#tryLogin', 'url' => '/login', 'verb' => 'POST'],
|
||||||
['name' => 'login#confirmPassword', 'url' => '/login/confirm', 'verb' => 'POST'],
|
['name' => 'login#confirmPassword', 'url' => '/login/confirm', 'verb' => 'POST'],
|
||||||
['name' => 'login#showLoginForm', 'url' => '/login', 'verb' => 'GET'],
|
['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');
|
throw new \OC\HintException('App file sharing is not enabled');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// used for heartbeat
|
|
||||||
$this->create('heartbeat', '/heartbeat')->action(function(){
|
|
||||||
// do nothing
|
|
||||||
});
|
|
||||||
|
|
|
@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue