diff --git a/.gitignore b/.gitignore index f0ea302..d14f26b 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ node_modules/ composer.lock npm-debug.log build/ +localsettings.php +.idea/** diff --git a/.idea/dictionaries/nohponex.xml b/.idea/dictionaries/nohponex.xml new file mode 100644 index 0000000..4bab161 --- /dev/null +++ b/.idea/dictionaries/nohponex.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..84462d2 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..3b31283 --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/.idea/php.xml b/.idea/php.xml new file mode 100644 index 0000000..4f0611e --- /dev/null +++ b/.idea/php.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Authentication/Authentication.php b/Authentication/Authentication.php new file mode 100644 index 0000000..f060585 --- /dev/null +++ b/Authentication/Authentication.php @@ -0,0 +1,34 @@ + + * @since 1.0.0 + */ +abstract class Authentication +{ + public abstract function __invoke( + ServerRequestInterface $request, + ResponseInterface $response, + callable $next + ) : ResponseInterface; +} diff --git a/Authentication/Manager.php b/Authentication/Manager.php new file mode 100644 index 0000000..db4d2e4 --- /dev/null +++ b/Authentication/Manager.php @@ -0,0 +1,81 @@ + + * @since 1.0.0 + */ +final class Manager +{ + /** + * @var callable + */ + protected static $userSessionCallback; + + /** + * Set method callback used to fetch a user by his unique identity + * @param callable $callback The callback should accept string $identity + * and return a UserSession object or null if user by this identity is not found. + * Returned object's password will be used to verify user's password + * against the provided password, password must be stored in a supported method + * in order Authentication methods to be able to use it. The use of + * password_hash is suggested for compatibility across all implementations. + * + */ + public static function setUserSessionCallback(callable $callback) + { + static::$userSessionCallback = $callback; + } + + /** + * @param string $identity + * @return UserSession|null + */ + public static function callUserSessionCallback(string $identity) + { + if (static::$userSessionCallback === null) { + //return an empty callable with return value null + return null; + } + + $callback = static::$userSessionCallback; + + return $callback($identity); + } + + /** + * Store session attribute containing the UserSession object at request + * @param ServerRequestInterface $request + * @param UserSession $session + * @return ServerRequestInterface + */ + public static function storeAttributes( + ServerRequestInterface $request, + UserSession $session + ) : ServerRequestInterface { + //Clear password for increased security against data leakage + $session->clearPassword(); + + //Add userSession object to session attribute + return $request->withAttribute('session', $session); + } +} diff --git a/Authentication/UserSession.php b/Authentication/UserSession.php new file mode 100644 index 0000000..a959389 --- /dev/null +++ b/Authentication/UserSession.php @@ -0,0 +1,95 @@ + + * @since 1.0.0 + */ +class UserSession +{ + /** + * @var string + */ + protected $id; + + /** + * @var string|null + */ + protected $password; + + /** + * @var string + */ + protected $level; + + /** + * @var \stdClass + */ + protected $attributes; + + public function __construct( + string $id, + string $password, + string $level = null, + \stdClass $attributes = null + ) { + $this->id = $id; + $this->password = $password; + $this->level = $level; + $this->attributes = $attributes ?? new \stdClass(); + } + + /** + * @return string + */ + public function getId() : string + { + return $this->id; + } + + /** + * @return string|null + */ + public function getPassword() + { + return $this->password; + } + + /** + * @return string|null + */ + public function getLevel() + { + return $this->level; + } + + /** + * @return \stdClass + */ + public function getAttributes() : \stdClass + { + return $this->attributes; + } + + public function clearPassword() + { + $this->password = null; + } + +} diff --git a/NOTICE b/NOTICE index 43e68ae..be8efb0 100644 --- a/NOTICE +++ b/NOTICE @@ -1,2 +1,2 @@ phramework/basic-authentication -Copyright 2015 Xenofon Spafaridis +Copyright 2015-2016 Xenofon Spafaridis diff --git a/README.md b/README.md index 5c38205..4d25272 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# jwt +# phramework/basic-authentication Basic authentication implementation for phramework [![Build Status](https://travis-ci.org/phramework/basic-authentication.svg?branch=master)](https://travis-ci.org/phramework/basic-authentication) @@ -40,7 +40,7 @@ composer test ``` # License -Copyright 2015 Xenofon Spafaridis +Copyright 2015-2016 Xenofon Spafaridis Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at diff --git a/composer.json b/composer.json index 7aca9fa..b77f5a1 100644 --- a/composer.json +++ b/composer.json @@ -10,9 +10,8 @@ "homepage": "https://nohponex.gr" }], "require": { - "php": ">=5.6", - "phramework/phramework": "1.*", - "ext-json": "*" + "php": ">= 7", + "psr/http-message": "^1.0" }, "require-dev": { "squizlabs/php_codesniffer": "*", @@ -22,6 +21,7 @@ "prefer-stable": true, "autoload": { "psr-4": { + "Phramework\\Authentication\\": "Authentication", "Phramework\\Authentication\\BasicAuthentication\\": "src" } }, diff --git a/src/BasicAuthentication.php b/src/BasicAuthentication.php index 6c0419a..6b901cc 100644 --- a/src/BasicAuthentication.php +++ b/src/BasicAuthentication.php @@ -1,6 +1,6 @@ * @uses password_verify to verify user's password - * + * @since 1.0.0 */ -class BasicAuthentication implements \Phramework\Authentication\IAuthentication +class BasicAuthentication extends Authentication { - /** - * Test if current request holds authorization data - * @param array $params Request parameters - * @param string $method Request method - * @param array $headers Request headers - * @return boolean + * Middleware handler + * @param ServerRequestInterface $request + * @param ResponseInterface $response + * @param callable $next + * @return ResponseInterface */ - public function testProvidedMethod($params, $method, $headers) - { - if (!isset($headers['Authorization'])) { - return false; + public function __invoke( + ServerRequestInterface $request, + ResponseInterface $response, + callable $next + ) : ResponseInterface { + $test = static::extractAuthentication($request); + + if ($test === null) { + goto ret; } - list($token) = sscanf($headers['Authorization'], 'Basic %s'); + list($identity, $password) = $test; - if (!$token) { - return false; - } - - return true; - } + $userSession = Manager::callUserSessionCallback($identity); - /** - * @param array $params Request parameters - * @param string $method Request method - * @param array $headers Request headers - * @return object|FALSE Returns false on error or the user object on success - */ - public function check($params, $method, $headers) - { - if (!isset($headers['Authorization'])) { - return false; + if ($userSession === null) { + goto ret; } - list($token) = sscanf($headers['Authorization'], 'Basic %s'); - - if (!$token) { - return false; + if (!password_verify($password, $userSession->getPassword())) { + goto ret; } - $tokenDecoded = base64_decode($token); - - $tokenParts = explode(':', $tokenDecoded); + //Store attribute at request + $request = Manager::storeAttributes($request, $userSession); - if (count($tokenParts) != 2) { - return false; - } - - $email = \Phramework\Validate\EmailValidator::parseStatic($tokenParts[0]); - $password = $tokenParts[1]; - - list($user) = $this->authenticate( - [ - 'email' => $email, - 'password' => $password, - ], - $method, - $headers - ); - - if ($user !== false && ($callback = Manager::getOnCheckCallback()) !== null) { - call_user_func( - $callback, - $user - ); - } - - return $user; + ret: + return $next($request, $response); } /** - * Authenticate a user using JWT authentication method - * @param array $params Request parameters - * @param string $method Request method - * @param array $headers Request headers - * @return false|array Returns false on failure + * Extract identity and password from request + * @param ServerRequestInterface $request + * @return string[]|null On success returns [$identity, $password] else null */ - public function authenticate($params, $method, $headers) - { - $email = \Phramework\Validate\EmailValidator::parseStatic($params['email']); - $password = $params['password']; + protected static function extractAuthentication( + ServerRequestInterface $request + ) { + $header = $request->getHeader('Authorization'); - $user = call_user_func(Manager::getUserGetByEmailMethod(), $email); + foreach ($header as $line) { + list($token) = sscanf($line, 'Basic %s'); - if (!$user) { - return false; - } + if (!$token) { + continue; + } - if (!password_verify($password, $user['password'])) { - return false; - } + $tokenDecoded = base64_decode($token); + $tokenParts = explode(':', $tokenDecoded); - /* - * Create the token as an array - */ - $data = [ - 'id' => $user['id'] - ]; - - //copy user attributes to jwt's data - foreach (Manager::getAttributes() as $attribute) { - if (!isset($user[$attribute])) { - throw new \Phramework\Exceptions\ServerException(sprintf( - 'Attribute "%s" is not set in user object', - $attribute - )); + if (count($tokenParts) !== 2) { + continue; } - $data[$attribute] = $user[$attribute]; - } - //Convert to object - $data = (object)$data; + /*$identity = $tokenParts[0]; + $password = $tokenParts[1];*/ - //Call onAuthenticate callback if set - if (($callback = Manager::getOnAuthenticateCallback()) !== null) { - call_user_func( - $callback, - $data - ); + return $tokenParts; } - return [$data]; + return null; } } diff --git a/tests/src/BasicAuthenticationTest.php b/tests/src/BasicAuthenticationTest.php index 1b86cb9..64b54d2 100644 --- a/tests/src/BasicAuthenticationTest.php +++ b/tests/src/BasicAuthenticationTest.php @@ -1,269 +1,223 @@ $users[0]['email'] + ] + ); } - /** - * @var BasicAuthentication - */ - private $object; - /** - * Sets up the fixture, for example, opens a network connection. - * This method is called before a test is executed. - */ - protected function setUp() + public static function setUpBeforeClass() { - $this->object = new BasicAuthentication(); //NOTE, in order testAuthenticateSuccess to work all users must //have this password self::$users = [ [ - 'id' => 1, - 'email' => 'nohponex@gmail.com', - 'password' => password_hash('123456', PASSWORD_BCRYPT), + 'id' => '1', + 'email' => 'nohponex@gmail.com', + 'password' => password_hash('123456', PASSWORD_BCRYPT), 'user_type' => 'user' ], [ - 'id' => 2, - 'email' => 'xenofon@auth.gr', - 'password' => password_hash('123456', PASSWORD_BCRYPT), + 'id' => '2', + 'email' => 'nohponex+json@gmail.com', + 'password' => password_hash('123456', PASSWORD_BCRYPT), 'user_type' => 'moderator' ], ]; - //Initliaze Phramework - $phramework = new Phramework( - [], - (new \Phramework\URIStrategy\URITemplate([])) - ); - - //Set authentication class - \Phramework\Authentication\Manager::register( - BasicAuthentication::class - ); - //Set method to fetch user object, including password attribute - \Phramework\Authentication\Manager::setUserGetByEmailMethod( + Manager::setUserSessionCallback( [BasicAuthenticationTest::class, 'getByEmailWithPassword'] ); - - \Phramework\Authentication\Manager::setAttributes( - ['user_type', 'email'] - ); - - \Phramework\Authentication\Manager::setOnCheckCallback( - /** - * @param object $data User data object - */ - function ($params) { - //var_dump($params); - } - ); - - \Phramework\Authentication\Manager::setOnAuthenticateCallback( - /** - * @param object $data User data object - */ - function ($data) { - //var_dump($params); - } - ); } - /** - * Tears down the fixture, for example, closes a network connection. - * This method is called after a test is executed. - */ - protected function tearDown() + public function setUp() { - + $this->request = new ServerRequest('GET', 'http://localhost/'); + $this->response = new Response(); } /** - * @covers Phramework\Authentication\BasicAuthentication\BasicAuthentication::check + * @covers ::__invoke */ - public function testCheckFailure() + public function testInvokeNotSet() { - $this->assertFalse($this->object->check( - [], - Phramework::METHOD_GET, - [] - ), 'Expect false, since Authorization header is not provided'); - - $this->assertFalse($this->object->check( - [], - Phramework::METHOD_GET, - ['Authorization' => 'Bearer ABCDEF'] - ), 'Expect false, since Authorization header is not Basic'); - - $this->assertFalse($this->object->check( - [], - Phramework::METHOD_GET, - ['Authorization' => 'Basic fsdfser43gfdgdfgdfgdfgdf'] - ), 'Expect false, since token makes no sense'); - - $this->assertFalse($this->object->check( - [], - Phramework::METHOD_GET, - [ - 'Authorization' => 'Basic zm9ocG9uZXsg6MTIzNDU2Nzh4WA==' - ] - ), 'Expect false, since token is not correct'); + $next = function ( + ServerRequestInterface $request, + ResponseInterface $response + ) { + static::assertNull( + $request->getAttribute('session') + ); + + return $response; + }; + + $authentication = new BasicAuthentication(); + + $authentication( + $this->request, + $this->response, + $next + ); } - /** - * @covers Phramework\Authentication\BasicAuthentication\BasicAuthentication::testProvidedMethod - */ - public function testTestProvidedMethodFailure() + public function invokeInvalidOrUnrelated() { - $this->assertFalse($this->object->testProvidedMethod( - [], - Phramework::METHOD_GET, - [] - ), 'Expect false, since Authorization header is not provided'); - - $this->assertFalse($this->object->testProvidedMethod( - [], - Phramework::METHOD_GET, - ['Authorization' => 'Bearer ABCDEF'] - ), 'Expect false, since Authorization header is not Basic'); + return [ + ['ABCD xxxx'], //doesn't start with basic + ['Basic ' . base64_encode('xxxyyyzzz')], //not with two parts + ['Basic ' . base64_encode('some@mail.com:password')], //not with two parts + ['Basic ' . base64_encode('nohponex@gmail.com:xxx')] //wrong password + ]; } /** - * @covers Phramework\Authentication\BasicAuthentication\BasicAuthentication::testProvidedMethod + * @covers ::__invoke + * @dataProvider invokeInvalidOrUnrelated */ - public function testTestProvidedMethodSuccess() + public function testInvokeInvalidOrUnrelated(string $header) { - $this->assertTrue($this->object->testProvidedMethod( - [], - Phramework::METHOD_GET, - ['Authorization' => 'Basic zm9ocG9uZXsg6MTIzNDU2Nzh4WA=='] - ), 'Expect true, even though credentials are not correct'); + $next = function ( + ServerRequestInterface $request, + ResponseInterface $response + ) { + static::assertNull($request->getAttribute('session')); + + return $response; + }; + + $authentication = new BasicAuthentication(); + + $authentication( + $this->request + ->withHeader('Authorization', $header), + $this->response, + $next + ); } - /** - * @covers Phramework\Authentication\BasicAuthentication\BasicAuthentication::authenticate - * @expectedException Exception - */ - public function testAuthenticateExpectException() + public function invokeSuccess() { - $this->object->authenticate( - [ - 'email' => 'wrongEmailType', - 'password' => '123456' - ], - [Phramework::METHOD_POST], - [] - ); + return [ + ['Basic ' . base64_encode('nohponex+json@gmail.com:123456'), '2'], + ['Basic ' . base64_encode('nohponex@gmail.com:123456'), '1'] + ]; } /** - * @covers Phramework\Authentication\BasicAuthentication\BasicAuthentication::authenticate + * @covers ::__invoke + * @dataProvider invokeSuccess */ - public function testAuthenticateFailure() + public function testInvokeSuccess(string $header, string $identity) { - $this->assertFalse( - $this->object->authenticate( - [ - 'email' => 'not@found.com', - 'password' => '123456' - ], - Phramework::METHOD_POST, - [] - ), - 'Expect false, sinse user email doesn`t exist' - ); - - $this->assertFalse( - $this->object->authenticate( - [ - 'email' => self::$users[0]['email'], - 'password' => 'wrong' - ], - Phramework::METHOD_POST, - [] - ), - 'Expect false, sinse user password is wrong' + $next = function ( + ServerRequestInterface $request, + ResponseInterface $response + ) use ($identity) { + $session = $request->getAttribute('session'); + + static::assertInstanceOf(UserSession::class, $session); + + static::assertSame( + $identity, + $session->getId() + ); + + //defined by our getByEmailWithPassword + static::assertObjectHasAttribute( + 'email', + $session->getAttributes() + ); + + return $response; + }; + + $authentication = new BasicAuthentication(); + + $authentication( + $this->request + ->withHeader('Authorization', $header), + $this->response, + $next ); } /** - * @covers Phramework\Authentication\BasicAuthentication\BasicAuthentication::authenticate + * @covers ::extractAuthentication + * @dataProvider invokeInvalidOrUnrelated */ - public function testAuthenticateSuccess() + public function testInvoke(string $header) { - //Pick a random user index - $index = 0; //rand(0, count(self::$users) - 1); - - list($user) = $this->object->authenticate( - [ - 'email' => self::$users[$index]['email'], - 'password' => '123456' //Since password is the same for all of them - ], - Phramework::METHOD_POST, - [] - ); - - $this->assertInternalType('object', $user, 'Expect an object'); - - $this->assertObjectHasAttribute('id', $user); - $this->assertObjectHasAttribute('email', $user); - $this->assertObjectHasAttribute('user_type', $user); - $this->assertObjectNotHasAttribute('password', $user); - - $this->assertSame(self::$users[$index]['id'], $user->id); - $this->assertSame(self::$users[$index]['email'], $user->email); - $this->assertSame(self::$users[$index]['user_type'], $user->user_type); + return $this->testInvokeInvalidOrUnrelated($header); } - /** - * @covers Phramework\Authentication\BasicAuthentication\BasicAuthentication::check + * @covers ::extractAuthentication + * @dataProvider invokeSuccess */ - public function testCheckSuccess() + public function testMethodSuccess(string $header, string $identity) { - $index = 0; - - $user = \Phramework\Authentication\Manager::check( - [], - Phramework::METHOD_GET, - [ - 'Authorization' => 'Basic ' . base64_encode( - self::$users[$index]['email'] . ':' . '123456' - ) - ] - ); - - $this->assertInternalType('object', $user, 'Expect an object'); - - $this->assertObjectHasAttribute('id', $user); - $this->assertObjectHasAttribute('email', $user); - $this->assertObjectHasAttribute('user_type', $user); - $this->assertObjectNotHasAttribute('password', $user); - - $this->assertSame(self::$users[$index]['id'], $user->id); - $this->assertSame(self::$users[$index]['email'], $user->email); - $this->assertSame(self::$users[$index]['user_type'], $user->user_type); + return $this->testInvokeSuccess($header, $identity); } }