diff options
author | Jan-Christoph Borchardt <hey@jancborchardt.net> | 2017-04-26 00:11:55 +0200 |
---|---|---|
committer | Jan-Christoph Borchardt <hey@jancborchardt.net> | 2017-04-26 00:50:38 +0200 |
commit | 241e397326545ee3ecad1a6a50dbe7839faa5c21 (patch) | |
tree | 6d33b4e4cc22bb1bf4753d83f44e1bb8422082e3 /core | |
parent | 0f0b04b7d9b4fa8c3c74218c222194f0f2f9e8b7 (diff) | |
parent | 255c7df3bdbaccf00ba8e9fb00e750ffb9a50356 (diff) | |
download | nextcloud-server-241e397326545ee3ecad1a6a50dbe7839faa5c21.tar.gz nextcloud-server-241e397326545ee3ecad1a6a50dbe7839faa5c21.zip |
Merge branch 'master' into contactsmenu
Signed-off-by: Jan-Christoph Borchardt <hey@jancborchardt.net>
Diffstat (limited to 'core')
-rw-r--r-- | core/Controller/ClientFlowLoginController.php | 238 | ||||
-rw-r--r-- | core/css/login/authpicker.css | 9 | ||||
-rw-r--r-- | core/js/js.js | 1 | ||||
-rw-r--r-- | core/js/login/authpicker.js | 13 | ||||
-rw-r--r-- | core/js/login/redirect.js | 3 | ||||
-rw-r--r-- | core/routes.php | 3 | ||||
-rw-r--r-- | core/templates/loginflow/authpicker.php | 57 | ||||
-rw-r--r-- | core/templates/loginflow/redirect.php | 37 |
8 files changed, 360 insertions, 1 deletions
diff --git a/core/Controller/ClientFlowLoginController.php b/core/Controller/ClientFlowLoginController.php new file mode 100644 index 00000000000..ca9c092321a --- /dev/null +++ b/core/Controller/ClientFlowLoginController.php @@ -0,0 +1,238 @@ +<?php +/** + * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\Core\Controller; + +use OC\Authentication\Exceptions\InvalidTokenException; +use OC\Authentication\Exceptions\PasswordlessTokenException; +use OC\Authentication\Token\IProvider; +use OC\Authentication\Token\IToken; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Response; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\Defaults; +use OCP\IL10N; +use OCP\IRequest; +use OCP\ISession; +use OCP\IURLGenerator; +use OCP\IUserSession; +use OCP\Security\ISecureRandom; +use OCP\Session\Exceptions\SessionNotAvailableException; + +class ClientFlowLoginController extends Controller { + /** @var IUserSession */ + private $userSession; + /** @var IL10N */ + private $l10n; + /** @var Defaults */ + private $defaults; + /** @var ISession */ + private $session; + /** @var IProvider */ + private $tokenProvider; + /** @var ISecureRandom */ + private $random; + /** @var IURLGenerator */ + private $urlGenerator; + + const stateName = 'client.flow.state.token'; + + /** + * @param string $appName + * @param IRequest $request + * @param IUserSession $userSession + * @param IL10N $l10n + * @param Defaults $defaults + * @param ISession $session + * @param IProvider $tokenProvider + * @param ISecureRandom $random + * @param IURLGenerator $urlGenerator + */ + public function __construct($appName, + IRequest $request, + IUserSession $userSession, + IL10N $l10n, + Defaults $defaults, + ISession $session, + IProvider $tokenProvider, + ISecureRandom $random, + IURLGenerator $urlGenerator) { + parent::__construct($appName, $request); + $this->userSession = $userSession; + $this->l10n = $l10n; + $this->defaults = $defaults; + $this->session = $session; + $this->tokenProvider = $tokenProvider; + $this->random = $random; + $this->urlGenerator = $urlGenerator; + } + + /** + * @return string + */ + private function getClientName() { + return $this->request->getHeader('USER_AGENT') !== null ? $this->request->getHeader('USER_AGENT') : 'unknown'; + } + + /** + * @param string $stateToken + * @return bool + */ + private function isValidToken($stateToken) { + $currentToken = $this->session->get(self::stateName); + if(!is_string($stateToken) || !is_string($currentToken)) { + return false; + } + return hash_equals($currentToken, $stateToken); + } + + /** + * @return TemplateResponse + */ + private function stateTokenForbiddenResponse() { + $response = new TemplateResponse( + $this->appName, + '403', + [ + 'file' => $this->l10n->t('State token does not match'), + ], + 'guest' + ); + $response->setStatus(Http::STATUS_FORBIDDEN); + return $response; + } + + /** + * @PublicPage + * @NoCSRFRequired + * @UseSession + * + * @return TemplateResponse + */ + public function showAuthPickerPage() { + if($this->userSession->isLoggedIn()) { + return new TemplateResponse( + $this->appName, + '403', + [ + 'file' => $this->l10n->t('Auth flow can only be started unauthenticated.'), + ], + 'guest' + ); + } + + $stateToken = $this->random->generate( + 64, + ISecureRandom::CHAR_LOWER.ISecureRandom::CHAR_UPPER.ISecureRandom::CHAR_DIGITS + ); + $this->session->set(self::stateName, $stateToken); + + return new TemplateResponse( + $this->appName, + 'loginflow/authpicker', + [ + 'client' => $this->getClientName(), + 'instanceName' => $this->defaults->getName(), + 'urlGenerator' => $this->urlGenerator, + 'stateToken' => $stateToken, + 'serverHost' => $this->request->getServerHost(), + ], + 'guest' + ); + } + + /** + * @NoAdminRequired + * @NoCSRFRequired + * @UseSession + * + * @param string $stateToken + * @return TemplateResponse + */ + public function redirectPage($stateToken = '') { + if(!$this->isValidToken($stateToken)) { + return $this->stateTokenForbiddenResponse(); + } + + return new TemplateResponse( + $this->appName, + 'loginflow/redirect', + [ + 'urlGenerator' => $this->urlGenerator, + 'stateToken' => $stateToken, + ], + 'empty' + ); + } + + /** + * @NoAdminRequired + * @UseSession + * + * @param string $stateToken + * @return Http\RedirectResponse|Response + */ + public function generateAppPassword($stateToken) { + if(!$this->isValidToken($stateToken)) { + $this->session->remove(self::stateName); + return $this->stateTokenForbiddenResponse(); + } + + $this->session->remove(self::stateName); + + try { + $sessionId = $this->session->getId(); + } catch (SessionNotAvailableException $ex) { + $response = new Response(); + $response->setStatus(Http::STATUS_FORBIDDEN); + return $response; + } + + try { + $sessionToken = $this->tokenProvider->getToken($sessionId); + $loginName = $sessionToken->getLoginName(); + try { + $password = $this->tokenProvider->getPassword($sessionToken, $sessionId); + } catch (PasswordlessTokenException $ex) { + $password = null; + } + } catch (InvalidTokenException $ex) { + $response = new Response(); + $response->setStatus(Http::STATUS_FORBIDDEN); + return $response; + } + + $token = $this->random->generate(72); + $this->tokenProvider->generateToken( + $token, + $this->userSession->getUser()->getUID(), + $loginName, + $password, + $this->getClientName(), + IToken::PERMANENT_TOKEN, + IToken::DO_NOT_REMEMBER + ); + + return new Http\RedirectResponse('nc://' . urlencode($loginName) . ':' . urlencode($token) . '@' . $this->request->getServerHost()); + } + +} diff --git a/core/css/login/authpicker.css b/core/css/login/authpicker.css new file mode 100644 index 00000000000..85016ee6a0e --- /dev/null +++ b/core/css/login/authpicker.css @@ -0,0 +1,9 @@ +.picker-window { + display: block; + padding: 10px; + margin-bottom: 20px; + background-color: rgba(0,0,0,.3); + color: #fff; + border-radius: 3px; + cursor: default; +} diff --git a/core/js/js.js b/core/js/js.js index 03d831567d3..d601f79033e 100644 --- a/core/js/js.js +++ b/core/js/js.js @@ -1515,7 +1515,6 @@ function initCore() { var appList = $('#appmenu li'); var availableWidth = $('#header-left').width() - $('#nextcloud').width() - 44; var appCount = Math.floor((availableWidth)/44); - console.log(appCount); // show a maximum of 8 apps if(appCount >= maxApps) { appCount = maxApps; diff --git a/core/js/login/authpicker.js b/core/js/login/authpicker.js new file mode 100644 index 00000000000..6d8a6bb4160 --- /dev/null +++ b/core/js/login/authpicker.js @@ -0,0 +1,13 @@ +jQuery(document).ready(function() { + $('#app-token-login').click(function (e) { + e.preventDefault(); + $(this).addClass('hidden'); + $('#redirect-link').addClass('hidden'); + $('#app-token-login-field').removeClass('hidden'); + }); + + $('#submit-app-token-login').click(function(e) { + e.preventDefault(); + window.location.href = 'nc://' + encodeURIComponent($('#user').val()) + ':' + encodeURIComponent($('#password').val()) + '@' + encodeURIComponent($('#serverHost').val()); + }); +}); diff --git a/core/js/login/redirect.js b/core/js/login/redirect.js new file mode 100644 index 00000000000..ea214feab2d --- /dev/null +++ b/core/js/login/redirect.js @@ -0,0 +1,3 @@ +jQuery(document).ready(function() { + $('#submit-redirect-form').trigger('click'); +}); diff --git a/core/routes.php b/core/routes.php index 02556c3a50d..37db2642c1b 100644 --- a/core/routes.php +++ b/core/routes.php @@ -49,6 +49,9 @@ $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'], + ['name' => 'ClientFlowLogin#showAuthPickerPage', 'url' => '/login/flow', 'verb' => 'GET'], + ['name' => 'ClientFlowLogin#redirectPage', 'url' => '/login/flow/redirect', 'verb' => 'GET'], + ['name' => 'ClientFlowLogin#generateAppPassword', 'url' => '/login/flow', '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/loginflow/authpicker.php b/core/templates/loginflow/authpicker.php new file mode 100644 index 00000000000..c5eb6cb316d --- /dev/null +++ b/core/templates/loginflow/authpicker.php @@ -0,0 +1,57 @@ +<?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/>. + * + */ + +script('core', 'login/authpicker'); +style('core', 'login/authpicker'); + +/** @var array $_ */ +/** @var \OCP\IURLGenerator $urlGenerator */ +$urlGenerator = $_['urlGenerator']; +?> + +<div class="picker-window"> + <p class="info"> + <?php p($l->t('You are about to grant "%s" access to your %s account.', [$_['client'], $_['instanceName']])) ?> + </p> + + <br/> + + <p id="redirect-link"> + <a href="<?php p($urlGenerator->linkToRouteAbsolute('core.ClientFlowLogin.redirectPage', ['stateToken' => $_['stateToken']])) ?>"> + <input type="submit" class="login primary icon-confirm-white" value="<?php p('Grant access') ?>"> + </a> + </p> + + <fieldset id="app-token-login-field" class="hidden"> + <p class="grouptop"> + <input type="text" name="user" id="user" placeholder="<?php p($l->t('Username')) ?>"> + <label for="user" class="infield"><?php p($l->t('Username')) ?></label> + </p> + <p class="groupbottom"> + <input type="password" name="password" id="password" placeholder="<?php p($l->t('App token')) ?>"> + <label for="password" class="infield"><?php p($l->t('Password')) ?></label> + </p> + <input type="hidden" id="serverHost" value="<?php p($_['serverHost']) ?>" /> + <input id="submit-app-token-login" type="submit" class="login primary icon-confirm-white" value="<?php p('Grant access') ?>"> + </fieldset> +</div> + +<a id="app-token-login" class="warning" href="#"><?php p($l->t('Alternative login using app token')) ?></a> diff --git a/core/templates/loginflow/redirect.php b/core/templates/loginflow/redirect.php new file mode 100644 index 00000000000..7ef0184f61f --- /dev/null +++ b/core/templates/loginflow/redirect.php @@ -0,0 +1,37 @@ +<?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/>. + * + */ +script('core', 'login/redirect'); +style('core', 'login/authpicker'); + +/** @var array $_ */ +/** @var \OCP\IURLGenerator $urlGenerator */ +$urlGenerator = $_['urlGenerator']; +?> + +<div class="picker-window"> + <p class="info"><?php p($l->t('Redirecting …')) ?></p> +</div> + +<form method="POST" action="<?php p($urlGenerator->linkToRouteAbsolute('core.ClientFlowLogin.generateAppPassword')) ?>"> + <input type="hidden" name="requesttoken" value="<?php p($_['requesttoken']) ?>" /> + <input type="hidden" name="stateToken" value="<?php p($_['stateToken']) ?>" /> + <input id="submit-redirect-form" type="submit" class="hidden "/> +</form> |