diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..de1a9ba2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/vendor/ +/composer.lock +/docs/build/ \ No newline at end of file diff --git a/build/phpunit.xml b/build/phpunit.xml index 4181c278..3a5b9f99 100644 --- a/build/phpunit.xml +++ b/build/phpunit.xml @@ -8,9 +8,12 @@ stopOnIncomplete="false" stopOnSkipped="false"> - + ../tests/authentication + + ../tests/resource + @@ -19,11 +22,8 @@ - + - + \ No newline at end of file diff --git a/src/Oauth2/Authentication/Server.php b/src/Oauth2/Authentication/Server.php index 1e6ee1f5..733b51e3 100644 --- a/src/Oauth2/Authentication/Server.php +++ b/src/Oauth2/Authentication/Server.php @@ -513,6 +513,6 @@ class Server unset($args[0]); $params = array_values($args); - return call_user_func_array(array($this->db, $method), $args); + return call_user_func_array(array($this->db, $method), $params); } } diff --git a/src/Oauth2/Resource/Database.php b/src/Oauth2/Resource/Database.php index c39bb471..9c5d1b44 100644 --- a/src/Oauth2/Resource/Database.php +++ b/src/Oauth2/Resource/Database.php @@ -4,4 +4,56 @@ namespace Oauth2\Resource; interface Database { + /** + * Validate an access token and return the session details. + * + * Database query: + * + * + * SELECT id, owner_type, owner_id FROM oauth_sessions WHERE access_token = + * $accessToken AND stage = 'granted' AND + * access_token_expires > UNIX_TIMESTAMP(now()) + * + * + * Response: + * + * + * Array + * ( + * [id] => (int) The session ID + * [owner_type] => (string) The session owner type + * [owner_id] => (string) The session owner's ID + * ) + * + * + * @param string $accessToken The access token + * @return array|bool Return an array on success or false on failure + */ + public function validateAccessToken($accessToken); + + /** + * Returns the scopes that the session is authorised with. + * + * Database query: + * + * + * SELECT scope FROM oauth_session_scopes WHERE access_token = + * '291dca1c74900f5f252de351e0105aa3fc91b90b' + * + * + * Response: + * + * + * Array + * ( + * [0] => (string) A scope + * [1] => (string) Another scope + * ... + * ) + * + * + * @param int $sessionId The session ID + * @return array A list of scopes + */ + public function sessionScopes($sessionId); } \ No newline at end of file diff --git a/src/Oauth2/Resource/Server.php b/src/Oauth2/Resource/Server.php index 15d62fa1..ab4626c3 100644 --- a/src/Oauth2/Resource/Server.php +++ b/src/Oauth2/Resource/Server.php @@ -2,7 +2,223 @@ namespace Oauth2\Resource; +class OAuthResourceServerException extends \Exception +{ + +} + class Server { + /** + * Reference to the database abstractor + * @var object + */ + private $_db = null; + + /** + * The access token. + * @access private + */ + private $_accessToken = null; + + /** + * The scopes the access token has access to. + * @access private + */ + private $_scopes = array(); + + /** + * The type of owner of the access token. + * @access private + */ + private $_type = null; + + /** + * The ID of the owner of the access token. + * @access private + */ + private $_typeId = null; + + /** + * Server configuration + * @var array + */ + private $_config = array( + 'token_key' => 'oauth_token' + ); + + /** + * Error codes. + * + * To provide i8ln errors just overwrite the keys + * + * @var array + */ + public $errors = array( + 'missing_access_token' => 'An access token was not presented with the request', + 'invalid_access_token' => 'The access token is not registered with the resource server' + ); + + /** + * Constructor + * + * @access public + * @return void + */ + public function __construct($options = null) + { + if ($options !== null) { + $this->config = array_merge($this->config, $options); + } + } + + /** + * Magic method to test if access token represents a particular owner type + * @param string $method The method name + * @param mixed $arguements The method arguements + * @return bool If method is valid, and access token is owned by the requested party then true, + */ + public function __call($method, $arguements = null) + { + if (substr($method, 0, 2) === 'is') { + + if ($this->_type === strtolower(substr($method, 2))) { + return $this->_typeId; + } + + return false; + } + + trigger_error('Call to undefined function ' . $method . '()'); + } + + /** + * Register a database abstrator class + * + * @access public + * @param object $db A class that implements OAuth2ServerDatabase + * @return void + */ + public function registerDbAbstractor($db) + { + $this->_db = $db; + } + /** + * Init function + * + * @access public + * @return void + */ + public function init() + { + $accessToken = null; + + // Try and get the access token via an access_token or oauth_token parameter + switch ($server['REQUEST_METHOD']) + { + case 'POST': + $accessToken = isset($_POST[$this->_config['token_key']]) ? $_POST[$this->_config['token_key']] : null; + break; + + default: + $accessToken = isset($_GET[$this->_config['token_key']]) ? $_GET[$this->_config['token_key']] : null; + break; + } + + // Try and get an access token from the auth header + $headers = getallheaders(); + if (isset($headers['Authorization'])) { + + $rawToken = trim(str_replace('Bearer', '', $headers['Authorization'])); + if ( ! empty($rawToken)) + { + $accessToken = base64_decode($rawToken); + } + } + + if ($accessToken) { + + $result = $this->_dbCall('validateAccessToken', array($accessToken)); + + if ($result === false) + { + throw new OAuthResourceServerException($this->errors['invalid_access_token']); + } + + else + { + $this->_accessToken = $accessToken; + $this->_type = $result['owner_type']; + $this->_typeId = $result['owner_id']; + + // Get the scopes + $this->_scopes = $this->_dbCall('sessionScopes', array($result['id'])); + } + + } else { + + throw new OAuthResourceServerException($this->errors['missing_access_token']); + + } + } + + /** + * Test if the access token has a specific scope + * + * @param mixed $scopes Scope(s) to check + * + * @access public + * @return string|bool + */ + public function hasScope($scopes) + { + if (is_string($scopes)) + { + if (in_array($scopes, $this->_scopes)) + { + return true; + } + + return false; + } + + elseif (is_array($scopes)) + { + foreach ($scopes as $scope) + { + if ( ! in_array($scope, $this->_scopes)) + { + return false; + } + } + + return true; + } + + return false; + } + + /** + * Call database methods from the abstractor + * + * @return mixed The query result + */ + private function _dbCall() + { + if ($this->_db === null) { + throw new OAuthResourceServerException('No registered database abstractor'); + } + + if ( ! $this->_db instanceof Database) { + throw new OAuthResourceServerException('Registered database abstractor is not an instance of Oauth2\Resource\Database'); + } + + $args = func_get_args(); + $method = $args[0]; + unset($args[0]); + $params = array_values($args); + + return call_user_func_array(array($this->_db, $method), $params); + } } \ No newline at end of file diff --git a/tests/resource/database_mock.php b/tests/resource/database_mock.php new file mode 100644 index 00000000..15f9dc28 --- /dev/null +++ b/tests/resource/database_mock.php @@ -0,0 +1,29 @@ + array( + 'id' => 1, + 'owner_type' => 'user', + 'owner_id' => 123 + )); + + private $sessionScopes = array( + 1 => array( + 'foo', + 'bar' + ) + ); + + public function validateAccessToken($accessToken) + { + return (isset($this->accessTokens[$accessToken])) ? $this->accessTokens[$accessToken] : false; + } + + public function sessionScopes($sessionId) + { + return (isset($this->sessionScopes[$sessionId])) ? $this->sessionScopes[$sessionId] : array(); + } +} \ No newline at end of file diff --git a/tests/resource/server_test.php b/tests/resource/server_test.php new file mode 100644 index 00000000..1d91536e --- /dev/null +++ b/tests/resource/server_test.php @@ -0,0 +1,77 @@ +server = new Oauth2\Resource\Server(); + $this->db = new ResourceDB(); + + $this->server->registerDbAbstractor($this->db); + } + + function test_init_POST() + { + $_POST['oauth_token'] = 'test12345'; + + $this->server->init(); + + $this->assertEquals($this->server->_accessToken, $_POST['oauth_token']); + $this->assertEquals($this->server->_type, 'user'); + $this->assertEquals($this->server->_typeId, 123); + $this->assertEquals($this->server->_scopes, array('foo', 'bar')); + } + + function test_init_GET() + { + $_GET['oauth_token'] = 'test12345'; + + $this->server->init(); + + $this->assertEquals($this->server->_accessToken, $_GET['oauth_token']); + $this->assertEquals($this->server->_type, 'user'); + $this->assertEquals($this->server->_typeId, 123); + $this->assertEquals($this->server->_scopes, array('foo', 'bar')); + } + + function test_init_header() + { + // Test with authorisation header + } + + /** + * @exception OAuthResourceServerException + */ + function test_init_wrongToken() + { + $_POST['access_token'] = 'test12345'; + + $this->server->init(); + } + + function test_hasScope() + { + $_POST['oauth_token'] = 'test12345'; + + $this->server->init(); + + $this->assertEquals(true, $this->server->hasScope('foo')); + $this->assertEquals(true, $this->server->hasScope('bar')); + $this->assertEquals(true, $this->server->hasScope(array('foo', 'bar'))); + + $this->assertEquals(false, $this->server->hasScope('foobar')); + $this->assertEquals(false, $this->server->hasScope(array('foobar'))); + } + + function test___call() + { + $_POST['oauth_token'] = 'test12345'; + + $this->server->init(); + + $this->assertEquals(123, $this->server->isUser()); + $this->assertEquals(false, $this->server->isMachine()); + } + +} \ No newline at end of file