461 lines
14 KiB
PHP
461 lines
14 KiB
PHP
<?php
|
|
/**
|
|
* @copyright Copyright (c) 2017 Julius Härtl <jus@bitgrid.net>
|
|
* @copyright Copyright (c) 2017 Pellaeon Lin <pellaeon@hs.ntnu.edu.tw>
|
|
* @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch>
|
|
*
|
|
* @author Julius Härtl <jus@bitgrid.net>
|
|
* @author Pellaeon Lin <pellaeon@hs.ntnu.edu.tw>
|
|
* @author Lukas Reschke <lukas@statuscode.ch>
|
|
*
|
|
* @license GNU AGPL version 3 or any later version
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as
|
|
* published by the Free Software Foundation, either version 3 of the
|
|
* License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
namespace OCA\Registration\Service;
|
|
|
|
use OC\Authentication\Exceptions\InvalidTokenException;
|
|
use OC\Authentication\Exceptions\PasswordlessTokenException;
|
|
use OC\Authentication\Token\IProvider;
|
|
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\Security\ICrypto;
|
|
use OCP\Session\Exceptions\SessionNotAvailableException;
|
|
use \OCP\IUserManager;
|
|
use \OCP\IUserSession;
|
|
use \OCP\IGroupManager;
|
|
use \OCP\IL10N;
|
|
use \OCP\IConfig;
|
|
use \OCP\Security\ISecureRandom;
|
|
|
|
class RegistrationService {
|
|
|
|
/** @var string */
|
|
private $appName;
|
|
/** @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 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 __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) {
|
|
$this->appName = $appName;
|
|
$this->mailService = $mailService;
|
|
$this->l10n = $l10n;
|
|
$this->urlGenerator = $urlGenerator;
|
|
$this->registrationMapper = $registrationMapper;
|
|
$this->userManager = $userManager;
|
|
$this->config = $config;
|
|
$this->groupManager = $groupManager;
|
|
$this->random = $random;
|
|
$this->usersession = $us;
|
|
$this->request = $request;
|
|
$this->logger = $logger;
|
|
$this->session = $session;
|
|
$this->tokenProvider = $tokenProvider;
|
|
$this->crypto = $crypto;
|
|
}
|
|
|
|
/**
|
|
* @param Registration $registration
|
|
*/
|
|
public function confirmEmail(Registration $registration) {
|
|
$registration->setEmailConfirmed(true);
|
|
$this->registrationMapper->update($registration);
|
|
}
|
|
|
|
/**
|
|
* @param Registration $registration
|
|
*/
|
|
public function generateNewToken(Registration $registration) {
|
|
$this->registrationMapper->generateNewToken($registration);
|
|
$this->registrationMapper->update($registration);
|
|
}
|
|
/**
|
|
* Create registration request, used by both the API and form
|
|
* @param string $email
|
|
* @param string $username
|
|
* @param string $password
|
|
* @param string $displayname
|
|
* @return Registration
|
|
*/
|
|
public function createRegistration($email, $username="", $password="", $displayname="") {
|
|
$registration = new Registration();
|
|
$registration->setEmail($email);
|
|
$registration->setUsername($username);
|
|
$registration->setDisplayname($displayname);
|
|
if ($password !== "") {
|
|
$password = $this->crypto->encrypt($password);
|
|
$registration->setPassword($password);
|
|
}
|
|
$this->registrationMapper->generateNewToken($registration);
|
|
$this->registrationMapper->generateClientSecret($registration);
|
|
$this->registrationMapper->insert($registration);
|
|
return $registration;
|
|
}
|
|
|
|
/**
|
|
* @param string $email
|
|
* @throws RegistrationException
|
|
*/
|
|
public function validateEmail(string $email): void {
|
|
$this->mailService->validateEmail($email);
|
|
|
|
// check for pending registrations
|
|
try {
|
|
$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) {
|
|
}
|
|
|
|
if ($this->userManager->getByEmail($email)) {
|
|
throw new RegistrationException(
|
|
$this->l10n->t('A user has already taken this email, maybe you already have an account?'),
|
|
$this->l10n->t('You can <a href="%s">log in now</a>.', [$this->urlGenerator->getAbsoluteURL('/')])
|
|
);
|
|
}
|
|
|
|
if (!$this->checkAllowedDomains($email)) {
|
|
throw new RegistrationException(
|
|
$this->l10n->t(
|
|
'Registration is only allowed for the following domains: ' .
|
|
$this->config->getAppValue($this->appName, 'allowed_domains', '')
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param string $displayname
|
|
* @throws RegistrationException
|
|
*/
|
|
public function validateDisplayname($displayname) {
|
|
if ($displayname === "") {
|
|
throw new RegistrationException($this->l10n->t('Please provide a valid display name.'));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param string $username
|
|
* @throws RegistrationException
|
|
*/
|
|
public function validateUsername($username) {
|
|
if ($username === "") {
|
|
throw new RegistrationException($this->l10n->t('Please provide a valid user name.'));
|
|
}
|
|
|
|
if ($this->registrationMapper->usernameIsPending($username) || $this->userManager->get($username) !== null) {
|
|
throw new RegistrationException($this->l10n->t('The username you have chosen already exists.'));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* check if email domain is allowed
|
|
*
|
|
* @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);
|
|
$allowed = false;
|
|
foreach ($allowed_domains as $domain) {
|
|
$maildomain = explode("@", $email)[1];
|
|
// valid domain, everythings fine
|
|
if ($maildomain === $domain) {
|
|
$allowed = true;
|
|
break;
|
|
}
|
|
}
|
|
return $allowed;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @return array
|
|
*/
|
|
public function getAllowedDomains() {
|
|
$allowed_domains = $this->config->getAppValue($this->appName, 'allowed_domains', '');
|
|
$allowed_domains = explode(';', $allowed_domains);
|
|
return $allowed_domains;
|
|
}
|
|
|
|
/**
|
|
* Find registration entity for token
|
|
*
|
|
* @param string $token
|
|
* @return Registration
|
|
* @throws RegistrationException
|
|
*/
|
|
public function verifyToken(string $token): Registration {
|
|
try {
|
|
return $this->registrationMapper->findByToken($token);
|
|
} catch (DoesNotExistException $exception) {
|
|
throw new RegistrationException($this->l10n->t('Invalid verification URL. No registration request with this verification URL is found.', 404));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param $registration
|
|
* @param string $username
|
|
* @param string $password
|
|
* @return \OCP\IUser
|
|
* @throws RegistrationException|InvalidTokenException
|
|
*/
|
|
public function createAccount(Registration $registration, $username = null, $password = null) {
|
|
if ($password === null && $registration->getPassword() === null) {
|
|
$generatedPassword = $this->generateRandomDeviceToken();
|
|
$registration->setPassword($this->crypto->encrypt($generatedPassword));
|
|
}
|
|
|
|
if ($username === null) {
|
|
$username = $registration->getUsername();
|
|
}
|
|
|
|
if ($registration->getPassword() !== null) {
|
|
$password = $this->crypto->decrypt($registration->getPassword());
|
|
}
|
|
|
|
$this->validateUsername($username);
|
|
|
|
/* TODO
|
|
* createUser tests username validity once, but validateUsername already checked it,
|
|
* but createUser doesn't check if there is a pending registration with that name
|
|
*
|
|
* And validateUsername will throw RegistrationException while
|
|
* createUser throws \InvalidTokenException in NC, \Exception in OC
|
|
*/
|
|
$user = $this->userManager->createUser($username, $password);
|
|
if ($user === false) {
|
|
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());
|
|
} catch (\Exception $e) {
|
|
throw new RegistrationException($this->l10n->t('Unable to set user email: ' . $e->getMessage()));
|
|
}
|
|
|
|
// 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);
|
|
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.");
|
|
$groupId = '';
|
|
} else {
|
|
$group->addUser($user);
|
|
$groupId = $group->getGID();
|
|
}
|
|
} else {
|
|
$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") {
|
|
$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 string $email
|
|
* @return Registration
|
|
* @throws DoesNotExistException
|
|
*/
|
|
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 string $secret
|
|
* @return Registration
|
|
* @throws DoesNotExistException
|
|
*/
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* @param Registration $registration
|
|
*/
|
|
public function deleteRegistration(Registration $registration) {
|
|
$this->registrationMapper->delete($registration);
|
|
}
|
|
|
|
/**
|
|
* Return a 25 digit device password
|
|
*
|
|
* Example: AbCdE-fGhIj-KlMnO-pQrSt-12345
|
|
*
|
|
* @return string
|
|
*/
|
|
private function generateRandomDeviceToken() {
|
|
$groups = [];
|
|
for ($i = 0; $i < 5; $i++) {
|
|
$groups[] = $this->random->generate(5, ISecureRandom::CHAR_HUMAN_READABLE);
|
|
}
|
|
return implode('-', $groups);
|
|
}
|
|
|
|
/**
|
|
* @param string $uid
|
|
* @return string
|
|
* @throws RegistrationException
|
|
*/
|
|
public function generateAppPassword($uid) {
|
|
$name = $this->l10n->t('Registration app auto setup');
|
|
try {
|
|
$sessionId = $this->session->getId();
|
|
} catch (SessionNotAvailableException $ex) {
|
|
throw new RegistrationException('Failed to generate an app token.');
|
|
}
|
|
|
|
try {
|
|
$sessionToken = $this->tokenProvider->getToken($sessionId);
|
|
$loginName = $sessionToken->getLoginName();
|
|
try {
|
|
$password = $this->tokenProvider->getPassword($sessionToken, $sessionId);
|
|
} catch (PasswordlessTokenException $ex) {
|
|
$password = null;
|
|
}
|
|
} catch (InvalidTokenException $ex) {
|
|
throw new RegistrationException('Failed to generate an app token.');
|
|
}
|
|
|
|
$token = $this->generateRandomDeviceToken();
|
|
$this->tokenProvider->generateToken($token, $uid, $loginName, $password, $name, IToken::PERMANENT_TOKEN);
|
|
return $token;
|
|
}
|
|
|
|
/**
|
|
* @param $userId
|
|
* @param $username
|
|
* @param $password
|
|
* @param $decrypt
|
|
* @return RedirectResponse|TemplateResponse
|
|
*/
|
|
public function loginUser($userId, $username, $password, $decrypt = false) {
|
|
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'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Replicates OC::cleanupLoginTokens() since it's protected
|
|
* @param string $userId
|
|
*/
|
|
public function cleanupLoginTokens($userId) {
|
|
$cutoff = time() - $this->config->getSystemValue('remember_login_cookie_lifetime', 60 * 60 * 24 * 15);
|
|
$tokens = $this->config->getUserKeys($userId, 'login_token');
|
|
foreach ($tokens as $token) {
|
|
$time = $this->config->getUserValue($userId, 'login_token', $token);
|
|
if ($time < $cutoff) {
|
|
$this->config->deleteUserValue($userId, 'login_token', $token);
|
|
}
|
|
}
|
|
}
|
|
}
|