Added proof of concept for authorization system

This commit is contained in:
Marcin Kurczewski 2014-08-30 18:11:32 +02:00
parent ff310f56c5
commit db949dd361
9 changed files with 279 additions and 0 deletions

View file

@ -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
View 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
View 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
View 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
View file

@ -0,0 +1,8 @@
<?php
namespace Szurubooru\Entities;
final class User extends Entity
{
public $name;
public $passwordHash;
}

View file

@ -0,0 +1,14 @@
<?php
namespace Szurubooru\Helpers;
final class InputReader
{
public function __construct()
{
}
public function __get($key)
{
return null;
}
}

View 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;
}
}

View 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);
}
}

View 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();
}
}