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>
This commit is contained in:
Roeland Jago Douma 2019-02-12 09:26:46 +01:00
parent 5df6400e28
commit e819e97829
No known key found for this signature in database
GPG Key ID: F941078878347C0C
19 changed files with 1574 additions and 1 deletions

View File

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

View File

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

View File

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

View File

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

85
core/Db/LoginFlowV2.php Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -52,10 +52,18 @@ $application->registerRoutes($this, [
['name' => 'login#confirmPassword', 'url' => '/login/confirm', 'verb' => 'POST'],
['name' => 'login#showLoginForm', 'url' => '/login', 'verb' => 'GET'],
['name' => 'login#logout', 'url' => '/logout', 'verb' => 'GET'],
// Original login flow used by all clients
['name' => 'ClientFlowLogin#showAuthPickerPage', 'url' => '/login/flow', 'verb' => 'GET'],
['name' => 'ClientFlowLogin#generateAppPassword', 'url' => '/login/flow', 'verb' => 'POST'],
['name' => 'ClientFlowLogin#grantPage', 'url' => '/login/flow/grant', 'verb' => 'GET'],
['name' => 'ClientFlowLogin#apptokenRedirect', 'url' => '/login/flow/apptoken', 'verb' => 'POST'],
// NG login flow used by desktop client in case of Kerberos/fancy 2fa (smart cards for example)
['name' => 'ClientFlowLoginV2#poll', 'url' => '/login/v2/poll', 'verb' => 'POST'],
['name' => 'ClientFlowLoginV2#showAuthPickerPage', 'url' => '/login/v2/flow', 'verb' => 'GET'],
['name' => 'ClientFlowLoginV2#landing', 'url' => '/login/v2/flow/{token}', 'verb' => 'GET'],
['name' => 'ClientFlowLoginV2#grantPage', 'url' => '/login/v2/grant', 'verb' => 'GET'],
['name' => 'ClientFlowLoginV2#generateAppPassword', 'url' => '/login/v2/grant', 'verb' => 'POST'],
['name' => 'ClientFlowLoginV2#init', 'url' => '/login/v2', 'verb' => 'POST'],
['name' => 'TwoFactorChallenge#selectChallenge', 'url' => '/login/selectchallenge', 'verb' => 'GET'],
['name' => 'TwoFactorChallenge#showChallenge', 'url' => '/login/challenge/{challengeProviderId}', 'verb' => 'GET'],
['name' => 'TwoFactorChallenge#solveChallenge', 'url' => '/login/challenge/{challengeProviderId}', 'verb' => 'POST'],

View File

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

View File

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

View File

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

View File

@ -565,6 +565,7 @@ return array(
'OC\\Contacts\\ContactsMenu\\Providers\\EMailProvider' => $baseDir . '/lib/private/Contacts/ContactsMenu/Providers/EMailProvider.php',
'OC\\Core\\Application' => $baseDir . '/core/Application.php',
'OC\\Core\\BackgroundJobs\\BackgroundCleanupUpdaterBackupsJob' => $baseDir . '/core/BackgroundJobs/BackgroundCleanupUpdaterBackupsJob.php',
'OC\\Core\\BackgroundJobs\\CleanupLoginFlowV2' => $baseDir . '/core/BackgroundJobs/CleanupLoginFlowV2.php',
'OC\\Core\\Command\\App\\CheckCode' => $baseDir . '/core/Command/App/CheckCode.php',
'OC\\Core\\Command\\App\\Disable' => $baseDir . '/core/Command/App/Disable.php',
'OC\\Core\\Command\\App\\Enable' => $baseDir . '/core/Command/App/Enable.php',
@ -654,6 +655,7 @@ return array(
'OC\\Core\\Controller\\AvatarController' => $baseDir . '/core/Controller/AvatarController.php',
'OC\\Core\\Controller\\CSRFTokenController' => $baseDir . '/core/Controller/CSRFTokenController.php',
'OC\\Core\\Controller\\ClientFlowLoginController' => $baseDir . '/core/Controller/ClientFlowLoginController.php',
'OC\\Core\\Controller\\ClientFlowLoginV2Controller' => $baseDir . '/core/Controller/ClientFlowLoginV2Controller.php',
'OC\\Core\\Controller\\ContactsMenuController' => $baseDir . '/core/Controller/ContactsMenuController.php',
'OC\\Core\\Controller\\CssController' => $baseDir . '/core/Controller/CssController.php',
'OC\\Core\\Controller\\GuestAvatarController' => $baseDir . '/core/Controller/GuestAvatarController.php',
@ -671,6 +673,11 @@ return array(
'OC\\Core\\Controller\\UserController' => $baseDir . '/core/Controller/UserController.php',
'OC\\Core\\Controller\\WalledGardenController' => $baseDir . '/core/Controller/WalledGardenController.php',
'OC\\Core\\Controller\\WhatsNewController' => $baseDir . '/core/Controller/WhatsNewController.php',
'OC\\Core\\Data\\LoginFlowV2Credentials' => $baseDir . '/core/Data/LoginFlowV2Credentials.php',
'OC\\Core\\Data\\LoginFlowV2Tokens' => $baseDir . '/core/Data/LoginFlowV2Tokens.php',
'OC\\Core\\Db\\LoginFlowV2' => $baseDir . '/core/Db/LoginFlowV2.php',
'OC\\Core\\Db\\LoginFlowV2Mapper' => $baseDir . '/core/Db/LoginFlowV2Mapper.php',
'OC\\Core\\Exception\\LoginFlowV2NotFoundException' => $baseDir . '/core/Exception/LoginFlowV2NotFoundException.php',
'OC\\Core\\Middleware\\TwoFactorMiddleware' => $baseDir . '/core/Middleware/TwoFactorMiddleware.php',
'OC\\Core\\Migrations\\Version13000Date20170705121758' => $baseDir . '/core/Migrations/Version13000Date20170705121758.php',
'OC\\Core\\Migrations\\Version13000Date20170718121200' => $baseDir . '/core/Migrations/Version13000Date20170718121200.php',
@ -688,6 +695,8 @@ return array(
'OC\\Core\\Migrations\\Version15000Date20180926101451' => $baseDir . '/core/Migrations/Version15000Date20180926101451.php',
'OC\\Core\\Migrations\\Version15000Date20181015062942' => $baseDir . '/core/Migrations/Version15000Date20181015062942.php',
'OC\\Core\\Migrations\\Version15000Date20181029084625' => $baseDir . '/core/Migrations/Version15000Date20181029084625.php',
'OC\\Core\\Migrations\\Version16000Date20190212081545' => $baseDir . '/core/Migrations/Version16000Date20190212081545.php',
'OC\\Core\\Service\\LoginFlowV2Service' => $baseDir . '/core/Service/LoginFlowV2Service.php',
'OC\\DB\\Adapter' => $baseDir . '/lib/private/DB/Adapter.php',
'OC\\DB\\AdapterMySQL' => $baseDir . '/lib/private/DB/AdapterMySQL.php',
'OC\\DB\\AdapterOCI8' => $baseDir . '/lib/private/DB/AdapterOCI8.php',
@ -985,6 +994,7 @@ return array(
'OC\\Repair\\NC14\\AddPreviewBackgroundCleanupJob' => $baseDir . '/lib/private/Repair/NC14/AddPreviewBackgroundCleanupJob.php',
'OC\\Repair\\NC14\\RepairPendingCronJobs' => $baseDir . '/lib/private/Repair/NC14/RepairPendingCronJobs.php',
'OC\\Repair\\NC15\\SetVcardDatabaseUID' => $baseDir . '/lib/private/Repair/NC15/SetVcardDatabaseUID.php',
'OC\\Repair\\NC16\\AddClenupLoginFlowV2BackgroundJob' => $baseDir . '/lib/private/Repair/NC16/AddClenupLoginFlowV2BackgroundJob.php',
'OC\\Repair\\NC16\\CleanupCardDAVPhotoCache' => $baseDir . '/lib/private/Repair/NC16/CleanupCardDAVPhotoCache.php',
'OC\\Repair\\OldGroupMembershipShares' => $baseDir . '/lib/private/Repair/OldGroupMembershipShares.php',
'OC\\Repair\\Owncloud\\DropAccountTermsTable' => $baseDir . '/lib/private/Repair/Owncloud/DropAccountTermsTable.php',

View File

@ -595,6 +595,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
'OC\\Contacts\\ContactsMenu\\Providers\\EMailProvider' => __DIR__ . '/../../..' . '/lib/private/Contacts/ContactsMenu/Providers/EMailProvider.php',
'OC\\Core\\Application' => __DIR__ . '/../../..' . '/core/Application.php',
'OC\\Core\\BackgroundJobs\\BackgroundCleanupUpdaterBackupsJob' => __DIR__ . '/../../..' . '/core/BackgroundJobs/BackgroundCleanupUpdaterBackupsJob.php',
'OC\\Core\\BackgroundJobs\\CleanupLoginFlowV2' => __DIR__ . '/../../..' . '/core/BackgroundJobs/CleanupLoginFlowV2.php',
'OC\\Core\\Command\\App\\CheckCode' => __DIR__ . '/../../..' . '/core/Command/App/CheckCode.php',
'OC\\Core\\Command\\App\\Disable' => __DIR__ . '/../../..' . '/core/Command/App/Disable.php',
'OC\\Core\\Command\\App\\Enable' => __DIR__ . '/../../..' . '/core/Command/App/Enable.php',
@ -684,6 +685,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
'OC\\Core\\Controller\\AvatarController' => __DIR__ . '/../../..' . '/core/Controller/AvatarController.php',
'OC\\Core\\Controller\\CSRFTokenController' => __DIR__ . '/../../..' . '/core/Controller/CSRFTokenController.php',
'OC\\Core\\Controller\\ClientFlowLoginController' => __DIR__ . '/../../..' . '/core/Controller/ClientFlowLoginController.php',
'OC\\Core\\Controller\\ClientFlowLoginV2Controller' => __DIR__ . '/../../..' . '/core/Controller/ClientFlowLoginV2Controller.php',
'OC\\Core\\Controller\\ContactsMenuController' => __DIR__ . '/../../..' . '/core/Controller/ContactsMenuController.php',
'OC\\Core\\Controller\\CssController' => __DIR__ . '/../../..' . '/core/Controller/CssController.php',
'OC\\Core\\Controller\\GuestAvatarController' => __DIR__ . '/../../..' . '/core/Controller/GuestAvatarController.php',
@ -701,6 +703,11 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
'OC\\Core\\Controller\\UserController' => __DIR__ . '/../../..' . '/core/Controller/UserController.php',
'OC\\Core\\Controller\\WalledGardenController' => __DIR__ . '/../../..' . '/core/Controller/WalledGardenController.php',
'OC\\Core\\Controller\\WhatsNewController' => __DIR__ . '/../../..' . '/core/Controller/WhatsNewController.php',
'OC\\Core\\Data\\LoginFlowV2Credentials' => __DIR__ . '/../../..' . '/core/Data/LoginFlowV2Credentials.php',
'OC\\Core\\Data\\LoginFlowV2Tokens' => __DIR__ . '/../../..' . '/core/Data/LoginFlowV2Tokens.php',
'OC\\Core\\Db\\LoginFlowV2' => __DIR__ . '/../../..' . '/core/Db/LoginFlowV2.php',
'OC\\Core\\Db\\LoginFlowV2Mapper' => __DIR__ . '/../../..' . '/core/Db/LoginFlowV2Mapper.php',
'OC\\Core\\Exception\\LoginFlowV2NotFoundException' => __DIR__ . '/../../..' . '/core/Exception/LoginFlowV2NotFoundException.php',
'OC\\Core\\Middleware\\TwoFactorMiddleware' => __DIR__ . '/../../..' . '/core/Middleware/TwoFactorMiddleware.php',
'OC\\Core\\Migrations\\Version13000Date20170705121758' => __DIR__ . '/../../..' . '/core/Migrations/Version13000Date20170705121758.php',
'OC\\Core\\Migrations\\Version13000Date20170718121200' => __DIR__ . '/../../..' . '/core/Migrations/Version13000Date20170718121200.php',
@ -718,6 +725,8 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
'OC\\Core\\Migrations\\Version15000Date20180926101451' => __DIR__ . '/../../..' . '/core/Migrations/Version15000Date20180926101451.php',
'OC\\Core\\Migrations\\Version15000Date20181015062942' => __DIR__ . '/../../..' . '/core/Migrations/Version15000Date20181015062942.php',
'OC\\Core\\Migrations\\Version15000Date20181029084625' => __DIR__ . '/../../..' . '/core/Migrations/Version15000Date20181029084625.php',
'OC\\Core\\Migrations\\Version16000Date20190212081545' => __DIR__ . '/../../..' . '/core/Migrations/Version16000Date20190212081545.php',
'OC\\Core\\Service\\LoginFlowV2Service' => __DIR__ . '/../../..' . '/core/Service/LoginFlowV2Service.php',
'OC\\DB\\Adapter' => __DIR__ . '/../../..' . '/lib/private/DB/Adapter.php',
'OC\\DB\\AdapterMySQL' => __DIR__ . '/../../..' . '/lib/private/DB/AdapterMySQL.php',
'OC\\DB\\AdapterOCI8' => __DIR__ . '/../../..' . '/lib/private/DB/AdapterOCI8.php',
@ -1015,6 +1024,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
'OC\\Repair\\NC14\\AddPreviewBackgroundCleanupJob' => __DIR__ . '/../../..' . '/lib/private/Repair/NC14/AddPreviewBackgroundCleanupJob.php',
'OC\\Repair\\NC14\\RepairPendingCronJobs' => __DIR__ . '/../../..' . '/lib/private/Repair/NC14/RepairPendingCronJobs.php',
'OC\\Repair\\NC15\\SetVcardDatabaseUID' => __DIR__ . '/../../..' . '/lib/private/Repair/NC15/SetVcardDatabaseUID.php',
'OC\\Repair\\NC16\\AddClenupLoginFlowV2BackgroundJob' => __DIR__ . '/../../..' . '/lib/private/Repair/NC16/AddClenupLoginFlowV2BackgroundJob.php',
'OC\\Repair\\NC16\\CleanupCardDAVPhotoCache' => __DIR__ . '/../../..' . '/lib/private/Repair/NC16/CleanupCardDAVPhotoCache.php',
'OC\\Repair\\OldGroupMembershipShares' => __DIR__ . '/../../..' . '/lib/private/Repair/OldGroupMembershipShares.php',
'OC\\Repair\\Owncloud\\DropAccountTermsTable' => __DIR__ . '/../../..' . '/lib/private/Repair/Owncloud/DropAccountTermsTable.php',

View File

@ -43,6 +43,7 @@ use OC\Repair\NC13\RepairInvalidPaths;
use OC\Repair\NC14\AddPreviewBackgroundCleanupJob;
use OC\Repair\NC14\RepairPendingCronJobs;
use OC\Repair\NC15\SetVcardDatabaseUID;
use OC\Repair\NC16\AddClenupLoginFlowV2BackgroundJob;
use OC\Repair\NC16\CleanupCardDAVPhotoCache;
use OC\Repair\OldGroupMembershipShares;
use OC\Repair\Owncloud\DropAccountTermsTable;
@ -150,6 +151,7 @@ class Repair implements IOutput {
new RepairPendingCronJobs(\OC::$server->getDatabaseConnection(), \OC::$server->getConfig()),
new SetVcardDatabaseUID(\OC::$server->getDatabaseConnection(), \OC::$server->getConfig(), \OC::$server->getLogger()),
new CleanupCardDAVPhotoCache(\OC::$server->getConfig(), \OC::$server->getAppDataDir('dav-photocache'), \OC::$server->getLogger()),
new AddClenupLoginFlowV2BackgroundJob(\OC::$server->getJobList()),
];
}

View File

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

View File

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

View File

@ -29,7 +29,7 @@
// between betas, final and RCs. This is _not_ the public version number. Reset minor/patchlevel
// when updating major/minor version number.
$OC_Version = array(16, 0, 0, 0);
$OC_Version = array(16, 0, 0, 1);
// The human readable string
$OC_VersionString = '16.0.0 alpha';