]> source.dussan.org Git - nextcloud-server.git/commitdiff
Login flow V2 14161/head
authorRoeland Jago Douma <roeland@famdouma.nl>
Tue, 12 Feb 2019 08:26:46 +0000 (09:26 +0100)
committerRoeland Jago Douma <roeland@famdouma.nl>
Mon, 25 Feb 2019 06:24:50 +0000 (07:24 +0100)
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>
19 files changed:
core/BackgroundJobs/CleanupLoginFlowV2.php [new file with mode: 0644]
core/Controller/ClientFlowLoginV2Controller.php [new file with mode: 0644]
core/Data/LoginFlowV2Credentials.php [new file with mode: 0644]
core/Data/LoginFlowV2Tokens.php [new file with mode: 0644]
core/Db/LoginFlowV2.php [new file with mode: 0644]
core/Db/LoginFlowV2Mapper.php [new file with mode: 0644]
core/Exception/LoginFlowV2NotFoundException.php [new file with mode: 0644]
core/Migrations/Version16000Date20190212081545.php [new file with mode: 0644]
core/Service/LoginFlowV2Service.php [new file with mode: 0644]
core/routes.php
core/templates/loginflowv2/authpicker.php [new file with mode: 0644]
core/templates/loginflowv2/done.php [new file with mode: 0644]
core/templates/loginflowv2/grant.php [new file with mode: 0644]
lib/composer/composer/autoload_classmap.php
lib/composer/composer/autoload_static.php
lib/private/Repair.php
lib/private/Repair/NC16/AddClenupLoginFlowV2BackgroundJob.php [new file with mode: 0644]
tests/Core/Controller/ClientFlowLoginV2ControllerTest.php [new file with mode: 0644]
version.php

diff --git a/core/BackgroundJobs/CleanupLoginFlowV2.php b/core/BackgroundJobs/CleanupLoginFlowV2.php
new file mode 100644 (file)
index 0000000..79d8c5c
--- /dev/null
@@ -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();
+       }
+}
diff --git a/core/Controller/ClientFlowLoginV2Controller.php b/core/Controller/ClientFlowLoginV2Controller.php
new file mode 100644 (file)
index 0000000..cb73b32
--- /dev/null
@@ -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;
+       }
+}
diff --git a/core/Data/LoginFlowV2Credentials.php b/core/Data/LoginFlowV2Credentials.php
new file mode 100644 (file)
index 0000000..68dd772
--- /dev/null
@@ -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,
+               ];
+       }
+
+
+}
diff --git a/core/Data/LoginFlowV2Tokens.php b/core/Data/LoginFlowV2Tokens.php
new file mode 100644 (file)
index 0000000..e32278d
--- /dev/null
@@ -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;
+       }
+}
diff --git a/core/Db/LoginFlowV2.php b/core/Db/LoginFlowV2.php
new file mode 100644 (file)
index 0000000..07ecb65
--- /dev/null
@@ -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');
+       }
+}
diff --git a/core/Db/LoginFlowV2Mapper.php b/core/Db/LoginFlowV2Mapper.php
new file mode 100644 (file)
index 0000000..a910455
--- /dev/null
@@ -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;
+       }
+}
diff --git a/core/Exception/LoginFlowV2NotFoundException.php b/core/Exception/LoginFlowV2NotFoundException.php
new file mode 100644 (file)
index 0000000..1e2bbb7
--- /dev/null
@@ -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 {
+
+}
diff --git a/core/Migrations/Version16000Date20190212081545.php b/core/Migrations/Version16000Date20190212081545.php
new file mode 100644 (file)
index 0000000..6f6902b
--- /dev/null
@@ -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;
+       }
+}
diff --git a/core/Service/LoginFlowV2Service.php b/core/Service/LoginFlowV2Service.php
new file mode 100644 (file)
index 0000000..d8912ad
--- /dev/null
@@ -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;
+       }
+}
index c5de63b8f33c1bf7162948e3b723ec88f39793df..d79fea1ca21e79cef3762e681a6badebee3d1820 100644 (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'],
diff --git a/core/templates/loginflowv2/authpicker.php b/core/templates/loginflowv2/authpicker.php
new file mode 100644 (file)
index 0000000..79462ee
--- /dev/null
@@ -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>
diff --git a/core/templates/loginflowv2/done.php b/core/templates/loginflowv2/done.php
new file mode 100644 (file)
index 0000000..aa5fc89
--- /dev/null
@@ -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>
diff --git a/core/templates/loginflowv2/grant.php b/core/templates/loginflowv2/grant.php
new file mode 100644 (file)
index 0000000..e5991d1
--- /dev/null
@@ -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>
index d74b6d11978780f9295fab9ab31c0681c1c4acab..bb1ea11f2e0a7b20b18a9a068ebdb82dcb73604a 100644 (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',
index a0a6cb0af3bde2bae8a304fcca2baba56a043987..910835045656de50d18d447e3427b269bd939572 100644 (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',
index 72995a96132089c706025af7e6424a2dce0cd196..e4eb4cfcc1638250bffbaf686c0d9b2b8dca9657 100644 (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()),
                ];
        }
 
diff --git a/lib/private/Repair/NC16/AddClenupLoginFlowV2BackgroundJob.php b/lib/private/Repair/NC16/AddClenupLoginFlowV2BackgroundJob.php
new file mode 100644 (file)
index 0000000..9f8bdef
--- /dev/null
@@ -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);
+       }
+
+}
diff --git a/tests/Core/Controller/ClientFlowLoginV2ControllerTest.php b/tests/Core/Controller/ClientFlowLoginV2ControllerTest.php
new file mode 100644 (file)
index 0000000..911a492
--- /dev/null
@@ -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);
+       }
+}
+
index d89c3893a872580321ad76ed18167d89c9e23714..971223cd2013e9c0676cac992f9ccec5fe65d595 100644 (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';