plugin = $this->plugins->get('auth'); $this->session = $this->plugin->sessions->start(); $this->authConfig = $this->plugin->config; } /** * Attempt a login using a designated identifier and password. * * Sessions are automatically extended as they approach their end. Sessions with remember = false will last an hour. * Sessions with remember = true will last 3 months. * * Context data will be saved into the database for security purposes. * * Login may be prevented by the following causes: * - Username and password mismatch, or password is not set. * - The user is already currently logged in the current session. * - The user doesn't exist. * - User is set to inactive. * - Email verification threshold has expired and the user must verify before being allowed to log in. * - The event system cancels login for other reasons. * * @param string $identifier * @param string $password * @param bool $remember * @param array $context * @return Session * @throws LoginErrorException * @throws LoginWarningException */ public function login(string $identifier, string $password, bool $remember = false, array $context = []): Session { /** @var LoginEvent $event */ $event = Events::fireEvent(new LoginEvent($identifier, $password, $remember, $context)); if ($event->isCancelled()) throw new LoginErrorException("Login cancelled by system."); // Fetch the user $user = $this->plugin->users->getUserByEmail($event->identifier); foreach ($event->getIdentifierFields() as $field) { if (empty($user)) $user = $this->plugin->users->getUsersByProperty($field, $event->identifier); } // If user not found, notify if (empty($user)) throw new LoginWarningException("The provided username and/or password is invalid."); // Verify that the current session is not the same as the supposed login session if ($this->session->user->id === $user->id) throw new LoginWarningException("User is already logged in."); // Check if the password is correct if (!password_verify($event->password, $user->password)) throw new LoginWarningException("The provided username and/or password is invalid."); // Check if the password needs to be updated if (password_needs_rehash($user->password, $this->plugin->config->get("password_algorithm"), $this->plugin->config->get("password_options"))) { $this->plugin->users->changePassword($user, $event->password); } // Check if the user is still active if (!$user->active) throw new LoginErrorException("User is inactive. Login blocked."); // Check if email has been verified on time if (!is_null($user->emailVerifyToken) && date('U') > $user->emailVerifyExpiry) { $url = $this->plugin->getAuthenticationURL() . "/resend_verify_email?t=" . base64_encode($user->primaryEmail); throw new LoginWarningException("User must verify email before logging in. Click here to resend the verification email."); } // Create a session, log and return it $this->session = $this->plugin->sessions->createSession($user, $event->context, $event->remember); Logger::log("Logged in user '" . $user->primaryEmail . "'"); return $this->session; } /** * Log a user out of the current session. * * Will set the session active to false and remove the session cookie. * * @return bool */ public function logout(): bool { if ($this->session->user->id == "0") return false; return $this->plugin->sessions->endSession($this->session); } /** * Performs a registration of a new user using the provided data. * * The only absolutely required parameter is email. No user can exist without a valid email address. * * If password is null (for instance by creating the user from a panel instead of registration), the user shall be asked to set a password after verifying their email. * * Parameters should be an empty array, unless the developer wants to explicitly set internal values like emailExpiryTime. * * Properties can be any extra key => value data the developer wants to store about their users, such as usernames or social data. * * Use $bypassRestrictions in case registration is disabled by config, but a new user must be added anyway. * * Registration can be denied for the following reasons: * - Registration is disabled by config and $bypassRestrictions is not set to true. * - User already exists. * - The event system cancels registration of another reason. * * @param string $email * @param string|null $password * @param array $parameters * @param array $properties * @param bool $bypassRestrictions * @return bool * @throws RegisterErrorException * @throws RegisterWarningException */ public function register(string $email, ?string $password, array $parameters, array $properties, bool $bypassRestrictions = false): bool { // First check if registration is enabled if (!$this->authConfig->get("register_enabled") && !$bypassRestrictions) throw new RegisterErrorException("Registration is disabled."); /** @var RegisterEvent $event */ $event = Events::fireEvent(new RegisterEvent($email, $password, $parameters, $properties)); if ($event->isCancelled()) { $error = is_null($event->getError()) ? "Register cancelled by system." : $event->getError(); throw new RegisterErrorException($error); } // Create the user try { if (!$this->plugin->users->addUser($event->email, $event->password, true, $event->properties)) throw new RegisterErrorException("Could not create user. System error."); // Fetch the user $user = $this->plugin->users->getUserByEmail($event->email); $this->sendVerifyEmail($user); } catch (InputException|AuthenticationException $e) { throw new RegisterErrorException($e->getMessage()); } catch (Exception $e) { throw new RegisterErrorException("Could not add user. Failed to send mail to user: " . $e->getMessage()); } catch (FactoryException|LayoutException $e) { throw new RegisterErrorException("Could not add user. System failure."); } return true; } /** * Sends a new email verification message to the user, in case the existing message is lost or expired. * * @param string $email * @return bool * @throws LoginErrorException * @throws RegisterErrorException */ public function resendVerifyEmail(string $email): bool { $user = $this->plugin->users->getUserByEmail($email); if (is_null($user)) throw new LoginErrorException("User not found."); try { $this->sendVerifyEmail($user); } catch (FactoryException|LayoutException $e) { throw new LoginErrorException("Could not resend verify email. System failure."); } catch (Exception $e) { throw new RegisterErrorException("Could not resend verify email. Failed to send mail to user: " . $e->getMessage()); } return true; } /** * Sends a email verification message to the user. * * @param User $user * @return void */ private function sendVerifyEmail(User $user): void { // Load template engine /** @var Layout $layout */ $layout = Factory::getInstance("layouts"); $layout->assign("user", $user); $layout->assign("verifyURL", $this->plugin->getAuthenticationURL() . "/verify"); $html = $layout->get("mail/register"); $alt = $layout->get("mail/register_alt"); // Prepare mailer /** @var PHPMailerWrapper $mailer */ $mailer = $this->libraries->get("mailer"); $mailer->addAddress($user->primaryEmail); $serverName = $this->config->getConfig("web")->get("serverName"); $mailer->Subject = "Welcome to $serverName !"; $mailer->isHTML(); $mailer->Body = $html; $mailer->AltBody = $alt; // And send mail $mailer->send(); } /** * Verifies a user by their email token. * * @param string $token * @return User|null */ public function verifyEmail(string $token): ?User { // Fetch user $users = $this->plugin->users->getUsersByVariable("emailVerifyToken", $token); if (count($users) === 1) { // Select user /** @var User $user */ $user = $users[0]; $user->emailVerifyToken = null; $user->emailVerifyExpiry = null; // Update the user $this->plugin->users->updateUser($user); // And return true return $user; } return null; } /** * Sends a forgot password email to the user, based on the email address of the user. * * Will be denied if: * - Password resets are disabled by config. * - The user doesn't exist. * - The user is marked as inactive. * * @param string $email * @return bool * @throws FactoryException * @throws LoginErrorException * @throws LoginWarningException */ public function forgotPassword(string $email): bool { // Check if forgot password is enabled if (!$this->authConfig->get("forgot_password_enabled")) throw new LoginErrorException("Forgot password is disabled."); // Fetch the user $user = $this->plugin->users->getUserByEmail($email); // If user not found, notify if (empty($user)) throw new LoginWarningException("The provided email is unknown."); // Check if the user is still active if (!$user->active) throw new LoginErrorException("The provided email is unknown"); // Generate reset token $token = bin2hex(random_bytes(32)); // Save the token and user ID to ObjectStorage /** @var CacheInterface $cache */ $cache = Factory::getInstance("storage")->getCache(); $cache->set("forgot_password_" . $token, $user->id, 3600); // And send mail try { // Data is verified. Now send the email. /** @var Layout $layout */ $layout = Factory::getInstance("layouts"); $layout->assign("resetURL", $this->plugin->getAuthenticationURL() . "/reset_password"); $layout->assign("token", $token); $html = $layout->get("mail/forgot_password"); $alt = $layout->get("mail/forgot_password_alt"); // Prepare mailer /** @var PHPMailerWrapper $mailer */ $mailer = $this->libraries->get("mailer"); $mailer->addAddress($email); $mailer->Subject = "Password reset"; $mailer->isHTML(); $mailer->Body = $html; $mailer->AltBody = $alt; $mailer->send(); } catch (Exception $e) { throw new LoginErrorException("Could not send mail to user: " . $e->getMessage()); } catch (FactoryException|LayoutException $e) { throw new LoginErrorException("Could not send mail to user. System failure."); } return true; } /** * Method used by AdminView to generate a token to reset password with, after verifying email. * * @internal * @param User $user * @return string */ public function createResetToken(User $user): string { // Generate reset token $token = bin2hex(random_bytes(32)); // Save the token and user ID to ObjectStorage /** @var CacheInterface $cache */ $cache = Factory::getInstance("storage")->getCache(); $cache->set("forgot_password_" . $token, $user->id, 3600); return $token; } /** * Reset the password of the user with the selected token * * @param string $token * @param string $password * @return bool * @throws LoginErrorException */ public function resetPassword(string $token, string $password): bool { // Check token in cache /** @var CacheInterface $cache */ $cache = Factory::getInstance("storage")->getCache(); if (!$cache->has("forgot_password_" . $token)) throw new LoginErrorException("User was not found."); // Fetch user $user = $this->plugin->users->getUserByUid($cache->get("forgot_password_" . $token)); // Modify the user try { if ($this->plugin->users->changePassword($user, $password)) $cache->delete("forgot_password_" . $token); return true; } catch (InputException $e) { throw new LoginErrorException($e->getMessage()); } } /** * Returns an array of detailed user information. * * Returns ['user' => User, 'sessions' => Session[]] , or null if not found. * * @param string $userId * @return array|null */ public function getDetailedUserInformation(string $userId): ?array { $user = $this->plugin->users->getUserByUid($userId); if (is_null($user)) return null; $sessions = $this->plugin->sessions->getSessions(['user' => $userId]); return ['user' => $user, 'sessions' => $sessions]; } /** * Gives a list of users that match the filter. * * @param array $filter * @param int $index * @param int $limit * @return array */ public function listUsers(array $filter = [], int $index = 0, int $limit = 25): array { return $this->plugin->users->getUsers($index, $limit); } /** * Condition callable for register form, to check if password 1 and 2 are equivalent. * * @internal * @param Form $form * @return bool */ public static function registerFormCondition(Form $form): bool { // Verify password1 and password2 equivalency $pass1 = $form->getField("password1"); $pass2 = $form->getField("password2"); if ($pass1->getValue() !== $pass2->getValue()) { $pass1->invalidate(); $pass2->invalidate(); $pass1->addError("Passwords do not match"); $pass2->addError("Passwords do not match"); return false; } return true; } }