diff --git a/README.md b/README.md index 7199703..b7a01f3 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,8 @@ This application allows external request of new accounts.  - +# Test +## Web and Mobile Clients 1. Install and enable the application. 2. Go to the preferred providers settings and keep your token in reach. 3. Make a POST request to `/ocs/v2.php/account/request/YOURTOKEN` with the `{email: 'myawesomemail@nextcloud.com'}` data. @@ -17,5 +18,19 @@ This application allows external request of new accounts. 5. Meanwhile a mail confirmation is sent to the user. He have 6h to confirm or his account will be disabled 6. After 4, if you set up the `OCS-APIREQUEST` header, you will be redirected to a `nc://` url with valid app-password token for your application. If not, you will be logged and redirected to the home page. +## Desktop Client +1. Install and enable the application. +2. Go to the preferred providers settings and keep your token in reach. +3. Make a POST request to `/ocs/v2.php/account/request/YOURTOKEN` with the `{email: 'myawesomemail@nextcloud.com', flow: 'V3'}` data. + ``` js + $.post('/ocs/v2.php/account/request/56300a2bf7e06894a5b59c1eb47f7460', {email:'myawesomemail@nextcloud.com', flow: 'V3'}).complete((response) => { + console.log(JSON.parse(response.responseText).data.setPassword) + }) + ``` +4. The server will accept or not the request and provide a link for the user login and password definition https://cloud.yourdomain.com/apps/preferred_providers/password/set/yourawesomemail@nextcloud.com/aipTgstNeenUXe20BJTH8/flow/V3 +5. Meanwhile a mail confirmation is sent to the user. He have 6h to confirm or his account will be disabled +6. You set the passord for the user and will be locked in automatically +7. A `nc://` url with the server url will be triggered and the Desktop Client will open with the "Grant Access" Page + ## Website part The repo for the register modue on the website is https://github.com/nextcloud/nextcloud-register/ diff --git a/appinfo/routes.php b/appinfo/routes.php index 5a65fc8..14d8c94 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -33,6 +33,7 @@ 'routes' => [ ['name' => 'mail#confirm_mail_address', 'url' => '/login/confirm/{email}/{token}', 'verb' => 'GET'], ['name' => 'password#set_password', 'url' => '/password/set/{email}/{token}', 'verb' => 'GET'], + ['name' => 'password#set_password_flow', 'url' => '/password/set/{email}/{token}/flow/{flow}', 'verb' => 'GET'], ['name' => 'password#set_password_ocs', 'url' => '/password/set/{email}/{token}/{ocs}', 'verb' => 'GET'], ['name' => 'password#submit_password', 'url' => '/password/submit/{token}', 'verb' => 'POST'] ] diff --git a/js/flow-login.js b/js/flow-login.js new file mode 100644 index 0000000..109ec29 --- /dev/null +++ b/js/flow-login.js @@ -0,0 +1,19 @@ +(function() { + var flowLogin = document.getElementById('flow-login') + if (!flowLogin) { + return + } + + var ncLoginUrl = flowLogin.getAttribute('data-nc-login-url') + var redirectUrl = flowLogin.getAttribute('data-redirect-url') + + if (ncLoginUrl) { + window.location.assign(ncLoginUrl) + } + + if (redirectUrl) { + window.setTimeout(function() { + window.location.assign(redirectUrl) + }, 1000) + } +})() diff --git a/lib/Controller/AccountController.php b/lib/Controller/AccountController.php index affc44a..87fecb8 100644 --- a/lib/Controller/AccountController.php +++ b/lib/Controller/AccountController.php @@ -139,12 +139,13 @@ public function __construct(string $appName, * * @param string $token The security token required * @param string $email The email to create an account for + * @param string $flow The registration flow variant * * @return DataResponse the app password for the user * * @throws OCSForbiddenException */ - public function requestAccount(string $token = '', string $email = ''): DataResponse { + public function requestAccount(string $token = '', string $email = '', string $flow = ''): DataResponse { // checking if valid token $provider_token = $this->config->getAppValue($this->appName, 'provider_token'); if ($provider_token === '' || $provider_token !== $token) { @@ -213,7 +214,7 @@ public function requestAccount(string $token = '', string $email = ''): DataResp // generate set password token try { - $setPasswordUrl = $this->processSetPasswordToken($email); + $setPasswordUrl = $this->processSetPasswordToken($email, $flow); } catch (\Exception $e) { $this->logger->error("An error occured during the password token generation for $email", ['exception' => $e]); @@ -229,14 +230,19 @@ public function requestAccount(string $token = '', string $email = ''): DataResp * Generate token and process it * * @param string $email mail address + * @param string $flow The registration flow variant * @return string reset password url */ - private function processSetPasswordToken(string $email): string { + private function processSetPasswordToken(string $email, string $flow = ''): string { $token = $this->generateRandomToken(); $encryptedValue = $this->crypto->encrypt($token, $email . $this->config->getSystemValue('secret')); $this->config->setUserValue($email, $this->appName, 'set_password', $encryptedValue); $this->config->setUserValue($email, $this->appName, 'remind_password', strval(time())); + if ($flow === 'V3') { + return $this->urlGenerator->linkToRouteAbsolute($this->appName . '.password.set_password_flow', ['email' => $email, 'token' => $token, 'flow' => $flow]); + } + return $this->urlGenerator->linkToRouteAbsolute($this->appName . '.password.set_password', ['email' => $email, 'token' => $token]); } diff --git a/lib/Controller/PasswordController.php b/lib/Controller/PasswordController.php index 4217940..f6ece0b 100644 --- a/lib/Controller/PasswordController.php +++ b/lib/Controller/PasswordController.php @@ -123,7 +123,7 @@ public function __construct(string $appName, * * @param string $token The security token * @param string $email The user email - * @param string $ocsis this a ocs api request + * @param string $ocs is this a ocs api request * @return TemplateResponse */ public function setPassword(string $token, string $email, $ocs = false) { @@ -138,6 +138,25 @@ public function setPassword(string $token, string $email, $ocs = false) { return $this->generateTemplate($token, $email, '', $ocs !== false); } + /** + * @NoCSRFRequired + * + * @PublicPage + * + * shortcut for secondary route with flow parameter + */ + public function setPasswordFlow(string $token, string $email, string $flow = ''): TemplateResponse { + try { + $this->checkPasswordToken($token, $email); + } catch (\Exception $e) { + return new TemplateResponse('core', 'error', [ + 'errors' => [['error' => $e->getMessage()]] + ], 'guest'); + } + + return $this->generateTemplate($token, $email, '', false, $flow); + } + /** * @NoCSRFRequired * @@ -160,9 +179,10 @@ public function setPasswordOcs(string $token, string $email, $ocs = false): Temp * @param string $email The user email * @param string $password The user password * @param string $ocsapirequest OCS-APIREQUEST header check + * @param string $flow registration flow variant * @return TemplateResponse|RedirectResponse */ - public function submitPassword(string $token, string $email, string $password, string $ocsapirequest = '') { + public function submitPassword(string $token, string $email, string $password, string $ocsapirequest = '', string $flow = '') { // process token validation try { $this->checkPasswordToken($token, $email); @@ -182,14 +202,19 @@ public function submitPassword(string $token, string $email, string $password, s try { $user = $this->userManager->get($email); if (!$user->setPassword($password)) { - return $this->generateTemplate($token, $email, $this->l10n->t('Unable to set the password. Contact your provider.'), $ocsapirequest === '1'); + return $this->generateTemplate($token, $email, $this->l10n->t('Unable to set the password. Contact your provider.'), $ocsapirequest === '1', $flow); } $this->config->deleteUserValue($email, $this->appName, 'set_password'); $this->config->deleteUserValue($email, $this->appName, 'remind_password'); // logout and ignore failure @\OC::$server->getUserSession()->unsetMagicInCookie(); } catch (\Exception $e) { - return $this->generateTemplate($token, $email, $e->getMessage(), $ocsapirequest === '1'); + return $this->generateTemplate($token, $email, $e->getMessage(), $ocsapirequest === '1', $flow); + } + + if ($flow === 'V3') { + $this->loginUser($email, $password); + return $this->generateFlowLoginResponse(); } // redirect to ClientFlowLogin if the request comes from android/ios/desktop @@ -200,13 +225,7 @@ public function submitPassword(string $token, string $email, string $password, s } // login - try { - $loginResult = $this->userManager->checkPasswordNoLogging($email, $password); - $this->userSession->completeLogin($loginResult, ['loginName' => $email, 'password' => $password]); - $this->userSession->createSessionToken($this->request, $loginResult->getUID(), $email, $password); - } catch (\Exception $e) { - $this->logger->debug('Unable to perform auto login for ' . $email, ['app' => $this->appName]); - } + $this->loginUser($email, $password); return new RedirectResponse($this->urlGenerator->getAbsoluteURL('/')); } @@ -219,20 +238,22 @@ public function submitPassword(string $token, string $email, string $password, s * @param string $error optional * @return TemplateResponse */ - protected function generateTemplate(string $token, string $email, string $error = '', bool $ocs = false) { + protected function generateTemplate(string $token, string $email, string $error = '', bool $ocs = false, string $flow = '') { + $ocsapirequest = $flow === '' && ($this->request->getHeader('OCS-APIREQUEST') || $ocs) ? '1' : ''; $response = new TemplateResponse( $this->appName, 'password-public', [ 'link' => $this->urlGenerator->linkToRoute($this->appName . '.password.submit_password', ['token' => $token]), 'email' => $email, - 'ocsapirequest' => $this->request->getHeader('OCS-APIREQUEST') || $ocs, + 'ocsapirequest' => $ocsapirequest, + 'flow' => $flow, 'error' => $error ], 'guest' ); - if ($ocs) { + if ($ocsapirequest !== '') { // We need to set the CSP header to allow the redirect to the Nextcloud client // some browsers (e.g. Safari) seems to block the redirect if the CSP header is not set. $csp = new ContentSecurityPolicy(); @@ -243,6 +264,27 @@ protected function generateTemplate(string $token, string $email, string $error return $response; } + /** + * @return TemplateResponse + */ + protected function generateFlowLoginResponse() { + $response = new TemplateResponse( + $this->appName, + 'flow-login', + [ + 'ncLoginUrl' => $this->generateServerLoginUrl(), + 'redirectUrl' => $this->urlGenerator->getAbsoluteURL('/'), + ], + 'guest' + ); + + $csp = new ContentSecurityPolicy(); + $csp->addAllowedFormActionDomain('nc://*'); + $response->setContentSecurityPolicy($csp); + + return $response; + } + /** * Check token authenticity * @@ -280,6 +322,22 @@ private function getClientName() { return $userAgent !== '' ? $userAgent : 'unknown'; } + /** + * @param string $email the user email/userId + * @param string $password the user password + * + * @return void + */ + private function loginUser(string $email, string $password): void { + try { + $loginResult = $this->userManager->checkPasswordNoLogging($email, $password); + $this->userSession->completeLogin($loginResult, ['loginName' => $email, 'password' => $password]); + $this->userSession->createSessionToken($this->request, $loginResult->getUID(), $email, $password); + } catch (\Exception $e) { + $this->logger->debug('Unable to perform auto login for ' . $email, ['app' => $this->appName]); + } + } + /** * generate application password and return nc protocol formatted url * @@ -293,6 +351,25 @@ protected function generateAppPassword(string $email, string $clientName) { $token = $this->secureRandom->generate(72, ISecureRandom::CHAR_HUMAN_READABLE); $this->tokenProvider->generateToken($token, $email, $email, null, $clientName); + $serverPath = $this->getServerPath(); + $redirectUri = 'nc://login/server:' . $serverPath . '&user:' . urlencode($email) . '&password:' . urlencode($token); + + return $redirectUri; + } + + /** + * generate server-only nc protocol formatted url + * + * @return string + */ + protected function generateServerLoginUrl() { + return 'nc://login/server:' . rtrim($this->urlGenerator->getBaseUrl(), '/'); + } + + /** + * @return string + */ + private function getServerPath() { $serverPostfix = ''; if (strpos($this->request->getRequestUri(), '/index.php') !== false) { @@ -312,8 +389,6 @@ protected function generateAppPassword(string $email, string $clientName) { } $serverPath = $protocol . '://' . $this->request->getServerHost() . $serverPostfix; - $redirectUri = 'nc://login/server:' . $serverPath . '&user:' . urlencode($email) . '&password:' . urlencode($token); - - return $redirectUri; + return $serverPath; } } diff --git a/templates/flow-login.php b/templates/flow-login.php new file mode 100644 index 0000000..fe2a43f --- /dev/null +++ b/templates/flow-login.php @@ -0,0 +1,18 @@ + +
t('Your account is ready.')); ?>
+