Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/server.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--apps/dav/appinfo/v1/publicwebdav.php3
-rw-r--r--apps/dav/appinfo/v1/webdav.php10
-rw-r--r--apps/dav/lib/Connector/Sabre/Auth.php1
-rw-r--r--apps/dav/lib/Connector/Sabre/BearerAuth.php80
-rw-r--r--apps/dav/lib/Connector/Sabre/ServerFactory.php7
-rw-r--r--apps/dav/lib/Server.php8
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/BearerAuthTest.php88
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/RequestTest/RequestTestCase.php3
-rw-r--r--apps/oauth2/appinfo/database.xml100
-rw-r--r--apps/oauth2/appinfo/info.xml18
-rw-r--r--apps/oauth2/appinfo/routes.php45
-rw-r--r--apps/oauth2/css/setting-admin.css5
-rw-r--r--apps/oauth2/js/setting-admin.js15
-rw-r--r--apps/oauth2/lib/Controller/LoginRedirectorController.php79
-rw-r--r--apps/oauth2/lib/Controller/OauthApiController.php88
-rw-r--r--apps/oauth2/lib/Controller/SettingsController.php100
-rw-r--r--apps/oauth2/lib/Db/AccessToken.php53
-rw-r--r--apps/oauth2/lib/Db/AccessTokenMapper.php70
-rw-r--r--apps/oauth2/lib/Db/Client.php53
-rw-r--r--apps/oauth2/lib/Db/ClientMapper.php89
-rw-r--r--apps/oauth2/lib/Exceptions/AccessTokenNotFoundException.php24
-rw-r--r--apps/oauth2/lib/Exceptions/ClientNotFoundException.php24
-rw-r--r--apps/oauth2/lib/Settings/Admin.php66
-rw-r--r--apps/oauth2/templates/admin.php76
-rw-r--r--apps/oauth2/tests/Controller/LoginRedirectorControllerTest.php91
-rw-r--r--apps/oauth2/tests/Controller/OauthApiControllerTest.php106
-rw-r--r--apps/oauth2/tests/Controller/SettingsControllerTest.php139
-rw-r--r--apps/oauth2/tests/Db/AccessTokenMapperTest.php70
-rw-r--r--apps/oauth2/tests/Db/ClientMapperTest.php79
-rw-r--r--apps/oauth2/tests/Settings/AdminTest.php66
-rw-r--r--apps/theming/lib/Controller/IconController.php50
-rw-r--r--apps/theming/lib/IconBuilder.php32
-rw-r--r--apps/theming/tests/Controller/IconControllerTest.php40
-rw-r--r--build/integration/features/auth.feature8
-rw-r--r--build/integration/features/bootstrap/Auth.php4
-rw-r--r--build/integration/features/provisioning-v1.feature1
-rw-r--r--build/integration/features/webdav-related.feature4
-rw-r--r--core/Controller/ClientFlowLoginController.php101
-rw-r--r--core/css/login/authpicker.css2
-rw-r--r--core/templates/loginflow/authpicker.php4
-rw-r--r--core/templates/loginflow/redirect.php2
-rw-r--r--lib/composer/composer/autoload_classmap.php1
-rw-r--r--lib/composer/composer/autoload_static.php1
-rw-r--r--lib/private/Authentication/Token/DefaultTokenMapper.php12
-rw-r--r--lib/private/Files/Cache/Scanner.php3
-rw-r--r--lib/private/Files/Cache/Wrapper/CachePermissionsMask.php1
-rw-r--r--lib/private/Files/Storage/Wrapper/PermissionsMask.php8
-rw-r--r--lib/private/Repair.php3
-rw-r--r--lib/private/Repair/NC11/MoveAvatarsBackgroundJob.php72
-rw-r--r--lib/private/Repair/Owncloud/SaveAccountsTableData.php176
-rw-r--r--lib/private/Updater.php19
-rw-r--r--lib/private/User/Session.php4
-rw-r--r--settings/personal.php2
-rw-r--r--tests/Core/Controller/ClientFlowLoginControllerTest.php200
-rw-r--r--tests/lib/Authentication/Token/DefaultTokenMapperTest.php15
-rw-r--r--tests/lib/Files/Storage/Wrapper/PermissionsMaskTest.php47
-rw-r--r--tests/lib/User/SessionTest.php2
-rw-r--r--tests/phpunit-autotest.xml1
-rw-r--r--version.php1
60 files changed, 2368 insertions, 105 deletions
diff --git a/.gitignore b/.gitignore
index 6a8e6723376..9d9f09c2da9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,6 +24,7 @@
!/apps/files_versions
!/apps/lookup_server_connector
!/apps/user_ldap
+!/apps/oauth2
!/apps/provisioning_api
!/apps/systemtags
!/apps/testing
diff --git a/apps/dav/appinfo/v1/publicwebdav.php b/apps/dav/appinfo/v1/publicwebdav.php
index 95fb71032d5..3ef1c2e62a5 100644
--- a/apps/dav/appinfo/v1/publicwebdav.php
+++ b/apps/dav/appinfo/v1/publicwebdav.php
@@ -42,6 +42,7 @@ $authBackend = new OCA\DAV\Connector\PublicAuth(
\OC::$server->getShareManager(),
\OC::$server->getSession()
);
+$authPlugin = new \Sabre\DAV\Auth\Plugin($authBackend);
$serverFactory = new OCA\DAV\Connector\Sabre\ServerFactory(
\OC::$server->getConfig(),
@@ -59,7 +60,7 @@ $requestUri = \OC::$server->getRequest()->getRequestUri();
$linkCheckPlugin = new \OCA\DAV\Files\Sharing\PublicLinkCheckPlugin();
$filesDropPlugin = new \OCA\DAV\Files\Sharing\FilesDropPlugin();
-$server = $serverFactory->createServer($baseuri, $requestUri, $authBackend, function (\Sabre\DAV\Server $server) use ($authBackend, $linkCheckPlugin, $filesDropPlugin) {
+$server = $serverFactory->createServer($baseuri, $requestUri, $authPlugin, function (\Sabre\DAV\Server $server) use ($authBackend, $linkCheckPlugin, $filesDropPlugin) {
$isAjax = (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest');
$federatedSharingApp = new \OCA\FederatedFileSharing\AppInfo\Application();
$federatedShareProvider = $federatedSharingApp->getFederatedShareProvider();
diff --git a/apps/dav/appinfo/v1/webdav.php b/apps/dav/appinfo/v1/webdav.php
index 32f93b27760..a1ad4ab489d 100644
--- a/apps/dav/appinfo/v1/webdav.php
+++ b/apps/dav/appinfo/v1/webdav.php
@@ -52,9 +52,17 @@ $authBackend = new \OCA\DAV\Connector\Sabre\Auth(
\OC::$server->getBruteForceThrottler(),
'principals/'
);
+$authPlugin = new \Sabre\DAV\Auth\Plugin($authBackend);
+$bearerAuthPlugin = new \OCA\DAV\Connector\Sabre\BearerAuth(
+ \OC::$server->getUserSession(),
+ \OC::$server->getSession(),
+ \OC::$server->getRequest()
+);
+$authPlugin->addBackend($bearerAuthPlugin);
+
$requestUri = \OC::$server->getRequest()->getRequestUri();
-$server = $serverFactory->createServer($baseuri, $requestUri, $authBackend, function() {
+$server = $serverFactory->createServer($baseuri, $requestUri, $authPlugin, function() {
// use the view for the logged in user
return \OC\Files\Filesystem::getView();
});
diff --git a/apps/dav/lib/Connector/Sabre/Auth.php b/apps/dav/lib/Connector/Sabre/Auth.php
index bdaf73d46e7..9147e79594c 100644
--- a/apps/dav/lib/Connector/Sabre/Auth.php
+++ b/apps/dav/lib/Connector/Sabre/Auth.php
@@ -210,6 +210,7 @@ class Auth extends AbstractBasic {
*/
private function auth(RequestInterface $request, ResponseInterface $response) {
$forcedLogout = false;
+
if(!$this->request->passesCSRFCheck() &&
$this->requiresCSRFCheck()) {
// In case of a fail with POST we need to recheck the credentials
diff --git a/apps/dav/lib/Connector/Sabre/BearerAuth.php b/apps/dav/lib/Connector/Sabre/BearerAuth.php
new file mode 100644
index 00000000000..f0e0f389c33
--- /dev/null
+++ b/apps/dav/lib/Connector/Sabre/BearerAuth.php
@@ -0,0 +1,80 @@
+<?php
+/**
+ * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch>
+ *
+ * @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 OCA\DAV\Connector\Sabre;
+
+use OCP\IRequest;
+use OCP\ISession;
+use OCP\IUserSession;
+use Sabre\DAV\Auth\Backend\AbstractBearer;
+
+class BearerAuth extends AbstractBearer {
+ /** @var IUserSession */
+ private $userSession;
+ /** @var ISession */
+ private $session;
+ /** @var IRequest */
+ private $request;
+ /** @var string */
+ private $principalPrefix;
+
+ /**
+ * @param IUserSession $userSession
+ * @param ISession $session
+ * @param string $principalPrefix
+ * @param IRequest $request
+ */
+ public function __construct(IUserSession $userSession,
+ ISession $session,
+ IRequest $request,
+ $principalPrefix = 'principals/users/') {
+ $this->userSession = $userSession;
+ $this->session = $session;
+ $this->request = $request;
+ $this->principalPrefix = $principalPrefix;
+
+ // setup realm
+ $defaults = new \OCP\Defaults();
+ $this->realm = $defaults->getName();
+ }
+
+ private function setupUserFs($userId) {
+ \OC_Util::setupFS($userId);
+ $this->session->close();
+ return $this->principalPrefix . $userId;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateBearerToken($bearerToken) {
+ \OC_Util::setupFS();
+
+ if(!$this->userSession->isLoggedIn()) {
+ $this->userSession->tryTokenLogin($this->request);
+ }
+ if($this->userSession->isLoggedIn()) {
+ return $this->setupUserFs($this->userSession->getUser()->getUID());
+ }
+
+ return false;
+ }
+}
diff --git a/apps/dav/lib/Connector/Sabre/ServerFactory.php b/apps/dav/lib/Connector/Sabre/ServerFactory.php
index f04362dfc08..329aa335ea4 100644
--- a/apps/dav/lib/Connector/Sabre/ServerFactory.php
+++ b/apps/dav/lib/Connector/Sabre/ServerFactory.php
@@ -40,6 +40,7 @@ use OCP\IRequest;
use OCP\ITagManager;
use OCP\IUserSession;
use Sabre\DAV\Auth\Backend\BackendInterface;
+use Sabre\DAV\Auth\Plugin;
class ServerFactory {
/** @var IConfig */
@@ -92,13 +93,13 @@ class ServerFactory {
/**
* @param string $baseUri
* @param string $requestUri
- * @param BackendInterface $authBackend
+ * @param Plugin $authPlugin
* @param callable $viewCallBack callback that should return the view for the dav endpoint
* @return Server
*/
public function createServer($baseUri,
$requestUri,
- BackendInterface $authBackend,
+ Plugin $authPlugin,
callable $viewCallBack) {
// Fire up server
$objectTree = new \OCA\DAV\Connector\Sabre\ObjectTree();
@@ -110,7 +111,7 @@ class ServerFactory {
// Load plugins
$server->addPlugin(new \OCA\DAV\Connector\Sabre\MaintenancePlugin($this->config));
$server->addPlugin(new \OCA\DAV\Connector\Sabre\BlockLegacyClientPlugin($this->config));
- $server->addPlugin(new \Sabre\DAV\Auth\Plugin($authBackend));
+ $server->addPlugin($authPlugin);
// FIXME: The following line is a workaround for legacy components relying on being able to send a GET to /
$server->addPlugin(new \OCA\DAV\Connector\Sabre\DummyGetResponsePlugin());
$server->addPlugin(new \OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin('webdav', $this->logger));
diff --git a/apps/dav/lib/Server.php b/apps/dav/lib/Server.php
index df5b0ea05b6..994ac04033a 100644
--- a/apps/dav/lib/Server.php
+++ b/apps/dav/lib/Server.php
@@ -33,6 +33,7 @@ use OCA\DAV\CardDAV\ImageExportPlugin;
use OCA\DAV\CardDAV\PhotoCache;
use OCA\DAV\Comments\CommentsPlugin;
use OCA\DAV\Connector\Sabre\Auth;
+use OCA\DAV\Connector\Sabre\BearerAuth;
use OCA\DAV\Connector\Sabre\BlockLegacyClientPlugin;
use OCA\DAV\Connector\Sabre\CommentPropertiesPlugin;
use OCA\DAV\Connector\Sabre\CopyEtagHeaderPlugin;
@@ -52,6 +53,7 @@ use OCP\SabrePluginEvent;
use Sabre\CardDAV\VCFExportPlugin;
use Sabre\DAV\Auth\Plugin;
use OCA\DAV\Connector\Sabre\TagsPlugin;
+use Sabre\HTTP\Auth\Bearer;
use SearchDAV\DAV\SearchPlugin;
class Server {
@@ -100,6 +102,12 @@ class Server {
$event = new SabrePluginEvent($this->server);
$dispatcher->dispatch('OCA\DAV\Connector\Sabre::authInit', $event);
+ $bearerAuthBackend = new BearerAuth(
+ \OC::$server->getUserSession(),
+ \OC::$server->getSession(),
+ \OC::$server->getRequest()
+ );
+ $authPlugin->addBackend($bearerAuthBackend);
// because we are throwing exceptions this plugin has to be the last one
$authPlugin->addBackend($authBackend);
diff --git a/apps/dav/tests/unit/Connector/Sabre/BearerAuthTest.php b/apps/dav/tests/unit/Connector/Sabre/BearerAuthTest.php
new file mode 100644
index 00000000000..5eae75eb8e9
--- /dev/null
+++ b/apps/dav/tests/unit/Connector/Sabre/BearerAuthTest.php
@@ -0,0 +1,88 @@
+<?php
+/**
+ * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch>
+ *
+ * @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 OCA\DAV\Tests\unit\Connector\Sabre;
+
+use OC\Authentication\TwoFactorAuth\Manager;
+use OC\Security\Bruteforce\Throttler;
+use OC\User\Session;
+use OCA\DAV\Connector\Sabre\BearerAuth;
+use OCP\IRequest;
+use OCP\ISession;
+use OCP\IUser;
+use OCP\IUserSession;
+use Sabre\HTTP\RequestInterface;
+use Sabre\HTTP\ResponseInterface;
+use Test\TestCase;
+
+/**
+ * @group DB
+ */
+class BearerAuthTest extends TestCase {
+ /** @var IUserSession|\PHPUnit_Framework_MockObject_MockObject */
+ private $userSession;
+ /** @var ISession|\PHPUnit_Framework_MockObject_MockObject */
+ private $session;
+ /** @var IRequest|\PHPUnit_Framework_MockObject_MockObject */
+ private $request;
+ /** @var BearerAuth */
+ private $bearerAuth;
+
+ public function setUp() {
+ parent::setUp();
+
+ $this->userSession = $this->createMock(\OC\User\Session::class);
+ $this->session = $this->createMock(ISession::class);
+ $this->request = $this->createMock(IRequest::class);
+
+ $this->bearerAuth = new BearerAuth(
+ $this->userSession,
+ $this->session,
+ $this->request
+ );
+ }
+
+ public function testValidateBearerTokenNotLoggedIn() {
+ $this->assertFalse($this->bearerAuth->validateBearerToken('Token'));
+ }
+
+ public function testValidateBearerToken() {
+ $this->userSession
+ ->expects($this->at(0))
+ ->method('isLoggedIn')
+ ->willReturn(false);
+ $this->userSession
+ ->expects($this->at(2))
+ ->method('isLoggedIn')
+ ->willReturn(true);
+ $user = $this->createMock(IUser::class);
+ $user
+ ->expects($this->once())
+ ->method('getUID')
+ ->willReturn('admin');
+ $this->userSession
+ ->expects($this->once())
+ ->method('getUser')
+ ->willReturn($user);
+
+ $this->assertSame('principals/users/admin', $this->bearerAuth->validateBearerToken('Token'));
+ }
+}
diff --git a/apps/dav/tests/unit/Connector/Sabre/RequestTest/RequestTestCase.php b/apps/dav/tests/unit/Connector/Sabre/RequestTest/RequestTestCase.php
index 50e228b7e84..58a729e18ec 100644
--- a/apps/dav/tests/unit/Connector/Sabre/RequestTest/RequestTestCase.php
+++ b/apps/dav/tests/unit/Connector/Sabre/RequestTest/RequestTestCase.php
@@ -138,8 +138,9 @@ abstract class RequestTestCase extends TestCase {
*/
protected function getSabreServer(View $view, $user, $password, ExceptionPlugin $exceptionPlugin) {
$authBackend = new Auth($user, $password);
+ $authPlugin = new \Sabre\DAV\Auth\Plugin($authBackend);
- $server = $this->serverFactory->createServer('/', 'dummy', $authBackend, function () use ($view) {
+ $server = $this->serverFactory->createServer('/', 'dummy', $authPlugin, function () use ($view) {
return $view;
});
$server->addPlugin($exceptionPlugin);
diff --git a/apps/oauth2/appinfo/database.xml b/apps/oauth2/appinfo/database.xml
new file mode 100644
index 00000000000..db32e0cf97d
--- /dev/null
+++ b/apps/oauth2/appinfo/database.xml
@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<database xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/database.xsd">
+ <name>*dbname*</name>
+ <create>true</create>
+ <overwrite>false</overwrite>
+ <charset>utf8</charset>
+ <table>
+ <name>*dbprefix*oauth2_clients</name>
+ <declaration>
+ <field>
+ <name>id</name>
+ <type>integer</type>
+ <notnull>true</notnull>
+ <autoincrement>true</autoincrement>
+ <unsigned>true</unsigned>
+ <primary>true</primary>
+ </field>
+ <field>
+ <name>name</name>
+ <type>text</type>
+ <notnull>true</notnull>
+ <length>64</length>
+ </field>
+ <field>
+ <name>redirect_uri</name>
+ <type>text</type>
+ <notnull>true</notnull>
+ <length>2000</length>
+ </field>
+ <field>
+ <name>client_identifier</name>
+ <type>text</type>
+ <notnull>true</notnull>
+ <length>64</length>
+ </field>
+ <field>
+ <name>secret</name>
+ <type>text</type>
+ <notnull>true</notnull>
+ <length>64</length>
+ </field>
+ <index>
+ <name>oauth2_client_id_idx</name>
+ <unique>false</unique>
+ <field>
+ <name>client_identifier</name>
+ </field>
+ </index>
+ </declaration>
+ </table>
+ <table>
+ <name>*dbprefix*oauth2_access_tokens</name>
+ <declaration>
+ <field>
+ <name>id</name>
+ <type>integer</type>
+ <notnull>true</notnull>
+ <autoincrement>true</autoincrement>
+ <unsigned>true</unsigned>
+ <primary>true</primary>
+ </field>
+ <field>
+ <name>token_id</name>
+ <type>integer</type>
+ <notnull>true</notnull>
+ </field>
+ <field>
+ <name>client_id</name>
+ <type>integer</type>
+ <notnull>true</notnull>
+ </field>
+ <field>
+ <name>hashed_code</name>
+ <type>text</type>
+ <notnull>true</notnull>
+ <length>128</length>
+ </field>
+ <field>
+ <name>encrypted_token</name>
+ <type>text</type>
+ <notnull>true</notnull>
+ <length>786</length>
+ </field>
+ <index>
+ <name>oauth2_access_hash_idx</name>
+ <unique>true</unique>
+ <field>
+ <name>hashed_code</name>
+ </field>
+ </index>
+ <index>
+ <name>oauth2_access_client_id_idx</name>
+ <unique>false</unique>
+ <field>
+ <name>client_id</name>
+ </field>
+ </index>
+ </declaration>
+ </table>
+</database>
diff --git a/apps/oauth2/appinfo/info.xml b/apps/oauth2/appinfo/info.xml
new file mode 100644
index 00000000000..5e9e8dae06a
--- /dev/null
+++ b/apps/oauth2/appinfo/info.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0"?>
+<info xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd">
+ <id>oauth2</id>
+ <name>OAuth 2.0</name>
+ <description>The OAuth2 app allows administrators to configure the built-in authentication workflow to also allow OAuth2 compatible authentication from other web applications.</description>
+ <licence>agpl</licence>
+ <author>Lukas Reschke</author>
+ <namespace>OAuth2</namespace>
+ <version>1.0.5</version>
+ <default_enable/>
+ <types>
+ <authentication/>
+ </types>
+
+ <settings>
+ <admin>OCA\OAuth2\Settings\Admin</admin>
+ </settings>
+</info>
diff --git a/apps/oauth2/appinfo/routes.php b/apps/oauth2/appinfo/routes.php
new file mode 100644
index 00000000000..84b1336e37e
--- /dev/null
+++ b/apps/oauth2/appinfo/routes.php
@@ -0,0 +1,45 @@
+<?php
+/**
+ * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch>
+ *
+ * @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/>.
+ *
+ */
+
+return [
+ 'routes' => [
+ [
+ 'name' => 'Settings#addClient',
+ 'url' => '/settings',
+ 'verb' => 'POST',
+ ],
+ [
+ 'name' => 'Settings#deleteClient',
+ 'url' => '/clients/{id}/delete',
+ 'verb' => 'POST'
+ ],
+ [
+ 'name' => 'LoginRedirector#authorize',
+ 'url' => '/authorize',
+ 'verb' => 'GET',
+ ],
+ [
+ 'name' => 'OauthApi#getToken',
+ 'url' => '/api/v1/token',
+ 'verb' => 'POST'
+ ],
+ ],
+];
diff --git a/apps/oauth2/css/setting-admin.css b/apps/oauth2/css/setting-admin.css
new file mode 100644
index 00000000000..a57a56bb976
--- /dev/null
+++ b/apps/oauth2/css/setting-admin.css
@@ -0,0 +1,5 @@
+.show-oauth-credentials {
+ padding-left: 10px;
+ opacity: 0.3;
+ cursor: pointer;
+}
diff --git a/apps/oauth2/js/setting-admin.js b/apps/oauth2/js/setting-admin.js
new file mode 100644
index 00000000000..53163be1148
--- /dev/null
+++ b/apps/oauth2/js/setting-admin.js
@@ -0,0 +1,15 @@
+$(document).ready(function () {
+
+ $('.show-oauth-credentials').click(function() {
+ var row = $(this).parent();
+ var code = $(row).find('code');
+ if(code.text() === '****') {
+ code.text(row.data('value'));
+ $(this).css('opacity', 0.9);
+ } else {
+ code.text('****');
+ $(this).css('opacity', 0.3);
+ }
+ })
+
+});
diff --git a/apps/oauth2/lib/Controller/LoginRedirectorController.php b/apps/oauth2/lib/Controller/LoginRedirectorController.php
new file mode 100644
index 00000000000..9237b4b1b3c
--- /dev/null
+++ b/apps/oauth2/lib/Controller/LoginRedirectorController.php
@@ -0,0 +1,79 @@
+<?php
+/**
+ * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch>
+ *
+ * @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 OCA\OAuth2\Controller;
+
+use OCA\OAuth2\Db\ClientMapper;
+use OCP\AppFramework\Controller;
+use OCP\AppFramework\Http\RedirectResponse;
+use OCP\IRequest;
+use OCP\ISession;
+use OCP\IURLGenerator;
+
+class LoginRedirectorController extends Controller {
+ /** @var IURLGenerator */
+ private $urlGenerator;
+ /** @var ClientMapper */
+ private $clientMapper;
+ /** @var ISession */
+ private $session;
+
+ /**
+ * @param string $appName
+ * @param IRequest $request
+ * @param IURLGenerator $urlGenerator
+ * @param ClientMapper $clientMapper
+ * @param ISession $session
+ */
+ public function __construct($appName,
+ IRequest $request,
+ IURLGenerator $urlGenerator,
+ ClientMapper $clientMapper,
+ ISession $session) {
+ parent::__construct($appName, $request);
+ $this->urlGenerator = $urlGenerator;
+ $this->clientMapper = $clientMapper;
+ $this->session = $session;
+ }
+
+ /**
+ * @PublicPage
+ * @NoCSRFRequired
+ * @UseSession
+ *
+ * @param string $client_id
+ * @param string $state
+ * @return RedirectResponse
+ */
+ public function authorize($client_id,
+ $state) {
+ $client = $this->clientMapper->getByIdentifier($client_id);
+ $this->session->set('oauth.state', $state);
+
+ $targetUrl = $this->urlGenerator->linkToRouteAbsolute(
+ 'core.ClientFlowLogin.showAuthPickerPage',
+ [
+ 'clientIdentifier' => $client->getClientIdentifier(),
+ ]
+ );
+ return new RedirectResponse($targetUrl);
+ }
+}
diff --git a/apps/oauth2/lib/Controller/OauthApiController.php b/apps/oauth2/lib/Controller/OauthApiController.php
new file mode 100644
index 00000000000..b97d85ae3e6
--- /dev/null
+++ b/apps/oauth2/lib/Controller/OauthApiController.php
@@ -0,0 +1,88 @@
+<?php
+/**
+ * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch>
+ *
+ * @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 OCA\OAuth2\Controller;
+
+use OC\Authentication\Token\DefaultTokenMapper;
+use OCA\OAuth2\Db\AccessTokenMapper;
+use OCP\AppFramework\Controller;
+use OCP\AppFramework\Http\JSONResponse;
+use OCP\IRequest;
+use OCP\Security\ICrypto;
+use OCP\Security\ISecureRandom;
+
+class OauthApiController extends Controller {
+ /** @var AccessTokenMapper */
+ private $accessTokenMapper;
+ /** @var ICrypto */
+ private $crypto;
+ /** @var DefaultTokenMapper */
+ private $defaultTokenMapper;
+ /** @var ISecureRandom */
+ private $secureRandom;
+
+ /**
+ * @param string $appName
+ * @param IRequest $request
+ * @param ICrypto $crypto
+ * @param AccessTokenMapper $accessTokenMapper
+ * @param DefaultTokenMapper $defaultTokenMapper
+ * @param ISecureRandom $secureRandom
+ */
+ public function __construct($appName,
+ IRequest $request,
+ ICrypto $crypto,
+ AccessTokenMapper $accessTokenMapper,
+ DefaultTokenMapper $defaultTokenMapper,
+ ISecureRandom $secureRandom) {
+ parent::__construct($appName, $request);
+ $this->crypto = $crypto;
+ $this->accessTokenMapper = $accessTokenMapper;
+ $this->defaultTokenMapper = $defaultTokenMapper;
+ $this->secureRandom = $secureRandom;
+ }
+
+ /**
+ * @PublicPage
+ * @NoCSRFRequired
+ *
+ * @param string $code
+ * @return JSONResponse
+ */
+ public function getToken($code) {
+ $accessToken = $this->accessTokenMapper->getByCode($code);
+ $decryptedToken = $this->crypto->decrypt($accessToken->getEncryptedToken(), $code);
+ $newCode = $this->secureRandom->generate(128);
+ $accessToken->setHashedCode(hash('sha512', $newCode));
+ $accessToken->setEncryptedToken($this->crypto->encrypt($decryptedToken, $newCode));
+ $this->accessTokenMapper->update($accessToken);
+
+ return new JSONResponse(
+ [
+ 'access_token' => $decryptedToken,
+ 'token_type' => 'Bearer',
+ 'expires_in' => 3600,
+ 'refresh_token' => $newCode,
+ 'user_id' => $this->defaultTokenMapper->getTokenById($accessToken->getTokenId())->getUID(),
+ ]
+ );
+ }
+}
diff --git a/apps/oauth2/lib/Controller/SettingsController.php b/apps/oauth2/lib/Controller/SettingsController.php
new file mode 100644
index 00000000000..f9ded6c0968
--- /dev/null
+++ b/apps/oauth2/lib/Controller/SettingsController.php
@@ -0,0 +1,100 @@
+<?php
+/**
+ * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch>
+ *
+ * @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 OCA\OAuth2\Controller;
+
+use OC\Authentication\Token\DefaultTokenMapper;
+use OCA\OAuth2\Db\AccessTokenMapper;
+use OCA\OAuth2\Db\Client;
+use OCA\OAuth2\Db\ClientMapper;
+use OCP\AppFramework\Controller;
+use OCP\AppFramework\Http\RedirectResponse;
+use OCP\IRequest;
+use OCP\IURLGenerator;
+use OCP\Security\ISecureRandom;
+
+class SettingsController extends Controller {
+ /** @var IURLGenerator */
+ private $urlGenerator;
+ /** @var ClientMapper */
+ private $clientMapper;
+ /** @var ISecureRandom */
+ private $secureRandom;
+ /** @var AccessTokenMapper */
+ private $accessTokenMapper;
+ /** @var DefaultTokenMapper */
+ private $defaultTokenMapper;
+
+ const validChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+
+ /**
+ * @param string $appName
+ * @param IRequest $request
+ * @param IURLGenerator $urlGenerator
+ * @param ClientMapper $clientMapper
+ * @param ISecureRandom $secureRandom
+ * @param AccessTokenMapper $accessTokenMapper
+ * @param DefaultTokenMapper $defaultTokenMapper
+ */
+ public function __construct($appName,
+ IRequest $request,
+ IURLGenerator $urlGenerator,
+ ClientMapper $clientMapper,
+ ISecureRandom $secureRandom,
+ AccessTokenMapper $accessTokenMapper,
+ DefaultTokenMapper $defaultTokenMapper
+ ) {
+ parent::__construct($appName, $request);
+ $this->urlGenerator = $urlGenerator;
+ $this->secureRandom = $secureRandom;
+ $this->clientMapper = $clientMapper;
+ $this->accessTokenMapper = $accessTokenMapper;
+ $this->defaultTokenMapper = $defaultTokenMapper;
+ }
+
+ /**
+ * @param string $name
+ * @param string $redirectUri
+ * @return RedirectResponse
+ */
+ public function addClient($name,
+ $redirectUri) {
+ $client = new Client();
+ $client->setName($name);
+ $client->setRedirectUri($redirectUri);
+ $client->setSecret($this->secureRandom->generate(64, self::validChars));
+ $client->setClientIdentifier($this->secureRandom->generate(64, self::validChars));
+ $this->clientMapper->insert($client);
+ return new RedirectResponse($this->urlGenerator->getAbsoluteURL('/index.php/settings/admin/security'));
+ }
+
+ /**
+ * @param int $id
+ * @return RedirectResponse
+ */
+ public function deleteClient($id) {
+ $client = $this->clientMapper->getByUid($id);
+ $this->accessTokenMapper->deleteByClientId($id);
+ $this->defaultTokenMapper->deleteByName($client->getName());
+ $this->clientMapper->delete($client);
+ return new RedirectResponse($this->urlGenerator->getAbsoluteURL('/index.php/settings/admin/security'));
+ }
+}
diff --git a/apps/oauth2/lib/Db/AccessToken.php b/apps/oauth2/lib/Db/AccessToken.php
new file mode 100644
index 00000000000..8266a9a0068
--- /dev/null
+++ b/apps/oauth2/lib/Db/AccessToken.php
@@ -0,0 +1,53 @@
+<?php
+/**
+ * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch>
+ *
+ * @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 OCA\OAuth2\Db;
+
+use OCP\AppFramework\Db\Entity;
+
+/**
+ * @method int getTokenId()
+ * @method void setTokenId(int $identifier)
+ * @method int getClientId()
+ * @method void setClientId(int $identifier)
+ * @method string getEncryptedToken()
+ * @method void setEncryptedToken(string $token)
+ * @method string getHashedCode()
+ * @method void setHashedCode(string $token)
+ */
+class AccessToken extends Entity {
+ /** @var int */
+ protected $tokenId;
+ /** @var int */
+ protected $clientId;
+ /** @var string */
+ protected $hashedCode;
+ /** @var string */
+ protected $encryptedToken;
+
+ public function __construct() {
+ $this->addType('id', 'int');
+ $this->addType('token_id', 'int');
+ $this->addType('client_id', 'int');
+ $this->addType('hashed_code', 'string');
+ $this->addType('encrypted_token', 'string');
+ }
+}
diff --git a/apps/oauth2/lib/Db/AccessTokenMapper.php b/apps/oauth2/lib/Db/AccessTokenMapper.php
new file mode 100644
index 00000000000..2661c853372
--- /dev/null
+++ b/apps/oauth2/lib/Db/AccessTokenMapper.php
@@ -0,0 +1,70 @@
+<?php
+/**
+ * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch>
+ *
+ * @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 OCA\OAuth2\Db;
+
+use OCA\OAuth2\Exceptions\AccessTokenNotFoundException;
+use OCP\AppFramework\Db\Mapper;
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\IDBConnection;
+
+class AccessTokenMapper extends Mapper {
+
+ /**
+ * @param IDBConnection $db
+ */
+ public function __construct(IDBConnection $db) {
+ parent::__construct($db, 'oauth2_access_tokens');
+ }
+
+ /**
+ * @param string $code
+ * @return AccessToken
+ * @throws AccessTokenNotFoundException
+ */
+ public function getByCode($code) {
+ $qb = $this->db->getQueryBuilder();
+ $qb
+ ->select('*')
+ ->from($this->tableName)
+ ->where($qb->expr()->eq('hashed_code', $qb->createNamedParameter(hash('sha512', $code))));
+ $result = $qb->execute();
+ $row = $result->fetch();
+ $result->closeCursor();
+ if($row === false) {
+ throw new AccessTokenNotFoundException();
+ }
+ return AccessToken::fromRow($row);
+ }
+
+ /**
+ * delete all access token from a given client
+ *
+ * @param int $id
+ */
+ public function deleteByClientId($id) {
+ $qb = $this->db->getQueryBuilder();
+ $qb
+ ->delete($this->tableName)
+ ->where($qb->expr()->eq('client_id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
+ $qb->execute();
+ }
+}
diff --git a/apps/oauth2/lib/Db/Client.php b/apps/oauth2/lib/Db/Client.php
new file mode 100644
index 00000000000..85c1630cb15
--- /dev/null
+++ b/apps/oauth2/lib/Db/Client.php
@@ -0,0 +1,53 @@
+<?php
+/**
+ * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch>
+ *
+ * @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 OCA\OAuth2\Db;
+
+use OCP\AppFramework\Db\Entity;
+
+/**
+ * @method string getClientIdentifier()
+ * @method void setClientIdentifier(string $identifier)
+ * @method string getSecret()
+ * @method void setSecret(string $secret)
+ * @method string getRedirectUri()
+ * @method void setRedirectUri(string $redirectUri)
+ * @method string getName()
+ * @method void setName(string $name)
+ */
+class Client extends Entity {
+ /** @var string */
+ protected $name;
+ /** @var string */
+ protected $redirectUri;
+ /** @var string */
+ protected $clientIdentifier;
+ /** @var string */
+ protected $secret;
+
+ public function __construct() {
+ $this->addType('id', 'int');
+ $this->addType('name', 'string');
+ $this->addType('redirect_uri', 'string');
+ $this->addType('client_identifier', 'string');
+ $this->addType('secret', 'string');
+ }
+}
diff --git a/apps/oauth2/lib/Db/ClientMapper.php b/apps/oauth2/lib/Db/ClientMapper.php
new file mode 100644
index 00000000000..9df07e2789f
--- /dev/null
+++ b/apps/oauth2/lib/Db/ClientMapper.php
@@ -0,0 +1,89 @@
+<?php
+/**
+ * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch>
+ *
+ * @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 OCA\OAuth2\Db;
+
+use OCA\OAuth2\Exceptions\ClientNotFoundException;
+use OCP\AppFramework\Db\Mapper;
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\IDBConnection;
+
+class ClientMapper extends Mapper {
+
+ /**
+ * @param IDBConnection $db
+ */
+ public function __construct(IDBConnection $db) {
+ parent::__construct($db, 'oauth2_clients');
+ }
+
+ /**
+ * @param string $clientIdentifier
+ * @return Client
+ * @throws ClientNotFoundException
+ */
+ public function getByIdentifier($clientIdentifier) {
+ $qb = $this->db->getQueryBuilder();
+ $qb
+ ->select('*')
+ ->from($this->tableName)
+ ->where($qb->expr()->eq('client_identifier', $qb->createNamedParameter($clientIdentifier)));
+ $result = $qb->execute();
+ $row = $result->fetch();
+ $result->closeCursor();
+ if($row === false) {
+ throw new ClientNotFoundException();
+ }
+ return Client::fromRow($row);
+ }
+
+ /**
+ * @param string $uid internal uid of the client
+ * @return Client
+ * @throws ClientNotFoundException
+ */
+ public function getByUid($uid) {
+ $qb = $this->db->getQueryBuilder();
+ $qb
+ ->select('*')
+ ->from($this->tableName)
+ ->where($qb->expr()->eq('id', $qb->createNamedParameter($uid, IQueryBuilder::PARAM_INT)));
+ $result = $qb->execute();
+ $row = $result->fetch();
+ $result->closeCursor();
+ if($row === false) {
+ throw new ClientNotFoundException();
+ }
+ return Client::fromRow($row);
+ }
+
+ /**
+ * @return Client[]
+ */
+ public function getClients() {
+ $qb = $this->db->getQueryBuilder();
+ $qb
+ ->select('*')
+ ->from($this->tableName);
+
+ return $this->findEntities($qb->getSQL());
+ }
+}
diff --git a/apps/oauth2/lib/Exceptions/AccessTokenNotFoundException.php b/apps/oauth2/lib/Exceptions/AccessTokenNotFoundException.php
new file mode 100644
index 00000000000..a1eb632a9eb
--- /dev/null
+++ b/apps/oauth2/lib/Exceptions/AccessTokenNotFoundException.php
@@ -0,0 +1,24 @@
+<?php
+/**
+ * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch>
+ *
+ * @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 OCA\OAuth2\Exceptions;
+
+class AccessTokenNotFoundException extends \Exception {}
diff --git a/apps/oauth2/lib/Exceptions/ClientNotFoundException.php b/apps/oauth2/lib/Exceptions/ClientNotFoundException.php
new file mode 100644
index 00000000000..b2395c7bc9e
--- /dev/null
+++ b/apps/oauth2/lib/Exceptions/ClientNotFoundException.php
@@ -0,0 +1,24 @@
+<?php
+/**
+ * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch>
+ *
+ * @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 OCA\OAuth2\Exceptions;
+
+class ClientNotFoundException extends \Exception {}
diff --git a/apps/oauth2/lib/Settings/Admin.php b/apps/oauth2/lib/Settings/Admin.php
new file mode 100644
index 00000000000..07c3fe733ad
--- /dev/null
+++ b/apps/oauth2/lib/Settings/Admin.php
@@ -0,0 +1,66 @@
+<?php
+/**
+ * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch>
+ *
+ * @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 OCA\OAuth2\Settings;
+
+use OCA\OAuth2\Db\ClientMapper;
+use OCP\AppFramework\Http\TemplateResponse;
+use OCP\Settings\ISettings;
+
+class Admin implements ISettings {
+ /** @var ClientMapper */
+ private $clientMapper;
+
+ /**
+ * @param ClientMapper $clientMapper
+ */
+ public function __construct(ClientMapper $clientMapper) {
+ $this->clientMapper = $clientMapper;
+ }
+
+ /**
+ * @return TemplateResponse
+ */
+ public function getForm() {
+ return new TemplateResponse(
+ 'oauth2',
+ 'admin',
+ [
+ 'clients' => $this->clientMapper->getClients(),
+ ],
+ ''
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSection() {
+ return 'security';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getPriority() {
+ return 0;
+ }
+}
diff --git a/apps/oauth2/templates/admin.php b/apps/oauth2/templates/admin.php
new file mode 100644
index 00000000000..d2e34e08db8
--- /dev/null
+++ b/apps/oauth2/templates/admin.php
@@ -0,0 +1,76 @@
+<?php
+/**
+ * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch>
+ *
+ * @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/>.
+ *
+ */
+
+$urlGenerator = \OC::$server->getURLGenerator();
+$themingDefaults = \OC::$server->getThemingDefaults();
+
+script('oauth2', 'setting-admin');
+style('oauth2', 'setting-admin');
+
+/** @var array $_ */
+/** @var \OCA\OAuth2\Db\Client[] $clients */
+$clients = $_['clients'];
+?>
+
+<div id="oauth2" class="section">
+ <h2><?php p($l->t('OAuth 2.0 clients')); ?></h2>
+ <p class="settings-hint"><?php p($l->t('OAuth 2.0 allows external services to request access to your %s.', [$themingDefaults->getName()])); ?></p>
+
+ <table class="grid">
+ <thead>
+ <tr>
+ <th id="headerName" scope="col"><?php p($l->t('Name')); ?></th>
+ <th id="headerRedirectUri" scope="col"><?php p($l->t('Redirection URI')); ?></th>
+ <th id="headerClientIdentifier" scope="col"><?php p($l->t('Client Identifier')); ?></th>
+ <th id="headerSecret" scope="col"><?php p($l->t('Secret')); ?></th>
+ <th id="headerRemove">&nbsp;</th>
+ </tr>
+ </thead>
+ <tbody>
+ <?php
+ $imageUrl = $urlGenerator->imagePath('core', 'actions/toggle.svg');
+ foreach ($clients as $client) {
+ ?>
+ <tr>
+ <td><?php p($client->getName()); ?></td>
+ <td><?php p($client->getRedirectUri()); ?></td>
+ <td><code><?php p($client->getClientIdentifier()); ?></code></td>
+ <td data-value="<?php p($client->getSecret()); ?>"><code>****</code><img class='show-oauth-credentials' src="<?php p($imageUrl); ?>"/></td>
+ <td>
+ <form id="form-inline" class="delete" action="<?php p($urlGenerator->linkToRoute('oauth2.Settings.deleteClient', ['id' => $client->getId()])); ?>" method="POST">
+ <input type="hidden" name="requesttoken" value="<?php p($_['requesttoken']) ?>" />
+ <input type="submit" class="button icon-delete" value="">
+ </form>
+ </td>
+ </tr>
+ <?php } ?>
+ </tbody>
+ </table>
+
+ <br/>
+ <h3><?php p($l->t('Add client')); ?></h3>
+ <form action="<?php p($urlGenerator->linkToRoute('oauth2.Settings.addClient')); ?>" method="POST">
+ <input type="text" id="name" name="name" placeholder="<?php p($l->t('Name')); ?>">
+ <input type="url" id="redirectUri" name="redirectUri" placeholder="<?php p($l->t('Redirection URI')); ?>">
+ <input type="hidden" name="requesttoken" value="<?php p($_['requesttoken']) ?>" />
+ <input type="submit" class="button" value="<?php p($l->t('Add')); ?>">
+ </form>
+</div>
diff --git a/apps/oauth2/tests/Controller/LoginRedirectorControllerTest.php b/apps/oauth2/tests/Controller/LoginRedirectorControllerTest.php
new file mode 100644
index 00000000000..b33d3379be4
--- /dev/null
+++ b/apps/oauth2/tests/Controller/LoginRedirectorControllerTest.php
@@ -0,0 +1,91 @@
+<?php
+/**
+ * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch>
+ *
+ * @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 OCA\OAuth2\Tests\Controller;
+
+use OCA\Files_Sharing\Tests\TestCase;
+use OCA\OAuth2\Controller\LoginRedirectorController;
+use OCA\OAuth2\Db\Client;
+use OCA\OAuth2\Db\ClientMapper;
+use OCP\AppFramework\Http\RedirectResponse;
+use OCP\IRequest;
+use OCP\ISession;
+use OCP\IURLGenerator;
+
+/**
+ * @group DB
+ */
+class LoginRedirectorControllerTest extends TestCase {
+ /** @var IRequest|\PHPUnit_Framework_MockObject_MockObject */
+ private $request;
+ /** @var IURLGenerator|\PHPUnit_Framework_MockObject_MockObject */
+ private $urlGenerator;
+ /** @var ClientMapper|\PHPUnit_Framework_MockObject_MockObject */
+ private $clientMapper;
+ /** @var ISession|\PHPUnit_Framework_MockObject_MockObject */
+ private $session;
+ /** @var LoginRedirectorController */
+ private $loginRedirectorController;
+
+ public function setUp() {
+ parent::setUp();
+
+ $this->request = $this->createMock(IRequest::class);
+ $this->urlGenerator = $this->createMock(IURLGenerator::class);
+ $this->clientMapper = $this->createMock(ClientMapper::class);
+ $this->session = $this->createMock(ISession::class);
+
+ $this->loginRedirectorController = new LoginRedirectorController(
+ 'oauth2',
+ $this->request,
+ $this->urlGenerator,
+ $this->clientMapper,
+ $this->session
+ );
+ }
+
+ public function testAuthorize() {
+ $client = new Client();
+ $client->setClientIdentifier('MyClientIdentifier');
+ $this->clientMapper
+ ->expects($this->once())
+ ->method('getByIdentifier')
+ ->with('MyClientId')
+ ->willReturn($client);
+ $this->session
+ ->expects($this->once())
+ ->method('set')
+ ->with('oauth.state', 'MyState');
+ $this->urlGenerator
+ ->expects($this->once())
+ ->method('linkToRouteAbsolute')
+ ->with(
+ 'core.ClientFlowLogin.showAuthPickerPage',
+ [
+ 'clientIdentifier' => 'MyClientIdentifier',
+ ]
+ )
+ ->willReturn('https://example.com/?clientIdentifier=foo');
+
+ $expected = new RedirectResponse('https://example.com/?clientIdentifier=foo');
+ $this->assertEquals($expected, $this->loginRedirectorController->authorize('MyClientId', 'MyState'));
+ }
+}
diff --git a/apps/oauth2/tests/Controller/OauthApiControllerTest.php b/apps/oauth2/tests/Controller/OauthApiControllerTest.php
new file mode 100644
index 00000000000..c90e2bf711f
--- /dev/null
+++ b/apps/oauth2/tests/Controller/OauthApiControllerTest.php
@@ -0,0 +1,106 @@
+<?php
+/**
+ * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch>
+ *
+ * @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 OCA\OAuth2\Tests\Controller;
+
+use OC\Authentication\Token\DefaultToken;
+use OC\Authentication\Token\DefaultTokenMapper;
+use OCA\OAuth2\Controller\OauthApiController;
+use OCA\OAuth2\Db\AccessToken;
+use OCA\OAuth2\Db\AccessTokenMapper;
+use OCP\AppFramework\Http\JSONResponse;
+use OCP\IRequest;
+use OCP\Security\ICrypto;
+use OCP\Security\ISecureRandom;
+use Test\TestCase;
+
+class OauthApiControllerTest extends TestCase {
+ /** @var IRequest|\PHPUnit_Framework_MockObject_MockObject */
+ private $request;
+ /** @var ICrypto|\PHPUnit_Framework_MockObject_MockObject */
+ private $crypto;
+ /** @var AccessTokenMapper|\PHPUnit_Framework_MockObject_MockObject */
+ private $accessTokenMapper;
+ /** @var DefaultTokenMapper|\PHPUnit_Framework_MockObject_MockObject */
+ private $defaultTokenMapper;
+ /** @var ISecureRandom|\PHPUnit_Framework_MockObject_MockObject */
+ private $secureRandom;
+ /** @var OauthApiController */
+ private $oauthApiController;
+
+ public function setUp() {
+ parent::setUp();
+
+ $this->request = $this->createMock(IRequest::class);
+ $this->crypto = $this->createMock(ICrypto::class);
+ $this->accessTokenMapper = $this->createMock(AccessTokenMapper::class);
+ $this->defaultTokenMapper = $this->createMock(DefaultTokenMapper::class);
+ $this->secureRandom = $this->createMock(ISecureRandom::class);
+
+ $this->oauthApiController = new OauthApiController(
+ 'oauth2',
+ $this->request,
+ $this->crypto,
+ $this->accessTokenMapper,
+ $this->defaultTokenMapper,
+ $this->secureRandom
+ );
+ }
+
+ public function testGetToken() {
+ $accessToken = new AccessToken();
+ $accessToken->setEncryptedToken('MyEncryptedToken');
+ $accessToken->setTokenId(123);
+ $this->accessTokenMapper
+ ->expects($this->once())
+ ->method('getByCode')
+ ->willReturn($accessToken);
+ $this->crypto
+ ->expects($this->once())
+ ->method('decrypt')
+ ->with('MyEncryptedToken', 'MySecretCode')
+ ->willReturn('MyDecryptedToken');
+ $this->secureRandom
+ ->expects($this->once())
+ ->method('generate')
+ ->with(128)
+ ->willReturn('NewToken');
+ $token = new DefaultToken();
+ $token->setUid('JohnDoe');
+ $this->defaultTokenMapper
+ ->expects($this->once())
+ ->method('getTokenById')
+ ->with(123)
+ ->willReturn($token);
+
+ $expected = new JSONResponse(
+ [
+ 'access_token' => 'MyDecryptedToken',
+ 'token_type' => 'Bearer',
+ 'expires_in' => 3600,
+ 'refresh_token' => 'NewToken',
+ 'user_id' => 'JohnDoe',
+ ]
+ );
+ $this->assertEquals($expected, $this->oauthApiController->getToken('MySecretCode'));
+ }
+
+}
diff --git a/apps/oauth2/tests/Controller/SettingsControllerTest.php b/apps/oauth2/tests/Controller/SettingsControllerTest.php
new file mode 100644
index 00000000000..a6c036949ed
--- /dev/null
+++ b/apps/oauth2/tests/Controller/SettingsControllerTest.php
@@ -0,0 +1,139 @@
+<?php
+/**
+ * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch>
+ *
+ * @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 OCA\OAuth2\Tests\Controller;
+
+use OC\Authentication\Token\DefaultTokenMapper;
+use OCA\OAuth2\Controller\SettingsController;
+use OCA\OAuth2\Db\AccessTokenMapper;
+use OCA\OAuth2\Db\Client;
+use OCA\OAuth2\Db\ClientMapper;
+use OCP\AppFramework\Http\RedirectResponse;
+use OCP\IRequest;
+use OCP\IURLGenerator;
+use OCP\Security\ISecureRandom;
+use Test\TestCase;
+
+class SettingsControllerTest extends TestCase {
+ /** @var IRequest|\PHPUnit_Framework_MockObject_MockObject */
+ private $request;
+ /** @var IURLGenerator|\PHPUnit_Framework_MockObject_MockObject */
+ private $urlGenerator;
+ /** @var ClientMapper|\PHPUnit_Framework_MockObject_MockObject */
+ private $clientMapper;
+ /** @var ISecureRandom|\PHPUnit_Framework_MockObject_MockObject */
+ private $secureRandom;
+ /** @var AccessTokenMapper|\PHPUnit_Framework_MockObject_MockObject */
+ private $accessTokenMapper;
+ /** @var DefaultTokenMapper|\PHPUnit_Framework_MockObject_MockObject */
+ private $defaultTokenMapper;
+ /** @var SettingsController */
+ private $settingsController;
+
+ public function setUp() {
+ parent::setUp();
+
+ $this->request = $this->createMock(IRequest::class);
+ $this->urlGenerator = $this->createMock(IURLGenerator::class);
+ $this->clientMapper = $this->createMock(ClientMapper::class);
+ $this->secureRandom = $this->createMock(ISecureRandom::class);
+ $this->accessTokenMapper = $this->createMock(AccessTokenMapper::class);
+ $this->defaultTokenMapper = $this->createMock(DefaultTokenMapper::class);
+
+ $this->settingsController = new SettingsController(
+ 'oauth2',
+ $this->request,
+ $this->urlGenerator,
+ $this->clientMapper,
+ $this->secureRandom,
+ $this->accessTokenMapper,
+ $this->defaultTokenMapper
+ );
+ }
+
+ public function testAddClient() {
+ $this->secureRandom
+ ->expects($this->at(0))
+ ->method('generate')
+ ->with(64, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789')
+ ->willReturn('MySecret');
+ $this->secureRandom
+ ->expects($this->at(1))
+ ->method('generate')
+ ->with(64, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789')
+ ->willReturn('MyClientIdentifier');
+
+ $client = new Client();
+ $client->setName('My Client Name');
+ $client->setRedirectUri('https://example.com/');
+ $client->setSecret('MySecret');
+ $client->setClientIdentifier('MyClientIdentifier');
+
+ $this->clientMapper
+ ->expects($this->once())
+ ->method('insert')
+ ->with($client);
+
+ $this->urlGenerator
+ ->expects($this->once())
+ ->method('getAbsoluteURL')
+ ->with('/index.php/settings/admin/security')
+ ->willReturn('https://example.com/index.php/settings/admin/security');
+
+ $expected = new RedirectResponse('https://example.com/index.php/settings/admin/security');
+ $this->assertEquals($expected, $this->settingsController->addClient('My Client Name', 'https://example.com/'));
+ }
+
+ public function testDeleteClient() {
+ $client = new Client();
+ $client->setName('My Client Name');
+ $client->setRedirectUri('https://example.com/');
+ $client->setSecret('MySecret');
+ $client->setClientIdentifier('MyClientIdentifier');
+
+ $this->clientMapper
+ ->expects($this->at(0))
+ ->method('getByUid')
+ ->with(123)
+ ->willReturn($client);
+ $this->accessTokenMapper
+ ->expects($this->once())
+ ->method('deleteByClientId')
+ ->with(123);
+ $this->defaultTokenMapper
+ ->expects($this->once())
+ ->method('deleteByName')
+ ->with('My Client Name');
+ $this->clientMapper
+ ->expects($this->at(1))
+ ->method('delete')
+ ->with($client);
+
+ $this->urlGenerator
+ ->expects($this->once())
+ ->method('getAbsoluteURL')
+ ->with('/index.php/settings/admin/security')
+ ->willReturn('https://example.com/index.php/settings/admin/security');
+
+ $expected = new RedirectResponse('https://example.com/index.php/settings/admin/security');
+ $this->assertEquals($expected, $this->settingsController->deleteClient(123));
+ }
+}
diff --git a/apps/oauth2/tests/Db/AccessTokenMapperTest.php b/apps/oauth2/tests/Db/AccessTokenMapperTest.php
new file mode 100644
index 00000000000..ebc6b55a382
--- /dev/null
+++ b/apps/oauth2/tests/Db/AccessTokenMapperTest.php
@@ -0,0 +1,70 @@
+<?php
+/**
+ * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch>
+ *
+ * @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 OCA\OAuth2\Tests\Db;
+
+use OCA\OAuth2\Db\AccessToken;
+use OCA\OAuth2\Db\AccessTokenMapper;
+use Test\TestCase;
+
+/**
+ * @group DB
+ */
+class AccessTokenMapperTest extends TestCase {
+ /** @var AccessTokenMapper */
+ private $accessTokenMapper;
+
+ public function setUp() {
+ parent::setUp();
+ $this->accessTokenMapper = new AccessTokenMapper(\OC::$server->getDatabaseConnection());
+ }
+
+ public function testGetByCode() {
+ $this->accessTokenMapper->deleteByClientId(1234);
+ $token = new AccessToken();
+ $token->setClientId(1234);
+ $token->setTokenId((string)time());
+ $token->setEncryptedToken('MyEncryptedToken');
+ $token->setHashedCode(hash('sha512', 'MyAwesomeToken'));
+ $this->accessTokenMapper->insert($token);
+ $token->resetUpdatedFields();
+
+ $result = $this->accessTokenMapper->getByCode('MyAwesomeToken');
+ $this->assertEquals($token, $result);
+ $this->accessTokenMapper->delete($token);
+ }
+
+ /**
+ * @expectedException \OCA\OAuth2\Exceptions\AccessTokenNotFoundException
+ */
+ public function testDeleteByClientId() {
+ $this->accessTokenMapper->deleteByClientId(1234);
+ $token = new AccessToken();
+ $token->setClientId(1234);
+ $token->setTokenId((string)time());
+ $token->setEncryptedToken('MyEncryptedToken');
+ $token->setHashedCode(hash('sha512', 'MyAwesomeToken'));
+ $this->accessTokenMapper->insert($token);
+ $token->resetUpdatedFields();
+ $this->accessTokenMapper->deleteByClientId(1234);
+ $this->accessTokenMapper->getByCode('MyAwesomeToken');
+ }
+}
diff --git a/apps/oauth2/tests/Db/ClientMapperTest.php b/apps/oauth2/tests/Db/ClientMapperTest.php
new file mode 100644
index 00000000000..80d69c3b1b8
--- /dev/null
+++ b/apps/oauth2/tests/Db/ClientMapperTest.php
@@ -0,0 +1,79 @@
+<?php
+/**
+ * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch>
+ *
+ * @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 OCA\OAuth2\Tests\Db;
+
+use OCA\OAuth2\Db\Client;
+use OCA\OAuth2\Db\ClientMapper;
+use Test\TestCase;
+
+/**
+ * @group DB
+ */
+class ClientMapperTest extends TestCase {
+ /** @var ClientMapper */
+ private $clientMapper;
+
+ public function setUp() {
+ parent::setUp();
+ $this->clientMapper = new ClientMapper(\OC::$server->getDatabaseConnection());
+ }
+
+ public function testGetByIdentifier() {
+ $client = new Client();
+ $client->setClientIdentifier('MyAwesomeClientIdentifier');
+ $client->setName('Client Name');
+ $client->setRedirectUri('https://example.com/');
+ $client->setSecret('TotallyNotSecret');
+ $this->clientMapper->insert($client);
+ $client->resetUpdatedFields();
+ $this->assertEquals($client, $this->clientMapper->getByIdentifier('MyAwesomeClientIdentifier'));
+ }
+
+ /**
+ * @expectedException \OCA\OAuth2\Exceptions\ClientNotFoundException
+ */
+ public function testGetByIdentifierNotExisting() {
+ $this->clientMapper->getByIdentifier('MyTotallyNotExistingClient');
+ }
+
+ public function testGetByUid() {
+ $client = new Client();
+ $client->setClientIdentifier('MyNewClient');
+ $client->setName('Client Name');
+ $client->setRedirectUri('https://example.com/');
+ $client->setSecret('TotallyNotSecret');
+ $this->clientMapper->insert($client);
+ $client->resetUpdatedFields();
+ $this->assertEquals($client, $this->clientMapper->getByUid($client->getId()));
+ }
+
+ /**
+ * @expectedException \OCA\OAuth2\Exceptions\ClientNotFoundException
+ */
+ public function testGetByUidNotExisting() {
+ $this->clientMapper->getByUid(1234);
+ }
+
+ public function testGetClients() {
+ $this->assertSame('array', gettype($this->clientMapper->getClients()));
+ }
+}
diff --git a/apps/oauth2/tests/Settings/AdminTest.php b/apps/oauth2/tests/Settings/AdminTest.php
new file mode 100644
index 00000000000..9c3d5ed1449
--- /dev/null
+++ b/apps/oauth2/tests/Settings/AdminTest.php
@@ -0,0 +1,66 @@
+<?php
+/**
+ * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch>
+ *
+ * @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 OCA\OAuth2\Tests\Settings;
+
+use OCA\OAuth2\Db\ClientMapper;
+use OCA\OAuth2\Settings\Admin;
+use OCP\AppFramework\Http\TemplateResponse;
+use Test\TestCase;
+
+class AdminTest extends TestCase {
+ /** @var ClientMapper|\PHPUnit_Framework_MockObject_MockObject */
+ private $clientMapper;
+ /** @var Admin|\PHPUnit_Framework_MockObject_MockObject */
+ private $admin;
+
+ public function setUp() {
+ parent::setUp();
+
+ $this->clientMapper = $this->createMock(ClientMapper::class);
+ $this->admin = new Admin($this->clientMapper);
+ }
+
+ public function testGetForm() {
+ $this->clientMapper
+ ->expects($this->once())
+ ->method('getClients')
+ ->willReturn(['MyClients']);
+
+ $expected = new TemplateResponse(
+ 'oauth2',
+ 'admin',
+ [
+ 'clients' => ['MyClients'],
+ ],
+ ''
+ );
+ $this->assertEquals($expected, $this->admin->getForm());
+ }
+
+ public function testGetSection() {
+ $this->assertSame('security', $this->admin->getSection());
+ }
+
+ public function testGetPriority() {
+ $this->assertSame(0, $this->admin->getPriority());
+ }
+}
diff --git a/apps/theming/lib/Controller/IconController.php b/apps/theming/lib/Controller/IconController.php
index 7c4e209d0df..5532a08c83c 100644
--- a/apps/theming/lib/Controller/IconController.php
+++ b/apps/theming/lib/Controller/IconController.php
@@ -22,6 +22,7 @@
*/
namespace OCA\Theming\Controller;
+use OC\IntegrityCheck\Helpers\FileAccessHelper;
use OCA\Theming\IconBuilder;
use OCA\Theming\ImageManager;
use OCA\Theming\ThemingDefaults;
@@ -29,6 +30,7 @@ use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\NotFoundResponse;
use OCP\AppFramework\Http\FileDisplayResponse;
+use OCP\AppFramework\Http\DataDisplayResponse;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Files\NotFoundException;
use OCP\IRequest;
@@ -48,6 +50,8 @@ class IconController extends Controller {
private $iconBuilder;
/** @var ImageManager */
private $imageManager;
+ /** @var FileAccessHelper */
+ private $fileAccessHelper;
/**
* IconController constructor.
@@ -69,7 +73,8 @@ class IconController extends Controller {
ITimeFactory $timeFactory,
IConfig $config,
IconBuilder $iconBuilder,
- ImageManager $imageManager
+ ImageManager $imageManager,
+ FileAccessHelper $fileAccessHelper
) {
parent::__construct($appName, $request);
@@ -79,6 +84,7 @@ class IconController extends Controller {
$this->config = $config;
$this->iconBuilder = $iconBuilder;
$this->imageManager = $imageManager;
+ $this->fileAccessHelper = $fileAccessHelper;
}
/**
@@ -120,9 +126,10 @@ class IconController extends Controller {
* @NoCSRFRequired
*
* @param $app string app name
- * @return FileDisplayResponse|NotFoundResponse
+ * @return FileDisplayResponse|DataDisplayResponse
*/
public function getFavicon($app = "core") {
+ $response = null;
if ($this->themingDefaults->shouldReplaceIcons()) {
try {
$iconFile = $this->imageManager->getCachedImage('favIcon-' . $app);
@@ -132,16 +139,19 @@ class IconController extends Controller {
}
if ($iconFile !== false) {
$response = new FileDisplayResponse($iconFile, Http::STATUS_OK, ['Content-Type' => 'image/x-icon']);
- $response->cacheFor(86400);
- $expires = new \DateTime();
- $expires->setTimestamp($this->timeFactory->getTime());
- $expires->add(new \DateInterval('PT24H'));
- $response->addHeader('Expires', $expires->format(\DateTime::RFC2822));
- $response->addHeader('Pragma', 'cache');
- return $response;
}
}
- return new NotFoundResponse();
+ if($response === null) {
+ $fallbackLogo = \OC::$SERVERROOT . '/core/img/favicon.png';
+ $response = new DataDisplayResponse($this->fileAccessHelper->file_get_contents($fallbackLogo), Http::STATUS_OK, ['Content-Type' => 'image/x-icon']);
+ }
+ $response->cacheFor(86400);
+ $expires = new \DateTime();
+ $expires->setTimestamp($this->timeFactory->getTime());
+ $expires->add(new \DateInterval('PT24H'));
+ $response->addHeader('Expires', $expires->format(\DateTime::RFC2822));
+ $response->addHeader('Pragma', 'cache');
+ return $response;
}
/**
@@ -154,6 +164,7 @@ class IconController extends Controller {
* @return FileDisplayResponse|NotFoundResponse
*/
public function getTouchIcon($app = "core") {
+ $response = null;
if ($this->themingDefaults->shouldReplaceIcons()) {
try {
$iconFile = $this->imageManager->getCachedImage('touchIcon-' . $app);
@@ -163,15 +174,18 @@ class IconController extends Controller {
}
if ($iconFile !== false) {
$response = new FileDisplayResponse($iconFile, Http::STATUS_OK, ['Content-Type' => 'image/png']);
- $response->cacheFor(86400);
- $expires = new \DateTime();
- $expires->setTimestamp($this->timeFactory->getTime());
- $expires->add(new \DateInterval('PT24H'));
- $response->addHeader('Expires', $expires->format(\DateTime::RFC2822));
- $response->addHeader('Pragma', 'cache');
- return $response;
}
}
- return new NotFoundResponse();
+ if($response === null) {
+ $fallbackLogo = \OC::$SERVERROOT . '/core/img/favicon-touch.png';
+ $response = new DataDisplayResponse($this->fileAccessHelper->file_get_contents($fallbackLogo), Http::STATUS_OK, ['Content-Type' => 'image/png']);
+ }
+ $response->cacheFor(86400);
+ $expires = new \DateTime();
+ $expires->setTimestamp($this->timeFactory->getTime());
+ $expires->add(new \DateInterval('PT24H'));
+ $response->addHeader('Expires', $expires->format(\DateTime::RFC2822));
+ $response->addHeader('Pragma', 'cache');
+ return $response;
}
}
diff --git a/apps/theming/lib/IconBuilder.php b/apps/theming/lib/IconBuilder.php
index 42a7031f405..4383ccc7cd9 100644
--- a/apps/theming/lib/IconBuilder.php
+++ b/apps/theming/lib/IconBuilder.php
@@ -53,14 +53,18 @@ class IconBuilder {
* @return string|false image blob
*/
public function getFavicon($app) {
- $icon = $this->renderAppIcon($app, 32);
- if($icon === false) {
+ try {
+ $icon = $this->renderAppIcon($app, 32);
+ if ($icon === false) {
+ return false;
+ }
+ $icon->setImageFormat("png24");
+ $data = $icon->getImageBlob();
+ $icon->destroy();
+ return $data;
+ } catch (\ImagickException $e) {
return false;
}
- $icon->setImageFormat("png24");
- $data = $icon->getImageBlob();
- $icon->destroy();
- return $data;
}
/**
@@ -68,14 +72,18 @@ class IconBuilder {
* @return string|false image blob
*/
public function getTouchIcon($app) {
- $icon = $this->renderAppIcon($app, 512);
- if($icon === false) {
+ try {
+ $icon = $this->renderAppIcon($app, 512);
+ if ($icon === false) {
+ return false;
+ }
+ $icon->setImageFormat("png24");
+ $data = $icon->getImageBlob();
+ $icon->destroy();
+ return $data;
+ } catch (\ImagickException $e) {
return false;
}
- $icon->setImageFormat("png24");
- $data = $icon->getImageBlob();
- $icon->destroy();
- return $data;
}
/**
diff --git a/apps/theming/tests/Controller/IconControllerTest.php b/apps/theming/tests/Controller/IconControllerTest.php
index add11df3e6d..c6a40b5942e 100644
--- a/apps/theming/tests/Controller/IconControllerTest.php
+++ b/apps/theming/tests/Controller/IconControllerTest.php
@@ -24,10 +24,12 @@ namespace OCA\Theming\Tests\Controller;
use OC\Files\SimpleFS\SimpleFile;
+use OC\IntegrityCheck\Helpers\FileAccessHelper;
use OCA\Theming\IconBuilder;
use OCA\Theming\ImageManager;
use OCA\Theming\ThemingDefaults;
use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\DataDisplayResponse;
use OCP\AppFramework\Http\NotFoundResponse;
use OCP\Files\NotFoundException;
use OCP\IConfig;
@@ -53,6 +55,8 @@ class IconControllerTest extends TestCase {
private $config;
/** @var IconBuilder|\PHPUnit_Framework_MockObject_MockObject */
private $iconBuilder;
+ /** @var FileAccessHelper|\PHPUnit_Framework_MockObject_MockObject */
+ private $fileAccessHelper;
/** @var ImageManager */
private $imageManager;
@@ -69,6 +73,7 @@ class IconControllerTest extends TestCase {
$this->iconBuilder = $this->getMockBuilder('OCA\Theming\IconBuilder')
->disableOriginalConstructor()->getMock();
$this->imageManager = $this->getMockBuilder('OCA\Theming\ImageManager')->disableOriginalConstructor()->getMock();
+ $this->fileAccessHelper = $this->createMock(FileAccessHelper::class);
$this->timeFactory->expects($this->any())
->method('getTime')
->willReturn(123);
@@ -81,7 +86,8 @@ class IconControllerTest extends TestCase {
$this->timeFactory,
$this->config,
$this->iconBuilder,
- $this->imageManager
+ $this->imageManager,
+ $this->fileAccessHelper
);
parent::setUp();
@@ -150,11 +156,19 @@ class IconControllerTest extends TestCase {
$this->themingDefaults->expects($this->any())
->method('shouldReplaceIcons')
->willReturn(false);
- $expected = new Http\Response();
- $expected->setStatus(Http::STATUS_NOT_FOUND);
- $expected->cacheFor(0);
- $expected->setLastModified(new \DateTime('now', new \DateTimeZone('GMT')));
- $this->assertInstanceOf(NotFoundResponse::class, $this->iconController->getFavicon());
+ $fallbackLogo = \OC::$SERVERROOT . '/core/img/favicon.png';
+ $this->fileAccessHelper->expects($this->once())
+ ->method('file_get_contents')
+ ->with($fallbackLogo)
+ ->willReturn(file_get_contents($fallbackLogo));
+ $expected = new DataDisplayResponse(file_get_contents($fallbackLogo), Http::STATUS_OK, ['Content-Type' => 'image/x-icon']);
+ $expected->cacheFor(86400);
+ $expires = new \DateTime();
+ $expires->setTimestamp($this->timeFactory->getTime());
+ $expires->add(new \DateInterval('PT24H'));
+ $expected->addHeader('Expires', $expires->format(\DateTime::RFC2822));
+ $expected->addHeader('Pragma', 'cache');
+ $this->assertEquals($expected, $this->iconController->getFavicon());
}
public function testGetTouchIconDefault() {
@@ -195,7 +209,19 @@ class IconControllerTest extends TestCase {
$this->themingDefaults->expects($this->any())
->method('shouldReplaceIcons')
->willReturn(false);
- $this->assertInstanceOf(NotFoundResponse::class, $this->iconController->getTouchIcon());
+ $fallbackLogo = \OC::$SERVERROOT . '/core/img/favicon-touch.png';
+ $this->fileAccessHelper->expects($this->once())
+ ->method('file_get_contents')
+ ->with($fallbackLogo)
+ ->willReturn(file_get_contents($fallbackLogo));
+ $expected = new DataDisplayResponse(file_get_contents($fallbackLogo), Http::STATUS_OK, ['Content-Type' => 'image/png']);
+ $expected->cacheFor(86400);
+ $expires = new \DateTime();
+ $expires->setTimestamp($this->timeFactory->getTime());
+ $expires->add(new \DateInterval('PT24H'));
+ $expected->addHeader('Expires', $expires->format(\DateTime::RFC2822));
+ $expected->addHeader('Pragma', 'cache');
+ $this->assertEquals($expected, $this->iconController->getTouchIcon());
}
}
diff --git a/build/integration/features/auth.feature b/build/integration/features/auth.feature
index b9f423a9e93..679b2465659 100644
--- a/build/integration/features/auth.feature
+++ b/build/integration/features/auth.feature
@@ -53,6 +53,14 @@ Feature: auth
When requesting "/remote.php/webdav" with "PROPFIND" using restricted basic token auth
Then the HTTP status code should be "207"
+ Scenario: using old WebDAV endpoint with unrestricted client token
+ When requesting "/remote.php/webdav" with "PROPFIND" using an unrestricted client token
+ Then the HTTP status code should be "207"
+
+ Scenario: using new WebDAV endpoint with unrestricted client token
+ When requesting "/remote.php/dav/" with "PROPFIND" using an unrestricted client token
+ Then the HTTP status code should be "207"
+
Scenario: using WebDAV with browser session
Given a new browser session is started
When requesting "/remote.php/webdav" with "PROPFIND" using browser session
diff --git a/build/integration/features/bootstrap/Auth.php b/build/integration/features/bootstrap/Auth.php
index 7addcab5f97..ae411cc7ab3 100644
--- a/build/integration/features/bootstrap/Auth.php
+++ b/build/integration/features/bootstrap/Auth.php
@@ -187,7 +187,7 @@ trait Auth {
* @param string $method
*/
public function requestingWithUsingAnUnrestrictedClientToken($url, $method) {
- $this->sendRequest($url, $method, 'token ' . $this->unrestrictedClientToken);
+ $this->sendRequest($url, $method, 'Bearer ' . $this->unrestrictedClientToken);
}
/**
@@ -197,7 +197,7 @@ trait Auth {
* @param string $method
*/
public function requestingWithUsingARestrictedClientToken($url, $method) {
- $this->sendRequest($url, $method, 'token ' . $this->restrictedClientToken);
+ $this->sendRequest($url, $method, 'Bearer ' . $this->restrictedClientToken);
}
/**
diff --git a/build/integration/features/provisioning-v1.feature b/build/integration/features/provisioning-v1.feature
index 555960b8a55..84e2e30baa1 100644
--- a/build/integration/features/provisioning-v1.feature
+++ b/build/integration/features/provisioning-v1.feature
@@ -342,6 +342,7 @@ Feature: provisioning
| updatenotification |
| workflowengine |
| files_external |
+ | oauth2 |
Scenario: get app info
Given As an "admin"
diff --git a/build/integration/features/webdav-related.feature b/build/integration/features/webdav-related.feature
index b8ed1c4a778..b4fd0511356 100644
--- a/build/integration/features/webdav-related.feature
+++ b/build/integration/features/webdav-related.feature
@@ -8,7 +8,7 @@ Feature: webdav-related
Then the HTTP status code should be "401"
And there are no duplicate headers
And The following headers should be set
- |WWW-Authenticate|Basic realm="Nextcloud"|
+ |WWW-Authenticate|Basic realm="Nextcloud", Bearer realm="Nextcloud"|
Scenario: Unauthenticated call new dav path
Given using new dav path
@@ -16,7 +16,7 @@ Feature: webdav-related
Then the HTTP status code should be "401"
And there are no duplicate headers
And The following headers should be set
- |WWW-Authenticate|Basic realm="Nextcloud"|
+ |WWW-Authenticate|Bearer realm="Nextcloud", Basic realm="Nextcloud"|
Scenario: Moving a file
Given using old dav path
diff --git a/core/Controller/ClientFlowLoginController.php b/core/Controller/ClientFlowLoginController.php
index 8c2c121d5b2..bec81a89d53 100644
--- a/core/Controller/ClientFlowLoginController.php
+++ b/core/Controller/ClientFlowLoginController.php
@@ -25,6 +25,9 @@ use OC\Authentication\Exceptions\InvalidTokenException;
use OC\Authentication\Exceptions\PasswordlessTokenException;
use OC\Authentication\Token\IProvider;
use OC\Authentication\Token\IToken;
+use OCA\OAuth2\Db\AccessToken;
+use OCA\OAuth2\Db\AccessTokenMapper;
+use OCA\OAuth2\Db\ClientMapper;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Response;
@@ -35,6 +38,7 @@ use OCP\IRequest;
use OCP\ISession;
use OCP\IURLGenerator;
use OCP\IUserSession;
+use OCP\Security\ICrypto;
use OCP\Security\ISecureRandom;
use OCP\Session\Exceptions\SessionNotAvailableException;
@@ -53,6 +57,12 @@ class ClientFlowLoginController extends Controller {
private $random;
/** @var IURLGenerator */
private $urlGenerator;
+ /** @var ClientMapper */
+ private $clientMapper;
+ /** @var AccessTokenMapper */
+ private $accessTokenMapper;
+ /** @var ICrypto */
+ private $crypto;
const stateName = 'client.flow.state.token';
@@ -66,6 +76,9 @@ class ClientFlowLoginController extends Controller {
* @param IProvider $tokenProvider
* @param ISecureRandom $random
* @param IURLGenerator $urlGenerator
+ * @param ClientMapper $clientMapper
+ * @param AccessTokenMapper $accessTokenMapper
+ * @param ICrypto $crypto
*/
public function __construct($appName,
IRequest $request,
@@ -75,7 +88,10 @@ class ClientFlowLoginController extends Controller {
ISession $session,
IProvider $tokenProvider,
ISecureRandom $random,
- IURLGenerator $urlGenerator) {
+ IURLGenerator $urlGenerator,
+ ClientMapper $clientMapper,
+ AccessTokenMapper $accessTokenMapper,
+ ICrypto $crypto) {
parent::__construct($appName, $request);
$this->userSession = $userSession;
$this->l10n = $l10n;
@@ -84,13 +100,17 @@ class ClientFlowLoginController extends Controller {
$this->tokenProvider = $tokenProvider;
$this->random = $random;
$this->urlGenerator = $urlGenerator;
+ $this->clientMapper = $clientMapper;
+ $this->accessTokenMapper = $accessTokenMapper;
+ $this->crypto = $crypto;
}
/**
* @return string
*/
private function getClientName() {
- return $this->request->getHeader('USER_AGENT') !== null ? $this->request->getHeader('USER_AGENT') : 'unknown';
+ $userAgent = $this->request->getHeader('USER_AGENT');
+ return $userAgent !== null ? $userAgent : 'unknown';
}
/**
@@ -126,15 +146,32 @@ class ClientFlowLoginController extends Controller {
* @NoCSRFRequired
* @UseSession
*
+ * @param string $clientIdentifier
+ *
* @return TemplateResponse
*/
- public function showAuthPickerPage() {
- if($this->userSession->isLoggedIn()) {
+ public function showAuthPickerPage($clientIdentifier = '') {
+ $clientName = $this->getClientName();
+ $client = null;
+ if($clientIdentifier !== '') {
+ $client = $this->clientMapper->getByIdentifier($clientIdentifier);
+ $clientName = $client->getName();
+ }
+
+ // No valid clientIdentifier given and no valid API Request (APIRequest header not set)
+ $clientRequest = $this->request->getHeader('OCS-APIREQUEST');
+ if ($clientRequest !== 'true' && $client === null) {
return new TemplateResponse(
$this->appName,
- '403',
+ 'error',
[
- 'file' => $this->l10n->t('Auth flow can only be started unauthenticated.'),
+ 'errors' =>
+ [
+ [
+ 'error' => 'Access Forbidden',
+ 'hint' => 'Invalid request',
+ ],
+ ],
],
'guest'
);
@@ -150,7 +187,8 @@ class ClientFlowLoginController extends Controller {
$this->appName,
'loginflow/authpicker',
[
- 'client' => $this->getClientName(),
+ 'client' => $clientName,
+ 'clientIdentifier' => $clientIdentifier,
'instanceName' => $this->defaults->getName(),
'urlGenerator' => $this->urlGenerator,
'stateToken' => $stateToken,
@@ -166,9 +204,11 @@ class ClientFlowLoginController extends Controller {
* @UseSession
*
* @param string $stateToken
+ * @param string $clientIdentifier
* @return TemplateResponse
*/
- public function redirectPage($stateToken = '') {
+ public function redirectPage($stateToken = '',
+ $clientIdentifier = '') {
if(!$this->isValidToken($stateToken)) {
return $this->stateTokenForbiddenResponse();
}
@@ -179,6 +219,8 @@ class ClientFlowLoginController extends Controller {
[
'urlGenerator' => $this->urlGenerator,
'stateToken' => $stateToken,
+ 'clientIdentifier' => $clientIdentifier,
+ 'oauthState' => $this->session->get('oauth.state'),
],
'empty'
);
@@ -189,9 +231,11 @@ class ClientFlowLoginController extends Controller {
* @UseSession
*
* @param string $stateToken
+ * @param string $clientIdentifier
* @return Http\RedirectResponse|Response
*/
- public function generateAppPassword($stateToken) {
+ public function generateAppPassword($stateToken,
+ $clientIdentifier = '') {
if(!$this->isValidToken($stateToken)) {
$this->session->remove(self::stateName);
return $this->stateTokenForbiddenResponse();
@@ -221,18 +265,45 @@ class ClientFlowLoginController extends Controller {
return $response;
}
- $token = $this->random->generate(72);
- $this->tokenProvider->generateToken(
+ $clientName = $this->getClientName();
+ $client = false;
+ if($clientIdentifier !== '') {
+ $client = $this->clientMapper->getByIdentifier($clientIdentifier);
+ $clientName = $client->getName();
+ }
+
+ $token = $this->random->generate(72, ISecureRandom::CHAR_UPPER.ISecureRandom::CHAR_LOWER.ISecureRandom::CHAR_DIGITS);
+ $uid = $this->userSession->getUser()->getUID();
+ $generatedToken = $this->tokenProvider->generateToken(
$token,
- $this->userSession->getUser()->getUID(),
+ $uid,
$loginName,
$password,
- $this->getClientName(),
+ $clientName,
IToken::PERMANENT_TOKEN,
IToken::DO_NOT_REMEMBER
);
- return new Http\RedirectResponse('nc://login/server:' . $this->request->getServerHost() . '&user:' . urlencode($loginName) . '&password:' . urlencode($token));
- }
+ if($client) {
+ $code = $this->random->generate(128);
+ $accessToken = new AccessToken();
+ $accessToken->setClientId($client->getId());
+ $accessToken->setEncryptedToken($this->crypto->encrypt($token, $code));
+ $accessToken->setHashedCode(hash('sha512', $code));
+ $accessToken->setTokenId($generatedToken->getId());
+ $this->accessTokenMapper->insert($accessToken);
+ $redirectUri = sprintf(
+ '%s?state=%s&code=%s',
+ $client->getRedirectUri(),
+ urlencode($this->session->get('oauth.state')),
+ urlencode($code)
+ );
+ $this->session->remove('oauth.state');
+ } else {
+ $redirectUri = 'nc://login/server:' . $this->request->getServerHost() . '&user:' . urlencode($loginName) . '&password:' . urlencode($token);
+ }
+
+ return new Http\RedirectResponse($redirectUri);
+ }
}
diff --git a/core/css/login/authpicker.css b/core/css/login/authpicker.css
index 85016ee6a0e..3603a7906e4 100644
--- a/core/css/login/authpicker.css
+++ b/core/css/login/authpicker.css
@@ -1,7 +1,7 @@
.picker-window {
display: block;
padding: 10px;
- margin-bottom: 20px;
+ margin: 20px 0;
background-color: rgba(0,0,0,.3);
color: #fff;
border-radius: 3px;
diff --git a/core/templates/loginflow/authpicker.php b/core/templates/loginflow/authpicker.php
index c5eb6cb316d..c427d657e4a 100644
--- a/core/templates/loginflow/authpicker.php
+++ b/core/templates/loginflow/authpicker.php
@@ -35,7 +35,7 @@ $urlGenerator = $_['urlGenerator'];
<br/>
<p id="redirect-link">
- <a href="<?php p($urlGenerator->linkToRouteAbsolute('core.ClientFlowLogin.redirectPage', ['stateToken' => $_['stateToken']])) ?>">
+ <a href="<?php p($urlGenerator->linkToRouteAbsolute('core.ClientFlowLogin.redirectPage', ['stateToken' => $_['stateToken'], 'clientIdentifier' => $_['clientIdentifier'], 'oauthState' => $_['oauthState']])) ?>">
<input type="submit" class="login primary icon-confirm-white" value="<?php p('Grant access') ?>">
</a>
</p>
@@ -54,4 +54,6 @@ $urlGenerator = $_['urlGenerator'];
</fieldset>
</div>
+<?php if(empty($_['oauthState'])): ?>
<a id="app-token-login" class="warning" href="#"><?php p($l->t('Alternative login using app token')) ?></a>
+<?php endif; ?>
diff --git a/core/templates/loginflow/redirect.php b/core/templates/loginflow/redirect.php
index 7ef0184f61f..1c6e105b88d 100644
--- a/core/templates/loginflow/redirect.php
+++ b/core/templates/loginflow/redirect.php
@@ -31,7 +31,9 @@ $urlGenerator = $_['urlGenerator'];
</div>
<form method="POST" action="<?php p($urlGenerator->linkToRouteAbsolute('core.ClientFlowLogin.generateAppPassword')) ?>">
+ <input type="hidden" name="clientIdentifier" value="<?php p($_['clientIdentifier']) ?>" />
<input type="hidden" name="requesttoken" value="<?php p($_['requesttoken']) ?>" />
<input type="hidden" name="stateToken" value="<?php p($_['stateToken']) ?>" />
+ <input type="hidden" name="oauthState" value="<?php p($_['oauthState']) ?>" />
<input id="submit-redirect-form" type="submit" class="hidden "/>
</form>
diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php
index 2b3a237bfdc..326c0856e50 100644
--- a/lib/composer/composer/autoload_classmap.php
+++ b/lib/composer/composer/autoload_classmap.php
@@ -738,6 +738,7 @@ return array(
'OC\\Repair\\NC12\\InstallCoreBundle' => $baseDir . '/lib/private/Repair/NC12/InstallCoreBundle.php',
'OC\\Repair\\NC12\\UpdateLanguageCodes' => $baseDir . '/lib/private/Repair/NC12/UpdateLanguageCodes.php',
'OC\\Repair\\OldGroupMembershipShares' => $baseDir . '/lib/private/Repair/OldGroupMembershipShares.php',
+ 'OC\\Repair\\Owncloud\\SaveAccountsTableData' => $baseDir . '/lib/private/Repair/Owncloud/SaveAccountsTableData.php',
'OC\\Repair\\RemoveRootShares' => $baseDir . '/lib/private/Repair/RemoveRootShares.php',
'OC\\Repair\\RepairInvalidShares' => $baseDir . '/lib/private/Repair/RepairInvalidShares.php',
'OC\\Repair\\RepairMimeTypes' => $baseDir . '/lib/private/Repair/RepairMimeTypes.php',
diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php
index dc9aabcba73..c1ef42c1273 100644
--- a/lib/composer/composer/autoload_static.php
+++ b/lib/composer/composer/autoload_static.php
@@ -768,6 +768,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
'OC\\Repair\\NC12\\InstallCoreBundle' => __DIR__ . '/../../..' . '/lib/private/Repair/NC12/InstallCoreBundle.php',
'OC\\Repair\\NC12\\UpdateLanguageCodes' => __DIR__ . '/../../..' . '/lib/private/Repair/NC12/UpdateLanguageCodes.php',
'OC\\Repair\\OldGroupMembershipShares' => __DIR__ . '/../../..' . '/lib/private/Repair/OldGroupMembershipShares.php',
+ 'OC\\Repair\\Owncloud\\SaveAccountsTableData' => __DIR__ . '/../../..' . '/lib/private/Repair/Owncloud/SaveAccountsTableData.php',
'OC\\Repair\\RemoveRootShares' => __DIR__ . '/../../..' . '/lib/private/Repair/RemoveRootShares.php',
'OC\\Repair\\RepairInvalidShares' => __DIR__ . '/../../..' . '/lib/private/Repair/RepairInvalidShares.php',
'OC\\Repair\\RepairMimeTypes' => __DIR__ . '/../../..' . '/lib/private/Repair/RepairMimeTypes.php',
diff --git a/lib/private/Authentication/Token/DefaultTokenMapper.php b/lib/private/Authentication/Token/DefaultTokenMapper.php
index 8848cd3ec56..44bc553a92e 100644
--- a/lib/private/Authentication/Token/DefaultTokenMapper.php
+++ b/lib/private/Authentication/Token/DefaultTokenMapper.php
@@ -149,4 +149,16 @@ class DefaultTokenMapper extends Mapper {
$qb->execute();
}
+ /**
+ * delete all auth token which belong to a specific client if the client was deleted
+ *
+ * @param string $name
+ */
+ public function deleteByName($name) {
+ $qb = $this->db->getQueryBuilder();
+ $qb->delete('authtoken')
+ ->where($qb->expr()->eq('name', $qb->createNamedParameter($name)));
+ $qb->execute();
+ }
+
}
diff --git a/lib/private/Files/Cache/Scanner.php b/lib/private/Files/Cache/Scanner.php
index fe25da6d09a..229c6fc7d66 100644
--- a/lib/private/Files/Cache/Scanner.php
+++ b/lib/private/Files/Cache/Scanner.php
@@ -266,6 +266,9 @@ class Scanner extends BasicEmitter implements IScanner {
* @return int the id of the added file
*/
protected function addToCache($path, $data, $fileId = -1) {
+ if (isset($data['scan_permissions'])) {
+ $data['permissions'] = $data['scan_permissions'];
+ }
\OC_Hook::emit('Scanner', 'addToCache', array('file' => $path, 'data' => $data));
$this->emit('\OC\Files\Cache\Scanner', 'addToCache', array($path, $this->storageId, $data));
if ($this->cacheActive) {
diff --git a/lib/private/Files/Cache/Wrapper/CachePermissionsMask.php b/lib/private/Files/Cache/Wrapper/CachePermissionsMask.php
index 83cf01045a7..6125887fb83 100644
--- a/lib/private/Files/Cache/Wrapper/CachePermissionsMask.php
+++ b/lib/private/Files/Cache/Wrapper/CachePermissionsMask.php
@@ -40,6 +40,7 @@ class CachePermissionsMask extends CacheWrapper {
protected function formatCacheEntry($entry) {
if (isset($entry['permissions'])) {
+ $entry['scan_permissions'] = $entry['permissions'];
$entry['permissions'] &= $this->mask;
}
return $entry;
diff --git a/lib/private/Files/Storage/Wrapper/PermissionsMask.php b/lib/private/Files/Storage/Wrapper/PermissionsMask.php
index d66390dad49..1bda92f13aa 100644
--- a/lib/private/Files/Storage/Wrapper/PermissionsMask.php
+++ b/lib/private/Files/Storage/Wrapper/PermissionsMask.php
@@ -143,12 +143,16 @@ class PermissionsMask extends Wrapper {
$data = parent::getMetaData($path);
if ($data && isset($data['permissions'])) {
- $data['permissions'] = $data['permissions'] & $this->mask;
+ $data['scan_permissions'] = $data['permissions'];
+ $data['permissions'] &= $this->mask;
}
return $data;
}
public function getScanner($path = '', $storage = null) {
- return parent::getScanner($path, $this->storage);
+ if (!$storage) {
+ $storage = $this->storage;
+ }
+ return parent::getScanner($path, $storage);
}
}
diff --git a/lib/private/Repair.php b/lib/private/Repair.php
index 65e0342905a..4d14bf2550c 100644
--- a/lib/private/Repair.php
+++ b/lib/private/Repair.php
@@ -40,6 +40,7 @@ use OC\Repair\NC11\MoveAvatars;
use OC\Repair\NC12\InstallCoreBundle;
use OC\Repair\NC12\UpdateLanguageCodes;
use OC\Repair\OldGroupMembershipShares;
+use OC\Repair\Owncloud\SaveAccountsTableData;
use OC\Repair\RemoveRootShares;
use OC\Repair\SqliteAutoincrement;
use OC\Repair\RepairMimeTypes;
@@ -166,9 +167,11 @@ class Repair implements IOutput{
*/
public static function getBeforeUpgradeRepairSteps() {
$connection = \OC::$server->getDatabaseConnection();
+ $config = \OC::$server->getConfig();
$steps = [
new Collation(\OC::$server->getConfig(), \OC::$server->getLogger(), $connection, true),
new SqliteAutoincrement($connection),
+ new SaveAccountsTableData($connection, $config),
];
return $steps;
diff --git a/lib/private/Repair/NC11/MoveAvatarsBackgroundJob.php b/lib/private/Repair/NC11/MoveAvatarsBackgroundJob.php
index f8c0d9b3abf..d46b6fec8fc 100644
--- a/lib/private/Repair/NC11/MoveAvatarsBackgroundJob.php
+++ b/lib/private/Repair/NC11/MoveAvatarsBackgroundJob.php
@@ -28,6 +28,7 @@ use OCP\Files\Folder;
use OCP\Files\IAppData;
use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException;
+use OCP\Files\SimpleFS\ISimpleFolder;
use OCP\ILogger;
use OCP\IUser;
use OCP\IUserManager;
@@ -63,8 +64,14 @@ class MoveAvatarsBackgroundJob extends QueuedJob {
}
private function moveAvatars() {
+ try {
+ $ownCloudAvatars = $this->rootFolder->get('avatars');
+ } catch (NotFoundException $e) {
+ $ownCloudAvatars = null;
+ }
+
$counter = 0;
- $this->userManager->callForSeenUsers(function (IUser $user) use ($counter) {
+ $this->userManager->callForSeenUsers(function (IUser $user) use ($counter, $ownCloudAvatars) {
$uid = $user->getUID();
\OC\Files\Filesystem::initMountPoints($uid);
@@ -77,26 +84,61 @@ class MoveAvatarsBackgroundJob extends QueuedJob {
$userData = $this->appData->newFolder($uid);
}
+ $foundAvatars = $this->copyAvatarsFromFolder($userFolder, $userData);
- $regex = '/^avatar\.([0-9]+\.)?(jpg|png)$/';
- $avatars = $userFolder->getDirectoryListing();
-
- foreach ($avatars as $avatar) {
- /** @var File $avatar */
- if (preg_match($regex, $avatar->getName())) {
- /*
- * This is not the most effective but it is the most abstract way
- * to handle this. Avatars should be small anyways.
- */
- $newAvatar = $userData->newFile($avatar->getName());
- $newAvatar->putContent($avatar->getContent());
- $avatar->delete();
+ // ownCloud migration?
+ if ($foundAvatars === 0 && $ownCloudAvatars instanceof Folder) {
+ $parts = $this->buildOwnCloudAvatarPath($uid);
+ $userOwnCloudAvatar = $ownCloudAvatars;
+ foreach ($parts as $part) {
+ try {
+ $userOwnCloudAvatar = $userOwnCloudAvatar->get($part);
+ } catch (NotFoundException $e) {
+ return;
+ }
}
+
+ $this->copyAvatarsFromFolder($userOwnCloudAvatar, $userData);
}
+
$counter++;
- if ($counter % 100) {
+ if ($counter % 100 === 0) {
$this->logger->info('{amount} avatars migrated', ['amount' => $counter]);
}
});
}
+
+ /**
+ * @param Folder $source
+ * @param ISimpleFolder $target
+ * @return int
+ * @throws \OCP\Files\NotPermittedException
+ * @throws NotFoundException
+ */
+ protected function copyAvatarsFromFolder(Folder $source, ISimpleFolder $target) {
+ $foundAvatars = 0;
+ $avatars = $source->getDirectoryListing();
+ $regex = '/^avatar\.([0-9]+\.)?(jpg|png)$/';
+
+ foreach ($avatars as $avatar) {
+ /** @var File $avatar */
+ if (preg_match($regex, $avatar->getName())) {
+ /*
+ * This is not the most effective but it is the most abstract way
+ * to handle this. Avatars should be small anyways.
+ */
+ $newAvatar = $target->newFile($avatar->getName());
+ $newAvatar->putContent($avatar->getContent());
+ $avatar->delete();
+ $foundAvatars++;
+ }
+ }
+
+ return $foundAvatars;
+ }
+
+ protected function buildOwnCloudAvatarPath($userId) {
+ $avatar = substr_replace(substr_replace(md5($userId), '/', 4, 0), '/', 2, 0);
+ return explode('/', $avatar);
+ }
}
diff --git a/lib/private/Repair/Owncloud/SaveAccountsTableData.php b/lib/private/Repair/Owncloud/SaveAccountsTableData.php
new file mode 100644
index 00000000000..35e5560856b
--- /dev/null
+++ b/lib/private/Repair/Owncloud/SaveAccountsTableData.php
@@ -0,0 +1,176 @@
+<?php
+/**
+ * @copyright Copyright (c) 2017 Joas Schilling <coding@schilljs.com>
+ *
+ * @author Joas Schilling <coding@schilljs.com>
+ *
+ * @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\Repair\Owncloud;
+
+use Doctrine\DBAL\Exception\InvalidFieldNameException;
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\IConfig;
+use OCP\IDBConnection;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+use OCP\PreConditionNotMetException;
+
+/**
+ * Copies the email address from the accounts table to the preference table,
+ * before the data structure is changed and the information is gone
+ */
+class SaveAccountsTableData implements IRepairStep {
+
+ const BATCH_SIZE = 75;
+
+ /** @var IDBConnection */
+ protected $db;
+
+ /** @var IConfig */
+ protected $config;
+
+ /**
+ * @param IDBConnection $db
+ * @param IConfig $config
+ */
+ public function __construct(IDBConnection $db, IConfig $config) {
+ $this->db = $db;
+ $this->config = $config;
+ }
+
+ /**
+ * @return string
+ */
+ public function getName() {
+ return 'Copy data from accounts table when migrating from ownCloud';
+ }
+
+ /**
+ * @param IOutput $output
+ */
+ public function run(IOutput $output) {
+ if (!$this->shouldRun()) {
+ return;
+ }
+
+ $offset = 0;
+ $numUsers = $this->runStep($offset);
+
+ while ($numUsers === self::BATCH_SIZE) {
+ $offset += $numUsers;
+ $numUsers = $this->runStep($offset);
+ }
+
+ // Remove the table
+ $this->db->dropTable('accounts');
+ }
+
+ /**
+ * @return bool
+ */
+ protected function shouldRun() {
+ $query = $this->db->getQueryBuilder();
+ $query->select('*')
+ ->from('accounts')
+ ->where($query->expr()->isNotNull('user_id'))
+ ->setMaxResults(1);
+
+ try {
+ $query->execute();
+ return true;
+ } catch (InvalidFieldNameException $e) {
+ return false;
+ }
+ }
+
+ /**
+ * @param int $offset
+ * @return int Number of copied users
+ */
+ protected function runStep($offset) {
+ $query = $this->db->getQueryBuilder();
+ $query->select('*')
+ ->from('accounts')
+ ->orderBy('id')
+ ->setMaxResults(self::BATCH_SIZE);
+
+ if ($offset > 0) {
+ $query->setFirstResult($offset);
+ }
+
+ $result = $query->execute();
+
+ $update = $this->db->getQueryBuilder();
+ $update->update('users')
+ ->set('displayname', $update->createParameter('displayname'))
+ ->where($update->expr()->eq('uid', $update->createParameter('userid')));
+
+ $updatedUsers = 0;
+ while ($row = $result->fetch()) {
+ try {
+ $this->migrateUserInfo($update, $row);
+ } catch (PreConditionNotMetException $e) {
+ // Ignore and continue
+ } catch (\UnexpectedValueException $e) {
+ // Ignore and continue
+ }
+ $updatedUsers++;
+ }
+ $result->closeCursor();
+
+ return $updatedUsers;
+ }
+
+ /**
+ * @param IQueryBuilder $update
+ * @param array $userdata
+ * @throws PreConditionNotMetException
+ * @throws \UnexpectedValueException
+ */
+ protected function migrateUserInfo(IQueryBuilder $update, $userdata) {
+ $state = (int) $userdata['state'];
+ if ($state === 3) {
+ // Deleted user, ignore
+ return;
+ }
+
+ if ($userdata['email'] !== null) {
+ $this->config->setUserValue($userdata['user_id'], 'settings', 'email', $userdata['email']);
+ }
+ if ($userdata['quota'] !== null) {
+ $this->config->setUserValue($userdata['user_id'], 'files', 'quota', $userdata['quota']);
+ }
+ if ($userdata['last_login'] !== null) {
+ $this->config->setUserValue($userdata['user_id'], 'login', 'lastLogin', $userdata['last_login']);
+ }
+ if ($state === 1) {
+ $this->config->setUserValue($userdata['user_id'], 'core', 'enabled', 'true');
+ } else if ($state === 2) {
+ $this->config->setUserValue($userdata['user_id'], 'core', 'enabled', 'false');
+ }
+
+ if ($userdata['display_name'] !== null) {
+ $update->setParameter('displayname', $userdata['display_name'])
+ ->setParameter('userid', $userdata['user_id']);
+ $update->execute();
+ }
+
+ }
+}
+
diff --git a/lib/private/Updater.php b/lib/private/Updater.php
index 6f81e6175f3..6d08e5d4cc0 100644
--- a/lib/private/Updater.php
+++ b/lib/private/Updater.php
@@ -190,6 +190,22 @@ class Updater extends BasicEmitter {
$majorMinor = $version[0] . '.' . $version[1];
$currentVendor = $this->config->getAppValue('core', 'vendor', '');
+
+ // Vendor was not set correctly on install, so we have to white-list known versions
+ if ($currentVendor === '') {
+ if (in_array($oldVersion, [
+ '11.0.2.7',
+ '11.0.1.2',
+ '11.0.0.10',
+ ], true)) {
+ $currentVendor = 'nextcloud';
+ } else if (in_array($oldVersion, [
+ '10.0.0.12',
+ ], true)) {
+ $currentVendor = 'owncloud';
+ }
+ }
+
if ($currentVendor === 'nextcloud') {
return isset($allowedPreviousVersions[$currentVendor][$majorMinor])
&& (version_compare($oldVersion, $newVersion, '<=') ||
@@ -197,7 +213,8 @@ class Updater extends BasicEmitter {
}
// Check if the instance can be migrated
- return isset($allowedPreviousVersions[$currentVendor][$majorMinor]);
+ return isset($allowedPreviousVersions[$currentVendor][$majorMinor]) ||
+ isset($allowedPreviousVersions[$currentVendor][$oldVersion]);
}
/**
diff --git a/lib/private/User/Session.php b/lib/private/User/Session.php
index f818666c374..0291c1baecb 100644
--- a/lib/private/User/Session.php
+++ b/lib/private/User/Session.php
@@ -725,7 +725,7 @@ class Session implements IUserSession, Emitter {
*/
public function tryTokenLogin(IRequest $request) {
$authHeader = $request->getHeader('Authorization');
- if (strpos($authHeader, 'token ') === false) {
+ if (strpos($authHeader, 'Bearer ') === false) {
// No auth header, let's try session id
try {
$token = $this->session->getId();
@@ -733,7 +733,7 @@ class Session implements IUserSession, Emitter {
return false;
}
} else {
- $token = substr($authHeader, 6);
+ $token = substr($authHeader, 7);
}
if (!$this->loginWithToken($token)) {
diff --git a/settings/personal.php b/settings/personal.php
index 86ac4f753f4..7f99dc3259b 100644
--- a/settings/personal.php
+++ b/settings/personal.php
@@ -49,7 +49,7 @@ $config = \OC::$server->getConfig();
$urlGenerator = \OC::$server->getURLGenerator();
// Highlight navigation entry
-OC_Util::addScript('settings', 'authtoken');
+OC_Util::addScript('settings', 'AuthToken');
OC_Util::addScript('settings', 'authtoken_collection');
OC_Util::addScript('settings', 'authtoken_view');
OC_Util::addScript('settings', 'usersettings');
diff --git a/tests/Core/Controller/ClientFlowLoginControllerTest.php b/tests/Core/Controller/ClientFlowLoginControllerTest.php
index 7a98e5c26c6..1132c0f540c 100644
--- a/tests/Core/Controller/ClientFlowLoginControllerTest.php
+++ b/tests/Core/Controller/ClientFlowLoginControllerTest.php
@@ -26,6 +26,9 @@ use OC\Authentication\Exceptions\PasswordlessTokenException;
use OC\Authentication\Token\IProvider;
use OC\Authentication\Token\IToken;
use OC\Core\Controller\ClientFlowLoginController;
+use OCA\OAuth2\Db\AccessTokenMapper;
+use OCA\OAuth2\Db\Client;
+use OCA\OAuth2\Db\ClientMapper;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\Defaults;
@@ -35,6 +38,7 @@ use OCP\ISession;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserSession;
+use OCP\Security\ICrypto;
use OCP\Security\ISecureRandom;
use OCP\Session\Exceptions\SessionNotAvailableException;
use Test\TestCase;
@@ -56,6 +60,13 @@ class ClientFlowLoginControllerTest extends TestCase {
private $random;
/** @var IURLGenerator|\PHPUnit_Framework_MockObject_MockObject */
private $urlGenerator;
+ /** @var ClientMapper|\PHPUnit_Framework_MockObject_MockObject */
+ private $clientMapper;
+ /** @var AccessTokenMapper|\PHPUnit_Framework_MockObject_MockObject */
+ private $accessTokenMapper;
+ /** @var ICrypto|\PHPUnit_Framework_MockObject_MockObject */
+ private $crypto;
+
/** @var ClientFlowLoginController */
private $clientFlowLoginController;
@@ -76,6 +87,9 @@ class ClientFlowLoginControllerTest extends TestCase {
$this->tokenProvider = $this->createMock(IProvider::class);
$this->random = $this->createMock(ISecureRandom::class);
$this->urlGenerator = $this->createMock(IURLGenerator::class);
+ $this->clientMapper = $this->createMock(ClientMapper::class);
+ $this->accessTokenMapper = $this->createMock(AccessTokenMapper::class);
+ $this->crypto = $this->createMock(ICrypto::class);
$this->clientFlowLoginController = new ClientFlowLoginController(
'core',
@@ -86,32 +100,43 @@ class ClientFlowLoginControllerTest extends TestCase {
$this->session,
$this->tokenProvider,
$this->random,
- $this->urlGenerator
+ $this->urlGenerator,
+ $this->clientMapper,
+ $this->accessTokenMapper,
+ $this->crypto
);
}
- public function testShowAuthPickerPageNotAuthenticated() {
- $this->userSession
- ->expects($this->once())
- ->method('isLoggedIn')
- ->willReturn(true);
-
+ public function testShowAuthPickerPageNoClientOrOauthRequest() {
$expected = new TemplateResponse(
'core',
- '403',
+ 'error',
[
- 'file' => 'Auth flow can only be started unauthenticated.',
+ 'errors' =>
+ [
+ [
+ 'error' => 'Access Forbidden',
+ 'hint' => 'Invalid request',
+ ],
+ ],
],
'guest'
);
+
$this->assertEquals($expected, $this->clientFlowLoginController->showAuthPickerPage());
}
- public function testShowAuthPickerPage() {
- $this->userSession
- ->expects($this->once())
- ->method('isLoggedIn')
- ->willReturn(false);
+ public function testShowAuthPickerPageWithOcsHeader() {
+ $this->request
+ ->expects($this->at(0))
+ ->method('getHeader')
+ ->with('USER_AGENT')
+ ->willReturn('Mac OS X Sync Client');
+ $this->request
+ ->expects($this->at(1))
+ ->method('getHeader')
+ ->with('OCS-APIREQUEST')
+ ->willReturn('true');
$this->random
->expects($this->once())
->method('generate')
@@ -124,11 +149,56 @@ class ClientFlowLoginControllerTest extends TestCase {
->expects($this->once())
->method('set')
->with('client.flow.state.token', 'StateToken');
+ $this->defaults
+ ->expects($this->once())
+ ->method('getName')
+ ->willReturn('ExampleCloud');
+ $this->request
+ ->expects($this->once())
+ ->method('getServerHost')
+ ->willReturn('example.com');
+
+ $expected = new TemplateResponse(
+ 'core',
+ 'loginflow/authpicker',
+ [
+ 'client' => 'Mac OS X Sync Client',
+ 'clientIdentifier' => '',
+ 'instanceName' => 'ExampleCloud',
+ 'urlGenerator' => $this->urlGenerator,
+ 'stateToken' => 'StateToken',
+ 'serverHost' => 'example.com',
+ ],
+ 'guest'
+ );
+ $this->assertEquals($expected, $this->clientFlowLoginController->showAuthPickerPage());
+ }
+
+ public function testShowAuthPickerPageWithOauth() {
$this->request
- ->expects($this->exactly(2))
+ ->expects($this->at(0))
->method('getHeader')
->with('USER_AGENT')
->willReturn('Mac OS X Sync Client');
+ $client = new Client();
+ $client->setName('My external service');
+ $this->clientMapper
+ ->expects($this->once())
+ ->method('getByIdentifier')
+ ->with('MyClientIdentifier')
+ ->willReturn($client);
+ $this->random
+ ->expects($this->once())
+ ->method('generate')
+ ->with(
+ 64,
+ ISecureRandom::CHAR_LOWER.ISecureRandom::CHAR_UPPER.ISecureRandom::CHAR_DIGITS
+ )
+ ->willReturn('StateToken');
+ $this->session
+ ->expects($this->once())
+ ->method('set')
+ ->with('client.flow.state.token', 'StateToken');
$this->defaults
->expects($this->once())
->method('getName')
@@ -142,7 +212,8 @@ class ClientFlowLoginControllerTest extends TestCase {
'core',
'loginflow/authpicker',
[
- 'client' => 'Mac OS X Sync Client',
+ 'client' => 'My external service',
+ 'clientIdentifier' => 'MyClientIdentifier',
'instanceName' => 'ExampleCloud',
'urlGenerator' => $this->urlGenerator,
'stateToken' => 'StateToken',
@@ -150,7 +221,7 @@ class ClientFlowLoginControllerTest extends TestCase {
],
'guest'
);
- $this->assertEquals($expected, $this->clientFlowLoginController->showAuthPickerPage());
+ $this->assertEquals($expected, $this->clientFlowLoginController->showAuthPickerPage('MyClientIdentifier'));
}
public function testRedirectPageWithInvalidToken() {
@@ -193,10 +264,15 @@ class ClientFlowLoginControllerTest extends TestCase {
public function testRedirectPage() {
$this->session
- ->expects($this->once())
+ ->expects($this->at(0))
->method('get')
->with('client.flow.state.token')
->willReturn('MyStateToken');
+ $this->session
+ ->expects($this->at(1))
+ ->method('get')
+ ->with('oauth.state')
+ ->willReturn('MyOauthStateToken');
$expected = new TemplateResponse(
'core',
@@ -204,10 +280,12 @@ class ClientFlowLoginControllerTest extends TestCase {
[
'urlGenerator' => $this->urlGenerator,
'stateToken' => 'MyStateToken',
+ 'clientIdentifier' => 'Identifier',
+ 'oauthState' => 'MyOauthStateToken',
],
'empty'
);
- $this->assertEquals($expected, $this->clientFlowLoginController->redirectPage('MyStateToken'));
+ $this->assertEquals($expected, $this->clientFlowLoginController->redirectPage('MyStateToken', 'Identifier'));
}
public function testGenerateAppPasswordWithInvalidToken() {
@@ -342,6 +420,90 @@ class ClientFlowLoginControllerTest extends TestCase {
$this->assertEquals($expected, $this->clientFlowLoginController->generateAppPassword('MyStateToken'));
}
+ public function testGeneratePasswordWithPasswordForOauthClient() {
+ $this->session
+ ->expects($this->at(0))
+ ->method('get')
+ ->with('client.flow.state.token')
+ ->willReturn('MyStateToken');
+ $this->session
+ ->expects($this->at(1))
+ ->method('remove')
+ ->with('client.flow.state.token');
+ $this->session
+ ->expects($this->at(3))
+ ->method('get')
+ ->with('oauth.state')
+ ->willReturn('MyOauthState');
+ $this->session
+ ->expects($this->at(4))
+ ->method('remove')
+ ->with('oauth.state');
+ $this->session
+ ->expects($this->once())
+ ->method('getId')
+ ->willReturn('SessionId');
+ $myToken = $this->createMock(IToken::class);
+ $myToken
+ ->expects($this->once())
+ ->method('getLoginName')
+ ->willReturn('MyLoginName');
+ $this->tokenProvider
+ ->expects($this->once())
+ ->method('getToken')
+ ->with('SessionId')
+ ->willReturn($myToken);
+ $this->tokenProvider
+ ->expects($this->once())
+ ->method('getPassword')
+ ->with($myToken, 'SessionId')
+ ->willReturn('MyPassword');
+ $this->random
+ ->expects($this->at(0))
+ ->method('generate')
+ ->with(72)
+ ->willReturn('MyGeneratedToken');
+ $this->random
+ ->expects($this->at(1))
+ ->method('generate')
+ ->with(128)
+ ->willReturn('MyAccessCode');
+ $user = $this->createMock(IUser::class);
+ $user
+ ->expects($this->once())
+ ->method('getUID')
+ ->willReturn('MyUid');
+ $this->userSession
+ ->expects($this->once())
+ ->method('getUser')
+ ->willReturn($user);
+ $token = $this->createMock(IToken::class);
+ $this->tokenProvider
+ ->expects($this->once())
+ ->method('generateToken')
+ ->with(
+ 'MyGeneratedToken',
+ 'MyUid',
+ 'MyLoginName',
+ 'MyPassword',
+ 'My OAuth client',
+ IToken::PERMANENT_TOKEN,
+ IToken::DO_NOT_REMEMBER
+ )
+ ->willReturn($token);
+ $client = new Client();
+ $client->setName('My OAuth client');
+ $client->setRedirectUri('https://example.com/redirect.php');
+ $this->clientMapper
+ ->expects($this->once())
+ ->method('getByIdentifier')
+ ->with('MyClientIdentifier')
+ ->willReturn($client);
+
+ $expected = new Http\RedirectResponse('https://example.com/redirect.php?state=MyOauthState&code=MyAccessCode');
+ $this->assertEquals($expected, $this->clientFlowLoginController->generateAppPassword('MyStateToken', 'MyClientIdentifier'));
+ }
+
public function testGeneratePasswordWithoutPassword() {
$this->session
->expects($this->once())
diff --git a/tests/lib/Authentication/Token/DefaultTokenMapperTest.php b/tests/lib/Authentication/Token/DefaultTokenMapperTest.php
index 8fe0762daad..b5d24a7ab5e 100644
--- a/tests/lib/Authentication/Token/DefaultTokenMapperTest.php
+++ b/tests/lib/Authentication/Token/DefaultTokenMapperTest.php
@@ -190,6 +190,7 @@ class DefaultTokenMapperTest extends TestCase {
}
public function testGetTokenByUser() {
+ /** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */
$user = $this->createMock(IUser::class);
$user->expects($this->once())
->method('getUID')
@@ -199,6 +200,7 @@ class DefaultTokenMapperTest extends TestCase {
}
public function testGetTokenByUserNotFound() {
+ /** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */
$user = $this->createMock(IUser::class);
$user->expects($this->once())
->method('getUID')
@@ -208,6 +210,7 @@ class DefaultTokenMapperTest extends TestCase {
}
public function testDeleteById() {
+ /** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */
$user = $this->createMock(IUser::class);
$qb = $this->dbConnection->getQueryBuilder();
$qb->select('id')
@@ -224,6 +227,7 @@ class DefaultTokenMapperTest extends TestCase {
}
public function testDeleteByIdWrongUser() {
+ /** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */
$user = $this->createMock(IUser::class);
$id = 33;
$user->expects($this->once())
@@ -234,4 +238,15 @@ class DefaultTokenMapperTest extends TestCase {
$this->assertEquals(3, $this->getNumberOfTokens());
}
+ public function testDeleteByName() {
+ $qb = $this->dbConnection->getQueryBuilder();
+ $qb->select('name')
+ ->from('authtoken')
+ ->where($qb->expr()->eq('token', $qb->createNamedParameter('9c5a2e661482b65597408a6bb6c4a3d1af36337381872ac56e445a06cdb7fea2b1039db707545c11027a4966919918b19d875a8b774840b18c6cbb7ae56fe206')));
+ $result = $qb->execute();
+ $name = $result->fetch()['name'];
+ $this->mapper->deleteByName($name);
+ $this->assertEquals(2, $this->getNumberOfTokens());
+ }
+
}
diff --git a/tests/lib/Files/Storage/Wrapper/PermissionsMaskTest.php b/tests/lib/Files/Storage/Wrapper/PermissionsMaskTest.php
index d0903ce5f97..354db9d069d 100644
--- a/tests/lib/Files/Storage/Wrapper/PermissionsMaskTest.php
+++ b/tests/lib/Files/Storage/Wrapper/PermissionsMaskTest.php
@@ -8,7 +8,9 @@
namespace Test\Files\Storage\Wrapper;
+use OC\Files\Storage\Wrapper\Wrapper;
use OCP\Constants;
+use OCP\Files\Cache\IScanner;
/**
* @group DB
@@ -114,4 +116,49 @@ class PermissionsMaskTest extends \Test\Files\Storage\Storage {
$this->assertEquals(Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE, $this->sourceStorage->getCache()->get('foo')->getPermissions());
$this->assertEquals(Constants::PERMISSION_READ, $storage->getCache()->get('foo')->getPermissions());
}
+
+ public function testScanNewWrappedFiles() {
+ $storage = $this->getMaskedStorage(Constants::PERMISSION_READ + Constants::PERMISSION_CREATE);
+ $wrappedStorage = new Wrapper(['storage' => $storage]);
+ $wrappedStorage->file_put_contents('foo', 'bar');
+ $wrappedStorage->getScanner()->scan('');
+
+ $this->assertEquals(Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE, $this->sourceStorage->getCache()->get('foo')->getPermissions());
+ $this->assertEquals(Constants::PERMISSION_READ, $storage->getCache()->get('foo')->getPermissions());
+ }
+
+ public function testScanUnchanged() {
+ $this->sourceStorage->mkdir('foo');
+ $this->sourceStorage->file_put_contents('foo/bar.txt', 'bar');
+
+ $this->sourceStorage->getScanner()->scan('foo');
+
+ $storage = $this->getMaskedStorage(Constants::PERMISSION_READ);
+ $scanner = $storage->getScanner();
+ $called = false;
+ $scanner->listen('\OC\Files\Cache\Scanner', 'addToCache', function () use (&$called) {
+ $called = true;
+ });
+ $scanner->scan('foo', IScanner::SCAN_RECURSIVE, IScanner::REUSE_ETAG | IScanner::REUSE_SIZE);
+
+ $this->assertFalse($called);
+ }
+
+ public function testScanUnchangedWrapped() {
+ $this->sourceStorage->mkdir('foo');
+ $this->sourceStorage->file_put_contents('foo/bar.txt', 'bar');
+
+ $this->sourceStorage->getScanner()->scan('foo');
+
+ $storage = $this->getMaskedStorage(Constants::PERMISSION_READ);
+ $wrappedStorage = new Wrapper(['storage' => $storage]);
+ $scanner = $wrappedStorage->getScanner();
+ $called = false;
+ $scanner->listen('\OC\Files\Cache\Scanner', 'addToCache', function () use (&$called) {
+ $called = true;
+ });
+ $scanner->scan('foo', IScanner::SCAN_RECURSIVE, IScanner::REUSE_ETAG | IScanner::REUSE_SIZE);
+
+ $this->assertFalse($called);
+ }
}
diff --git a/tests/lib/User/SessionTest.php b/tests/lib/User/SessionTest.php
index 1bcc6ce3a4d..fcff4f64726 100644
--- a/tests/lib/User/SessionTest.php
+++ b/tests/lib/User/SessionTest.php
@@ -956,7 +956,7 @@ class SessionTest extends \Test\TestCase {
$request->expects($this->once())
->method('getHeader')
->with('Authorization')
- ->will($this->returnValue('token xxxxx'));
+ ->will($this->returnValue('Bearer xxxxx'));
$this->tokenProvider->expects($this->once())
->method('getToken')
->with('xxxxx')
diff --git a/tests/phpunit-autotest.xml b/tests/phpunit-autotest.xml
index 9a9c9c957e3..34166a09e2e 100644
--- a/tests/phpunit-autotest.xml
+++ b/tests/phpunit-autotest.xml
@@ -30,6 +30,7 @@
<directory suffix=".php">../apps/files_sharing/tests</directory>
<directory suffix=".php">../apps/files_trashbin/tests</directory>
<directory suffix=".php">../apps/files_versions/tests</directory>
+ <directory suffix=".php">../apps/oauth2/tests</directory>
<directory suffix=".php">../apps/provisioning_api/tests</directory>
<directory suffix=".php">../apps/systemtags/tests</directory>
<directory suffix=".php">../apps/theming/tests</directory>
diff --git a/version.php b/version.php
index 37e15f35e1f..8673939044f 100644
--- a/version.php
+++ b/version.php
@@ -37,6 +37,7 @@ $OC_VersionCanBeUpgradedFrom = [
'12.0' => true,
],
'owncloud' => [
+ '10.0.0.12' => true,
],
];