Merge pull request #234 from nextcloud/techdebt/223/login-flow-v2

Login Flow v2 compatibility
This commit is contained in:
Joas Schilling 2020-08-27 18:13:16 +02:00 committed by GitHub
commit 10bce3e131
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1233 additions and 443 deletions

2
.gitignore vendored
View File

@ -1,3 +1,5 @@
/.php_cs.cache
/tests/clover.xml
/tests/coverage-html
/tests/.phpunit.result.cache
/vendor

View File

@ -13,14 +13,16 @@
return [
'routes' => [
['name' => 'settings#admin', 'url' => '/settings', 'verb' => 'POST'],
['name' => 'register#askEmail', 'url' => '/', 'verb' => 'GET'],
['name' => 'register#validateEmail', 'url' => '/', 'verb' => 'POST'],
['name' => 'register#verifyToken', 'url' => '/verify/{token}', 'verb' => 'GET'],
['name' => 'register#createAccount', 'url' => '/verify/{token}', 'verb' => 'POST']
['name' => 'register#showEmailForm', 'url' => '/', 'verb' => 'GET'],
['name' => 'register#submitEmailForm', 'url' => '/', 'verb' => 'POST'],
['name' => 'register#showVerificationForm', 'url' => '/verify/{secret}', 'verb' => 'GET'],
['name' => 'register#submitVerificationForm', 'url' => '/verify/{secret}', 'verb' => 'POST'],
['name' => 'register#showUserForm', 'url' => '/register/{secret}/{token}', 'verb' => 'GET'],
['name' => 'register#submitUserForm', 'url' => '/register/{secret}/{token}', 'verb' => 'POST'],
],
'ocs' => [
['root' => '/registration', 'name' => 'api#validate', 'url' => '/v1/validate', 'verb' => 'POST'],
['root' => '/registration', 'name' => 'api#status', 'url' => '/v1/status', 'verb' => 'POST'],
['root' => '/registration', 'name' => 'api#register', 'url' => '/v1/register', 'verb' => 'POST']
['root' => '/registration', 'name' => 'api#register', 'url' => '/v1/register', 'verb' => 'POST'],
]
];

View File

@ -2,7 +2,11 @@
color: var(--color-primary-element);
}
.register-button:only-child {
#alternative-logins .register-button:only-child {
width: 220px;
margin-left: 70px !important;
}
#alternative-logins .register-button.hidden {
display: none;
}

View File

@ -1,8 +1,14 @@
#body-login #email, #body-login #username, #body-login #password {
#body-login #email,
#body-login #token,
#body-login #username,
#body-login #password {
width: calc(100% - 56px);
padding-left: 36px;
}
#email-icon, #username-icon, #password-icon {
#email-icon,
#token-icon,
#username-icon,
#password-icon {
position: absolute;
left: 16px;
top: 22px;

1
img/verify.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewbox="0 0 16 16" width="16" height="16"><path d="m8 0a3 3 0 0 0 -2.828 2 3 3 0 0 0 -0.172 -0 3 3 0 0 0 -3 3 3 3 0 0 0 0 0.172 3 3 0 0 0 -2 2.828 3 3 0 0 0 2 2.828 3 3 0 0 0 -0 0.172 3 3 0 0 0 3 3 3 3 0 0 0 0.172 -0 3 3 0 0 0 2.828 2 3 3 0 0 0 2.828 -2.01 3 3 0 0 0 0.172 0.01 3 3 0 0 0 3 -3 3 3 0 0 0 -0 -0.172 3 3 0 0 0 2 -2.828 3 3 0 0 0 -2.01 -2.828 3 3 0 0 0 0.01 -0.172 3 3 0 0 0 -3 -3 3 3 0 0 0 -0.172 0 3 3 0 0 0 -2.828 -2zm2.934 4.5625 1.433 1.4336-5.7772 5.7789-2.9511-2.9508 1.414-1.414 1.5371 1.5351 4.3442-4.3828z" fill="#000000"/></svg>

After

Width:  |  Height:  |  Size: 607 B

View File

@ -1,4 +1,7 @@
<?php
declare(strict_types=1);
/**
* ownCloud - registration
*
@ -7,51 +10,62 @@
*
* @author Pellaeon Lin <pellaeon@hs.ntnu.edu.tw>
* @author Julius Härtl <jus@bitgrid.net>
* @author 2020 Joas Schilling <coding@schilljs.com>
* @copyright Pellaeon Lin 2014
*/
namespace OCA\Registration\Controller;
use Exception;
use OCA\Registration\Db\Registration;
use OCA\Registration\Service\LoginFlowService;
use OCA\Registration\Service\MailService;
use OCA\Registration\Service\RegistrationException;
use OCA\Registration\Service\RegistrationService;
use \OCP\IRequest;
use \OCP\AppFramework\Http\TemplateResponse;
use \OCP\AppFramework\Http\RedirectResponse;
use \OCP\AppFramework\Controller;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\RedirectResponse;
use OCP\AppFramework\Http\RedirectToDefaultAppResponse;
use OCP\AppFramework\Http\Response;
use OCP\AppFramework\Http\StandaloneTemplateResponse;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\IL10N;
use OCP\IRequest;
use OCP\IURLGenerator;
use \OCP\IConfig;
use \OCP\IL10N;
use OCP\IConfig;
class RegisterController extends Controller {
/** @var IL10N */
private $l10n;
/** @var IURLGenerator */
private $urlgenerator;
private $urlGenerator;
/** @var IConfig */
private $config;
/** @var RegistrationService */
private $registrationService;
/** @var MailService */
private $mailService;
/** @var LoginFlowService */
private $loginFlowService;
public function __construct(
$appName,
string $appName,
IRequest $request,
IL10N $l10n,
IURLGenerator $urlgenerator,
IURLGenerator $urlGenerator,
IConfig $config,
RegistrationService $registrationService,
LoginFlowService $loginFlowService,
MailService $mailService
) {
parent::__construct($appName, $request);
$this->l10n = $l10n;
$this->urlgenerator = $urlgenerator;
$this->urlGenerator = $urlGenerator;
$this->config = $config;
$this->registrationService = $registrationService;
$this->loginFlowService = $loginFlowService;
$this->mailService = $mailService;
}
@ -59,136 +73,213 @@ class RegisterController extends Controller {
* @NoCSRFRequired
* @PublicPage
*
* @param $errormsg
* @param $entered
* @param string $email
* @param string $message
* @return TemplateResponse
*/
public function askEmail($errormsg, $entered) {
public function showEmailForm(string $email = '', string $message = ''): TemplateResponse {
$params = [
'errormsg' => $errormsg ? $errormsg : $this->request->getParam('errormsg'),
'entered' => $entered ? $entered : $this->request->getParam('entered')
'email' => $email,
'message' => $message,
];
return new TemplateResponse('registration', 'register', $params, 'guest');
return new TemplateResponse('registration', 'form/email', $params, 'guest');
}
/**
* User POST email, if email is valid and not duplicate, we send token by mail
* @PublicPage
* @AnonRateThrottle(limit=5, period=1)
*
* @param string $email
* @return TemplateResponse
*/
public function validateEmail($email) {//TODO rename to receiveUserEmail
if (!$this->registrationService->checkAllowedDomains($email)) {//TODO Duplicate code with Service
return new TemplateResponse('registration', 'domains', [
'domains' => $this->registrationService->getAllowedDomains()
], 'guest');
}
public function submitEmailForm(string $email): Response {
try {
$reg = $this->registrationService->validateEmail($email);
if ($reg === true) {
$registration = $this->registrationService->createRegistration($email);
$this->mailService->sendTokenByMail($registration);
} else {
$this->registrationService->generateNewToken($reg);
$this->mailService->sendTokenByMail($reg);
return new TemplateResponse('registration', 'message', ['msg' =>
$this->l10n->t('There is already a pending registration with this email, a new verification email has been sent to the address.')
], 'guest');
// Registration already in progress, update token and continue with verification
$registration = $this->registrationService->getRegistrationForEmail($email);
$this->registrationService->generateNewToken($registration);
} catch (DoesNotExistException $e) {
// No registration in progress
try {
$this->registrationService->validateEmail($email);
} catch (RegistrationException $e) {
return $this->showEmailForm($email, $e->getMessage());
}
} catch (RegistrationException $e) {
return new TemplateResponse('registration', 'message', ['msg' =>
$e->getMessage().'<br/>'.$e->getHint()
], 'guest');
$registration = $this->registrationService->createRegistration($email);
}
try {
$this->mailService->sendTokenByMail($registration);
} catch (RegistrationException $e) {
return $this->showEmailForm($email, $e->getMessage());
}
return new TemplateResponse('registration', 'message', ['msg' =>
$this->l10n->t('Verification email successfully sent.')
], 'guest');
return new RedirectResponse(
$this->urlGenerator->linkToRoute(
'registration.register.showVerificationForm',
['secret' => $registration->getClientSecret()]
)
);
}
/**
* @NoCSRFRequired
* @PublicPage
*
* @param $token
* @param string $secret
* @param string $message
* @return TemplateResponse
*/
public function verifyToken($token) {
public function showVerificationForm(string $secret, string $message = ''): TemplateResponse {
try {
/** @var Registration $registration */
$registration = $this->registrationService->verifyToken($token);
$this->registrationService->confirmEmail($registration);
$this->registrationService->getRegistrationForSecret($secret);
} catch (DoesNotExistException $e) {
return $this->validateSecretAndTokenErrorPage();
}
// create account without form if username/password are already stored
if ($registration->getUsername() !== "" && $registration->getPassword() !== "") {
$this->registrationService->createAccount($registration);
return new TemplateResponse('registration', 'message',
['msg' => $this->l10n->t('Your account has been successfully created, you can <a href="%s">log in now</a>.', [$this->urlgenerator->getAbsoluteURL('/')])],
'guest'
return new TemplateResponse('registration', 'form/verification', [
'message' => $message,
], 'guest');
}
/**
* @PublicPage
* @AnonRateThrottle(limit=5, period=1)
*
* @param string $secret
* @param string $token
* @return Response
*/
public function submitVerificationForm(string $secret, string $token): Response {
try {
$registration = $this->registrationService->getRegistrationForSecret($secret);
if ($registration->getToken() !== $token) {
return $this->showVerificationForm(
$secret,
$this->l10n->t('The entered verification code is wrong')
);
}
return new TemplateResponse('registration', 'form', [
'email' => $registration->getEmail(),
'email_is_login' => $this->config->getAppValue('registration', 'email_is_login', '0') === '1',
'token' => $registration->getToken(),
], 'guest');
} catch (RegistrationException $exception) {
return $this->renderError($exception->getMessage(), $exception->getHint());
} catch (DoesNotExistException $e) {
return $this->validateSecretAndTokenErrorPage();
}
return new RedirectResponse(
$this->urlGenerator->linkToRoute(
'registration.register.showUserForm',
[
'secret' => $secret,
'token' => $token,
]
)
);
}
/**
* @NoCSRFRequired
* @PublicPage
*
* @param string $secret
* @param string $token
* @param string $username
* @param string $message
* @return TemplateResponse
*/
public function showUserForm(string $secret, string $token, string $username = '', string $message = ''): TemplateResponse {
try {
$registration = $this->validateSecretAndToken($secret, $token);
} catch (RegistrationException $e) {
return $this->validateSecretAndTokenErrorPage();
}
return new TemplateResponse('registration', 'form/user', [
'email' => $registration->getEmail(),
'email_is_login' => $this->config->getAppValue('registration', 'email_is_login', '0') === '1',
'username' => $username,
'message' => $message,
], 'guest');
}
/**
* @PublicPage
* @UseSession
* @AnonRateThrottle(limit=5, period=1)
*
* @param $token
* @param string $secret
* @param string $token
* @param string $username
* @param string $password
* @return RedirectResponse|TemplateResponse
*/
public function createAccount($token) {
$registration = $this->registrationService->getRegistrationForToken($token);
public function submitUserForm(string $secret, string $token, string $username, string $password): Response {
try {
$registration = $this->validateSecretAndToken($secret, $token);
} catch (RegistrationException $e) {
return $this->validateSecretAndTokenErrorPage();
}
if ($this->config->getAppValue('registration', 'email_is_login', '0') === '1') {
$username = $registration->getEmail();
} else {
$username = $this->request->getParam('username');
}
$password = $this->request->getParam('password');
try {
$user = $this->registrationService->createAccount($registration, $username, $password);
} catch (\Exception $exception) {
// Render form with previously sent values
return new TemplateResponse('registration', 'form',
[
'email' => $registration->getEmail(),
'entered_data' => ['user' => $username],
'errormsgs' => [$exception->getMessage()],
'token' => $token
], 'guest');
} catch (Exception $exception) {
return $this->showUserForm($secret, $token, $username, $exception->getMessage());
}
// Delete registration
$this->registrationService->deleteRegistration($registration);
if ($user->isEnabled()) {
// log the user
return $this->registrationService->loginUser($user->getUID(), $username, $password, false);
} else {
// warn the user their account needs admin validation
return new TemplateResponse(
'registration',
'message',
['msg' => $this->l10n->t("Your account has been successfully created, but it still needs approval from an administrator.")],
'guest');
$this->registrationService->loginUser($user->getUID(), $user->getUID(), $password);
if ($this->loginFlowService->isUsingLoginFlow(2)) {
$response = $this->loginFlowService->tryLoginFlowV2($user);
if ($response instanceof Response) {
return $response;
}
}
if ($this->loginFlowService->isUsingLoginFlow(1)) {
$response = $this->loginFlowService->tryLoginFlowV1();
if ($response instanceof Response && $response->getStatus() === Http::STATUS_SEE_OTHER) {
return $response;
}
}
return new RedirectToDefaultAppResponse();
}
// warn the user their account needs admin validation
return new StandaloneTemplateResponse('registration', 'approval-required', [], 'guest');
}
private function renderError($error, $hint="") {
return new TemplateResponse('', 'error', [
'errors' => [[
'error' => $error,
'hint' => $hint
]]
/**
* @param string $secret
* @param string $token
* @return Registration
* @throws RegistrationException
*/
protected function validateSecretAndToken(string $secret, string $token): Registration {
try {
$registration = $this->registrationService->getRegistrationForSecret($secret);
} catch (DoesNotExistException $e) {
throw new RegistrationException('Invalid secret');
}
if ($registration->getToken() !== $token) {
throw new RegistrationException('Invalid token');
}
return $registration;
}
protected function validateSecretAndTokenErrorPage(): TemplateResponse {
return new TemplateResponse('core', 'error', [
'errors' => [
$this->l10n->t('The verification failed.'),
],
], 'error');
}
}

View File

@ -47,7 +47,7 @@ class RegistrationLoginOption implements IAlternativeLogin {
}
public function getLink(): string {
return $this->url->linkToRoute('registration.register.askEmail');
return $this->url->linkToRoute('registration.register.showEmailForm');
}
public function getClass(): string {

View File

@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020 Joas Schilling <coding@schilljs.com>
*
* @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 OCA\Registration\Service;
use OC\Core\Controller\ClientFlowLoginController;
use OC\Core\Controller\ClientFlowLoginV2Controller;
use OC\Core\Service\LoginFlowV2Service;
use OCP\AppFramework\Http\Response;
use OCP\AppFramework\Http\StandaloneTemplateResponse;
use OCP\IRequest;
use OCP\ISession;
use OCP\IUser;
class LoginFlowService {
/** @var IRequest */
protected $request;
/** @var ISession */
protected $session;
/** @var LoginFlowV2Service */
protected $loginFlowV2Service;
public function __construct(
IRequest $request,
ISession $session,
LoginFlowV2Service $loginFlowV2Service
) {
$this->request = $request;
$this->session = $session;
$this->loginFlowV2Service = $loginFlowV2Service;
}
public function isUsingLoginFlow(?int $version = null): bool {
if (($version === 1 || $version === null) && $this->session->get(ClientFlowLoginController::STATE_NAME) !== null) {
return true;
}
if (($version === 2 || $version === null) && $this->session->get(ClientFlowLoginV2Controller::TOKEN_NAME) !== null) {
return true;
}
return false;
}
public function tryLoginFlowV1(): ?Response {
/** @var ClientFlowLoginController $controller */
$container = \OC::$server->getRegisteredAppContainer('core');
$controller = $container->query(ClientFlowLoginController::class);
return $controller->generateAppPassword(
$this->session->get(ClientFlowLoginController::STATE_NAME)
);
}
public function tryLoginFlowV2(IUser $user): ?StandaloneTemplateResponse {
$result = $this->loginFlowV2Service->flowDone(
$this->session->get(ClientFlowLoginV2Controller::TOKEN_NAME),
$this->session->getId(),
$this->getServerPath(),
$user->getUID()
);
if (!$result) {
return null;
}
return new StandaloneTemplateResponse(
'core',
'loginflowv2/done',
[],
'guest'
);
}
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'));
} elseif (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;
}
}

View File

@ -81,7 +81,10 @@ class MailService {
* @throws RegistrationException
*/
public function sendTokenByMail(Registration $registration): void {
$link = $this->urlGenerator->linkToRouteAbsolute('registration.register.verifyToken', ['token' => $registration->getToken()]);
$link = $this->urlGenerator->linkToRouteAbsolute('registration.register.showUserForm', [
'secret' => $registration->getClientSecret(),
'token' => $registration->getToken(),
]);
$subject = $this->l10n->t('Verify your %s registration request', [$this->defaults->getName()]);
$template = $this->mailer->createEMailTemplate('registration_verify', [
@ -100,6 +103,10 @@ class MailService {
$body
);
$template->addBodyText(
$this->l10n->t('Verification code: %s', $registration->getToken())
);
$template->addBodyButton(
$this->l10n->t('Continue registration'),
$link

View File

@ -1,4 +1,7 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2017 Julius Härtl <jus@bitgrid.net>
* @copyright Copyright (c) 2017 Pellaeon Lin <pellaeon@hs.ntnu.edu.tw>
@ -34,12 +37,11 @@ use OC\Authentication\Token\IToken;
use OCA\Registration\Db\Registration;
use OCA\Registration\Db\RegistrationMapper;
use OCP\AppFramework\Db\DoesNotExistException;
use \OCP\AppFramework\Http\TemplateResponse;
use \OCP\AppFramework\Http\RedirectResponse;
use OCP\ILogger;
use OCP\IRequest;
use OCP\ISession;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\Security\ICrypto;
use OCP\Session\Exceptions\SessionNotAvailableException;
use \OCP\IUserManager;
@ -70,7 +72,7 @@ class RegistrationService {
/** @var ISecureRandom */
private $random;
/** @var IUserSession */
private $usersession;
private $userSession;
/** @var IRequest */
private $request;
/** @var ILogger */
@ -82,9 +84,23 @@ class RegistrationService {
/** @var ICrypto */
private $crypto;
public function __construct($appName, MailService $mailService, IL10N $l10n, IURLGenerator $urlGenerator,
RegistrationMapper $registrationMapper, IUserManager $userManager, IConfig $config, IGroupManager $groupManager,
ISecureRandom $random, IUserSession $us, IRequest $request, ILogger $logger, ISession $session, IProvider $tokenProvider, ICrypto $crypto) {
public function __construct(
string $appName,
MailService $mailService,
IL10N $l10n,
IURLGenerator $urlGenerator,
RegistrationMapper $registrationMapper,
IUserManager $userManager,
IConfig $config,
IGroupManager $groupManager,
ISecureRandom $random,
IUserSession $userSession,
IRequest $request,
ILogger $logger,
ISession $session,
IProvider $tokenProvider,
ICrypto $crypto
) {
$this->appName = $appName;
$this->mailService = $mailService;
$this->l10n = $l10n;
@ -94,7 +110,7 @@ class RegistrationService {
$this->config = $config;
$this->groupManager = $groupManager;
$this->random = $random;
$this->usersession = $us;
$this->userSession = $userSession;
$this->request = $request;
$this->logger = $logger;
$this->session = $session;
@ -102,21 +118,16 @@ class RegistrationService {
$this->crypto = $crypto;
}
/**
* @param Registration $registration
*/
public function confirmEmail(Registration $registration) {
public function confirmEmail(Registration $registration): void {
$registration->setEmailConfirmed(true);
$this->registrationMapper->update($registration);
}
/**
* @param Registration $registration
*/
public function generateNewToken(Registration $registration) {
public function generateNewToken(Registration $registration): void {
$this->registrationMapper->generateNewToken($registration);
$this->registrationMapper->update($registration);
}
/**
* Create registration request, used by both the API and form
* @param string $email
@ -125,7 +136,7 @@ class RegistrationService {
* @param string $displayname
* @return Registration
*/
public function createRegistration($email, $username="", $password="", $displayname="") {
public function createRegistration(string $email, string $username = '', string $password = '', string $displayname = ''): Registration {
$registration = new Registration();
$registration->setEmail($email);
$registration->setUsername($username);
@ -135,24 +146,24 @@ class RegistrationService {
$registration->setPassword($password);
}
$this->registrationMapper->generateNewToken($registration);
if ($password !== '' && $username !== '') {
$this->registrationMapper->generateClientSecret($registration);
}
$this->registrationMapper->generateClientSecret($registration);
$this->registrationMapper->insert($registration);
return $registration;
}
/**
* @param string $email
* @return Registration|true if there is a pending reg with this email, return the pending reg, if there are no problems with the email, return true.
* @throws RegistrationException
*/
public function validateEmail($email) {
public function validateEmail(string $email): void {
$this->mailService->validateEmail($email);
// check for pending registrations
try {
return $this->registrationMapper->find($email);//if not found DB will throw a exception
$this->registrationMapper->find($email);//if not found DB will throw a exception
throw new RegistrationException(
$this->l10n->t('A user has already taken this email, maybe you already have an account?')
);
} catch (DoesNotExistException $e) {
}
@ -171,15 +182,14 @@ class RegistrationService {
)
);
}
return true;
}
/**
* @param string $displayname
* @throws RegistrationException
*/
public function validateDisplayname($displayname) {
if ($displayname === "") {
public function validateDisplayname(string $displayname): void {
if ($displayname === '') {
throw new RegistrationException($this->l10n->t('Please provide a valid display name.'));
}
}
@ -188,7 +198,7 @@ class RegistrationService {
* @param string $username
* @throws RegistrationException
*/
public function validateUsername($username) {
public function validateUsername(string $username): void {
if ($username === "") {
throw new RegistrationException($this->l10n->t('Please provide a valid user name.'));
}
@ -204,15 +214,15 @@ class RegistrationService {
* @param string $email
* @return bool
*/
public function checkAllowedDomains($email) {
$allowed_domains = $this->config->getAppValue($this->appName, 'allowed_domains', '');
if ($allowed_domains !== '') {
$allowed_domains = explode(';', $allowed_domains);
public function checkAllowedDomains(string $email): bool {
$allowedDomains = $this->config->getAppValue($this->appName, 'allowed_domains', '');
if ($allowedDomains !== '') {
$allowedDomains = explode(';', $allowedDomains);
$allowed = false;
foreach ($allowed_domains as $domain) {
$maildomain = explode("@", $email)[1];
// valid domain, everythings fine
if ($maildomain === $domain) {
foreach ($allowedDomains as $domain) {
[,$mailDomain] = explode('@', $email, 2);
// valid domain, everything's fine
if ($mailDomain === $domain) {
$allowed = true;
break;
}
@ -223,22 +233,22 @@ class RegistrationService {
}
/**
* @return array
* @return string[]
*/
public function getAllowedDomains() {
$allowed_domains = $this->config->getAppValue($this->appName, 'allowed_domains', '');
$allowed_domains = explode(';', $allowed_domains);
return $allowed_domains;
public function getAllowedDomains(): array {
$allowedDomains = $this->config->getAppValue($this->appName, 'allowed_domains', '');
$allowedDomains = explode(';', $allowedDomains);
return $allowedDomains;
}
/**
* Find registration entity for token
*
* @param string $token
* @return string
* @return Registration
* @throws RegistrationException
*/
public function verifyToken($token) {
public function verifyToken(string $token): Registration {
try {
return $this->registrationMapper->findByToken($token);
} catch (DoesNotExistException $exception) {
@ -248,12 +258,12 @@ class RegistrationService {
/**
* @param $registration
* @param string $username
* @param string $password
* @return \OCP\IUser
* @param string|null $username
* @param string|null $password
* @return IUser
* @throws RegistrationException|InvalidTokenException
*/
public function createAccount(Registration $registration, $username = null, $password = null) {
public function createAccount(Registration $registration, ?string $username = null, ?string $password = null): IUser {
if ($password === null && $registration->getPassword() === null) {
$generatedPassword = $this->generateRandomDeviceToken();
$registration->setPassword($this->crypto->encrypt($generatedPassword));
@ -281,6 +291,7 @@ class RegistrationService {
throw new RegistrationException($this->l10n->t('Unable to create user, there are problems with the user backend.'));
}
$userId = $user->getUID();
// Set user email
try {
$user->setEMailAddress($registration->getEmail());
@ -289,72 +300,64 @@ class RegistrationService {
}
// Add user to group
$registered_user_group = $this->config->getAppValue($this->appName, 'registered_user_group', 'none');
if ($registered_user_group !== 'none') {
$group = $this->groupManager->get($registered_user_group);
$registeredUserGroup = $this->config->getAppValue($this->appName, 'registered_user_group', 'none');
if ($registeredUserGroup !== 'none') {
$group = $this->groupManager->get($registeredUserGroup);
if ($group === null) {
// This might happen if $registered_user_group is deleted after setting the value
// Here I choose to log error instead of stopping the user to register
$this->logger->error("You specified newly registered users be added to '$registered_user_group' group, but it does not exist.");
$this->logger->error("You specified newly registered users be added to '$registeredUserGroup' group, but it does not exist.");
$groupId = '';
} else {
$group->addUser($user);
$groupId = $group->getGID();
}
} else {
$groupId = "";
$groupId = '';
}
// disable user if this is requested by config
$admin_approval_required = $this->config->getAppValue($this->appName, 'admin_approval_required', "no");
if ($admin_approval_required === "yes") {
$adminApprovalRequired = $this->config->getAppValue($this->appName, 'admin_approval_required', 'no');
if ($adminApprovalRequired === 'yes') {
$user->setEnabled(false);
}
// Delete pending registration if no client secret is stored
// with client secret implies registered via API
// without client secret implies registered via form
// if registered via API, the registration request will be deleted in apicontroller::status
if ($registration->getClientSecret() === null) {
$res = $this->registrationMapper->delete($registration);
if ($res === false) {
throw new RegistrationException($this->l10n->t('Failed to delete pending registration request'));
}
}
$this->mailService->notifyAdmins($userId, $user->isEnabled(), $groupId);
return $user;
}
/**
* @param $token
* @param string $email
* @return Registration
* @throws DoesNotExistException
*/
public function getRegistrationForToken($token) {
public function getRegistrationForEmail(string $email): Registration {
return $this->registrationMapper->find($email);
}
/**
* @param string $token
* @return Registration
* @throws DoesNotExistException
*/
public function getRegistrationForToken(string $token): Registration {
return $this->registrationMapper->findByToken($token);
}
/**
* @param $secret
* @param string $secret
* @return Registration
* @throws DoesNotExistException
*/
public function getRegistrationForSecret($secret) {
public function getRegistrationForSecret(string $secret): Registration {
return $this->registrationMapper->findBySecret($secret);
}
/**
* @param Registration $registation
* @return null|\OCP\IUser
*/
public function getUserAccount(Registration $registation) {
$user = $this->userManager->get($registation->getUsername());
return $user;
public function getUserAccount(Registration $registration): ?IUser {
return $this->userManager->get($registration->getUsername());
}
/**
* @param Registration $registration
*/
public function deleteRegistration(Registration $registration) {
public function deleteRegistration(Registration $registration): void {
$this->registrationMapper->delete($registration);
}
@ -365,7 +368,7 @@ class RegistrationService {
*
* @return string
*/
private function generateRandomDeviceToken() {
private function generateRandomDeviceToken(): string {
$groups = [];
for ($i = 0; $i < 5; $i++) {
$groups[] = $this->random->generate(5, ISecureRandom::CHAR_HUMAN_READABLE);
@ -378,7 +381,7 @@ class RegistrationService {
* @return string
* @throws RegistrationException
*/
public function generateAppPassword($uid) {
public function generateAppPassword(string $uid): string {
$name = $this->l10n->t('Registration app auto setup');
try {
$sessionId = $this->session->getId();
@ -404,40 +407,25 @@ class RegistrationService {
}
/**
* @param $userId
* @param $username
* @param $password
* @param $decrypt
* @return RedirectResponse|TemplateResponse
* @param string $userId
* @param string $username
* @param string $password
* @param bool $decrypt
*/
public function loginUser($userId, $username, $password, $decrypt = false) {
public function loginUser(string $userId, string $username, string $password, bool $decrypt = false): void {
if ($decrypt) {
$password = $this->crypto->decrypt($password);
}
if (method_exists($this->usersession, 'createSessionToken')) {
$this->usersession->login($username, $password);
$this->usersession->createSessionToken($this->request, $userId, $username, $password);
return new RedirectResponse($this->urlGenerator->linkTo('', 'index.php'));
} elseif (\OC_User::login($username, $password)) {
$this->cleanupLoginTokens($userId);
// FIXME unsetMagicInCookie will fail from session already closed, so now we always remember
$logintoken = $this->random->generate(32);
$this->config->setUserValue($userId, 'login_token', $logintoken, time());
\OC_User::setMagicInCookie($userId, $logintoken);
\OC_Util::redirectToDefaultPage();
}
// Render message in case redirect failed
return new TemplateResponse('registration', 'message',
['msg' => $this->l10n->t('Your account has been successfully created, you can <a href="%s">log in now</a>.', [$this->urlGenerator->getAbsoluteURL('/')])]
, 'guest'
);
$this->userSession->login($username, $password);
$this->userSession->createSessionToken($this->request, $userId, $username, $password);
}
/**
* Replicates OC::cleanupLoginTokens() since it's protected
* @param string $userId
*/
public function cleanupLoginTokens($userId) {
public function cleanupLoginTokens(string $userId): void {
$cutoff = time() - $this->config->getSystemValue('remember_login_cookie_lifetime', 60 * 60 * 24 * 15);
$tokens = $this->config->getUserKeys($userId, 'login_token');
foreach ($tokens as $token) {

View File

@ -26,9 +26,13 @@ foreach ($_['groups'] as $group) {
</p>
<div style="margin-top: 10px;">
<input type="checkbox" id="admin_approval_required" class="checkbox" name="admin_approval_required" <?php if ($_['approval_required'] === "yes") {
<p>
<input type="checkbox" id="admin_approval_required" class="checkbox" name="admin_approval_required" <?php if ($_['approval_required'] === "yes") {
echo " checked";
} ?>>
<label for="admin_approval_required"><?php p($l->t('Require admin approval?')); ?></label>
<label for="admin_approval_required"><?php p($l->t('Require admin approval?')); ?></label>
</p>
<em><?php p($l->t('Enabling "admin approval" will prevent registrations from mobile and desktop clients to complete as the credentials can not be verified by the client until the user was enabled.'));?></em>
</div>
</form>

View File

@ -0,0 +1,13 @@
<?php
/** @var array $_ */
/** @var \OCP\IL10N $l */
style('registration', 'style');
?>
<div class="error">
<h2><?php p($l->t('Approval required')) ?></h2>
<ul>
<li>
<p><?php p($l->t('Your account has been successfully created, but it still needs approval from an administrator.')) ?></p>
</li>
</ul>
</div>

View File

@ -1,16 +0,0 @@
<?php
/** @var array $_ */
/** @var \OCP\IL10N $l */
style('registration', 'style');
?>
<ul class="error-wide">
<li class='error'><?php p($l->t('Registration is only allowed for the following domains:')); ?>
<?php
foreach ($_['domains'] as $domain) {
echo "<p class='hint'>";
p($domain);
echo "</p>";
}
?>
</li>
</ul>

26
templates/form/email.php Normal file
View File

@ -0,0 +1,26 @@
<?php
/** @var array $_ */
/** @var \OCP\IL10N $l */
style('registration', 'style');
?>
<form action="" method="post">
<fieldset>
<?php if ($_['message']): ?>
<ul class="error">
<li><?php p($_['message']); ?></li>
</ul>
<?php endif; ?>
<p class="groupofone">
<input type="email" name="email" id="email" placeholder="<?php p($l->t('Email')); ?>" value="<?php p($_['email']); ?>" required autofocus />
<label for="email" class="infield"><?php p($l->t('Email')); ?></label>
<img id="email-icon" class="svg" src="<?php print_unescaped(image_path('', 'actions/mail.svg')); ?>" alt=""/>
</p>
<input type="hidden" name="requesttoken" value="<?php p($_['requesttoken']); ?>" />
<input type="submit" id="submit" value="<?php p($l->t('Request verification link')); ?>" />
<a id="lost-password-back" href="<?php print_unescaped(\OC::$server->getURLGenerator()->linkToRoute('core.login.showLoginForm')) ?>">
<?php p($l->t('Back to login')); ?>
</a>
</fieldset>
</form>

View File

@ -23,7 +23,7 @@ script('registration', 'form');
<img id="email-icon" class="svg" src="<?php print_unescaped(image_path('', 'actions/mail.svg')); ?>" alt=""/>
</p>
<?php if ($_['email_is_login']) { ?>
<?php if (!$_['email_is_login']) { ?>
<p class="groupmiddle">
<input type="text" name="username" id="username" value="<?php if (!empty($_['entered_data']['user'])) {
p($_['entered_data']['user']);
@ -31,6 +31,8 @@ script('registration', 'form');
<label for="username" class="infield"><?php p($l->t('Username')); ?></label>
<img id="username-icon" class="svg" src="<?php print_unescaped(image_path('', 'actions/user.svg')); ?>" alt=""/>
</p>
<?php } else { ?>
<input type="hidden" name="username" value="<?php p($_['email']); ?>" />
<?php } ?>
<p class="groupbottom">

View File

@ -0,0 +1,26 @@
<?php
/** @var array $_ */
/** @var \OCP\IL10N $l */
style('registration', 'style');
?>
<form action="" method="post">
<fieldset>
<?php if ($_['message']): ?>
<ul class="error">
<li><?php p($_['message']); ?></li>
</ul>
<?php endif; ?>
<p class="groupofone">
<input type="text" name="token" id="token" placeholder="<?php p($l->t('Verification code')); ?>" value="" required autofocus />
<label for="token" class="infield"><?php p($l->t('Verification code')); ?></label>
<img id="token-icon" class="svg" src="<?php print_unescaped(image_path('registration', 'verify.svg')); ?>" alt=""/>
</p>
<input type="hidden" name="requesttoken" value="<?php p($_['requesttoken']); ?>" />
<input type="submit" id="submit" value="<?php p($l->t('Verify')); ?>" />
<a id="lost-password-back" href="<?php print_unescaped(\OC::$server->getURLGenerator()->linkToRoute('core.login.showLoginForm')) ?>">
<?php p($l->t('Back to login')); ?>
</a>
</fieldset>
</form>

View File

@ -1,8 +0,0 @@
<?php
/** @var array $_ */
/** @var \OCP\IL10N $l */
style('registration', 'style');
?>
<ul class="msg error-wide nc-theming-main-text">
<li><?php print_unescaped($_['msg'])?></li>
</ul>

View File

@ -1,58 +0,0 @@
<?php
/** @var array $_ */
/** @var \OCP\IL10N $l */
style('registration', 'style');
if ($_['entered']): ?>
<?php if (empty($_['errormsg'])): ?>
<ul class="success">
<li>
<?php p($l->t('Thank you for registering, you should receive a verification link in a few minutes.')); ?>
</li>
</ul>
<?php else: ?>
<form action="<?php print_unescaped(\OC::$server->getURLGenerator()->linkToRoute('registration.register.validateEmail')) ?>" method="post">
<fieldset>
<ul class="error">
<li><?php p($_['errormsg']); ?></li>
</ul>
<p class="groupofone">
<input type="email" name="email" id="email" placeholder="<?php p($l->t('Email')); ?>" value="" required autofocus />
<label for="email" class="infield"><?php p($l->t('Email')); ?></label>
<img id="email-icon" class="svg" src="<?php print_unescaped(image_path('', 'actions/mail.svg')); ?>" alt=""/>
</p>
<input type="hidden" name="requesttoken" value="<?php p($_['requesttoken']); ?>" />
<input type="submit" id="submit" value="<?php p($l->t('Request verification link')); ?>" />
<a id="lost-password-back" href="<?php print_unescaped(\OC::$server->getURLGenerator()->linkToRoute('core.login.showLoginForm')) ?>">
<?php p($l->t('Back to login')); ?>
</a>
</fieldset>
</form>
<?php endif; ?>
<?php else: ?>
<form action="<?php print_unescaped(\OC::$server->getURLGenerator()->linkToRoute('registration.register.validateEmail')) ?>" method="post">
<fieldset>
<?php if ($_['errormsg']): ?>
<ul class="error">
<li><?php p($_['errormsg']); ?></li>
<li><?php p($l->t('Please re-enter a valid email address')); ?></li>
</ul>
<?php else: ?>
<ul class="msg">
<li><?php p($l->t('You will receive an email with a verification link')); ?></li>
</ul>
<?php endif; ?>
<p class="groupofone">
<input type="email" name="email" id="email" placeholder="<?php p($l->t('Email')); ?>" value="" required autofocus />
<label for="email" class="infield"><?php p($l->t('Email')); ?></label>
<img id="email-icon" class="svg" src="<?php print_unescaped(image_path('', 'actions/mail.svg')); ?>" alt=""/>
</p>
<input type="hidden" name="requesttoken" value="<?php p($_['requesttoken']); ?>" />
<input type="submit" id="submit" value="<?php p($l->t('Request verification link')); ?>" />
<a id="lost-password-back" href="<?php print_unescaped(\OC::$server->getURLGenerator()->linkToRoute('core.login.showLoginForm')) ?>">
<?php p($l->t('Back to login')); ?>
</a>
</fieldset>
</form>
<?php endif; ?>

View File

@ -1,138 +0,0 @@
<?php
namespace OCA\Registration\Tests\Integration\Controller;
use OCA\Registration\Controller\RegisterController;
use OCA\Registration\Db\RegistrationMapper;
use OCA\Registration\Service\MailService;
use OCA\Registration\Service\RegistrationService;
use OCP\IConfig;
use OCP\IGroupManager;
use OCP\IL10N;
use OCP\ILogger;
use OC\Authentication\Token\IProvider;
use OCP\IRequest;
use OCP\Security\ISecureRandom;
use OCP\Security\ICrypto;
use OCP\ISession;
use OCP\IURLGenerator;
use OCP\IUserManager;
use OCP\IUserSession;
use \OCP\AppFramework\Http\TemplateResponse;
use ChristophWurst\Nextcloud\Testing\DatabaseTransaction;
use ChristophWurst\Nextcloud\Testing\TestCase;
/**
* class RegistrationControllerTest
*
* @group DB
*/
class RegisterControllerTest extends TestCase {
use DatabaseTransaction;
/** @var MailService */
private $mailService;
/** @var IL10N */
private $l10n;
/** @var IURLGenerator */
private $urlGenerator;
/** @var RegistrationMapper */
private $registrationMapper;
/** @var IUserManager */
private $userManager;
/** @var IConfig */
private $config;
/** @var IGroupManager */
private $groupManager;
/** @var \OCP\Defaults */
private $defaults;
/** @var ISecureRandom */
private $random;
/** @var IUserSession */
private $usersession;
/** @var IRequest */
private $request;
/** @var ILogger */
private $logger;
/** @var ISession */
private $session;
/** @var IProvider */
private $tokenProvider;
/** @var ICrypto */
private $crypto;
public function setUp(): void {
parent::setUp();
$this->mailService = $this->createMock(MailService::class);
$this->l10n = $this->createMock(IL10N::class);
$this->urlGenerator = $this->createMock(IURLGenerator::class);
#$this->userManager = $this->createMock(IUserManager::class);
$this->userManager = \OC::$server->getUserManager();
$this->config = $this->createMock(IConfig::class);
$this->groupManager = \OC::$server->getGroupManager();
$this->random = \OC::$server->getSecureRandom();
$this->usersession = $this->createMock(IUserSession::class);
$this->request = $this->createMock(IRequest::class);
$this->logger = $this->createMock(ILogger::class);
$this->session = $this->createMock(ISession::class);
$this->tokenProvider = $this->createMock(IProvider::class);
$this->crypto = $this->createMock(ICrypto::class);
$this->registrationMapper = new RegistrationMapper(
\OC::$server->getDatabaseConnection(),
$this->random
);
$this->registrationService = new RegistrationService(
'registration',
$this->mailService,
$this->l10n,
$this->urlGenerator,
$this->registrationMapper,
$this->userManager,
$this->config,
$this->groupManager,
$this->random,
$this->usersession,
$this->request,
$this->logger,
$this->session,
$this->tokenProvider,
$this->crypto
);
$this->controller = new RegisterController(
'registration',
$this->request,
$this->l10n,
$this->urlGenerator,
$this->config,
$this->registrationService,
$this->mailService
);
}
public function testValidateEmailNormal() {
$email = 'aaaa@example.com';
$this->config->expects($this->atLeastOnce())
->method('getAppValue')
->with("registration", 'allowed_domains', '')
->willReturn('');
$this->mailService->expects($this->once())
->method('sendTokenByMail');
$this->assertEquals($this->registrationService->validateEmail($email), true);
$ret = $this->controller->validateEmail($email);
$expected = new TemplateResponse('registration', 'message', ['msg' =>
$this->l10n->t('Verification email successfully sent.')
], 'guest');
$this->assertEquals($expected, $ret, print_r($ret, true));
}
}

View File

@ -61,6 +61,9 @@ class RegistrationServiceTest extends TestCase {
/** @var ICrypto */
private $crypto;
/** @var RegistrationService */
private $service;
public function setUp(): void {
parent::setUp();
$this->mailService = $this->createMock(MailService::class);
@ -107,13 +110,10 @@ class RegistrationServiceTest extends TestCase {
$this->config->expects($this->once())
->method('getAppValue')
->with("registration", 'allowed_domains', '')
->with('registration', 'allowed_domains', '')
->willReturn('');
$ret = $this->service->validateEmail($email);
//$this->assertInstanceOf(Registration::class, $ret);
$this->assertTrue($ret);
$this->service->validateEmail($email);
}
public function testValidateNewEmailWithinAllowedDomain() {
@ -121,18 +121,23 @@ class RegistrationServiceTest extends TestCase {
$this->config->expects($this->atLeastOnce())
->method('getAppValue')
->with("registration", 'allowed_domains', '')
->with('registration', 'allowed_domains', '')
->willReturn('example.com');
$ret = $this->service->validateEmail($email);
$this->assertTrue($ret, print_r($ret, true));
$this->service->validateEmail($email);
}
/**
* @depends testValidateNewEmailWithinAllowedDomain
*/
public function testValidateNewEmailNotWithinAllowedDomain() {
$email2 = 'bbbb@gmail.com';
$this->config->expects($this->atLeastOnce())
->method('getAppValue')
->with('registration', 'allowed_domains', '')
->willReturn('example.com');
$this->expectException(RegistrationException::class);
$this->service->validateEmail($email2);
}
@ -143,18 +148,24 @@ class RegistrationServiceTest extends TestCase {
$this->config->expects($this->atLeastOnce())
->method('getAppValue')
->with("registration", 'allowed_domains', '')
->with('registration', 'allowed_domains', '')
->willReturn('example.com;gmail.com');
$this->assertTrue($this->service->validateEmail($email));
$this->assertTrue($this->service->validateEmail($email2));
$this->service->validateEmail($email);
$this->service->validateEmail($email2);
}
/**
* @depends testValidateNewEmailWithinMultipleAllowedDomain
*/
public function testValidateNewEmailNotWithinMultipleAllowedDomain() {
$email2 = 'cccc@yahoo.com';
$this->config->expects($this->atLeastOnce())
->method('getAppValue')
->with('registration', 'allowed_domains', '')
->willReturn('example.com;gmail.com');
$this->expectException(RegistrationException::class);
$this->service->validateEmail($email2);
}
@ -180,10 +191,8 @@ class RegistrationServiceTest extends TestCase {
$email = 'aaaa@example.com';
$this->service->createRegistration($email, 'alice');
$ret = $this->service->validateEmail($email);
$this->assertInstanceOf(Registration::class, $ret);
$this->assertEquals($email, $ret->getEmail());
$this->expectException(RegistrationException::class);
$this->service->validateEmail($email);
}
public function testCreateAccountWebForm() {

View File

@ -157,7 +157,11 @@ class ApiControllerTest extends TestCase {
$registration = new Registration();
$registration->setEmailConfirmed(true);
$registration->setClientSecret('mysecret');
$registration->setUsername('user');
$registration->setPassword('password');
$user = $this->createMock(IUser::class);
$user->method('getUID')
->willReturn('user');
$this->registrationService
->method('getRegistrationForSecret')
->with('mysecret')

View File

@ -0,0 +1,717 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020 Joas Schilling <coding@schilljs.com>
*
* @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 OCA\Registration\Tests\Unit\Controller;
use OCA\Registration\Controller\RegisterController;
use OCA\Registration\Db\Registration;
use OCA\Registration\Service\LoginFlowService;
use OCA\Registration\Service\MailService;
use OCA\Registration\Service\RegistrationException;
use OCA\Registration\Service\RegistrationService;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\RedirectResponse;
use OCP\AppFramework\Http\RedirectToDefaultAppResponse;
use OCP\AppFramework\Http\StandaloneTemplateResponse;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IRequest;
use OCP\IURLGenerator;
use ChristophWurst\Nextcloud\Testing\TestCase;
use OCP\IUser;
use PHPUnit\Framework\MockObject\MockObject;
class RegisterControllerTest extends TestCase {
/** @var IRequest */
private $request;
/** @var IL10N|MockObject */
private $l10n;
/** @var IURLGenerator|MockObject */
private $urlGenerator;
/** @var IConfig|MockObject */
private $config;
/** @var RegistrationService|MockObject */
private $registrationService;
/** @var LoginFlowService|MockObject */
private $loginFlowService;
/** @var MailService|MockObject */
private $mailService;
public function setUp(): void {
parent::setUp();
$this->request = $this->createMock(IRequest::class);
$this->l10n = $this->createMock(IL10N::class);
$this->urlGenerator = $this->createMock(IURLGenerator::class);
$this->config = $this->createMock(IConfig::class);
$this->registrationService = $this->createMock(RegistrationService::class);
$this->loginFlowService = $this->createMock(LoginFlowService::class);
$this->mailService = $this->createMock(MailService::class);
$this->l10n->expects($this->any())
->method('t')
->willReturnCallback(function ($text, $parameters = []) {
return vsprintf($text, $parameters);
});
}
/**
* @param string[] $methods
* @return RegisterController|MockObject
*/
protected function getController(array $methods = []) {
if (empty($methods)) {
return new RegisterController(
'registration',
$this->request,
$this->l10n,
$this->urlGenerator,
$this->config,
$this->registrationService,
$this->loginFlowService,
$this->mailService
);
}
return $this->getMockBuilder(RegisterController::class)
->onlyMethods($methods)
->setConstructorArgs([
'registration',
$this->request,
$this->l10n,
$this->urlGenerator,
$this->config,
$this->registrationService,
$this->loginFlowService,
$this->mailService,
])
->getMock();
}
public function dataShowEmailForm(): array {
return [
['', ''],
['test@example.tld', 'Registration is only allowed for the following domains: nextcloud.com'],
];
}
/**
* @dataProvider dataShowEmailForm
* @param string $email
* @param string $message
*/
public function testShowEmailForm(string $email, string $message): void {
$controller = $this->getController();
$response = $controller->showEmailForm($email, $message);
self::assertSame(TemplateResponse::RENDER_AS_GUEST, $response->getRenderAs());
self::assertSame('form/email', $response->getTemplateName());
self::assertSame([
'email' => $email,
'message' => $message,
], $response->getParams());
}
public function testSubmitEmailForm(): void {
$email = 'nextcloud@example.tld';
$this->registrationService
->method('getRegistrationForEmail')
->with($email)
->willThrowException(new DoesNotExistException($email));
$registration = Registration::fromParams([
'clientSecret' => 'clientSecret',
]);
$this->registrationService
->expects($this->once())
->method('validateEmail')
->with($email);
$this->registrationService
->expects($this->once())
->method('createRegistration')
->with($email)
->willReturn($registration);
$this->mailService
->expects($this->once())
->method('sendTokenByMail')
->with($registration);
$this->urlGenerator
->method('linkToRoute')
->willReturnCallback(function () {
return json_encode(func_get_args());
});
$controller = $this->getController();
$response = $controller->submitEmailForm($email);
self::assertInstanceOf(RedirectResponse::class, $response);
/** @var RedirectResponse $response */
self::assertSame('["registration.register.showVerificationForm",{"secret":"clientSecret"}]', $response->getRedirectURL());
}
public function testSubmitEmailFormInvalidEmail(): void {
$email = 'nextcloud@example.tld';
$this->registrationService
->method('getRegistrationForEmail')
->with($email)
->willThrowException(new DoesNotExistException($email));
$this->registrationService
->expects($this->once())
->method('validateEmail')
->with($email)
->willThrowException(new RegistrationException('Invalid email'));
$this->registrationService
->expects($this->never())
->method('createRegistration');
$controller = $this->getController([
'showEmailForm',
]);
$response = $this->createMock(TemplateResponse::class);
$controller->expects($this->once())
->method('showEmailForm')
->with($email, 'Invalid email')
->willReturn($response);
self::assertSame($response, $controller->submitEmailForm($email));
}
public function testSubmitEmailFormErrorSendingEmail(): void {
$email = 'nextcloud@example.tld';
$this->registrationService
->method('getRegistrationForEmail')
->with($email)
->willThrowException(new DoesNotExistException($email));
$registration = Registration::fromParams([
'clientSecret' => 'clientSecret',
]);
$this->registrationService
->expects($this->once())
->method('validateEmail')
->with($email);
$this->registrationService
->expects($this->once())
->method('createRegistration')
->with($email)
->willReturn($registration);
$this->mailService
->expects($this->once())
->method('sendTokenByMail')
->with($registration)
->willThrowException(new RegistrationException('Error sending email'));
$controller = $this->getController([
'showEmailForm',
]);
$response = $this->createMock(TemplateResponse::class);
$controller->expects($this->once())
->method('showEmailForm')
->with($email, 'Error sending email')
->willReturn($response);
self::assertSame($response, $controller->submitEmailForm($email));
}
public function testSubmitEmailFormResendPendingRequest(): void {
$email = 'nextcloud@example.tld';
$registration = Registration::fromParams([
'clientSecret' => 'clientSecret',
]);
$this->registrationService
->method('getRegistrationForEmail')
->with($email)
->willReturn($registration);
$this->registrationService
->expects($this->once())
->method('generateNewToken')
->with($registration);
$this->mailService
->expects($this->once())
->method('sendTokenByMail')
->with($registration);
$this->urlGenerator
->method('linkToRoute')
->willReturnCallback(function () {
return json_encode(func_get_args());
});
$controller = $this->getController();
$response = $controller->submitEmailForm($email);
self::assertInstanceOf(RedirectResponse::class, $response);
/** @var RedirectResponse $response */
self::assertSame('["registration.register.showVerificationForm",{"secret":"clientSecret"}]', $response->getRedirectURL());
}
public function dataShowVerificationForm(): array {
return [
[''],
['The entered verification code is wrong'],
];
}
/**
* @dataProvider dataShowVerificationForm
* @param string $message
*/
public function testShowVerificationForm(string $message): void {
$secret = '123456789';
$this->registrationService
->expects($this->once())
->method('getRegistrationForSecret')
->with($secret);
$controller = $this->getController();
$response = $controller->showVerificationForm($secret, $message);
self::assertSame(TemplateResponse::RENDER_AS_GUEST, $response->getRenderAs());
self::assertSame('form/verification', $response->getTemplateName());
self::assertSame([
'message' => $message,
], $response->getParams());
}
public function testShowVerificationFormInvalidSecret(): void {
$secret = '123456789';
$message = '';
$this->registrationService
->expects($this->once())
->method('getRegistrationForSecret')
->with($secret)
->willThrowException(new DoesNotExistException('Not found'));
$response = $this->createMock(TemplateResponse::class);
$controller = $this->getController([
'validateSecretAndTokenErrorPage'
]);
$controller->expects($this->once())
->method('validateSecretAndTokenErrorPage')
->willReturn($response);
self::assertSame($response, $controller->showVerificationForm($secret, $message));
}
public function testSubmitVerificationForm(): void {
$secret = '123456789';
$token = 'abcdefghi';
$registration = Registration::fromParams([
'clientSecret' => $secret,
'token' => $token,
]);
$this->registrationService
->expects($this->once())
->method('getRegistrationForSecret')
->with($secret)
->willReturn($registration);
$this->urlGenerator
->method('linkToRoute')
->willReturnCallback(function () {
return json_encode(func_get_args());
});
$controller = $this->getController();
$response = $controller->submitVerificationForm($secret, $token);
self::assertInstanceOf(RedirectResponse::class, $response);
/** @var RedirectResponse $response */
self::assertSame('["registration.register.showUserForm",{"secret":"123456789","token":"abcdefghi"}]', $response->getRedirectURL());
}
public function testSubmitVerificationFormInvalidToken(): void {
$secret = '123456789';
$token = 'abcdefghi';
$registration = Registration::fromParams([
'clientSecret' => $secret,
'token' => 'zyxwvu',
]);
$this->registrationService
->expects($this->once())
->method('getRegistrationForSecret')
->with($secret)
->willReturn($registration);
$response = $this->createMock(TemplateResponse::class);
$controller = $this->getController([
'showVerificationForm',
]);
$controller->expects($this->once())
->method('showVerificationForm')
->with($secret, 'The entered verification code is wrong')
->willReturn($response);
self::assertSame($response, $controller->submitVerificationForm($secret, $token));
}
public function testSubmitVerificationFormInvalidSecret(): void {
$secret = '123456789';
$token = 'abcdefghi';
$registration = Registration::fromParams([
'clientSecret' => $secret,
'token' => $token,
]);
$this->registrationService
->expects($this->once())
->method('getRegistrationForSecret')
->with($secret)
->willThrowException(new DoesNotExistException('Invalid secret'));
$response = $this->createMock(TemplateResponse::class);
$controller = $this->getController([
'validateSecretAndTokenErrorPage',
]);
$controller->expects($this->once())
->method('validateSecretAndTokenErrorPage')
->willReturn($response);
self::assertSame($response, $controller->submitVerificationForm($secret, $token));
}
public function dataShowUserForm(): array {
return [
['', ''],
['tester', ''],
['', 'Unable to create user, there are problems with the user backend.'],
];
}
/**
* @dataProvider dataShowUserForm
* @param string $username
* @param string $message
*/
public function testShowUserForm(string $username, string $message): void {
$secret = '123456789';
$token = 'abcdefghi';
$email = 'nextcloud@example.tld';
$registration = Registration::fromParams([
'email' => 'nextcloud@example.tld',
]);
$controller = $this->getController([
'validateSecretAndToken'
]);
$controller->expects($this->once())
->method('validateSecretAndToken')
->willReturn($registration);
$response = $controller->showUserForm($secret, $token, $username, $message);
self::assertSame(TemplateResponse::RENDER_AS_GUEST, $response->getRenderAs());
self::assertSame('form/user', $response->getTemplateName());
self::assertSame([
'email' => $email,
'email_is_login' => false,
'username' => $username,
'message' => $message,
], $response->getParams());
}
public function testShowUserFormInvalidSecretAndToken(): void {
$secret = '123456789';
$token = 'abcdefghi';
$controller = $this->getController([
'validateSecretAndToken',
'validateSecretAndTokenErrorPage',
]);
$controller->expects($this->once())
->method('validateSecretAndToken')
->willThrowException(new RegistrationException('Invalid secret or token'));
$response = $this->createMock(TemplateResponse::class);
$controller->expects($this->once())
->method('validateSecretAndTokenErrorPage')
->willReturn($response);
self::assertSame($response, $controller->showUserForm($secret, $token));
}
public function testSubmitUserFormInvalidSecretAndToken(): void {
$secret = '123456789';
$token = 'abcdefghi';
$controller = $this->getController([
'validateSecretAndToken',
'validateSecretAndTokenErrorPage',
]);
$controller->expects($this->once())
->method('validateSecretAndToken')
->willThrowException(new RegistrationException('Invalid secret or token'));
$response = $this->createMock(TemplateResponse::class);
$controller->expects($this->once())
->method('validateSecretAndTokenErrorPage')
->willReturn($response);
self::assertSame($response, $controller->submitUserForm($secret, $token, '', ''));
}
public function testSubmitUserFormCreateAccountException(): void {
$secret = '123456789';
$token = 'abcdefghi';
$username = 'user';
$password = 'password';
$registration = Registration::fromParams([
'email' => 'nextcloud@example.tld',
]);
$controller = $this->getController([
'validateSecretAndToken',
'showUserForm'
]);
$controller->expects($this->once())
->method('validateSecretAndToken')
->willReturn($registration);
$response = $this->createMock(TemplateResponse::class);
$controller->expects($this->once())
->method('showUserForm')
->willReturn($response);
$this->registrationService->expects($this->once())
->method('createAccount')
->with($registration, $username, $password)
->willThrowException(new RegistrationException('Invalid account data'));
self::assertSame($response, $controller->submitUserForm($secret, $token, $username, $password));
}
public function testSubmitUserFormRequiresAdminApproval(): void {
$secret = '123456789';
$token = 'abcdefghi';
$username = 'user';
$password = 'password';
$registration = Registration::fromParams([
'email' => 'nextcloud@example.tld',
]);
$controller = $this->getController([
'validateSecretAndToken',
'showUserForm'
]);
$controller->expects($this->once())
->method('validateSecretAndToken')
->willReturn($registration);
$user = $this->createMock(IUser::class);
$user->method('isEnabled')
->willReturn(false);
$this->registrationService->expects($this->once())
->method('createAccount')
->with($registration, $username, $password)
->willReturn($user);
$this->registrationService->expects($this->once())
->method('deleteRegistration')
->with($registration);
$response = $controller->submitUserForm($secret, $token, $username, $password);
self::assertInstanceOf(StandaloneTemplateResponse::class, $response);
self::assertSame(TemplateResponse::RENDER_AS_GUEST, $response->getRenderAs());
self::assertSame('approval-required', $response->getTemplateName());
}
public function testSubmitUserFormSuccessful(): void {
$secret = '123456789';
$token = 'abcdefghi';
$username = 'user';
$password = 'password';
$registration = Registration::fromParams([
'email' => 'nextcloud@example.tld',
]);
$controller = $this->getController([
'validateSecretAndToken',
'showUserForm'
]);
$controller->expects($this->once())
->method('validateSecretAndToken')
->willReturn($registration);
$user = $this->createMock(IUser::class);
$user->method('isEnabled')
->willReturn(true);
$user->method('getUID')
->willReturn($username);
$this->registrationService->expects($this->once())
->method('createAccount')
->with($registration, $username, $password)
->willReturn($user);
$this->registrationService->expects($this->once())
->method('deleteRegistration')
->with($registration);
$this->registrationService->expects($this->once())
->method('loginUser')
->with($username, $username, $password);
$response = $controller->submitUserForm($secret, $token, $username, $password);
self::assertInstanceOf(RedirectToDefaultAppResponse::class, $response);
}
public function testSubmitUserFormSuccessfulLoginFlow2(): void {
$secret = '123456789';
$token = 'abcdefghi';
$username = 'user';
$password = 'password';
$registration = Registration::fromParams([
'email' => 'nextcloud@example.tld',
]);
$controller = $this->getController([
'validateSecretAndToken',
'showUserForm'
]);
$controller->expects($this->once())
->method('validateSecretAndToken')
->willReturn($registration);
$user = $this->createMock(IUser::class);
$user->method('isEnabled')
->willReturn(true);
$user->method('getUID')
->willReturn($username);
$this->registrationService->expects($this->once())
->method('createAccount')
->with($registration, $username, $password)
->willReturn($user);
$this->registrationService->expects($this->once())
->method('deleteRegistration')
->with($registration);
$this->registrationService->expects($this->once())
->method('loginUser')
->with($username, $username, $password);
$this->loginFlowService->method('isUsingLoginFlow')
->with(2)
->willReturn(true);
$response = $this->createMock(StandaloneTemplateResponse::class);
$this->loginFlowService->method('tryLoginFlowV2')
->with($user)
->willReturn($response);
self::assertSame($response, $controller->submitUserForm($secret, $token, $username, $password));
}
public function testSubmitUserFormSuccessfulLoginFlow1(): void {
$secret = '123456789';
$token = 'abcdefghi';
$username = 'user';
$password = 'password';
$registration = Registration::fromParams([
'email' => 'nextcloud@example.tld',
]);
$controller = $this->getController([
'validateSecretAndToken',
'showUserForm'
]);
$controller->expects($this->once())
->method('validateSecretAndToken')
->willReturn($registration);
$user = $this->createMock(IUser::class);
$user->method('isEnabled')
->willReturn(true);
$user->method('getUID')
->willReturn($username);
$this->registrationService->expects($this->once())
->method('createAccount')
->with($registration, $username, $password)
->willReturn($user);
$this->registrationService->expects($this->once())
->method('deleteRegistration')
->with($registration);
$this->registrationService->expects($this->once())
->method('loginUser')
->with($username, $username, $password);
$this->loginFlowService->method('isUsingLoginFlow')
->withConsecutive([2], [1])
->willReturnOnConsecutiveCalls(false, true);
$response = $this->createMock(RedirectResponse::class);
$response->method('getStatus')
->willReturn(Http::STATUS_SEE_OTHER);
$this->loginFlowService->method('tryLoginFlowV1')
->willReturn($response);
self::assertSame($response, $controller->submitUserForm($secret, $token, $username, $password));
}
}

View File

@ -9,7 +9,8 @@
<!-- filters for code coverage -->
<filter>
<whitelist>
<directory suffix=".php">../</directory>
<directory suffix=".php">../../registration/appinfo</directory>
<directory suffix=".php">../../registration/lib</directory>
</whitelist>
</filter>
</phpunit>