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