Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve login #31

Merged
merged 7 commits into from
Jul 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions db/migrations/20220630135944_AddUserAuthTokenTable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php declare(strict_types=1);

use Phinx\Migration\AbstractMigration;

final class AddUserAuthTokenTable extends AbstractMigration
{
public function down() : void
{
$this->execute(
<<<SQL
DROP TABLE `user_auth_token`
SQL
);
}

public function up() : void
{
$this->execute(
<<<SQL
CREATE TABLE `user_auth_token` (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`token` VARCHAR(255) NOT NULL,
`expiration_date` DATETIME NOT NULL,
`created_at` DATETIME NOT NULL DEFAULT NOW(),
PRIMARY KEY (`id`),
UNIQUE (`token`)
) COLLATE="utf8mb4_unicode_ci" ENGINE=InnoDB
SQL
);
}
}
11 changes: 0 additions & 11 deletions src/Application/SessionService.php

This file was deleted.

33 changes: 33 additions & 0 deletions src/Application/User/Repository.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Movary\Application\User;

use Doctrine\DBAL\Connection;
use Movary\ValueObject\DateTime;

class Repository
{
Expand All @@ -12,6 +13,27 @@ public function __construct(private readonly Connection $dbConnection)
{
}

public function createAuthToken(string $token, DateTime $expirationDate) : void
{
$this->dbConnection->insert(
'user_auth_token',
[
'token' => $token,
'expiration_date' => (string)$expirationDate,
]
);
}

public function deleteAuthToken(string $token) : void
{
$this->dbConnection->delete(
'user_auth_token',
[
'token' => $token,
]
);
}

public function fetchAdminUser() : Entity
{
$data = $this->dbConnection->fetchAssociative('SELECT * FROM `user` WHERE `id` = ?', [self::ADMIN_USER_IO]);
Expand All @@ -23,6 +45,17 @@ public function fetchAdminUser() : Entity
return Entity::createFromArray($data);
}

public function findAuthTokenExpirationDate(string $token) : ?DateTime
{
$expirationDate = $this->dbConnection->fetchOne('SELECT `expiration_date` FROM `user_auth_token` WHERE `token` = ?', [$token]);

if ($expirationDate === false) {
return null;
}

return DateTime::createFromString($expirationDate);
}

public function setPlexWebhookId(?string $plexWebhookId) : void
{
$this->dbConnection->update(
Expand Down
116 changes: 116 additions & 0 deletions src/Application/User/Service/Authentication.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<?php declare(strict_types=1);

namespace Movary\Application\User\Service;

use Movary\Application\User\Exception\InvalidPassword;
use Movary\Application\User\Repository;
use Movary\ValueObject\DateTime;

class Authentication
{
private const AUTHENTICATION_COOKIE_NAME = 'id';

private const MAX_EXPIRATION_AGE_IN_DAYS = 30;

public function __construct(private readonly Repository $repository)
{
}

public function deleteToken(string $token) : void
{
$this->repository->deleteAuthToken($token);
}

public function isUserAuthenticated() : bool
{
$token = filter_input(INPUT_COOKIE, self::AUTHENTICATION_COOKIE_NAME);

if (empty($token) === false && $this->isValidToken($token) === true) {
return true;
}

if (empty($token) === false) {
unset($_COOKIE[self::AUTHENTICATION_COOKIE_NAME]);
setcookie(self::AUTHENTICATION_COOKIE_NAME, '', -1);
}

return false;
}

public function login(string $password, bool $rememberMe) : void
{
if ($this->isUserAuthenticated() === true) {
return;
}

$user = $this->repository->fetchAdminUser();

if (password_verify($password, $user->getPasswordHash()) === false) {
throw InvalidPassword::create();
}

$authTokenExpirationDate = $this->createExpirationDate();
$cookieExpiration = 0;

if ($rememberMe === true) {
$authTokenExpirationDate = $this->createExpirationDate(self::MAX_EXPIRATION_AGE_IN_DAYS);
$cookieExpiration = (int)$authTokenExpirationDate->format('U');
}

$token = $this->generateToken(DateTime::createFromString((string)$authTokenExpirationDate));

setcookie(self::AUTHENTICATION_COOKIE_NAME, $token, $cookieExpiration);
}

public function logout() : void
{
$token = filter_input(INPUT_COOKIE, 'id');

if ($token !== null) {
$this->deleteToken($token);
unset($_COOKIE[self::AUTHENTICATION_COOKIE_NAME]);
setcookie(self::AUTHENTICATION_COOKIE_NAME, '', -1);
}

session_regenerate_id();
}

private function createExpirationDate(int $days = 1) : DateTime
{
$timestamp = strtotime('+' . $days . ' day');

if ($timestamp === false) {
throw new \RuntimeException('Could not generate timestamp for auth token expiration date.');
}

return DateTime::createFromString(date('Y-m-d H:i:s', $timestamp));
}

private function generateToken(?DateTime $expirationDate = null) : string
{
if ($expirationDate === null) {
$expirationDate = $this->createExpirationDate();
}

$token = bin2hex(random_bytes(16));

$this->repository->createAuthToken($token, $expirationDate);

return $token;
}

private function isValidToken(string $token) : bool
{
$tokenExpirationDate = $this->repository->findAuthTokenExpirationDate($token);

if ($tokenExpirationDate === null || $tokenExpirationDate->isAfter(DateTime::create()) === false) {
if ($tokenExpirationDate !== null) {
$this->repository->deleteAuthToken($token);
}

return false;
}

return true;
}
}
40 changes: 0 additions & 40 deletions src/Application/User/Service/Login.php

This file was deleted.

9 changes: 3 additions & 6 deletions src/Factory.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,9 @@
use Movary\Api\Tmdb;
use Movary\Api\Trakt;
use Movary\Api\Trakt\Cache\User\Movie\Watched;
use Movary\Application\Movie;
use Movary\Application\Service\Tmdb\SyncMovie;
use Movary\Application\SessionService;
use Movary\Application\SyncLog;
use Movary\Application\User\Service\Authentication;
use Movary\Command;
use Movary\HttpController\PlexController;
use Movary\HttpController\SettingsController;
use Movary\ValueObject\Config;
use Movary\ValueObject\Http\Request;
Expand Down Expand Up @@ -96,7 +93,7 @@ public static function createSettingsController(ContainerInterface $container, C
return new SettingsController(
$container->get(Twig\Environment::class),
$container->get(SyncLog\Repository::class),
$container->get(SessionService::class),
$container->get(Authentication::class),
$applicationVersion
);
}
Expand Down Expand Up @@ -130,7 +127,7 @@ public static function createTwigEnvironment(ContainerInterface $container) : Tw
{
$twig = new Twig\Environment($container->get(Twig\Loader\LoaderInterface::class));

$twig->addGlobal('loggedIn', $container->get(SessionService::class)->isCurrentUserLoggedIn());
$twig->addGlobal('loggedIn', $container->get(Authentication::class)->isUserAuthenticated());

return $twig;
}
Expand Down
28 changes: 8 additions & 20 deletions src/HttpController/AuthenticationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Movary\HttpController;

use Movary\Application\SessionService;
use Movary\Application\User\Exception\InvalidPassword;
use Movary\Application\User\Service;
use Movary\ValueObject\Http\Header;
Expand All @@ -12,28 +13,16 @@

class AuthenticationController
{
private Environment $twig;

private Service\Login $userLoginService;

public function __construct(Environment $twig, Service\Login $userLoginService)
{
$this->twig = $twig;
$this->userLoginService = $userLoginService;
public function __construct(
private readonly Environment $twig,
private readonly Service\Authentication $authenticationService,
) {
}

public function login(Request $request) : Response
{
if (isset($_SESSION['user']) === true) {
return Response::create(
StatusCode::createSeeOther(),
null,
[Header::createLocation($_SERVER['HTTP_REFERER'])]
);
}

try {
$this->userLoginService->authenticate(
$this->authenticationService->login(
$request->getPostParameters()['password'],
isset($request->getPostParameters()['rememberMe']) === true
);
Expand All @@ -50,8 +39,7 @@ public function login(Request $request) : Response

public function logout() : Response
{
unset($_SESSION['user']);
session_regenerate_id();
$this->authenticationService->logout();

return Response::create(
StatusCode::createSeeOther(),
Expand All @@ -62,7 +50,7 @@ public function logout() : Response

public function renderLoginPage() : Response
{
if (isset($_SESSION['user']) === true) {
if ($this->authenticationService->isUserAuthenticated() === true) {
return Response::create(
StatusCode::createSeeOther(),
null,
Expand Down
6 changes: 3 additions & 3 deletions src/HttpController/ExportController.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,21 @@
namespace Movary\HttpController;

use Movary\Application\ExportService;
use Movary\Application\SessionService;
use Movary\Application\User\Service\Authentication;
use Movary\ValueObject\Http\Request;
use Movary\ValueObject\Http\Response;

class ExportController
{
public function __construct(
private readonly SessionService $sessionService,
private readonly Authentication $authenticationService,
private readonly ExportService $exportService
) {
}

public function getCsvExport(Request $request) : Response
{
if ($this->sessionService->isCurrentUserLoggedIn() === false) {
if ($this->authenticationService->isUserAuthenticated() === false) {
return Response::createFoundRedirect('/login');
}

Expand Down
Loading