--- /dev/null
+<?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();
+ }
+}
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?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,
+ ];
+ }
+
+
+}
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?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');
+ }
+}
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?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 {
+
+}
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?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;
+ }
+}
['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'],
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+<?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>
'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',
'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',
'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',
'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',
'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',
'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',
'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',
'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',
'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',
'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',
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;
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()),
];
}
--- /dev/null
+<?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);
+ }
+
+}
--- /dev/null
+<?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);
+ }
+}
+
// 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';