Merge pull request #4704 from nextcloud/add-oauth-code-flow-support

Add oauth code flow support
This commit is contained in:
Lukas Reschke 2017-05-18 23:30:44 +02:00 committed by GitHub
commit 0eb4970ec8
46 changed files with 1971 additions and 52 deletions

1
.gitignore vendored
View File

@ -24,6 +24,7 @@
!/apps/files_versions
!/apps/lookup_server_connector
!/apps/user_ldap
!/apps/oauth2
!/apps/provisioning_api
!/apps/systemtags
!/apps/testing

View File

@ -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();

View File

@ -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();
});

View File

@ -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

View File

@ -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;
}
}

View File

@ -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));

View File

@ -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);

View File

@ -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'));
}
}

View File

@ -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);

View File

@ -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>

View File

@ -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>

View File

@ -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'
],
],
];

View File

@ -0,0 +1,5 @@
.show-oauth-credentials {
padding-left: 10px;
opacity: 0.3;
cursor: pointer;
}

View File

@ -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);
}
})
});

View File

@ -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);
}
}

View File

@ -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(),
]
);
}
}

View File

@ -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'));
}
}

View File

@ -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');
}
}

View File

@ -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();
}
}

View File

@ -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');
}
}

View File

@ -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());
}
}

View File

@ -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 {}

View File

@ -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 {}

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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'));
}
}

View File

@ -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'));
}
}

View File

@ -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));
}
}

View File

@ -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');
}
}

View File

@ -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()));
}
}

View File

@ -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());
}
}

View File

@ -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

View File

@ -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);
}
/**

View File

@ -342,6 +342,7 @@ Feature: provisioning
| updatenotification |
| workflowengine |
| files_external |
| oauth2 |
Scenario: get app info
Given As an "admin"

View File

@ -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

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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; ?>

View File

@ -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>

View File

@ -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();
}
}

View File

@ -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)) {

View File

@ -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');

View File

@ -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,6 @@ class ClientFlowLoginControllerTest extends TestCase {
->expects($this->once())
->method('set')
->with('client.flow.state.token', 'StateToken');
$this->request
->expects($this->exactly(2))
->method('getHeader')
->with('USER_AGENT')
->willReturn('Mac OS X Sync Client');
$this->defaults
->expects($this->once())
->method('getName')
@ -143,6 +163,7 @@ class ClientFlowLoginControllerTest extends TestCase {
'loginflow/authpicker',
[
'client' => 'Mac OS X Sync Client',
'clientIdentifier' => '',
'instanceName' => 'ExampleCloud',
'urlGenerator' => $this->urlGenerator,
'stateToken' => 'StateToken',
@ -153,6 +174,56 @@ class ClientFlowLoginControllerTest extends TestCase {
$this->assertEquals($expected, $this->clientFlowLoginController->showAuthPickerPage());
}
public function testShowAuthPickerPageWithOauth() {
$this->request
->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')
->willReturn('ExampleCloud');
$this->request
->expects($this->once())
->method('getServerHost')
->willReturn('example.com');
$expected = new TemplateResponse(
'core',
'loginflow/authpicker',
[
'client' => 'My external service',
'clientIdentifier' => 'MyClientIdentifier',
'instanceName' => 'ExampleCloud',
'urlGenerator' => $this->urlGenerator,
'stateToken' => 'StateToken',
'serverHost' => 'example.com',
],
'guest'
);
$this->assertEquals($expected, $this->clientFlowLoginController->showAuthPickerPage('MyClientIdentifier'));
}
public function testRedirectPageWithInvalidToken() {
$this->session
->expects($this->once())
@ -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())

View File

@ -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());
}
}

View File

@ -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')

View File

@ -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>