From db949dd361260c5411c2fe071ca0af4b0565e99c Mon Sep 17 00:00:00 2001 From: Marcin Kurczewski Date: Sat, 30 Aug 2014 18:11:32 +0200 Subject: [PATCH] Added proof of concept for authorization system --- src/Controllers/AuthController.php | 10 ++++ src/Dao/TokenDao.php | 21 +++++++ src/Dao/UserDao.php | 16 +++++ src/Entities/Token.php | 11 ++++ src/Entities/User.php | 8 +++ src/Helpers/InputReader.php | 14 +++++ src/Services/AuthService.php | 87 +++++++++++++++++++++++++++ src/Services/PasswordService.php | 17 ++++++ tests/Services/AuthServiceTest.php | 95 ++++++++++++++++++++++++++++++ 9 files changed, 279 insertions(+) create mode 100644 src/Dao/TokenDao.php create mode 100644 src/Dao/UserDao.php create mode 100644 src/Entities/Token.php create mode 100644 src/Entities/User.php create mode 100644 src/Helpers/InputReader.php create mode 100644 src/Services/AuthService.php create mode 100644 src/Services/PasswordService.php create mode 100644 tests/Services/AuthServiceTest.php diff --git a/src/Controllers/AuthController.php b/src/Controllers/AuthController.php index cacbfbef..ad7acfe9 100644 --- a/src/Controllers/AuthController.php +++ b/src/Controllers/AuthController.php @@ -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(); } } diff --git a/src/Dao/TokenDao.php b/src/Dao/TokenDao.php new file mode 100644 index 00000000..fa1da838 --- /dev/null +++ b/src/Dao/TokenDao.php @@ -0,0 +1,21 @@ +collection->findOne(['name' => $tokenName]); + return $this->entityConverter->toEntity($arrayEntity); + } + + public function deleteByName($tokenName) + { + $this->collection->remove(['name' => $tokenName]); + } +} diff --git a/src/Dao/UserDao.php b/src/Dao/UserDao.php new file mode 100644 index 00000000..6b04cff0 --- /dev/null +++ b/src/Dao/UserDao.php @@ -0,0 +1,16 @@ +collection->findOne(['name' => $userName]); + return $this->entityConverter->toEntity($arrayEntity); + } +} diff --git a/src/Entities/Token.php b/src/Entities/Token.php new file mode 100644 index 00000000..138af4cc --- /dev/null +++ b/src/Entities/Token.php @@ -0,0 +1,11 @@ +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; + } +} diff --git a/src/Services/PasswordService.php b/src/Services/PasswordService.php new file mode 100644 index 00000000..fdbf6b7e --- /dev/null +++ b/src/Services/PasswordService.php @@ -0,0 +1,17 @@ +config = $config; + } + + public function getHash($password) + { + return hash('sha256', $this->config->security->secret . '/' . $password); + } +} diff --git a/tests/Services/AuthServiceTest.php b/tests/Services/AuthServiceTest.php new file mode 100644 index 00000000..966f34b9 --- /dev/null +++ b/tests/Services/AuthServiceTest.php @@ -0,0 +1,95 @@ +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(); + } +}