diff --git a/lib/private/Authentication/Token/DefaultTokenProvider.php b/lib/private/Authentication/Token/DefaultTokenProvider.php index 6c69d852d7..3527f4155a 100644 --- a/lib/private/Authentication/Token/DefaultTokenProvider.php +++ b/lib/private/Authentication/Token/DefaultTokenProvider.php @@ -134,6 +134,7 @@ class DefaultTokenProvider implements IProvider { /** * @param IToken $savedToken * @param string $tokenId session token + * @throws InvalidTokenException * @return string */ public function getPassword(IToken $savedToken, $tokenId) { @@ -203,6 +204,7 @@ class DefaultTokenProvider implements IProvider { * * @param string $password * @param string $token + * @throws InvalidTokenException * @return string the decrypted key */ private function decryptPassword($password, $token) { diff --git a/lib/private/Authentication/Token/IProvider.php b/lib/private/Authentication/Token/IProvider.php index a5c5faa563..b8648dda5b 100644 --- a/lib/private/Authentication/Token/IProvider.php +++ b/lib/private/Authentication/Token/IProvider.php @@ -35,7 +35,7 @@ interface IProvider { * @param string $password * @param string $name * @param int $type token type - * @return DefaultToken + * @return IToken */ public function generateToken($token, $uid, $password, $name, $type = IToken::TEMPORARY_TOKEN); @@ -85,6 +85,7 @@ interface IProvider { * * @param IToken $token * @param string $tokenId + * @throws InvalidTokenException * @return string */ public function getPassword(IToken $token, $tokenId); diff --git a/settings/Application.php b/settings/Application.php index 7069fc9c35..728c2bf9de 100644 --- a/settings/Application.php +++ b/settings/Application.php @@ -104,6 +104,8 @@ class Application extends App { $c->query('Request'), $c->query('ServerContainer')->query('OC\Authentication\Token\IProvider'), $c->query('UserManager'), + $c->query('ServerContainer')->getSession(), + $c->query('ServerContainer')->getSecureRandom(), $c->query('UserId') ); }); diff --git a/settings/Controller/AuthSettingsController.php b/settings/Controller/AuthSettingsController.php index 1d874193d3..71868b7688 100644 --- a/settings/Controller/AuthSettingsController.php +++ b/settings/Controller/AuthSettingsController.php @@ -22,41 +22,56 @@ namespace OC\Settings\Controller; +use OC\AppFramework\Http; +use OC\Authentication\Exceptions\InvalidTokenException; use OC\Authentication\Token\IProvider; +use OC\Authentication\Token\IToken; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\JSONResponse; use OCP\IRequest; +use OCP\ISession; use OCP\IUserManager; +use OCP\Security\ISecureRandom; +use OCP\Session\Exceptions\SessionNotAvailableException; class AuthSettingsController extends Controller { /** @var IProvider */ private $tokenProvider; - /** - * @var IUserManager - */ + /** @var IUserManager */ private $userManager; + /** @var ISession */ + private $session; + /** @var string */ private $uid; + /** @var ISecureRandom */ + private $random; + /** * @param string $appName * @param IRequest $request * @param IProvider $tokenProvider * @param IUserManager $userManager + * @param ISession $session + * @param ISecureRandom $random * @param string $uid */ - public function __construct($appName, IRequest $request, IProvider $tokenProvider, IUserManager $userManager, $uid) { + public function __construct($appName, IRequest $request, IProvider $tokenProvider, IUserManager $userManager, ISession $session, ISecureRandom $random, $uid) { parent::__construct($appName, $request); $this->tokenProvider = $tokenProvider; $this->userManager = $userManager; $this->uid = $uid; + $this->session = $session; + $this->random = $random; } /** * @NoAdminRequired + * @NoSubadminRequired * * @return JSONResponse */ @@ -68,4 +83,52 @@ class AuthSettingsController extends Controller { return $this->tokenProvider->getTokenByUser($user); } + /** + * @NoAdminRequired + * @NoSubadminRequired + * + * @return JSONResponse + */ + public function create($name) { + try { + $sessionId = $this->session->getId(); + } catch (SessionNotAvailableException $ex) { + $resp = new JSONResponse(); + $resp->setStatus(Http::STATUS_SERVICE_UNAVAILABLE); + return $resp; + } + + try { + $sessionToken = $this->tokenProvider->getToken($sessionId); + $password = $this->tokenProvider->getPassword($sessionToken, $sessionId); + } catch (InvalidTokenException $ex) { + $resp = new JSONResponse(); + $resp->setStatus(Http::STATUS_SERVICE_UNAVAILABLE); + return $resp; + } + + $token = $this->generateRandomDeviceToken(); + $deviceToken = $this->tokenProvider->generateToken($token, $this->uid, $password, $name, IToken::PERMANENT_TOKEN); + + return [ + 'token' => $token, + 'deviceToken' => $deviceToken + ]; + } + + /** + * Return a 20 digit device password + * + * Example: ABCDE-FGHIJ-KLMNO-PQRST + * + * @return string + */ + private function generateRandomDeviceToken() { + $groups = []; + for ($i = 0; $i < 4; $i++) { + $groups[] = $this->random->generate(5, implode('', range('A', 'Z'))); + } + return implode('-', $groups); + } + } diff --git a/settings/css/settings.css b/settings/css/settings.css index be61265935..418c5f9551 100644 --- a/settings/css/settings.css +++ b/settings/css/settings.css @@ -100,10 +100,6 @@ input#identity { table.nostyle label { margin-right: 2em; } table.nostyle td { padding: 0.2em 0; } -#sessions, -#devices { - min-height: 180px; -} #sessions table, #devices table { width: 100%; @@ -114,6 +110,24 @@ table.nostyle td { padding: 0.2em 0; } #devices table th { font-weight: 800; } +#sessions table th, +#sessions table td, +#devices table th, +#devices table td { + padding: 10px; +} + +#sessions .token-list td, +#devices .token-list td { + border-top: 1px solid #DDD; +} + +#device-new-token { + padding: 10px; + font-family: monospace; + font-size: 1.4em; + background-color: lightyellow; +} /* USERS */ #newgroup-init a span { margin-left: 20px; } diff --git a/settings/js/authtoken_view.js b/settings/js/authtoken_view.js index 0ca1682123..8ca38d80d8 100644 --- a/settings/js/authtoken_view.js +++ b/settings/js/authtoken_view.js @@ -1,4 +1,4 @@ -/* global Backbone, Handlebars */ +/* global Backbone, Handlebars, moment */ /** * @author Christoph Wurst @@ -20,16 +20,16 @@ * */ -(function(OC, _, Backbone, $, Handlebars) { +(function(OC, _, Backbone, $, Handlebars, moment) { 'use strict'; OC.Settings = OC.Settings || {}; var TEMPLATE_TOKEN = - '' - + '{{name}}' - + '{{lastActivity}}' - + ''; + '' + + '{{name}}' + + '{{lastActivity}}' + + ''; var SubView = Backbone.View.extend({ collection: null, @@ -46,48 +46,115 @@ var tokens = this.collection.filter(function(token) { return parseInt(token.get('type')) === _this.type; }); - list.removeClass('icon-loading'); list.html(''); tokens.forEach(function(token) { - var html = _this.template(token.toJSON()); + var viewData = token.toJSON(); + viewData.lastActivity = moment(viewData.lastActivity, 'X'). + format('LLL'); + var html = _this.template(viewData); list.append(html); }); }, + toggleLoading: function(state) { + this.$el.find('.token-list').toggleClass('icon-loading', state); + } }); var AuthTokenView = Backbone.View.extend({ collection: null, - views - : [], + _views: [], + _form: undefined, + _tokenName: undefined, + _addTokenBtn: undefined, + _result: undefined, + _newToken: undefined, + _hideTokenBtn: undefined, + _addingToken: false, initialize: function(options) { this.collection = options.collection; var tokenTypes = [0, 1]; var _this = this; _.each(tokenTypes, function(type) { - _this.views.push(new SubView({ + _this._views.push(new SubView({ el: type === 0 ? '#sessions' : '#devices', type: type, collection: _this.collection })); }); + + this._form = $('#device-token-form'); + this._tokenName = $('#device-token-name'); + this._addTokenBtn = $('#device-add-token'); + this._addTokenBtn.click(_.bind(this._addDeviceToken, this)); + + this._result = $('#device-token-result'); + this._newToken = $('#device-new-token'); + this._hideTokenBtn = $('#device-token-hide'); + this._hideTokenBtn.click(_.bind(this._hideToken, this)); }, render: function() { - _.each(this.views, function(view) { + _.each(this._views, function(view) { view.render(); + view.toggleLoading(false); }); }, reload: function() { + var _this = this; + + _.each(this._views, function(view) { + view.toggleLoading(true); + }); + var loadingTokens = this.collection.fetch(); - var _this = this; $.when(loadingTokens).done(function() { _this.render(); }); + $.when(loadingTokens).fail(function() { + OC.Notification.showTemporary(t('core', 'Error while loading browser sessions and device tokens')); + }); + }, + _addDeviceToken: function() { + var _this = this; + this._toggleAddingToken(true); + + var deviceName = this._tokenName.val(); + var creatingToken = $.ajax(OC.generateUrl('/settings/personal/authtokens'), { + method: 'POST', + data: { + name: deviceName + } + }); + + $.when(creatingToken).done(function(resp) { + _this.collection.add(resp.deviceToken); + _this.render(); + _this._newToken.text(resp.token); + _this._toggleFormResult(false); + _this._tokenName.val(''); + }); + $.when(creatingToken).fail(function() { + OC.Notification.showTemporary(t('core', 'Error while creating device token')); + }); + $.when(creatingToken).always(function() { + _this._toggleAddingToken(false); + }); + }, + _hideToken: function() { + this._toggleFormResult(true); + }, + _toggleAddingToken: function(state) { + this._addingToken = state; + this._addTokenBtn.toggleClass('icon-loading-small', state); + }, + _toggleFormResult: function(showForm) { + this._form.toggleClass('hidden', !showForm); + this._result.toggleClass('hidden', showForm); } }); OC.Settings.AuthTokenView = AuthTokenView; -})(OC, _, Backbone, $, Handlebars); +})(OC, _, Backbone, $, Handlebars, moment); diff --git a/settings/templates/personal.php b/settings/templates/personal.php index a7e86b50a5..4f8d564f54 100644 --- a/settings/templates/personal.php +++ b/settings/templates/personal.php @@ -147,6 +147,7 @@ if($_['passwordChangeSupported']) { Browser Most recent activity + @@ -162,11 +163,21 @@ if($_['passwordChangeSupported']) { Name Most recent activity + +

t('A device password is a passcode that gives an app or device permissions to access your ownCloud account.'));?>

+
+ + +
+
diff --git a/tests/settings/controller/AuthSettingsControllerTest.php b/tests/settings/controller/AuthSettingsControllerTest.php index d236f9f5eb..3b46a2caa2 100644 --- a/tests/settings/controller/AuthSettingsControllerTest.php +++ b/tests/settings/controller/AuthSettingsControllerTest.php @@ -22,7 +22,12 @@ namespace Test\Settings\Controller; +use OC\AppFramework\Http; +use OC\Authentication\Exceptions\InvalidTokenException; +use OC\Authentication\Token\IToken; use OC\Settings\Controller\AuthSettingsController; +use OCP\AppFramework\Http\JSONResponse; +use OCP\Session\Exceptions\SessionNotAvailableException; use Test\TestCase; class AuthSettingsControllerTest extends TestCase { @@ -32,6 +37,8 @@ class AuthSettingsControllerTest extends TestCase { private $request; private $tokenProvider; private $userManager; + private $session; + private $secureRandom; private $uid; protected function setUp() { @@ -40,10 +47,12 @@ class AuthSettingsControllerTest extends TestCase { $this->request = $this->getMock('\OCP\IRequest'); $this->tokenProvider = $this->getMock('\OC\Authentication\Token\IProvider'); $this->userManager = $this->getMock('\OCP\IUserManager'); + $this->session = $this->getMock('\OCP\ISession'); + $this->secureRandom = $this->getMock('\OCP\Security\ISecureRandom'); $this->uid = 'jane'; $this->user = $this->getMock('\OCP\IUser'); - $this->controller = new AuthSettingsController('core', $this->request, $this->tokenProvider, $this->userManager, $this->uid); + $this->controller = new AuthSettingsController('core', $this->request, $this->tokenProvider, $this->userManager, $this->session, $this->secureRandom, $this->uid); } public function testIndex() { @@ -63,4 +72,70 @@ class AuthSettingsControllerTest extends TestCase { $this->assertEquals($result, $this->controller->index()); } + public function testCreate() { + $name = 'Nexus 4'; + $sessionToken = $this->getMock('\OC\Authentication\Token\IToken'); + $deviceToken = $this->getMock('\OC\Authentication\Token\IToken'); + $password = '123456'; + + $this->session->expects($this->once()) + ->method('getId') + ->will($this->returnValue('sessionid')); + $this->tokenProvider->expects($this->once()) + ->method('getToken') + ->with('sessionid') + ->will($this->returnValue($sessionToken)); + $this->tokenProvider->expects($this->once()) + ->method('getPassword') + ->with($sessionToken, 'sessionid') + ->will($this->returnValue($password)); + + $this->secureRandom->expects($this->exactly(4)) + ->method('generate') + ->with(5, implode('', range('A', 'Z'))) + ->will($this->returnValue('XXXXX')); + $newToken = 'XXXXX-XXXXX-XXXXX-XXXXX'; + + $this->tokenProvider->expects($this->once()) + ->method('generateToken') + ->with($newToken, $this->uid, $password, $name, IToken::PERMANENT_TOKEN) + ->will($this->returnValue($deviceToken)); + + $expected = [ + 'token' => $newToken, + 'deviceToken' => $deviceToken, + ]; + $this->assertEquals($expected, $this->controller->create($name)); + } + + public function testCreateSessionNotAvailable() { + $name = 'personal phone'; + + $this->session->expects($this->once()) + ->method('getId') + ->will($this->throwException(new SessionNotAvailableException())); + + $expected = new JSONResponse(); + $expected->setStatus(Http::STATUS_SERVICE_UNAVAILABLE); + + $this->assertEquals($expected, $this->controller->create($name)); + } + + public function testCreateInvalidToken() { + $name = 'Company IPhone'; + + $this->session->expects($this->once()) + ->method('getId') + ->will($this->returnValue('sessionid')); + $this->tokenProvider->expects($this->once()) + ->method('getToken') + ->with('sessionid') + ->will($this->throwException(new InvalidTokenException())); + + $expected = new JSONResponse(); + $expected->setStatus(Http::STATUS_SERVICE_UNAVAILABLE); + + $this->assertEquals($expected, $this->controller->create($name)); + } + }