Added proof of concept for authorization system
This commit is contained in:
parent
ff310f56c5
commit
db949dd361
9 changed files with 279 additions and 0 deletions
|
@ -3,6 +3,13 @@ namespace Szurubooru\Controllers;
|
|||
|
||||
final class AuthController extends AbstractController
|
||||
{
|
||||
private $authService;
|
||||
|
||||
public function __construct(\Szurubooru\Services\AuthService $authService)
|
||||
{
|
||||
$this->authService = $authService;
|
||||
}
|
||||
|
||||
public function registerRoutes(\Szurubooru\Router $router)
|
||||
{
|
||||
$router->post('/api/login', [$this, 'login']);
|
||||
|
@ -11,5 +18,8 @@ final class AuthController extends AbstractController
|
|||
|
||||
public function login()
|
||||
{
|
||||
$input = new \Szurubooru\Helpers\InputReader();
|
||||
$this->authService->loginFromCredentials($input->userName, $input->password);
|
||||
return $this->authService->getLoginToken();
|
||||
}
|
||||
}
|
||||
|
|
21
src/Dao/TokenDao.php
Normal file
21
src/Dao/TokenDao.php
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
namespace Szurubooru\Dao;
|
||||
|
||||
class TokenDao extends AbstractDao
|
||||
{
|
||||
public function __construct(\Szurubooru\DatabaseConnection $databaseConnection)
|
||||
{
|
||||
parent::__construct($databaseConnection, 'tokens', '\Szurubooru\Entities\Token');
|
||||
}
|
||||
|
||||
public function getByName($tokenName)
|
||||
{
|
||||
$arrayEntity = $this->collection->findOne(['name' => $tokenName]);
|
||||
return $this->entityConverter->toEntity($arrayEntity);
|
||||
}
|
||||
|
||||
public function deleteByName($tokenName)
|
||||
{
|
||||
$this->collection->remove(['name' => $tokenName]);
|
||||
}
|
||||
}
|
16
src/Dao/UserDao.php
Normal file
16
src/Dao/UserDao.php
Normal file
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
namespace Szurubooru\Dao;
|
||||
|
||||
class UserDao extends AbstractDao implements ICrudDao
|
||||
{
|
||||
public function __construct(\Szurubooru\DatabaseConnection $databaseConnection)
|
||||
{
|
||||
parent::__construct($databaseConnection, 'users', '\Szurubooru\Entities\User');
|
||||
}
|
||||
|
||||
public function getByName($userName)
|
||||
{
|
||||
$arrayEntity = $this->collection->findOne(['name' => $userName]);
|
||||
return $this->entityConverter->toEntity($arrayEntity);
|
||||
}
|
||||
}
|
11
src/Entities/Token.php
Normal file
11
src/Entities/Token.php
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
namespace Szurubooru\Entities;
|
||||
|
||||
final class Token extends Entity
|
||||
{
|
||||
const PURPOSE_LOGIN = 'login';
|
||||
|
||||
public $name;
|
||||
public $purpose;
|
||||
public $additionalData;
|
||||
}
|
8
src/Entities/User.php
Normal file
8
src/Entities/User.php
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
namespace Szurubooru\Entities;
|
||||
|
||||
final class User extends Entity
|
||||
{
|
||||
public $name;
|
||||
public $passwordHash;
|
||||
}
|
14
src/Helpers/InputReader.php
Normal file
14
src/Helpers/InputReader.php
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
namespace Szurubooru\Helpers;
|
||||
|
||||
final class InputReader
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function __get($key)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
87
src/Services/AuthService.php
Normal file
87
src/Services/AuthService.php
Normal file
|
@ -0,0 +1,87 @@
|
|||
<?php
|
||||
namespace Szurubooru\Services;
|
||||
|
||||
final class AuthService
|
||||
{
|
||||
private $loggedInUser = null;
|
||||
private $loginToken = null;
|
||||
|
||||
private $passwordService;
|
||||
private $userDao;
|
||||
private $tokenDao;
|
||||
|
||||
public function __construct(
|
||||
\Szurubooru\Services\PasswordService $passwordService,
|
||||
\Szurubooru\Dao\TokenDao $tokenDao,
|
||||
\Szurubooru\Dao\UserDao $userDao)
|
||||
{
|
||||
$this->loggedInUser = new \Szurubooru\Entities\User();
|
||||
$this->loggedInUser->name = 'Anonymous';
|
||||
$this->userDao = $userDao;
|
||||
$this->tokenDao = $tokenDao;
|
||||
$this->passwordService = $passwordService;
|
||||
}
|
||||
|
||||
public function isLoggedIn()
|
||||
{
|
||||
return $this->loginToken !== null;
|
||||
}
|
||||
|
||||
public function getLoggedInUser()
|
||||
{
|
||||
return $this->loggedInUser;
|
||||
}
|
||||
|
||||
public function getLoginToken()
|
||||
{
|
||||
return $this->token;
|
||||
}
|
||||
|
||||
public function loginFromCredentials($userName, $password)
|
||||
{
|
||||
$user = $this->userDao->getByName($userName);
|
||||
if (!$user)
|
||||
throw new \InvalidArgumentException('User not found.');
|
||||
|
||||
$passwordHash = $this->passwordService->getHash($password);
|
||||
if ($user->passwordHash != $passwordHash)
|
||||
throw new \InvalidArgumentException('Specified password is invalid.');
|
||||
|
||||
$this->loggedInUser = $user;
|
||||
$this->loginToken = $this->createAndSaveLoginToken($user);
|
||||
}
|
||||
|
||||
public function loginFromToken($loginTokenName)
|
||||
{
|
||||
$loginToken = $this->tokenDao->getByName($loginTokenName);
|
||||
if (!$loginToken)
|
||||
throw new \Exception('Error while logging in (invalid token.)');
|
||||
|
||||
$this->loginToken = $loginToken;
|
||||
$this->loggedInUser = $this->userDao->getById($loginToken->additionalData);
|
||||
if (!$this->loggedInUser)
|
||||
{
|
||||
$this->logout();
|
||||
throw new \RuntimeException('Token is correct, but user is not. Have you deleted your account?');
|
||||
}
|
||||
}
|
||||
|
||||
public function logout()
|
||||
{
|
||||
if (!$this->isLoggedIn())
|
||||
throw new \Exception('Not logged in.');
|
||||
|
||||
$this->tokenDao->deleteByName($this->loginToken);
|
||||
$this->loginToken = null;
|
||||
}
|
||||
|
||||
private function createAndSaveLoginToken(\Szurubooru\Entities\User $user)
|
||||
{
|
||||
$loginToken = new \Szurubooru\Entities\Token();
|
||||
$loginToken->name = hash('sha256', $user->name . '/' . microtime(true));
|
||||
$loginToken->additionalData = $user->id;
|
||||
$loginToken->purpose = \Szurubooru\Entities\Token::PURPOSE_LOGIN;
|
||||
$this->tokenDao->save($loginToken);
|
||||
return $loginToken;
|
||||
}
|
||||
}
|
17
src/Services/PasswordService.php
Normal file
17
src/Services/PasswordService.php
Normal file
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
namespace Szurubooru\Services;
|
||||
|
||||
class PasswordService
|
||||
{
|
||||
private $config;
|
||||
|
||||
public function __construct(\Szurubooru\Config $config)
|
||||
{
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
public function getHash($password)
|
||||
{
|
||||
return hash('sha256', $this->config->security->secret . '/' . $password);
|
||||
}
|
||||
}
|
95
tests/Services/AuthServiceTest.php
Normal file
95
tests/Services/AuthServiceTest.php
Normal file
|
@ -0,0 +1,95 @@
|
|||
<?php
|
||||
namespace Szurubooru\Tests\Services;
|
||||
|
||||
class AuthServiceTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
public function testInvalidUser()
|
||||
{
|
||||
$passwordServiceMock = $this->getPasswordServiceMock();
|
||||
$tokenDaoMock = $this->getTokenDaoMock();
|
||||
$userDaoMock = $this->getUserDaoMock();
|
||||
|
||||
$this->setExpectedException(\InvalidArgumentException::class, 'User not found');
|
||||
|
||||
$authService = new \Szurubooru\Services\AuthService($passwordServiceMock, $tokenDaoMock, $userDaoMock);
|
||||
$authService->loginFromCredentials('dummy', 'godzilla');
|
||||
}
|
||||
|
||||
public function testInvalidPassword()
|
||||
{
|
||||
$passwordServiceMock = $this->getPasswordServiceMock();
|
||||
$tokenDaoMock = $this->getTokenDaoMock();
|
||||
$userDaoMock = $this->getUserDaoMock();
|
||||
|
||||
$passwordServiceMock->method('getHash')->willReturn('unmatchingHash');
|
||||
|
||||
$testUser = new \Szurubooru\Entities\User();
|
||||
$testUser->name = 'dummy';
|
||||
$testUser->passwordHash = 'hash';
|
||||
$userDaoMock->expects($this->once())->method('getByName')->willReturn($testUser);
|
||||
|
||||
$authService = new \Szurubooru\Services\AuthService($passwordServiceMock, $tokenDaoMock, $userDaoMock);
|
||||
$this->setExpectedException(\InvalidArgumentException::class, 'Specified password is invalid');
|
||||
$authService->loginFromCredentials('dummy', 'godzilla');
|
||||
}
|
||||
|
||||
public function testValidCredentials()
|
||||
{
|
||||
$passwordServiceMock = $this->getPasswordServiceMock();
|
||||
$tokenDaoMock = $this->getTokenDaoMock();
|
||||
$userDaoMock = $this->getUserDaoMock();
|
||||
|
||||
$tokenDaoMock->expects($this->once())->method('save');
|
||||
$passwordServiceMock->method('getHash')->willReturn('hash');
|
||||
|
||||
$testUser = new \Szurubooru\Entities\User();
|
||||
$testUser->name = 'dummy';
|
||||
$testUser->passwordHash = 'hash';
|
||||
$userDaoMock->expects($this->once())->method('getByName')->willReturn($testUser);
|
||||
|
||||
$authService = new \Szurubooru\Services\AuthService($passwordServiceMock, $tokenDaoMock, $userDaoMock);
|
||||
$authService->loginFromCredentials('dummy', 'godzilla');
|
||||
|
||||
$this->assertTrue($authService->isLoggedIn());
|
||||
$this->assertEquals($testUser, $authService->getLoggedInUser());
|
||||
}
|
||||
|
||||
public function testValidToken()
|
||||
{
|
||||
$passwordServiceMock = $this->getPasswordServiceMock();
|
||||
$tokenDaoMock = $this->getTokenDaoMock();
|
||||
$userDaoMock = $this->getUserDaoMock();
|
||||
|
||||
$testUser = new \Szurubooru\Entities\User();
|
||||
$testUser->id = 5;
|
||||
$testUser->name = 'dummy';
|
||||
$testUser->passwordHash = 'hash';
|
||||
$userDaoMock->expects($this->once())->method('getById')->willReturn($testUser);
|
||||
|
||||
$testToken = new \Szurubooru\Entities\Token();
|
||||
$testToken->name = 'dummy_token';
|
||||
$testToken->additionalData = $testUser->id;
|
||||
$tokenDaoMock->expects($this->once())->method('getByName')->willReturn($testToken);
|
||||
|
||||
$authService = new \Szurubooru\Services\AuthService($passwordServiceMock, $tokenDaoMock, $userDaoMock);
|
||||
$authService->loginFromToken($testToken->name);
|
||||
|
||||
$this->assertTrue($authService->isLoggedIn());
|
||||
$this->assertEquals($testUser, $authService->getLoggedInUser());
|
||||
}
|
||||
|
||||
private function getTokenDaoMock()
|
||||
{
|
||||
return $this->getMockBuilder(\Szurubooru\Dao\TokenDao::class)->disableOriginalConstructor()->getMock();
|
||||
}
|
||||
|
||||
private function getUserDaoMock()
|
||||
{
|
||||
return $this->getMockBuilder(\Szurubooru\Dao\UserDao::class)->disableOriginalConstructor()->getMock();
|
||||
}
|
||||
|
||||
private function getPasswordServiceMock()
|
||||
{
|
||||
return $this->getMockBuilder(\Szurubooru\Services\PasswordService::class)->disableOriginalConstructor()->getMock();
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue