Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ This application allows external request of new accounts.

![screen](https://user-images.githubusercontent.com/14975046/45147329-f829de80-b1c4-11e8-8024-8e53dec98f6c.png)


# 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.
Expand All @@ -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/
1 change: 1 addition & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -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']
]
Expand Down
19 changes: 19 additions & 0 deletions js/flow-login.js
Original file line number Diff line number Diff line change
@@ -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)
}
})()
12 changes: 9 additions & 3 deletions lib/Controller/AccountController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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]);

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

Expand Down
109 changes: 92 additions & 17 deletions lib/Controller/PasswordController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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
*
Expand All @@ -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);
Expand All @@ -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
Expand All @@ -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('/'));
}
Expand All @@ -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();
Expand All @@ -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
*
Expand Down Expand Up @@ -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
*
Expand All @@ -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) {
Expand All @@ -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;
}
}
18 changes: 18 additions & 0 deletions templates/flow-login.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2026
*
* @license GNU AGPL version 3 or any later version
*/

\OCP\Util::addScript('preferred_providers', 'flow-login');
\OCP\Util::addStyle('preferred_providers', 'password-public');

?>
<div class="guest-box login-box" id="flow-login"
data-nc-login-url="<?php p($_['ncLoginUrl']); ?>"
data-redirect-url="<?php p($_['redirectUrl']); ?>">
<h2><?php p($l->t('Log in')); ?></h2>
<p class="info"><?php p($l->t('Your account is ready.')); ?></p>
</div>
1 change: 1 addition & 0 deletions templates/password-public.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
<!-- Submit -->
<div id="submit-wrapper">
<input type="hidden" value="<?php print_unescaped($_['ocsapirequest']) ?>" name="ocsapirequest">
<input type="hidden" value="<?php p($_['flow']) ?>" name="flow">
<input class="login primary" type="submit" id="submit" value="<?php p($l->t('Log in')); ?>" />
<div class="submit-icon icon-confirm-white"></div>
</div>
Expand Down
Loading