Skip to content

Commit

Permalink
Merge pull request #31 from leepeuker/login
Browse files Browse the repository at this point in the history
Improve login
  • Loading branch information
leepeuker authored Jul 1, 2022
2 parents d3d1c8f + 452dfbe commit 21bd2a7
Show file tree
Hide file tree
Showing 17 changed files with 237 additions and 112 deletions.
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

0 comments on commit 21bd2a7

Please sign in to comment.