Browse Source
Login flow V2
Login flow V2
This adds the new login flow. The desktop client will open up a browser and poll a returned endpoint at regular intervals to check if the flow is done. Signed-off-by: Roeland Jago Douma <roeland@famdouma.nl>pull/14161/head
No known key found for this signature in database
GPG Key ID: F941078878347C0C
19 changed files with 1574 additions and 1 deletions
-
46core/BackgroundJobs/CleanupLoginFlowV2.php
-
299core/Controller/ClientFlowLoginV2Controller.php
-
71core/Data/LoginFlowV2Credentials.php
-
47core/Data/LoginFlowV2Tokens.php
-
85core/Db/LoginFlowV2.php
-
100core/Db/LoginFlowV2Mapper.php
-
29core/Exception/LoginFlowV2NotFoundException.php
-
101core/Migrations/Version16000Date20190212081545.php
-
260core/Service/LoginFlowV2Service.php
-
8core/routes.php
-
46core/templates/loginflowv2/authpicker.php
-
39core/templates/loginflowv2/done.php
-
50core/templates/loginflowv2/grant.php
-
10lib/composer/composer/autoload_classmap.php
-
10lib/composer/composer/autoload_static.php
-
2lib/private/Repair.php
-
49lib/private/Repair/NC16/AddClenupLoginFlowV2BackgroundJob.php
-
321tests/Core/Controller/ClientFlowLoginV2ControllerTest.php
-
2version.php
@ -0,0 +1,46 @@ |
|||
<?php |
|||
declare(strict_types=1); |
|||
/** |
|||
* @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl> |
|||
* |
|||
* @author Roeland Jago Douma <roeland@famdouma.nl> |
|||
* |
|||
* @license GNU AGPL version 3 or any later version |
|||
* |
|||
* This program is free software: you can redistribute it and/or modify |
|||
* it under the terms of the GNU Affero General Public License as |
|||
* published by the Free Software Foundation, either version 3 of the |
|||
* License, or (at your option) any later version. |
|||
* |
|||
* This program is distributed in the hope that it will be useful, |
|||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
* GNU Affero General Public License for more details. |
|||
* |
|||
* You should have received a copy of the GNU Affero General Public License |
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
|||
* |
|||
*/ |
|||
|
|||
namespace OC\Core\BackgroundJobs; |
|||
|
|||
use OC\Core\Db\LoginFlowV2Mapper; |
|||
use OCP\AppFramework\Utility\ITimeFactory; |
|||
use OCP\BackgroundJob\TimedJob; |
|||
|
|||
class CleanupLoginFlowV2 extends TimedJob { |
|||
|
|||
/** @var LoginFlowV2Mapper */ |
|||
private $loginFlowV2Mapper; |
|||
|
|||
public function __construct(ITimeFactory $time, LoginFlowV2Mapper $loginFlowV2Mapper) { |
|||
parent::__construct($time); |
|||
$this->loginFlowV2Mapper = $loginFlowV2Mapper; |
|||
|
|||
$this->setInterval(3600); |
|||
} |
|||
|
|||
protected function run($argument) { |
|||
$this->loginFlowV2Mapper->cleanup(); |
|||
} |
|||
} |
|||
@ -0,0 +1,299 @@ |
|||
<?php |
|||
declare(strict_types=1); |
|||
/** |
|||
* @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl> |
|||
* |
|||
* @author Roeland Jago Douma <roeland@famdouma.nl> |
|||
* |
|||
* @license GNU AGPL version 3 or any later version |
|||
* |
|||
* This program is free software: you can redistribute it and/or modify |
|||
* it under the terms of the GNU Affero General Public License as |
|||
* published by the Free Software Foundation, either version 3 of the |
|||
* License, or (at your option) any later version. |
|||
* |
|||
* This program is distributed in the hope that it will be useful, |
|||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
* GNU Affero General Public License for more details. |
|||
* |
|||
* You should have received a copy of the GNU Affero General Public License |
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
|||
* |
|||
*/ |
|||
|
|||
namespace OC\Core\Controller; |
|||
|
|||
use OC\Core\Db\LoginFlowV2; |
|||
use OC\Core\Exception\LoginFlowV2NotFoundException; |
|||
use OC\Core\Service\LoginFlowV2Service; |
|||
use OCP\AppFramework\Controller; |
|||
use OCP\AppFramework\Http; |
|||
use OCP\AppFramework\Http\JSONResponse; |
|||
use OCP\AppFramework\Http\RedirectResponse; |
|||
use OCP\AppFramework\Http\Response; |
|||
use OCP\AppFramework\Http\StandaloneTemplateResponse; |
|||
use OCP\Defaults; |
|||
use OCP\IL10N; |
|||
use OCP\IRequest; |
|||
use OCP\ISession; |
|||
use OCP\IURLGenerator; |
|||
use OCP\Security\ISecureRandom; |
|||
|
|||
class ClientFlowLoginV2Controller extends Controller { |
|||
|
|||
private const tokenName = 'client.flow.v2.login.token'; |
|||
private const stateName = 'client.flow.v2.state.token'; |
|||
|
|||
/** @var LoginFlowV2Service */ |
|||
private $loginFlowV2Service; |
|||
/** @var IURLGenerator */ |
|||
private $urlGenerator; |
|||
/** @var ISession */ |
|||
private $session; |
|||
/** @var ISecureRandom */ |
|||
private $random; |
|||
/** @var Defaults */ |
|||
private $defaults; |
|||
/** @var string */ |
|||
private $userId; |
|||
/** @var IL10N */ |
|||
private $l10n; |
|||
|
|||
public function __construct(string $appName, |
|||
IRequest $request, |
|||
LoginFlowV2Service $loginFlowV2Service, |
|||
IURLGenerator $urlGenerator, |
|||
ISession $session, |
|||
ISecureRandom $random, |
|||
Defaults $defaults, |
|||
?string $userId, |
|||
IL10N $l10n) { |
|||
parent::__construct($appName, $request); |
|||
$this->loginFlowV2Service = $loginFlowV2Service; |
|||
$this->urlGenerator = $urlGenerator; |
|||
$this->session = $session; |
|||
$this->random = $random; |
|||
$this->defaults = $defaults; |
|||
$this->userId = $userId; |
|||
$this->l10n = $l10n; |
|||
} |
|||
|
|||
/** |
|||
* @NoCSRFRequired |
|||
* @PublicPage |
|||
*/ |
|||
public function poll(string $token): JSONResponse { |
|||
try { |
|||
$creds = $this->loginFlowV2Service->poll($token); |
|||
} catch (LoginFlowV2NotFoundException $e) { |
|||
return new JSONResponse([], Http::STATUS_NOT_FOUND); |
|||
} |
|||
|
|||
return new JSONResponse($creds); |
|||
} |
|||
|
|||
/** |
|||
* @NoCSRFRequired |
|||
* @PublicPage |
|||
* @UseSession |
|||
*/ |
|||
public function landing(string $token): Response { |
|||
if (!$this->loginFlowV2Service->startLoginFlow($token)) { |
|||
return $this->loginTokenForbiddenResponse(); |
|||
} |
|||
|
|||
$this->session->set(self::tokenName, $token); |
|||
|
|||
return new RedirectResponse( |
|||
$this->urlGenerator->linkToRouteAbsolute('core.ClientFlowLoginV2.showAuthPickerPage') |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* @NoCSRFRequired |
|||
* @PublicPage |
|||
* @UseSession |
|||
*/ |
|||
public function showAuthPickerPage(): StandaloneTemplateResponse { |
|||
try { |
|||
$flow = $this->getFlowByLoginToken(); |
|||
} catch (LoginFlowV2NotFoundException $e) { |
|||
return $this->loginTokenForbiddenResponse(); |
|||
} |
|||
|
|||
$stateToken = $this->random->generate( |
|||
64, |
|||
ISecureRandom::CHAR_LOWER.ISecureRandom::CHAR_UPPER.ISecureRandom::CHAR_DIGITS |
|||
); |
|||
$this->session->set(self::stateName, $stateToken); |
|||
|
|||
return new StandaloneTemplateResponse( |
|||
$this->appName, |
|||
'loginflowv2/authpicker', |
|||
[ |
|||
'client' => $flow->getClientName(), |
|||
'instanceName' => $this->defaults->getName(), |
|||
'urlGenerator' => $this->urlGenerator, |
|||
'stateToken' => $stateToken, |
|||
], |
|||
'guest' |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* @NoAdminRequired |
|||
* @UseSession |
|||
* @NoCSRFRequired |
|||
* @NoSameSiteCookieRequired |
|||
*/ |
|||
public function grantPage(string $stateToken): StandaloneTemplateResponse { |
|||
if(!$this->isValidStateToken($stateToken)) { |
|||
return $this->stateTokenForbiddenResponse(); |
|||
} |
|||
|
|||
try { |
|||
$flow = $this->getFlowByLoginToken(); |
|||
} catch (LoginFlowV2NotFoundException $e) { |
|||
return $this->loginTokenForbiddenResponse(); |
|||
} |
|||
|
|||
return new StandaloneTemplateResponse( |
|||
$this->appName, |
|||
'loginflowv2/grant', |
|||
[ |
|||
'client' => $flow->getClientName(), |
|||
'instanceName' => $this->defaults->getName(), |
|||
'urlGenerator' => $this->urlGenerator, |
|||
'stateToken' => $stateToken, |
|||
], |
|||
'guest' |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* @NoAdminRequired |
|||
* @UseSession |
|||
*/ |
|||
public function generateAppPassword(string $stateToken): Response { |
|||
if(!$this->isValidStateToken($stateToken)) { |
|||
return $this->stateTokenForbiddenResponse(); |
|||
} |
|||
|
|||
try { |
|||
$this->getFlowByLoginToken(); |
|||
} catch (LoginFlowV2NotFoundException $e) { |
|||
return $this->loginTokenForbiddenResponse(); |
|||
} |
|||
|
|||
$loginToken = $this->session->get(self::tokenName); |
|||
|
|||
// Clear session variables
|
|||
$this->session->remove(self::tokenName); |
|||
$this->session->remove(self::stateName); |
|||
$sessionId = $this->session->getId(); |
|||
|
|||
$result = $this->loginFlowV2Service->flowDone($loginToken, $sessionId, $this->getServerPath(), $this->userId); |
|||
|
|||
if ($result) { |
|||
return new StandaloneTemplateResponse( |
|||
$this->appName, |
|||
'loginflowv2/done', |
|||
[], |
|||
'guest' |
|||
); |
|||
} |
|||
|
|||
$response = new StandaloneTemplateResponse( |
|||
$this->appName, |
|||
'403', |
|||
[ |
|||
'message' => $this->l10n->t('Could not complete login'), |
|||
], |
|||
'guest' |
|||
); |
|||
$response->setStatus(Http::STATUS_FORBIDDEN); |
|||
return $response; |
|||
} |
|||
|
|||
/** |
|||
* @NoCSRFRequired |
|||
* @PublicPage |
|||
*/ |
|||
public function init(): JSONResponse { |
|||
// Get client user agent
|
|||
$userAgent = $this->request->getHeader('USER_AGENT'); |
|||
|
|||
$tokens = $this->loginFlowV2Service->createTokens($userAgent); |
|||
|
|||
$data = [ |
|||
'poll' => [ |
|||
'token' => $tokens->getPollToken(), |
|||
'endpoint' => $this->urlGenerator->linkToRouteAbsolute('core.ClientFlowLoginV2.poll') |
|||
], |
|||
'login' => $this->urlGenerator->linkToRouteAbsolute('core.ClientFlowLoginV2.landing', ['token' => $tokens->getLoginToken()]), |
|||
]; |
|||
|
|||
return new JSONResponse($data); |
|||
} |
|||
|
|||
private function isValidStateToken(string $stateToken): bool { |
|||
$currentToken = $this->session->get(self::stateName); |
|||
if(!is_string($stateToken) || !is_string($currentToken)) { |
|||
return false; |
|||
} |
|||
return hash_equals($currentToken, $stateToken); |
|||
} |
|||
|
|||
private function stateTokenForbiddenResponse(): StandaloneTemplateResponse { |
|||
$response = new StandaloneTemplateResponse( |
|||
$this->appName, |
|||
'403', |
|||
[ |
|||
'message' => $this->l10n->t('State token does not match'), |
|||
], |
|||
'guest' |
|||
); |
|||
$response->setStatus(Http::STATUS_FORBIDDEN); |
|||
return $response; |
|||
} |
|||
|
|||
/** |
|||
* @return LoginFlowV2 |
|||
* @throws LoginFlowV2NotFoundException |
|||
*/ |
|||
private function getFlowByLoginToken(): LoginFlowV2 { |
|||
$currentToken = $this->session->get(self::tokenName); |
|||
if(!is_string($currentToken)) { |
|||
throw new LoginFlowV2NotFoundException('Login token not set in session'); |
|||
} |
|||
|
|||
return $this->loginFlowV2Service->getByLoginToken($currentToken); |
|||
} |
|||
|
|||
private function loginTokenForbiddenResponse(): StandaloneTemplateResponse { |
|||
$response = new StandaloneTemplateResponse( |
|||
$this->appName, |
|||
'403', |
|||
[ |
|||
'message' => $this->l10n->t('Your login token is invalid or has expired'), |
|||
], |
|||
'guest' |
|||
); |
|||
$response->setStatus(Http::STATUS_FORBIDDEN); |
|||
return $response; |
|||
} |
|||
|
|||
private function getServerPath(): string { |
|||
$serverPostfix = ''; |
|||
|
|||
if (strpos($this->request->getRequestUri(), '/index.php') !== false) { |
|||
$serverPostfix = substr($this->request->getRequestUri(), 0, strpos($this->request->getRequestUri(), '/index.php')); |
|||
} else if (strpos($this->request->getRequestUri(), '/login/v2') !== false) { |
|||
$serverPostfix = substr($this->request->getRequestUri(), 0, strpos($this->request->getRequestUri(), '/login/v2')); |
|||
} |
|||
|
|||
$protocol = $this->request->getServerProtocol(); |
|||
return $protocol . '://' . $this->request->getServerHost() . $serverPostfix; |
|||
} |
|||
} |
|||
@ -0,0 +1,71 @@ |
|||
<?php |
|||
declare(strict_types=1); |
|||
/** |
|||
* @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl> |
|||
* |
|||
* @author Roeland Jago Douma <roeland@famdouma.nl> |
|||
* |
|||
* @license GNU AGPL version 3 or any later version |
|||
* |
|||
* This program is free software: you can redistribute it and/or modify |
|||
* it under the terms of the GNU Affero General Public License as |
|||
* published by the Free Software Foundation, either version 3 of the |
|||
* License, or (at your option) any later version. |
|||
* |
|||
* This program is distributed in the hope that it will be useful, |
|||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
* GNU Affero General Public License for more details. |
|||
* |
|||
* You should have received a copy of the GNU Affero General Public License |
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
|||
* |
|||
*/ |
|||
|
|||
namespace OC\Core\Data; |
|||
|
|||
class LoginFlowV2Credentials implements \JsonSerializable { |
|||
/** @var string */ |
|||
private $server; |
|||
/** @var string */ |
|||
private $loginName; |
|||
/** @var string */ |
|||
private $appPassword; |
|||
|
|||
public function __construct(string $server, string $loginName, string $appPassword) { |
|||
$this->server = $server; |
|||
$this->loginName = $loginName; |
|||
$this->appPassword = $appPassword; |
|||
} |
|||
|
|||
/** |
|||
* @return string |
|||
*/ |
|||
public function getServer(): string { |
|||
return $this->server; |
|||
} |
|||
|
|||
/** |
|||
* @return string |
|||
*/ |
|||
public function getLoginName(): string { |
|||
return $this->loginName; |
|||
} |
|||
|
|||
/** |
|||
* @return string |
|||
*/ |
|||
public function getAppPassword(): string { |
|||
return $this->appPassword; |
|||
} |
|||
|
|||
public function jsonSerialize(): array { |
|||
return [ |
|||
'server' => $this->server, |
|||
'loginName' => $this->loginName, |
|||
'appPassword' => $this->appPassword, |
|||
]; |
|||
} |
|||
|
|||
|
|||
} |
|||
@ -0,0 +1,47 @@ |
|||
<?php |
|||
declare(strict_types=1); |
|||
/** |
|||
* @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl> |
|||
* |
|||
* @author Roeland Jago Douma <roeland@famdouma.nl> |
|||
* |
|||
* @license GNU AGPL version 3 or any later version |
|||
* |
|||
* This program is free software: you can redistribute it and/or modify |
|||
* it under the terms of the GNU Affero General Public License as |
|||
* published by the Free Software Foundation, either version 3 of the |
|||
* License, or (at your option) any later version. |
|||
* |
|||
* This program is distributed in the hope that it will be useful, |
|||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
* GNU Affero General Public License for more details. |
|||
* |
|||
* You should have received a copy of the GNU Affero General Public License |
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
|||
* |
|||
*/ |
|||
|
|||
namespace OC\Core\Data; |
|||
|
|||
class LoginFlowV2Tokens { |
|||
|
|||
/** @var string */ |
|||
private $loginToken; |
|||
/** @var string */ |
|||
private $pollToken; |
|||
|
|||
public function __construct(string $loginToken, string $pollToken) { |
|||
$this->loginToken = $loginToken; |
|||
$this->pollToken = $pollToken; |
|||
} |
|||
|
|||
public function getPollToken(): string { |
|||
return $this->pollToken; |
|||
|
|||
} |
|||
|
|||
public function getLoginToken(): string { |
|||
return $this->loginToken; |
|||
} |
|||
} |
|||
@ -0,0 +1,85 @@ |
|||
<?php |
|||
declare(strict_types=1); |
|||
/** |
|||
* @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl> |
|||
* |
|||
* @author Roeland Jago Douma <roeland@famdouma.nl> |
|||
* |
|||
* @license GNU AGPL version 3 or any later version |
|||
* |
|||
* This program is free software: you can redistribute it and/or modify |
|||
* it under the terms of the GNU Affero General Public License as |
|||
* published by the Free Software Foundation, either version 3 of the |
|||
* License, or (at your option) any later version. |
|||
* |
|||
* This program is distributed in the hope that it will be useful, |
|||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
* GNU Affero General Public License for more details. |
|||
* |
|||
* You should have received a copy of the GNU Affero General Public License |
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
|||
* |
|||
*/ |
|||
|
|||
namespace OC\Core\Db; |
|||
|
|||
use OCP\AppFramework\Db\Entity; |
|||
|
|||
/** |
|||
* @method int getTimestamp() |
|||
* @method void setTimestamp(int $timestamp) |
|||
* @method int getStarted() |
|||
* @method void setStarted(int $started) |
|||
* @method string getPollToken() |
|||
* @method void setPollToken(string $token) |
|||
* @method string getLoginToken() |
|||
* @method void setLoginToken(string $token) |
|||
* @method string getPublicKey() |
|||
* @method void setPublicKey(string $key) |
|||
* @method string getPrivateKey() |
|||
* @method void setPrivateKey(string $key) |
|||
* @method string getClientName() |
|||
* @method void setClientName(string $clientName) |
|||
* @method string getLoginName() |
|||
* @method void setLoginName(string $loginName) |
|||
* @method string getServer() |
|||
* @method void setServer(string $server) |
|||
* @method string getAppPassword() |
|||
* @method void setAppPassword(string $appPassword) |
|||
*/ |
|||
class LoginFlowV2 extends Entity { |
|||
/** @var int */ |
|||
protected $timestamp; |
|||
/** @var int */ |
|||
protected $started; |
|||
/** @var string */ |
|||
protected $pollToken; |
|||
/** @var string */ |
|||
protected $loginToken; |
|||
/** @var string */ |
|||
protected $publicKey; |
|||
/** @var string */ |
|||
protected $privateKey; |
|||
/** @var string */ |
|||
protected $clientName; |
|||
/** @var string */ |
|||
protected $loginName; |
|||
/** @var string */ |
|||
protected $server; |
|||
/** @var string */ |
|||
protected $appPassword; |
|||
|
|||
public function __construct() { |
|||
$this->addType('timestamp', 'int'); |
|||
$this->addType('started', 'int'); |
|||
$this->addType('pollToken', 'string'); |
|||
$this->addType('loginToken', 'string'); |
|||
$this->addType('publicKey', 'string'); |
|||
$this->addType('privateKey', 'string'); |
|||
$this->addType('clientName', 'string'); |
|||
$this->addType('loginName', 'string'); |
|||
$this->addType('server', 'string'); |
|||
$this->addType('appPassword', 'string'); |
|||
} |
|||
} |
|||
@ -0,0 +1,100 @@ |
|||
<?php |
|||
declare(strict_types=1); |
|||
/** |
|||
* @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl> |
|||
* |
|||
* @author Roeland Jago Douma <roeland@famdouma.nl> |
|||
* |
|||
* @license GNU AGPL version 3 or any later version |
|||
* |
|||
* This program is free software: you can redistribute it and/or modify |
|||
* it under the terms of the GNU Affero General Public License as |
|||
* published by the Free Software Foundation, either version 3 of the |
|||
* License, or (at your option) any later version. |
|||
* |
|||
* This program is distributed in the hope that it will be useful, |
|||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
* GNU Affero General Public License for more details. |
|||
* |
|||
* You should have received a copy of the GNU Affero General Public License |
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
|||
* |
|||
*/ |
|||
|
|||
namespace OC\Core\Db; |
|||
|
|||
use OCP\AppFramework\Db\DoesNotExistException; |
|||
use OCP\AppFramework\Db\QBMapper; |
|||
use OCP\AppFramework\Utility\ITimeFactory; |
|||
use OCP\IDBConnection; |
|||
|
|||
class LoginFlowV2Mapper extends QBMapper { |
|||
private const lifetime = 1200; |
|||
|
|||
/** @var ITimeFactory */ |
|||
private $timeFactory; |
|||
|
|||
public function __construct(IDBConnection $db, ITimeFactory $timeFactory) { |
|||
parent::__construct($db, 'login_flow_v2', LoginFlowV2::class); |
|||
$this->timeFactory = $timeFactory; |
|||
} |
|||
|
|||
/** |
|||
* @param string $pollToken |
|||
* @return LoginFlowV2 |
|||
* @throws DoesNotExistException |
|||
*/ |
|||
public function getByPollToken(string $pollToken): LoginFlowV2 { |
|||
$qb = $this->db->getQueryBuilder(); |
|||
$qb->select('*') |
|||
->from($this->getTableName()) |
|||
->where( |
|||
$qb->expr()->eq('poll_token', $qb->createNamedParameter($pollToken)) |
|||
); |
|||
|
|||
$entity = $this->findEntity($qb); |
|||
return $this->validateTimestamp($entity); |
|||
} |
|||
|
|||
/** |
|||
* @param string $loginToken |
|||
* @return LoginFlowV2 |
|||
* @throws DoesNotExistException |
|||
*/ |
|||
public function getByLoginToken(string $loginToken): LoginFlowV2 { |
|||
$qb = $this->db->getQueryBuilder(); |
|||
$qb->select('*') |
|||
->from($this->getTableName()) |
|||
->where( |
|||
$qb->expr()->eq('login_token', $qb->createNamedParameter($loginToken)) |
|||
); |
|||
|
|||
$entity = $this->findEntity($qb); |
|||
return $this->validateTimestamp($entity); |
|||
} |
|||
|
|||
public function cleanup(): void { |
|||
$qb = $this->db->getQueryBuilder(); |
|||
$qb->delete($this->getTableName()) |
|||
->where( |
|||
$qb->expr()->lt('timestamp', $qb->createNamedParameter($this->timeFactory->getTime() - self::lifetime)) |
|||
); |
|||
|
|||
$qb->execute(); |
|||
} |
|||
|
|||
/** |
|||
* @param LoginFlowV2 $flowV2 |
|||
* @return LoginFlowV2 |
|||
* @throws DoesNotExistException |
|||
*/ |
|||
private function validateTimestamp(LoginFlowV2 $flowV2): LoginFlowV2 { |
|||
if ($flowV2->getTimestamp() < ($this->timeFactory->getTime() - self::lifetime)) { |
|||
$this->delete($flowV2); |
|||
throw new DoesNotExistException('Token expired'); |
|||
} |
|||
|
|||
return $flowV2; |
|||
} |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
<?php |
|||
declare(strict_types=1); |
|||
/** |
|||
* @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl> |
|||
* |
|||
* @author Roeland Jago Douma <roeland@famdouma.nl> |
|||
* |
|||
* @license GNU AGPL version 3 or any later version |
|||
* |
|||
* This program is free software: you can redistribute it and/or modify |
|||
* it under the terms of the GNU Affero General Public License as |
|||
* published by the Free Software Foundation, either version 3 of the |
|||
* License, or (at your option) any later version. |
|||
* |
|||
* This program is distributed in the hope that it will be useful, |
|||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
* GNU Affero General Public License for more details. |
|||
* |
|||
* You should have received a copy of the GNU Affero General Public License |
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
|||
* |
|||
*/ |
|||
|
|||
namespace OC\Core\Exception; |
|||
|
|||
class LoginFlowV2NotFoundException extends \Exception { |
|||
|
|||
} |
|||
@ -0,0 +1,101 @@ |
|||
<?php |
|||
declare(strict_types=1); |
|||
/** |
|||
* @copyright Copyright (c) 2018 Roeland Jago Douma <roeland@famdouma.nl> |
|||
* |
|||
* @author Roeland Jago Douma <roeland@famdouma.nl> |
|||
* |
|||
* @license GNU AGPL version 3 or any later version |
|||
* |
|||
* This program is free software: you can redistribute it and/or modify |
|||
* it under the terms of the GNU Affero General Public License as |
|||
* published by the Free Software Foundation, either version 3 of the |
|||
* License, or (at your option) any later version. |
|||
* |
|||
* This program is distributed in the hope that it will be useful, |
|||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
* GNU Affero General Public License for more details. |
|||
* |
|||
* You should have received a copy of the GNU Affero General Public License |
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
|||
* |
|||
*/ |
|||
|
|||
namespace OC\Core\Migrations; |
|||
|
|||
use Closure; |
|||
use Doctrine\DBAL\Types\Type; |
|||
use OCP\DB\ISchemaWrapper; |
|||
use OCP\Migration\SimpleMigrationStep; |
|||
use OCP\Migration\IOutput; |
|||
|
|||
class Version16000Date20190212081545 extends SimpleMigrationStep { |
|||
/** |
|||
* @param IOutput $output |
|||
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` |
|||
* @param array $options |
|||
* @return null|ISchemaWrapper |
|||
*/ |
|||
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ISchemaWrapper { |
|||
/** @var ISchemaWrapper $schema */ |
|||
$schema = $schemaClosure(); |
|||
|
|||
$table = $schema->createTable('login_flow_v2'); |
|||
$table->addColumn('id', Type::BIGINT, [ |
|||
'autoincrement' => true, |
|||
'notnull' => true, |
|||
'length' => 20, |
|||
'unsigned' => true, |
|||
]); |
|||
$table->addColumn('timestamp', Type::BIGINT, [ |
|||
'notnull' => true, |
|||
'length' => 20, |
|||
'unsigned' => true, |
|||
]); |
|||
$table->addColumn('started', Type::SMALLINT, [ |
|||
'notnull' => true, |
|||
'length' => 1, |
|||
'unsigned' => true, |
|||
'default' => 0, |
|||
]); |
|||
$table->addColumn('poll_token', Type::STRING, [ |
|||
'notnull' => true, |
|||
'length' => 255, |
|||
]); |
|||
$table->addColumn('login_token', Type::STRING, [ |
|||
'notnull' => true, |
|||
'length' => 255, |
|||
]); |
|||
$table->addColumn('public_key', Type::TEXT, [ |
|||
'notnull' => true, |
|||
'length' => 32768, |
|||
]); |
|||
$table->addColumn('private_key', Type::TEXT, [ |
|||
'notnull' => true, |
|||
'length' => 32768, |
|||
]); |
|||
$table->addColumn('client_name', Type::STRING, [ |
|||
'notnull' => true, |
|||
'length' => 255, |
|||
]); |
|||
$table->addColumn('login_name', Type::STRING, [ |
|||
'notnull' => false, |
|||
'length' => 255, |
|||
]); |
|||
$table->addColumn('server', Type::STRING, [ |
|||
'notnull' => false, |
|||
'length' => 255, |
|||
]); |
|||
$table->addColumn('app_password', Type::STRING, [ |
|||
'notnull' => false, |
|||
'length' => 1024, |
|||
]); |
|||
$table->setPrimaryKey(['id']); |
|||
$table->addUniqueIndex(['poll_token']); |
|||
$table->addUniqueIndex(['login_token']); |
|||
$table->addIndex(['timestamp']); |
|||
|
|||
return $schema; |
|||
} |
|||
} |
|||
@ -0,0 +1,260 @@ |
|||
<?php |
|||
declare(strict_types=1); |
|||
/** |
|||
* @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl> |
|||
* |
|||
* @author Roeland Jago Douma <roeland@famdouma.nl> |
|||
* |
|||
* @license GNU AGPL version 3 or any later version |
|||
* |
|||
* This program is free software: you can redistribute it and/or modify |
|||
* it under the terms of the GNU Affero General Public License as |
|||
* published by the Free Software Foundation, either version 3 of the |
|||
* License, or (at your option) any later version. |
|||
* |
|||
* This program is distributed in the hope that it will be useful, |
|||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
* GNU Affero General Public License for more details. |
|||
* |
|||
* You should have received a copy of the GNU Affero General Public License |
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
|||
* |
|||
*/ |
|||
|
|||
namespace OC\Core\Service; |
|||
|
|||
use OC\Authentication\Exceptions\InvalidTokenException; |
|||
use OC\Authentication\Exceptions\PasswordlessTokenException; |
|||
use OC\Authentication\Token\IProvider; |
|||
use OC\Authentication\Token\IToken; |
|||
use OC\Core\Data\LoginFlowV2Credentials; |
|||
use OC\Core\Data\LoginFlowV2Tokens; |
|||
use OC\Core\Db\LoginFlowV2; |
|||
use OC\Core\Db\LoginFlowV2Mapper; |
|||
use OC\Core\Exception\LoginFlowV2NotFoundException; |
|||
use OCP\AppFramework\Db\DoesNotExistException; |
|||
use OCP\AppFramework\Utility\ITimeFactory; |
|||
use OCP\IConfig; |
|||
use OCP\ILogger; |
|||
use OCP\Security\ICrypto; |
|||
use OCP\Security\ISecureRandom; |
|||
|
|||
class LoginFlowV2Service { |
|||
|
|||
/** @var LoginFlowV2Mapper */ |
|||
private $mapper; |
|||
/** @var ISecureRandom */ |
|||
private $random; |
|||
/** @var ITimeFactory */ |
|||
private $time; |
|||
/** @var IConfig */ |
|||
private $config; |
|||
/** @var ICrypto */ |
|||
private $crypto; |
|||
/** @var ILogger */ |
|||
private $logger; |
|||
/** @var IProvider */ |
|||
private $tokenProvider; |
|||
|
|||
public function __construct(LoginFlowV2Mapper $mapper, |
|||
ISecureRandom $random, |
|||
ITimeFactory $time, |
|||
IConfig $config, |
|||
ICrypto $crypto, |
|||
ILogger $logger, |
|||
IProvider $tokenProvider) { |
|||
$this->mapper = $mapper; |
|||
$this->random = $random; |
|||
$this->time = $time; |
|||
$this->config = $config; |
|||
$this->crypto = $crypto; |
|||
$this->logger = $logger; |
|||
$this->tokenProvider = $tokenProvider; |
|||
} |
|||
|
|||
/** |
|||
* @param string $pollToken |
|||
* @return LoginFlowV2Credentials |
|||
* @throws LoginFlowV2NotFoundException |
|||
*/ |
|||
public function poll(string $pollToken): LoginFlowV2Credentials { |
|||
try { |
|||
$data = $this->mapper->getByPollToken($this->hashToken($pollToken)); |
|||
} catch (DoesNotExistException $e) { |
|||
throw new LoginFlowV2NotFoundException('Invalid token'); |
|||
} |
|||
|
|||
$loginName = $data->getLoginName(); |
|||
$server = $data->getServer(); |
|||
$appPassword = $data->getAppPassword(); |
|||
|
|||
if ($loginName === null || $server === null || $appPassword === null) { |
|||
throw new LoginFlowV2NotFoundException('Token not yet ready'); |
|||
} |
|||
|
|||
// Remove the data from the DB
|
|||
$this->mapper->delete($data); |
|||
|
|||
try { |
|||
// Decrypt the apptoken
|
|||
$privateKey = $this->crypto->decrypt($data->getPrivateKey(), $pollToken); |
|||
$appPassword = $this->decryptPassword($data->getAppPassword(), $privateKey); |
|||
} catch (\Exception $e) { |
|||
throw new LoginFlowV2NotFoundException('Apptoken could not be decrypted'); |
|||
} |
|||
|
|||
return new LoginFlowV2Credentials($server, $loginName, $appPassword); |
|||
} |
|||
|
|||
/** |
|||
* @param string $loginToken |
|||
* @return LoginFlowV2 |
|||
* @throws LoginFlowV2NotFoundException |
|||
*/ |
|||
public function getByLoginToken(string $loginToken): LoginFlowV2 { |
|||
try { |
|||
return $this->mapper->getByLoginToken($loginToken); |
|||
} catch (DoesNotExistException $e) { |
|||
throw new LoginFlowV2NotFoundException('Login token invalid'); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* @param string $loginToken |
|||
* @return bool returns true if the start was successfull. False if not. |
|||
*/ |
|||
public function startLoginFlow(string $loginToken): bool { |
|||
try { |
|||
$data = $this->mapper->getByLoginToken($loginToken); |
|||
} catch (DoesNotExistException $e) { |
|||
return false; |
|||
} |
|||
|
|||
if ($data->getStarted() !== 0) { |
|||
return false; |
|||
} |
|||
|
|||
$data->setStarted(1); |
|||
$this->mapper->update($data); |
|||
|
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* @param string $loginToken |
|||
* @param string $sessionId |
|||
* @param string $server |
|||
* @param string $userId |
|||
* @return bool true if the flow was successfully completed false otherwise |
|||
*/ |
|||
public function flowDone(string $loginToken, string $sessionId, string $server, string $userId): bool { |
|||
try { |
|||
$data = $this->mapper->getByLoginToken($loginToken); |
|||
} catch (DoesNotExistException $e) { |
|||
return false; |
|||
} |
|||
|
|||
try { |
|||
$sessionToken = $this->tokenProvider->getToken($sessionId); |
|||
$loginName = $sessionToken->getLoginName(); |
|||
try { |
|||
$password = $this->tokenProvider->getPassword($sessionToken, $sessionId); |
|||
} catch (PasswordlessTokenException $ex) { |
|||
$password = null; |
|||
} |
|||
} catch (InvalidTokenException $ex) { |
|||
return false; |
|||
} |
|||
|
|||
$appPassword = $this->random->generate(72, ISecureRandom::CHAR_UPPER.ISecureRandom::CHAR_LOWER.ISecureRandom::CHAR_DIGITS); |
|||
$this->tokenProvider->generateToken( |
|||
$appPassword, |
|||
$userId, |
|||
$loginName, |
|||
$password, |
|||
$data->getClientName(), |
|||
IToken::PERMANENT_TOKEN, |
|||
IToken::DO_NOT_REMEMBER |
|||
); |
|||
|
|||
$data->setLoginName($loginName); |
|||
$data->setServer($server); |
|||
|
|||
// Properly encrypt
|
|||
$data->setAppPassword($this->encryptPassword($appPassword, $data->getPublicKey())); |
|||
|
|||
$this->mapper->update($data); |
|||
return true; |
|||
} |
|||
|
|||
public function createTokens(string $userAgent): LoginFlowV2Tokens { |
|||
$flow = new LoginFlowV2(); |
|||
$pollToken = $this->random->generate(128, ISecureRandom::CHAR_DIGITS.ISecureRandom::CHAR_LOWER.ISecureRandom::CHAR_UPPER); |
|||
$loginToken = $this->random->generate(128, ISecureRandom::CHAR_DIGITS.ISecureRandom::CHAR_LOWER.ISecureRandom::CHAR_UPPER); |
|||
$flow->setPollToken($this->hashToken($pollToken)); |
|||
$flow->setLoginToken($loginToken); |
|||
$flow->setStarted(0); |
|||
$flow->setTimestamp($this->time->getTime()); |
|||
$flow->setClientName($userAgent); |
|||
|
|||
[$publicKey, $privateKey] = $this->getKeyPair(); |
|||
$privateKey = $this->crypto->encrypt($privateKey, $pollToken); |
|||
|
|||
$flow->setPublicKey($publicKey); |
|||
$flow->setPrivateKey($privateKey); |
|||
|
|||
$this->mapper->insert($flow); |
|||
|
|||
return new LoginFlowV2Tokens($loginToken, $pollToken); |
|||
} |
|||
|
|||
private function hashToken(string $token): string { |
|||
$secret = $this->config->getSystemValue('secret'); |
|||
return hash('sha512', $token . $secret); |
|||
} |
|||
|
|||
private function getKeyPair(): array { |
|||
$config = array_merge([ |
|||
'digest_alg' => 'sha512', |
|||
'private_key_bits' => 2048, |
|||
], $this->config->getSystemValue('openssl', [])); |
|||
|
|||
// Generate new key
|
|||
$res = openssl_pkey_new($config); |
|||
if ($res === false) { |
|||
$this->logOpensslError(); |
|||
throw new \RuntimeException('Could not initialize keys'); |
|||
} |
|||
|
|||
openssl_pkey_export($res, $privateKey); |
|||
|
|||
// Extract the public key from $res to $pubKey
|
|||
$publicKey = openssl_pkey_get_details($res); |
|||
$publicKey = $publicKey['key']; |
|||
|
|||
return [$publicKey, $privateKey]; |
|||
} |
|||
|
|||
private function logOpensslError(): void { |
|||
$errors = []; |
|||
while ($error = openssl_error_string()) { |
|||
$errors[] = $error; |
|||
} |
|||
$this->logger->critical('Something is wrong with your openssl setup: ' . implode(', ', $errors)); |
|||
} |
|||
|
|||
private function encryptPassword(string $password, string $publicKey): string { |
|||
openssl_public_encrypt($password, $encryptedPassword, $publicKey, OPENSSL_PKCS1_OAEP_PADDING); |
|||
$encryptedPassword = base64_encode($encryptedPassword); |
|||
|
|||
return $encryptedPassword; |
|||
} |
|||
|
|||
private function decryptPassword(string $encryptedPassword, string $privateKey): string { |
|||
$encryptedPassword = base64_decode($encryptedPassword); |
|||
openssl_private_decrypt($encryptedPassword, $password, $privateKey, OPENSSL_PKCS1_OAEP_PADDING); |
|||
|
|||
return $password; |
|||
} |
|||
} |
|||
@ -0,0 +1,46 @@ |
|||
<?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/>. |
|||
* |
|||
*/ |
|||
|
|||
style('core', 'login/authpicker'); |
|||
|
|||
/** @var array $_ */ |
|||
/** @var \OCP\IURLGenerator $urlGenerator */ |
|||
$urlGenerator = $_['urlGenerator']; |
|||
?>
|
|||
|
|||
<div class="picker-window"> |
|||
<h2><?php p($l->t('Connect to your account')) ?></h2>
|
|||
<p class="info"> |
|||
<?php print_unescaped($l->t('Please log in before granting %1$s access to your %2$s account.', [ |
|||
'<strong>' . \OCP\Util::sanitizeHTML($_['client']) . '</strong>', |
|||
\OCP\Util::sanitizeHTML($_['instanceName']) |
|||
])) ?>
|
|||
</p> |
|||
|
|||
<br/> |
|||
|
|||
<p id="redirect-link"> |
|||
<a href="<?php p($urlGenerator->linkToRouteAbsolute('core.ClientFlowLoginV2.grantPage', ['stateToken' => $_['stateToken']])) ?>"> |
|||
<input type="submit" class="login primary icon-confirm-white" value="<?php p($l->t('Log in')) ?>"> |
|||
</a> |
|||
</p> |
|||
|
|||
</div> |
|||
@ -0,0 +1,39 @@ |
|||
<?php |
|||
declare(strict_types=1); |
|||
/** |
|||
* @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl> |
|||
* |
|||
* @author Roeland Jago Douma <roeland@famdouma.nl> |
|||
* |
|||
* @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/>. |
|||
* |
|||
*/ |
|||
|
|||
style('core', 'login/authpicker'); |
|||
|
|||
/** @var array $_ */ |
|||
/** @var \OCP\IURLGenerator $urlGenerator */ |
|||
$urlGenerator = $_['urlGenerator']; |
|||
?>
|
|||
|
|||
<div class="picker-window"> |
|||
<h2><?php p($l->t('Account connected')) ?></h2>
|
|||
<p class="info"> |
|||
<?php print_unescaped($l->t('Your client should now be connected! You can close this window.')) ?>
|
|||
</p> |
|||
|
|||
<br/> |
|||
</div> |
|||
@ -0,0 +1,50 @@ |
|||
<?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/>. |
|||
* |
|||
*/ |
|||
|
|||
style('core', 'login/authpicker'); |
|||
|
|||
/** @var array $_ */ |
|||
/** @var \OCP\IURLGenerator $urlGenerator */ |
|||
$urlGenerator = $_['urlGenerator']; |
|||
?>
|
|||
|
|||
<div class="picker-window"> |
|||
<h2><?php p($l->t('Account access')) ?></h2>
|
|||
<p class="info"> |
|||
<?php print_unescaped($l->t('You are about to grant %1$s access to your %2$s account.', [ |
|||
'<strong>' . \OCP\Util::sanitizeHTML($_['client']) . '</strong>', |
|||
\OCP\Util::sanitizeHTML($_['instanceName']) |
|||
])) ?>
|
|||
</p> |
|||
|
|||
<br/> |
|||
|
|||
<p id="redirect-link"> |
|||
<form method="POST" action="<?php p($urlGenerator->linkToRouteAbsolute('core.ClientFlowLoginV2.generateAppPassword')) ?>"> |
|||
<input type="hidden" name="requesttoken" value="<?php p($_['requesttoken']) ?>" /> |
|||
<input type="hidden" name="stateToken" value="<?php p($_['stateToken']) ?>" /> |
|||
<div id="submit-wrapper"> |
|||
<input type="submit" id="submit" class="login primary" title="" value="<?php p($l->t('Grant access')); ?>" /> |
|||
<div class="submit-icon icon-confirm-white"></div> |
|||
</div> |
|||
</form> |
|||
</p> |
|||
</div> |
|||
@ -0,0 +1,49 @@ |
|||
<?php |
|||
declare(strict_types=1); |
|||
/** |
|||
* @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl> |
|||
* |
|||
* @author Roeland Jago Douma <roeland@famdouma.nl> |
|||
* |
|||
* @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\NC16; |
|||
|
|||
use OC\Core\BackgroundJobs\CleanupLoginFlowV2; |
|||
use OCP\BackgroundJob\IJobList; |
|||
use OCP\Migration\IOutput; |
|||
use OCP\Migration\IRepairStep; |
|||
|
|||
class AddClenupLoginFlowV2BackgroundJob implements IRepairStep { |
|||
|
|||
/** @var IJobList */ |
|||
private $jobList; |
|||
|
|||
public function __construct(IJobList $jobList) { |
|||
$this->jobList = $jobList; |
|||
} |
|||
|
|||
public function getName(): string { |
|||
return 'Add background job to cleanup login flow v2 tokens'; |
|||
} |
|||
|
|||
public function run(IOutput $output) { |
|||
$this->jobList->add(CleanupLoginFlowV2::class); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,321 @@ |
|||
<?php |
|||
declare(strict_types=1); |
|||
/** |
|||
* @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl> |
|||
* |
|||
* @author Roeland Jago Douma <roeland@famdouma.nl> |
|||
* |
|||
* @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 Test\Core\Controller; |
|||
|
|||
use OC\Core\Controller\ClientFlowLoginV2Controller; |
|||
use OC\Core\Data\LoginFlowV2Credentials; |
|||
use OC\Core\Db\LoginFlowV2; |
|||
use OC\Core\Exception\LoginFlowV2NotFoundException; |
|||
use OC\Core\Service\LoginFlowV2Service; |
|||
use OCP\AppFramework\Http; |
|||
use OCP\Defaults; |
|||
use OCP\IL10N; |
|||
use OCP\IRequest; |
|||
use OCP\ISession; |
|||
use OCP\IURLGenerator; |
|||
use OCP\Security\ISecureRandom; |
|||
use PHPUnit\Framework\MockObject\MockObject; |
|||
use Test\TestCase; |
|||
|
|||
class ClientFlowLoginV2ControllerTest extends TestCase { |
|||
|
|||
/** @var IRequest|MockObject */ |
|||
private $request; |
|||
/** @var LoginFlowV2Service|MockObject */ |
|||
private $loginFlowV2Service; |
|||
/** @var IURLGenerator|MockObject */ |
|||
private $urlGenerator; |
|||
/** @var ISession|MockObject */ |
|||
private $session; |
|||
/** @var ISecureRandom|MockObject */ |
|||
private $random; |
|||
/** @var Defaults|MockObject */ |
|||
private $defaults; |
|||
/** @var IL10N|MockObject */ |
|||
private $l; |
|||
/** @var ClientFlowLoginV2Controller */ |
|||
private $controller; |
|||
|
|||
public function setUp() { |
|||
parent::setUp(); |
|||
|
|||
$this->request = $this->createMock(IRequest::class); |
|||
$this->loginFlowV2Service = $this->createMock(LoginFlowV2Service::class); |
|||
$this->urlGenerator = $this->createMock(IURLGenerator::class); |
|||
$this->session = $this->createMock(ISession::class); |
|||
$this->random = $this->createMock(ISecureRandom::class); |
|||
$this->defaults = $this->createMock(Defaults::class); |
|||
$this->l = $this->createMock(IL10N::class); |
|||
$this->controller = new ClientFlowLoginV2Controller( |
|||
'core', |
|||
$this->request, |
|||
$this->loginFlowV2Service, |
|||
$this->urlGenerator, |
|||
$this->session, |
|||
$this->random, |
|||
$this->defaults, |
|||
'user', |
|||
$this->l |
|||
); |
|||
} |
|||
|
|||
public function testPollInvalid() { |
|||
$this->loginFlowV2Service->method('poll') |
|||
->with('token') |
|||
->willThrowException(new LoginFlowV2NotFoundException()); |
|||
|
|||
$result = $this->controller->poll('token'); |
|||
|
|||
$this->assertSame([], $result->getData()); |
|||
$this->assertSame(Http::STATUS_NOT_FOUND, $result->getStatus()); |
|||
} |
|||
|
|||
public function testPollValid() { |
|||
$creds = new LoginFlowV2Credentials('server', 'login', 'pass'); |
|||
$this->loginFlowV2Service->method('poll') |
|||
->with('token') |
|||
->willReturn($creds); |
|||
|
|||
$result = $this->controller->poll('token'); |
|||
|
|||
$this->assertSame($creds, $result->getData()); |
|||
$this->assertSame(Http::STATUS_OK, $result->getStatus()); |
|||
} |
|||
|
|||
public function testLandingInvalid() { |
|||
$this->session->expects($this->never()) |
|||
->method($this->anything()); |
|||
|
|||
$this->loginFlowV2Service->method('startLoginFlow') |
|||
->with('token') |
|||
->willReturn(false); |
|||
|
|||
$result = $this->controller->landing('token'); |
|||
|
|||
$this->assertSame(Http::STATUS_FORBIDDEN, $result->getStatus()); |
|||
$this->assertInstanceOf(Http\StandaloneTemplateResponse::class, $result); |
|||
} |
|||
|
|||
public function testLandingValid() { |
|||
$this->session->expects($this->once()) |
|||
->method('set') |
|||
->with('client.flow.v2.login.token', 'token'); |
|||
|
|||
$this->loginFlowV2Service->method('startLoginFlow') |
|||
->with('token') |
|||
->willReturn(true); |
|||
|
|||
$this->urlGenerator->method('linkToRouteAbsolute') |
|||
->with('core.ClientFlowLoginV2.showAuthPickerPage') |
|||
->willReturn('https://server/path'); |
|||
|
|||
$result = $this->controller->landing('token'); |
|||
|
|||
$this->assertInstanceOf(Http\RedirectResponse::class, $result); |
|||
$this->assertSame(Http::STATUS_SEE_OTHER, $result->getStatus()); |
|||
$this->assertSame('https://server/path', $result->getRedirectURL()); |
|||
} |
|||
|
|||
public function testShowAuthPickerNoLoginToken() { |
|||
$this->session->method('get') |
|||
->willReturn(null); |
|||
|
|||
$result = $this->controller->showAuthPickerPage(); |
|||
|
|||
$this->assertSame(Http::STATUS_FORBIDDEN, $result->getStatus()); |
|||
} |
|||
|
|||
public function testShowAuthPickerInvalidLoginToken() { |
|||
$this->session->method('get') |
|||
->with('client.flow.v2.login.token') |
|||
->willReturn('loginToken'); |
|||
|
|||
$this->loginFlowV2Service->method('getByLoginToken') |
|||
->with('loginToken') |
|||
->willThrowException(new LoginFlowV2NotFoundException()); |
|||
|
|||
$result = $this->controller->showAuthPickerPage(); |
|||
|
|||
$this->assertSame(Http::STATUS_FORBIDDEN, $result->getStatus()); |
|||
} |
|||
|
|||
public function testShowAuthPickerValidLoginToken() { |
|||
$this->session->method('get') |
|||
->with('client.flow.v2.login.token') |
|||
->willReturn('loginToken'); |
|||
|
|||
$flow = new LoginFlowV2(); |
|||
$this->loginFlowV2Service->method('getByLoginToken') |
|||
->with('loginToken') |
|||
->willReturn($flow); |
|||
|
|||
$this->random->method('generate') |
|||
->with(64, ISecureRandom::CHAR_LOWER.ISecureRandom::CHAR_UPPER.ISecureRandom::CHAR_DIGITS) |
|||
->willReturn('random'); |
|||
$this->session->expects($this->once()) |
|||
->method('set') |
|||
->with('client.flow.v2.state.token', 'random'); |
|||
|
|||
$this->controller->showAuthPickerPage(); |
|||
} |
|||
|
|||
public function testGrantPageInvalidStateToken() { |
|||
$this->session->method('get') |
|||
->will($this->returnCallback(function($name) { |
|||
return null; |
|||
})); |
|||
|
|||
$result = $this->controller->grantPage('stateToken'); |
|||
$this->assertSame(Http::STATUS_FORBIDDEN, $result->getStatus()); |
|||
} |
|||
|
|||
public function testGrantPageInvalidLoginToken() { |
|||
$this->session->method('get') |
|||
->will($this->returnCallback(function($name) { |
|||
if ($name === 'client.flow.v2.state.token') { |
|||
return 'stateToken'; |
|||
} |
|||
if ($name === 'client.flow.v2.login.token') { |
|||
return 'loginToken'; |
|||
} |
|||
return null; |
|||
})); |
|||
|
|||
$this->loginFlowV2Service->method('getByLoginToken') |
|||
->with('loginToken') |
|||
->willThrowException(new LoginFlowV2NotFoundException()); |
|||
|
|||
$result = $this->controller->grantPage('stateToken'); |
|||
$this->assertSame(Http::STATUS_FORBIDDEN, $result->getStatus()); |
|||
} |
|||
|
|||
public function testGrantPageValid() { |
|||
$this->session->method('get') |
|||
->will($this->returnCallback(function($name) { |
|||
if ($name === 'client.flow.v2.state.token') { |
|||
return 'stateToken'; |
|||
} |
|||
if ($name === 'client.flow.v2.login.token') { |
|||
return 'loginToken'; |
|||
} |
|||
return null; |
|||
})); |
|||
|
|||
$flow = new LoginFlowV2(); |
|||
$this->loginFlowV2Service->method('getByLoginToken') |
|||
->with('loginToken') |
|||
->willReturn($flow); |
|||
|
|||
$result = $this->controller->grantPage('stateToken'); |
|||
$this->assertSame(Http::STATUS_OK, $result->getStatus()); |
|||
} |
|||
|
|||
|
|||
public function testGenerateAppPasswordInvalidStateToken() { |
|||
$this->session->method('get') |
|||
->will($this->returnCallback(function($name) { |
|||
return null; |
|||
})); |
|||
|
|||
$result = $this->controller->generateAppPassword('stateToken'); |
|||
$this->assertSame(Http::STATUS_FORBIDDEN, $result->getStatus()); |
|||
} |
|||
|
|||
public function testGenerateAppPassworInvalidLoginToken() { |
|||
$this->session->method('get') |
|||
->will($this->returnCallback(function($name) { |
|||
if ($name === 'client.flow.v2.state.token') { |
|||
return 'stateToken'; |
|||
} |
|||
if ($name === 'client.flow.v2.login.token') { |
|||
return 'loginToken'; |
|||
} |
|||
return null; |
|||
})); |
|||
|
|||
$this->loginFlowV2Service->method('getByLoginToken') |
|||
->with('loginToken') |
|||
->willThrowException(new LoginFlowV2NotFoundException()); |
|||
|
|||
$result = $this->controller->generateAppPassword('stateToken'); |
|||
$this->assertSame(Http::STATUS_FORBIDDEN, $result->getStatus()); |
|||
} |
|||
|
|||
public function testGenerateAppPassworValid() { |
|||
$this->session->method('get') |
|||
->will($this->returnCallback(function($name) { |
|||
if ($name === 'client.flow.v2.state.token') { |
|||
return 'stateToken'; |
|||
} |
|||
if ($name === 'client.flow.v2.login.token') { |
|||
return 'loginToken'; |
|||
} |
|||
return null; |
|||
})); |
|||
|
|||
$flow = new LoginFlowV2(); |
|||
$this->loginFlowV2Service->method('getByLoginToken') |
|||
->with('loginToken') |
|||
->willReturn($flow); |
|||
|
|||
$clearedState = false; |
|||
$clearedLogin = false; |
|||
$this->session->method('remove') |
|||
->will($this->returnCallback(function ($name) use (&$clearedLogin, &$clearedState) { |
|||
if ($name === 'client.flow.v2.state.token') { |
|||
$clearedState = true; |
|||
} |
|||
if ($name === 'client.flow.v2.login.token') { |
|||
$clearedLogin = true; |
|||
} |
|||
})); |
|||
|
|||
$this->session->method('getId') |
|||
->willReturn('sessionId'); |
|||
|
|||
$this->loginFlowV2Service->expects($this->once()) |
|||
->method('flowDone') |
|||
->with( |
|||
'loginToken', |
|||
'sessionId', |
|||
'https://server', |
|||
'user' |
|||
)->willReturn(true); |
|||
|
|||
$this->request->method('getServerProtocol') |
|||
->willReturn('https'); |
|||
$this->request->method('getRequestUri') |
|||
->willReturn('/login/v2'); |
|||
$this->request->method('getServerHost') |
|||
->willReturn('server'); |
|||
|
|||
$result = $this->controller->generateAppPassword('stateToken'); |
|||
$this->assertSame(Http::STATUS_OK, $result->getStatus()); |
|||
|
|||
$this->assertTrue($clearedLogin); |
|||
$this->assertTrue($clearedState); |
|||
} |
|||
} |
|||
|
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue