* @copyright Copyright (c) 2017 Pellaeon Lin * @copyright Copyright (c) 2017 Lukas Reschke * * @author Julius Härtl * @author Pellaeon Lin * @author Lukas Reschke * * @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 . * */ 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\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; 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( 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; $this->urlGenerator = $urlGenerator; $this->registrationMapper = $registrationMapper; $this->userManager = $userManager; $this->config = $config; $this->groupManager = $groupManager; $this->random = $random; $this->userSession = $userSession; $this->request = $request; $this->logger = $logger; $this->session = $session; $this->tokenProvider = $tokenProvider; $this->crypto = $crypto; } public function confirmEmail(Registration $registration): void { $registration->setEmailConfirmed(true); $this->registrationMapper->update($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 * @param string $username * @param string $password * @param string $displayname * @return Registration */ public function createRegistration(string $email, string $username = '', string $password = '', string $displayname = ''): Registration { $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 log in now.', [$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(string $displayname): void { if ($displayname === '') { throw new RegistrationException($this->l10n->t('Please provide a valid display name.')); } } /** * @param string $username * @throws RegistrationException */ public function validateUsername(string $username): void { 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(string $email): bool { $allowedDomains = $this->config->getAppValue($this->appName, 'allowed_domains', ''); if ($allowedDomains !== '') { [,$mailDomain] = explode('@', strtolower($email), 2); $allowedDomains = explode(';', strtolower($allowedDomains)); foreach ($allowedDomains as $domain) { // valid domain, everything's fine if ($mailDomain === $domain) { return true; } } return false; } return true; } /** * @return string[] */ 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 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|null $username * @param string|null $password * @return IUser * @throws RegistrationException|InvalidTokenException */ 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)); } 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 $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 '$registeredUserGroup' group, but it does not exist."); $groupId = ''; } else { $group->addUser($user); $groupId = $group->getGID(); } } else { $groupId = ''; } // disable user if this is requested by config $adminApprovalRequired = $this->config->getAppValue($this->appName, 'admin_approval_required', 'no'); if ($adminApprovalRequired === 'yes') { $user->setEnabled(false); } $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); } public function getUserAccount(Registration $registration): ?IUser { return $this->userManager->get($registration->getUsername()); } public function deleteRegistration(Registration $registration): void { $this->registrationMapper->delete($registration); } /** * Return a 25 digit device password * * Example: AbCdE-fGhIj-KlMnO-pQrSt-12345 * * @return string */ private function generateRandomDeviceToken(): string { $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(string $uid): string { $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 string $userId * @param string $username * @param string $password * @param bool $decrypt */ public function loginUser(string $userId, string $username, string $password, bool $decrypt = false): void { if ($decrypt) { $password = $this->crypto->decrypt($password); } $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(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) { $time = $this->config->getUserValue($userId, 'login_token', $token); if ($time < $cutoff) { $this->config->deleteUserValue($userId, 'login_token', $token); } } } }