2017-04-05 23:37:26 +00:00
|
|
|
<?php
|
2020-10-14 22:19:31 +00:00
|
|
|
|
2021-07-19 05:53:45 +00:00
|
|
|
declare(strict_types=1);
|
|
|
|
|
2017-04-05 23:37:26 +00:00
|
|
|
namespace App;
|
|
|
|
|
2018-08-04 22:05:14 +00:00
|
|
|
use App\Entity\Repository\UserRepository;
|
|
|
|
use App\Entity\User;
|
2019-09-10 16:40:31 +00:00
|
|
|
use App\Exception\NotLoggedInException;
|
2020-01-18 02:35:09 +00:00
|
|
|
use Mezzio\Session\SessionInterface;
|
2017-04-05 23:37:26 +00:00
|
|
|
|
|
|
|
class Auth
|
|
|
|
{
|
2019-09-23 04:51:44 +00:00
|
|
|
public const SESSION_IS_LOGIN_COMPLETE_KEY = 'is_login_complete';
|
|
|
|
public const SESSION_USER_ID_KEY = 'user_id';
|
|
|
|
public const SESSION_MASQUERADE_USER_ID_KEY = 'masquerade_user_id';
|
|
|
|
|
2019-03-14 08:40:02 +00:00
|
|
|
/** @var int The window of valid one-time passwords outside the current timestamp. */
|
|
|
|
public const TOTP_WINDOW = 5;
|
|
|
|
|
2021-04-23 22:12:47 +00:00
|
|
|
protected bool|User|null $user = null;
|
2017-04-05 23:37:26 +00:00
|
|
|
|
2021-04-23 22:12:47 +00:00
|
|
|
protected bool|User|null $masqueraded_user = null;
|
2017-04-05 23:37:26 +00:00
|
|
|
|
2020-07-10 04:15:27 +00:00
|
|
|
public function __construct(
|
2021-04-23 05:24:12 +00:00
|
|
|
protected UserRepository $userRepo,
|
|
|
|
protected SessionInterface $session,
|
|
|
|
protected Environment $environment
|
2020-07-10 04:15:27 +00:00
|
|
|
) {
|
2017-04-05 23:37:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Authenticate a given username and password combination against the User repository.
|
|
|
|
*
|
2019-01-31 05:25:55 +00:00
|
|
|
* @param string $username
|
|
|
|
* @param string $password
|
2017-04-05 23:37:26 +00:00
|
|
|
*/
|
2021-02-28 02:50:45 +00:00
|
|
|
public function authenticate(string $username, string $password): ?User
|
2017-04-05 23:37:26 +00:00
|
|
|
{
|
2019-09-24 02:22:08 +00:00
|
|
|
$user_auth = $this->userRepo->authenticate($username, $password);
|
2017-04-05 23:37:26 +00:00
|
|
|
|
|
|
|
if ($user_auth instanceof User) {
|
|
|
|
$this->setUser($user_auth);
|
2019-01-31 05:25:55 +00:00
|
|
|
return $user_auth;
|
2017-04-05 23:37:26 +00:00
|
|
|
}
|
2019-01-31 05:25:55 +00:00
|
|
|
|
|
|
|
return null;
|
2017-04-05 23:37:26 +00:00
|
|
|
}
|
|
|
|
|
2019-03-14 08:40:02 +00:00
|
|
|
/**
|
2019-09-04 18:00:51 +00:00
|
|
|
* Get the currently logged in user.
|
|
|
|
*
|
|
|
|
* @param bool $real_user_only
|
2019-09-20 16:44:38 +00:00
|
|
|
*
|
2019-09-04 18:00:51 +00:00
|
|
|
* @throws Exception
|
2019-03-14 08:40:02 +00:00
|
|
|
*/
|
2021-07-19 05:53:45 +00:00
|
|
|
public function getLoggedInUser(bool $real_user_only = false): ?User
|
2017-04-05 23:37:26 +00:00
|
|
|
{
|
2019-09-04 18:00:51 +00:00
|
|
|
if (!$real_user_only && $this->isMasqueraded()) {
|
|
|
|
return $this->getMasquerade();
|
|
|
|
}
|
2017-04-05 23:37:26 +00:00
|
|
|
|
2019-09-04 18:00:51 +00:00
|
|
|
if (!$this->isLoginComplete()) {
|
|
|
|
return null;
|
|
|
|
}
|
2017-04-05 23:37:26 +00:00
|
|
|
|
2019-09-04 18:00:51 +00:00
|
|
|
return $this->getUser();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if the current user is masquerading as another account.
|
|
|
|
*/
|
|
|
|
public function isMasqueraded(): bool
|
|
|
|
{
|
|
|
|
if (!$this->isLoggedIn()) {
|
|
|
|
$this->masqueraded_user = false;
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (null === $this->masqueraded_user) {
|
2019-09-23 04:51:44 +00:00
|
|
|
if (!$this->session->has(self::SESSION_MASQUERADE_USER_ID_KEY)) {
|
2019-09-04 18:00:51 +00:00
|
|
|
$this->masqueraded_user = false;
|
|
|
|
} else {
|
2019-09-23 04:51:44 +00:00
|
|
|
$mask_user_id = (int)$this->session->get(self::SESSION_MASQUERADE_USER_ID_KEY);
|
2019-09-04 18:00:51 +00:00
|
|
|
if (0 !== $mask_user_id) {
|
2019-09-29 07:50:24 +00:00
|
|
|
$user = $this->userRepo->getRepository()->find($mask_user_id);
|
2019-09-04 18:00:51 +00:00
|
|
|
} else {
|
|
|
|
$user = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($user instanceof User) {
|
|
|
|
$this->masqueraded_user = $user;
|
|
|
|
} else {
|
2019-09-23 04:51:44 +00:00
|
|
|
$this->session->clear();
|
2019-09-04 18:00:51 +00:00
|
|
|
$this->masqueraded_user = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return ($this->masqueraded_user instanceof User);
|
2017-04-05 23:37:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if a user account is currently authenticated.
|
|
|
|
*/
|
2019-01-31 05:25:55 +00:00
|
|
|
public function isLoggedIn(): bool
|
2017-04-05 23:37:26 +00:00
|
|
|
{
|
2020-12-03 04:18:06 +00:00
|
|
|
if ($this->environment->isCli() && !$this->environment->isTesting()) {
|
2017-04-05 23:37:26 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2019-03-14 08:40:02 +00:00
|
|
|
if (!$this->isLoginComplete()) {
|
|
|
|
return false;
|
|
|
|
}
|
2017-04-05 23:37:26 +00:00
|
|
|
|
2019-03-14 08:40:02 +00:00
|
|
|
$user = $this->getUser();
|
2017-04-05 23:37:26 +00:00
|
|
|
return ($user instanceof User);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-09-04 18:00:51 +00:00
|
|
|
* Indicate whether login is "complete", i.e. whether any necessary
|
|
|
|
* second-factor authentication steps have been completed.
|
2017-04-05 23:37:26 +00:00
|
|
|
*/
|
2019-09-04 18:00:51 +00:00
|
|
|
public function isLoginComplete(): bool
|
2017-04-05 23:37:26 +00:00
|
|
|
{
|
2019-09-23 04:51:44 +00:00
|
|
|
return $this->session->get(self::SESSION_IS_LOGIN_COMPLETE_KEY, false) ?? false;
|
2017-04-05 23:37:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the authenticated user entity.
|
|
|
|
*
|
2019-09-04 18:00:51 +00:00
|
|
|
* @throws Exception
|
2017-04-05 23:37:26 +00:00
|
|
|
*/
|
2019-01-31 05:25:55 +00:00
|
|
|
public function getUser(): ?User
|
2017-04-05 23:37:26 +00:00
|
|
|
{
|
2019-08-19 06:13:53 +00:00
|
|
|
if (null === $this->user) {
|
2019-09-23 04:51:44 +00:00
|
|
|
$user_id = (int)$this->session->get(self::SESSION_USER_ID_KEY);
|
2017-04-05 23:37:26 +00:00
|
|
|
|
2019-01-31 05:25:55 +00:00
|
|
|
if (0 === $user_id) {
|
2019-08-07 04:33:55 +00:00
|
|
|
$this->user = false;
|
2019-01-31 05:25:55 +00:00
|
|
|
return null;
|
2017-04-05 23:37:26 +00:00
|
|
|
}
|
|
|
|
|
2019-09-29 07:50:24 +00:00
|
|
|
$user = $this->userRepo->getRepository()->find($user_id);
|
2017-04-05 23:37:26 +00:00
|
|
|
if ($user instanceof User) {
|
2019-08-07 04:33:55 +00:00
|
|
|
$this->user = $user;
|
2017-04-05 23:37:26 +00:00
|
|
|
} else {
|
2019-08-07 04:33:55 +00:00
|
|
|
$this->user = false;
|
2017-04-05 23:37:26 +00:00
|
|
|
$this->logout();
|
|
|
|
|
2019-09-04 18:00:51 +00:00
|
|
|
throw new Exception('Invalid user!');
|
2017-04-05 23:37:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-05 06:27:12 +00:00
|
|
|
if (!$this->user instanceof User) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
/** @var User|null $user */
|
|
|
|
$user = $this->userRepo->getRepository()->find($this->user->getId());
|
|
|
|
return $user;
|
2017-04-05 23:37:26 +00:00
|
|
|
}
|
|
|
|
|
2019-09-04 18:00:51 +00:00
|
|
|
/**
|
|
|
|
* Masquerading
|
|
|
|
*/
|
|
|
|
|
2017-04-05 23:37:26 +00:00
|
|
|
/**
|
|
|
|
* Set the currently authenticated user.
|
|
|
|
*
|
|
|
|
* @param User $user
|
|
|
|
*/
|
2019-01-31 05:25:55 +00:00
|
|
|
public function setUser(User $user): void
|
2017-04-05 23:37:26 +00:00
|
|
|
{
|
2019-09-23 04:51:44 +00:00
|
|
|
$this->session->set(self::SESSION_IS_LOGIN_COMPLETE_KEY, null === $user->getTwoFactorSecret());
|
|
|
|
$this->session->set(self::SESSION_USER_ID_KEY, $user->getId());
|
2019-08-19 06:13:53 +00:00
|
|
|
|
2019-08-07 04:33:55 +00:00
|
|
|
$this->user = $user;
|
2017-04-05 23:37:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-09-04 18:00:51 +00:00
|
|
|
* End the user's currently logged in session.
|
2017-04-05 23:37:26 +00:00
|
|
|
*/
|
2019-09-04 18:00:51 +00:00
|
|
|
public function logout(): void
|
2017-04-05 23:37:26 +00:00
|
|
|
{
|
2019-12-07 12:19:49 +00:00
|
|
|
if (isset($this->session) && $this->session instanceof SessionInterface) {
|
2019-09-24 02:22:08 +00:00
|
|
|
$this->session->clear();
|
|
|
|
}
|
|
|
|
|
2019-09-04 18:00:51 +00:00
|
|
|
$this->user = null;
|
2017-04-05 23:37:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return the currently masqueraded user, if one is set.
|
|
|
|
*/
|
2019-01-31 05:25:55 +00:00
|
|
|
public function getMasquerade(): ?User
|
2017-04-05 23:37:26 +00:00
|
|
|
{
|
2021-07-19 05:53:45 +00:00
|
|
|
if ($this->masqueraded_user instanceof User) {
|
|
|
|
return $this->masqueraded_user;
|
|
|
|
}
|
|
|
|
return null;
|
2017-04-05 23:37:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-09-04 18:00:51 +00:00
|
|
|
* Become a different user across the application.
|
2017-04-05 23:37:26 +00:00
|
|
|
*
|
2021-07-19 05:53:45 +00:00
|
|
|
* @param array<string, mixed>|User $user_info
|
2017-04-05 23:37:26 +00:00
|
|
|
*/
|
2021-04-23 22:12:47 +00:00
|
|
|
public function masqueradeAsUser(User|array $user_info): void
|
2017-04-05 23:37:26 +00:00
|
|
|
{
|
2019-09-04 18:00:51 +00:00
|
|
|
if (!($user_info instanceof User)) {
|
2019-09-29 07:50:24 +00:00
|
|
|
$user_info = $this->userRepo->getRepository()->findOneBy($user_info);
|
2017-04-05 23:37:26 +00:00
|
|
|
}
|
|
|
|
|
2019-09-04 18:00:51 +00:00
|
|
|
if (!($user_info instanceof User)) {
|
|
|
|
throw new Exception('Invalid user!');
|
2017-04-05 23:37:26 +00:00
|
|
|
}
|
|
|
|
|
2019-09-23 04:51:44 +00:00
|
|
|
$this->session->set(self::SESSION_MASQUERADE_USER_ID_KEY, $user_info->getId());
|
2019-09-04 18:00:51 +00:00
|
|
|
$this->masqueraded_user = $user_info;
|
2017-04-05 23:37:26 +00:00
|
|
|
}
|
2019-03-14 08:40:02 +00:00
|
|
|
|
|
|
|
/**
|
2019-09-04 18:00:51 +00:00
|
|
|
* Return to the regular authenticated account.
|
2019-03-14 08:40:02 +00:00
|
|
|
*/
|
2019-09-04 18:00:51 +00:00
|
|
|
public function endMasquerade(): void
|
2019-03-14 08:40:02 +00:00
|
|
|
{
|
2019-09-24 02:22:08 +00:00
|
|
|
$this->session->unset(self::SESSION_MASQUERADE_USER_ID_KEY);
|
2019-09-04 18:00:51 +00:00
|
|
|
$this->masqueraded_user = null;
|
2019-03-14 08:40:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Verify a supplied one-time password.
|
|
|
|
*
|
|
|
|
* @param string $otp
|
|
|
|
*/
|
|
|
|
public function verifyTwoFactor(string $otp): bool
|
|
|
|
{
|
|
|
|
$user = $this->getUser();
|
|
|
|
|
|
|
|
if (!($user instanceof User)) {
|
2020-10-14 22:19:31 +00:00
|
|
|
throw new NotLoggedInException();
|
2019-03-14 08:40:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if ($user->verifyTwoFactor($otp)) {
|
2019-09-23 04:51:44 +00:00
|
|
|
$this->session->set(self::SESSION_IS_LOGIN_COMPLETE_KEY, true);
|
2019-03-14 08:40:02 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
2018-08-04 22:05:14 +00:00
|
|
|
}
|