diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..044880fd --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/vendor/ +/composer.lock +/docs/build/ +/build/logs/ +/build/coverage/ \ No newline at end of file diff --git a/README.md b/README.md index d2f4bc60..a1f48c37 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,43 @@ -# PHP OAuth server +# PHP OAuth Framework -The goal of this project is to develop a standards compliant [OAuth 2](http://tools.ietf.org/wg/oauth/draft-ietf-oauth-v2/) server that supports a number of different authentication flows, and two extensions, [JSON web tokens](http://tools.ietf.org/wg/oauth/draft-ietf-oauth-json-web-token/) and [SAML assertions](http://tools.ietf.org/wg/oauth/draft-ietf-oauth-saml2-bearer/). +The goal of this project is to develop a standards compliant [OAuth 2](http://tools.ietf.org/wg/oauth/draft-ietf-oauth-v2/) authentication server, resource server and client library with support for a major OAuth 2 providers. -The library will be a [composer](http://getcomposer.org/) package and will be framework agnostic. +## Package Installation -This code will be developed as part of the [Linkey](http://linkey.blogs.lincoln.ac.uk) project which has been funded by [JISC](http://jisc.ac.uk) under the access and identity management programme. \ No newline at end of file +The framework is provided as a Composer package which can be installed by adding the package to your composer.json file: + +```javascript +{ + "require": { + "lncd\Oauth2": "*" + } +} +``` + +## Package Integration + +Check out the [wiki](https://github.com/lncd/OAuth2/wiki) + +## Current Features + +### Authentication Server + +The authentication server is a flexible class that supports the standard authorization code grant. + +### Resource Server + +The resource server allows you to secure your API endpoints by checking for a valid OAuth access token in the request and ensuring the token has the correct permission to access resources. + + + + +## Future Goals + +### Authentication Server + +* Support for [JSON web tokens](http://tools.ietf.org/wg/oauth/draft-ietf-oauth-json-web-token/). +* Support for [SAML assertions](http://tools.ietf.org/wg/oauth/draft-ietf-oauth-saml2-bearer/). + +--- + +This code will be developed as part of the [Linkey](http://linkey.blogs.lincoln.ac.uk) project which has been funded by [JISC](http://jisc.ac.uk) under the Access and Identity Management programme. \ No newline at end of file diff --git a/build.xml b/build.xml new file mode 100644 index 00000000..8008f502 --- /dev/null +++ b/build.xml @@ -0,0 +1,142 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project name="PHP OAuth 2.0 Server" default="build"> + + <target name="build" depends="prepare,lint,phploc,pdepend,phpmd-ci,phpcs-ci,phpcpd,composer,phpunit,phpdox,phpcb"/> + + <target name="build-parallel" depends="prepare,lint,tools-parallel,phpcb"/> + + <target name="minimal" depends="prepare,lint,phploc,pdepend,phpcpd,composer,phpunit,phpdox,phpcb" /> + + <target name="tools-parallel" description="Run tools in parallel"> + <parallel threadCount="2"> + <sequential> + <antcall target="pdepend"/> + <antcall target="phpmd-ci"/> + </sequential> + <antcall target="phpcpd"/> + <antcall target="phpcs-ci"/> + <antcall target="phploc"/> + <antcall target="phpdox"/> + </parallel> + </target> + + <target name="clean" description="Cleanup build artifacts"> + <delete dir="${basedir}/build/api"/> + <delete dir="${basedir}/build/code-browser"/> + <delete dir="${basedir}/build/coverage"/> + <delete dir="${basedir}/build/logs"/> + <delete dir="${basedir}/build/pdepend"/> + </target> + + <target name="prepare" depends="clean" description="Prepare for build"> + <mkdir dir="${basedir}/build/api"/> + <mkdir dir="${basedir}/build/code-browser"/> + <mkdir dir="${basedir}/build/coverage"/> + <mkdir dir="${basedir}/build/logs"/> + <mkdir dir="${basedir}/build/pdepend"/> + <mkdir dir="${basedir}/build/phpdox"/> + </target> + + <target name="lint"> + <apply executable="php" failonerror="true"> + <arg value="-l" /> + + <fileset dir="${basedir}/src"> + <include name="**/*.php" /> + <modified /> + </fileset> + </apply> + </target> + + <target name="phploc" description="Measure project size using PHPLOC"> + <exec executable="phploc"> + <arg value="--log-csv" /> + <arg value="${basedir}/build/logs/phploc.csv" /> + <arg path="${basedir}/src" /> + </exec> + </target> + + <target name="pdepend" description="Calculate software metrics using PHP_Depend"> + <exec executable="pdepend"> + <arg value="--jdepend-xml=${basedir}/build/logs/jdepend.xml" /> + <arg value="--jdepend-chart=${basedir}/build/pdepend/dependencies.svg" /> + <arg value="--overview-pyramid=${basedir}/build/pdepend/overview-pyramid.svg" /> + <arg path="${basedir}/src" /> + </exec> + </target> + + <target name="phpmd" description="Perform project mess detection using PHPMD and print human readable output. Intended for usage on the command line before committing."> + <exec executable="phpmd"> + <arg path="${basedir}/src" /> + <arg value="text" /> + <arg value="${basedir}/build/phpmd.xml" /> + </exec> + </target> + + <target name="phpmd-ci" description="Perform project mess detection using PHPMD creating a log file for the continuous integration server"> + <exec executable="phpmd"> + <arg path="${basedir}/src" /> + <arg value="xml" /> + <arg value="${basedir}/build/phpmd.xml" /> + <arg value="--reportfile" /> + <arg value="${basedir}/build/logs/pmd.xml" /> + </exec> + </target> + + <target name="phpcs" description="Find coding standard violations using PHP_CodeSniffer and print human readable output. Intended for usage on the command line before committing."> + <exec executable="phpcs"> + <arg value="--standard=${basedir}/build/phpcs.xml" /> + <arg value="--extensions=php" /> + <arg value="--ignore=third_party/CIUnit" /> + <arg path="${basedir}/src" /> + </exec> + </target> + + <target name="phpcs-ci" description="Find coding standard violations using PHP_CodeSniffer creating a log file for the continuous integration server"> + <exec executable="phpcs" output="/dev/null"> + <arg value="--report=checkstyle" /> + <arg value="--report-file=${basedir}/build/logs/checkstyle.xml" /> + <arg value="--standard=${basedir}/build/phpcs.xml" /> + <arg value="--extensions=php" /> + <arg value="--ignore=third_party/CIUnit" /> + <arg path="${basedir}/src" /> + </exec> + </target> + + <target name="phpcpd" description="Find duplicate code using PHPCPD"> + <exec executable="phpcpd"> + <arg value="--log-pmd" /> + <arg value="${basedir}/build/logs/pmd-cpd.xml" /> + <arg path="${basedir}/src" /> + </exec> + </target> + + <target name="composer" description="Install Composer requirements"> + <exec executable="composer.phar" failonerror="true"> + <arg value="install" /> + <arg value="--dev" /> + </exec> + </target> + + <target name="phpunit" description="Run unit tests with PHPUnit"> + <exec executable="${basedir}/vendor/bin/phpunit" failonerror="true"> + <arg value="--configuration" /> + <arg value="${basedir}/build/phpunit.xml" /> + </exec> + </target> + + <target name="phpdox" description="Generate API documentation using phpDox"> + <exec executable="phpdox"/> + </target> + + <target name="phpcb" description="Aggregate tool output with PHP_CodeBrowser"> + <exec executable="phpcb"> + <arg value="--log" /> + <arg path="${basedir}/build/logs" /> + <arg value="--source" /> + <arg path="${basedir}/src" /> + <arg value="--output" /> + <arg path="${basedir}/build/code-browser" /> + </exec> + </target> +</project> \ No newline at end of file diff --git a/build/phpcs.xml b/build/phpcs.xml new file mode 100644 index 00000000..a6ee80da --- /dev/null +++ b/build/phpcs.xml @@ -0,0 +1,8 @@ +<?xml version="1.0"?> +<ruleset name="PHP_CodeSniffer"> + + <description>PHP_CodeSniffer configuration</description> + + <rule ref="PSR2"/> + +</ruleset> \ No newline at end of file diff --git a/build/phpmd.xml b/build/phpmd.xml new file mode 100644 index 00000000..11f54dc1 --- /dev/null +++ b/build/phpmd.xml @@ -0,0 +1,14 @@ +<ruleset name="OAuth 2.0 Server" + xmlns="http://pmd.sf.net/ruleset/1.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://pmd.sf.net/ruleset/1.0.0 + http://pmd.sf.net/ruleset_xml_schema.xsd" + xsi:noNamespaceSchemaLocation="http://pmd.sf.net/ruleset_xml_schema.xsd"> + + <description> + Ruleset for OAuth 2.0 server + </description> + + <!-- Import the entire unused code rule set --> + <rule ref="rulesets/unusedcode.xml" /> +</ruleset> \ No newline at end of file diff --git a/build/phpunit.xml b/build/phpunit.xml new file mode 100644 index 00000000..e74535da --- /dev/null +++ b/build/phpunit.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<phpunit colors="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" stopOnError="false" stopOnFailure="false" stopOnIncomplete="false" stopOnSkipped="false"> + <testsuites> + <testsuite name="Authentication Server"> + <directory suffix="test.php">../tests/authentication</directory> + </testsuite> + <testsuite name="Resource Server"> + <directory suffix="test.php">../tests/resource</directory> + </testsuite> + </testsuites> + <filter> + <blacklist> + <directory suffix=".php">PEAR_INSTALL_DIR</directory> + <directory suffix=".php">PHP_LIBDIR</directory> + <directory suffix=".php">../vendor/composer</directory> + </blacklist> + </filter> + <logging> + <log type="coverage-html" target="coverage" title="lncd/OAuth" charset="UTF-8" yui="true" highlight="true" lowUpperBound="35" highLowerBound="70"/> + <log type="coverage-clover" target="logs/clover.xml"/> + <log type="junit" target="logs/junit.xml" logIncompleteSkipped="false"/> + </logging> +</phpunit> \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 00000000..fc8f52b0 --- /dev/null +++ b/composer.json @@ -0,0 +1,40 @@ +{ + "name": "lncd/Oauth2", + "description": "OAuth 2.0 Framework", + "version": "0.1", + "homepage": "https://github.com/lncd/OAuth2", + "license": "MIT", + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "EHER/PHPUnit": "*" + }, + "repositories": [ + { + "type": "git", + "url": "https://github.com/lncd/OAuth2" + } + ], + "keywords": [ + "oauth", + "oauth2", + "server", + "authorization", + "authentication", + "resource" + ], + "authors": [ + { + "name": "Alex Bilbie", + "email": "oauth2server@alexbilbie.com", + "homepage": "http://www.httpster.org", + "role": "Developer" + } + ], + "autoload": { + "psr-0": { + "Oauth2": "src/" + } + } +} \ No newline at end of file diff --git a/license.txt b/license.txt new file mode 100644 index 00000000..91f8b897 --- /dev/null +++ b/license.txt @@ -0,0 +1,20 @@ +MIT License + +Copyright (C) 2012 University of Lincoln + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/src/Oauth2/Authentication/Database.php b/src/Oauth2/Authentication/Database.php new file mode 100644 index 00000000..ed526f89 --- /dev/null +++ b/src/Oauth2/Authentication/Database.php @@ -0,0 +1,320 @@ +<?php + +namespace Oauth2\Authentication; + +interface Database +{ + /** + * Validate a client + * + * Database query: + * + * <code> + * # Client ID + redirect URI + * SELECT clients.id FROM clients LEFT JOIN client_endpoints ON + * client_endpoints.client_id = clients.id WHERE clients.id = $clientId AND + * client_endpoints.redirect_uri = $redirectUri + * + * # Client ID + client secret + * SELECT clients.id FROM clients WHERE clients.id = $clientId AND + * clients.secret = $clientSecret + * + * # Client ID + client secret + redirect URI + * SELECT clients.id FROM clients LEFT JOIN client_endpoints ON + * client_endpoints.client_id = clients.id WHERE clients.id = $clientId AND + * clients.secret = $clientSecret AND client_endpoints.redirect_uri = + * $redirectUri + * </code> + * + * @param string $clientId The client's ID + * @param string $clientSecret The client's secret (default = "null") + * @param string $redirectUri The client's redirect URI (default = "null") + * @return [type] [description] + */ + public function validateClient( + $clientId, + $clientSecret = null, + $redirectUri = null + ); + + /** + * Create a new OAuth session + * + * Database query: + * + * <code> + * INSERT INTO oauth_sessions (client_id, redirect_uri, owner_type, + * owner_id, auth_code, access_token, stage, first_requested, last_updated) + * VALUES ($clientId, $redirectUri, $type, $typeId, $authCode, + * $accessToken, $stage, UNIX_TIMESTAMP(NOW()), UNIX_TIMESTAMP(NOW())) + * </code> + * + * @param string $clientId The client ID + * @param string $redirectUri The redirect URI + * @param string $type The session owner's type (default = "user") + * @param string $typeId The session owner's ID (default = "null") + * @param string $authCode The authorisation code (default = "null") + * @param string $accessToken The access token (default = "null") + * @param string $stage The stage of the session (default ="request") + * @return [type] [description] + */ + public function newSession( + $clientId, + $redirectUri, + $type = 'user', + $typeId = null, + $authCode = null, + $accessToken = null, + $accessTokenExpire = null, + $stage = 'requested' + ); + + /** + * Update an OAuth session + * + * Database query: + * + * <code> + * UPDATE oauth_sessions SET auth_code = $authCode, access_token = + * $accessToken, stage = $stage, last_updated = UNIX_TIMESTAMP(NOW()) WHERE + * id = $sessionId + * </code> + * + * @param string $sessionId The session ID + * @param string $authCode The authorisation code (default = "null") + * @param string $accessToken The access token (default = "null") + * @param string $stage The stage of the session (default ="request") + * @return void + */ + public function updateSession( + $sessionId, + $authCode = null, + $accessToken = null, + $accessTokenExpire = null, + $stage = 'requested' + ); + + /** + * Delete an OAuth session + * + * <code> + * DELETE FROM oauth_sessions WHERE client_id = $clientId AND owner_type = + * $type AND owner_id = $typeId + * </code> + * + * @param string $clientId The client ID + * @param string $type The session owner's type + * @param string $typeId The session owner's ID + * @return [type] [description] + */ + public function deleteSession( + $clientId, + $type, + $typeId + ); + + /** + * Validate that an authorisation code is valid + * + * Database query: + * + * <code> + * SELECT id FROM oauth_sessions WHERE client_id = $clientID AND + * redirect_uri = $redirectUri AND auth_code = $authCode + * </code> + * + * Response: + * + * <code> + * Array + * ( + * [id] => (int) The session ID + * [client_id] => (string) The client ID + * [redirect_uri] => (string) The redirect URI + * [owner_type] => (string) The session owner type + * [owner_id] => (string) The session owner's ID + * [auth_code] => (string) The authorisation code + * [stage] => (string) The session's stage + * [first_requested] => (int) Unix timestamp of the time the session was + * first generated + * [last_updated] => (int) Unix timestamp of the time the session was + * last updated + * ) + * </code> + * + * @param string $clientId The client ID + * @param string $redirectUri The redirect URI + * @param string $authCode The authorisation code + * @return int|bool Returns the session ID if the auth code + * is valid otherwise returns false + */ + public function validateAuthCode( + $clientId, + $redirectUri, + $authCode + ); + + /** + * Return the session ID for a given session owner and client combination + * + * Database query: + * + * <code> + * SELECT id FROM oauth_sessions WHERE client_id = $clientId + * AND owner_type = $type AND owner_id = $typeId + * </code> + * + * @param string $type The session owner's type + * @param string $typeId The session owner's ID + * @param string $clientId The client ID + * @return string|null Return the session ID as an integer if + * found otherwise returns false + */ + public function hasSession( + $type, + $typeId, + $clientId + ); + + /** + * Return the access token for a given session + * + * Database query: + * + * <code> + * SELECT access_token FROM oauth_sessions WHERE id = $sessionId + * </code> + * + * @param int $sessionId The OAuth session ID + * @return string|null Returns the access token as a string if + * found otherwise returns null + */ + public function getAccessToken($sessionId); + + /** + * Removes an authorisation code associated with a session + * + * Database query: + * + * <code> + * UPDATE oauth_sessions SET auth_code = NULL WHERE id = $sessionId + * </code> + * + * @param int $sessionId The OAuth session ID + * @return void + */ + public function removeAuthCode($sessionId); + + /** + * Sets a sessions access token + * + * Database query: + * + * <code> + * UPDATE oauth_sessions SET access_token = $accessToken WHERE id = + * $sessionId + * </code> + * + * @param int $sessionId The OAuth session ID + * @param string $accessToken The access token + * @return void + */ + public function setAccessToken( + $sessionId, + $accessToken + ); + + /** + * Associates a session with a scope + * + * Database query: + * + * <code> + * INSERT INTO oauth_session_scopes (session_id, scope) VALUE ($sessionId, + * $scope) + * </code> + * + * @param int $sessionId The session ID + * @param string $scope The scope + * @return void + */ + public function addSessionScope( + $sessionId, + $scope + ); + + /** + * Return information about a scope + * + * Database query: + * + * <code> + * SELECT * FROM scopes WHERE scope = $scope + * </code> + * + * Response: + * + * <code> + * Array + * ( + * [id] => (int) The scope's ID + * [scope] => (string) The scope itself + * [name] => (string) The scope's name + * [description] => (string) The scope's description + * ) + * </code> + * + * @param string $scope The scope + * @return array + */ + public function getScope($scope); + + /** + * Associate a session's scopes with an access token + * + * Database query: + * + * <code> + * UPDATE oauth_session_scopes SET access_token = $accessToken WHERE + * session_id = $sessionId + * </code> + * + * @param int $sessionId The session ID + * @param string $accessToken The access token + * @return void + */ + public function updateSessionScopeAccessToken( + $sessionId, + $accessToken + ); + + /** + * Return the scopes associated with an access token + * + * Database query: + * + * <code> + * SELECT scopes.scope, scopes.name, scopes.description FROM + * oauth_session_scopes JOIN scopes ON oauth_session_scopes.scope = + * scopes.scope WHERE access_token = $accessToken + * </code> + * + * Response: + * + * <code> + * Array + * ( + * [0] => Array + * ( + * [scope] => (string) The scope + * [name] => (string) The scope's name + * [description] => (string) The scope's description + * ) + * ) + * </code> + * + * @param string $accessToken The access token + * @return array + */ + public function accessTokenScopes($accessToken); +} diff --git a/src/Oauth2/Authentication/Server.php b/src/Oauth2/Authentication/Server.php new file mode 100644 index 00000000..0c6f9476 --- /dev/null +++ b/src/Oauth2/Authentication/Server.php @@ -0,0 +1,517 @@ +<?php + +namespace Oauth2\Authentication; + +class OAuthServerClientException extends \Exception +{ + +} + +class OAuthServerUserException extends \Exception +{ + +} + +class OAuthServerException extends \Exception +{ + +} + +class Server +{ + /** + * Reference to the database abstractor + * @var object + */ + private $db = null; + + /** + * Server configuration + * @var array + */ + private $config = array( + 'scope_delimeter' => ',', + 'access_token_ttl' => null + ); + + /** + * Supported response types + * @var array + */ + private $response_types = array( + 'code' + ); + + /** + * Supported grant types + * @var array + */ + private $grant_types = array( + 'authorization_code' + ); + + /** + * Exception error codes + * @var array + */ + public $exceptionCodes = array( + 0 => 'invalid_request', + 1 => 'unauthorized_client', + 2 => 'access_denied', + 3 => 'unsupported_response_type', + 4 => 'invalid_scope', + 5 => 'server_error', + 6 => 'temporarily_unavailable', + 7 => 'unsupported_grant_type', + 8 => 'invalid_client', + 9 => 'invalid_grant' + ); + + /** + * Error codes. + * + * To provide i8ln errors just overwrite the keys + * + * @var array + */ + public $errors = array( + 'invalid_request' => 'The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. Check the "%s" parameter.', + 'unauthorized_client' => 'The client is not authorized to request an access token using this method.', + 'access_denied' => 'The resource owner or authorization server denied the request.', + 'unsupported_response_type' => 'The authorization server does not support obtaining an access token using this method.', + 'invalid_scope' => 'The requested scope is invalid, unknown, or malformed. Check the "%s" scope.', + 'server_error' => 'The authorization server encountered an unexpected condition which prevented it from fulfilling the request.', + 'temporarily_unavailable' => 'The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server.', + 'unsupported_grant_type' => 'The authorization grant type is not supported by the authorization server', + 'invalid_client' => 'Client authentication failed', + 'invalid_grant' => 'The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client. Check the "%s" parameter.' + ); + + /** + * Constructor + * + * @access public + * @param array $options Optional list of options to overwrite the defaults + * @return void + */ + public function __construct($options = null) + { + if ($options !== null) { + $this->options = array_merge($this->config, $options); + } + } + + /** + * Register a database abstrator class + * + * @access public + * @param object $db A class that implements OAuth2ServerDatabase + * @return void + */ + public function registerDbAbstractor($db) + { + $this->db = $db; + } + + /** + * Check client authorise parameters + * + * @access public + * @param array $authParams Optional array of parsed $_GET keys + * @return array Authorise request parameters + */ + public function checkClientAuthoriseParams($authParams = null) + { + $params = array(); + + // Client ID + if ( ! isset($authParams['client_id']) && ! isset($_GET['client_id'])) { + + throw new OAuthServerClientException(sprintf($this->errors['invalid_request'], 'client_id'), 0); + + } else { + + $params['client_id'] = (isset($authParams['client_id'])) ? $authParams['client_id'] : $_GET['client_id']; + + } + + // Redirect URI + if ( ! isset($authParams['redirect_uri']) && ! isset($_GET['redirect_uri'])) { + + throw new OAuthServerClientException(sprintf($this->errors['invalid_request'], 'redirect_uri'), 0); + + } else { + + $params['redirect_uri'] = (isset($authParams['redirect_uri'])) ? $authParams['redirect_uri'] : $_GET['redirect_uri']; + + } + + // Validate client ID and redirect URI + $clientDetails = $this->dbcall('validateClient', $params['client_id'], null, $params['redirect_uri']); + + if ($clientDetails === false) { + + throw new OAuthServerClientException($this->errors['invalid_client'], 8); + } + + // Response type + if ( ! isset($authParams['response_type']) && ! isset($_GET['response_type'])) { + + throw new OAuthServerClientException(sprintf($this->errors['invalid_request'], 'response_type'), 0); + + } else { + + $params['response_type'] = (isset($authParams['response_type'])) ? $authParams['response_type'] : $_GET['response_type']; + + // Ensure response type is one that is recognised + if ( ! in_array($params['response_type'], $this->response_types)) { + + throw new OAuthServerClientException($this->errors['unsupported_response_type'], 3); + + } + } + + // Get and validate scopes + if (isset($authParams['scope']) || isset($_GET['scope'])) { + + $scopes = (isset($_GET['scope'])) ? $_GET['scope'] : $authParams['scope']; + + $scopes = explode($this->config['scope_delimeter'], $scopes); + + // Remove any junk scopes + for ($i = 0; $i < count($scopes); $i++) { + $scopes[$i] = trim($scopes[$i]); + + if ($scopes[$i] === '') { + unset($scopes[$i]); + } + } + + if (count($scopes) === 0) { + + throw new OAuthServerClientException(sprintf($this->errors['invalid_request'], 'scope'), 0); + } + + $params['scopes'] = array(); + + foreach ($scopes as $scope) { + + $scopeDetails = $this->dbcall('getScope', $scope); + + if ($scopeDetails === false) { + + throw new OAuthServerClientException(sprintf($this->errors['invalid_scope'], $scope), 4); + + } + + $params['scopes'][] = $scopeDetails; + + } + } + + return $params; + } + + /** + * Parse a new authorise request + * + * @param string $type The session owner's type + * @param string $typeId The session owner's ID + * @param array $authoriseParams The authorise request $_GET parameters + * @return string An authorisation code + */ + public function newAuthoriseRequest($type, $typeId, $authoriseParams) + { + // Remove any old sessions the user might have + $this->dbcall('deleteSession', + $authoriseParams['client_id'], + $type, + $typeId + ); + + // Create the new auth code + $authCode = $this->newAuthCode( + $authoriseParams['client_id'], + 'user', + $typeId, + $authoriseParams['redirect_uri'], + $authoriseParams['scopes'] + ); + + return $authCode; + } + + /** + * Generate a unique code + * + * Generate a unique code for an authorisation code, or token + * + * @return string A unique code + */ + private function generateCode() + { + return sha1(uniqid(microtime())); + } + + /** + * Create a new authorisation code + * + * @param string $clientId The client ID + * @param string $type The type of the owner of the session + * @param string $typeId The session owner's ID + * @param string $redirectUri The redirect URI + * @param array $scopes The requested scopes + * @param string $accessToken The access token (default = null) + * @return string An authorisation code + */ + private function newAuthCode($clientId, $type, $typeId, $redirectUri, $scopes = array(), $accessToken = null) + { + $authCode = $this->generateCode(); + + // If an access token exists then update the existing session with the + // new authorisation code otherwise create a new session + if ($accessToken !== null) { + + $this->dbcall('updateSession', + $clientId, + $type, + $typeId, + $authCode, + $accessToken, + 'request' + ); + + } else { + + // Delete any existing sessions just to be sure + $this->dbcall('deleteSession', $clientId, $type, $typeId); + + // Create a new session + $sessionId = $this->dbcall('newSession', + $clientId, + $redirectUri, + $type, + $typeId, + $authCode, + null, + null, + 'request' + ); + + // Add the scopes + foreach ($scopes as $key => $scope) { + + $this->dbcall('addSessionScope', $sessionId, $scope['scope']); + + } + + } + + return $authCode; + } + + /** + * Issue an access token + * + * @access public + * + * @param array $authParams Optional array of parsed $_POST keys + * + * @return array Authorise request parameters + */ + public function issueAccessToken($authParams = null) + { + $params = array(); + + if ( ! isset($authParams['grant_type']) && ! isset($_POST['grant_type'])) { + + throw new OAuthServerClientException(sprintf($this->errors['invalid_request'], 'grant_type'), 0); + + } else { + + $params['grant_type'] = (isset($authParams['grant_type'])) ? $authParams['grant_type'] : $_POST['grant_type']; + + // Ensure grant type is one that is recognised + if ( ! in_array($params['grant_type'], $this->grant_types)) { + + throw new OAuthServerClientException($this->errors['unsupported_grant_type'], 7); + + } + } + + switch ($params['grant_type']) + { + + case 'authorization_code': // Authorization code grant + return $this->completeAuthCodeGrant($authParams, $params); + break; + + case 'refresh_token': // Refresh token + case 'password': // Resource owner password credentials grant + case 'client_credentials': // Client credentials grant + default: // Unsupported + throw new OAuthServerException($this->errors['server_error'] . 'Tried to process an unsuppported grant type.', 5); + break; + } + } + + /** + * Complete the authorisation code grant + * + * @access private + * + * @param array $authParams Array of parsed $_POST keys + * @param array $params Generated parameters from issueAccessToken() + * + * @return array Authorise request parameters + */ + private function completeAuthCodeGrant($authParams = array(), $params = array()) + { + // Client ID + if ( ! isset($authParams['client_id']) && ! isset($_POST['client_id'])) { + + throw new OAuthServerClientException(sprintf($this->errors['invalid_request'], 'client_id'), 0); + + } else { + + $params['client_id'] = (isset($authParams['client_id'])) ? $authParams['client_id'] : $_POST['client_id']; + + } + + // Client secret + if ( ! isset($authParams['client_secret']) && ! isset($_POST['client_secret'])) { + + throw new OAuthServerClientException(sprintf($this->errors['invalid_request'], 'client_secret'), 0); + + } else { + + $params['client_secret'] = (isset($authParams['client_secret'])) ? $authParams['client_secret'] : $_POST['client_secret']; + + } + + // Redirect URI + if ( ! isset($authParams['redirect_uri']) && ! isset($_POST['redirect_uri'])) { + + throw new OAuthServerClientException(sprintf($this->errors['invalid_request'], 'redirect_uri'), 0); + + } else { + + $params['redirect_uri'] = (isset($authParams['redirect_uri'])) ? $authParams['redirect_uri'] : $_POST['redirect_uri']; + + } + + // Validate client ID and redirect URI + $clientDetails = $this->dbcall('validateClient', + $params['client_id'], + $params['client_secret'], + $params['redirect_uri'] + ); + + if ($clientDetails === false) { + + throw new OAuthServerClientException($this->errors['invalid_client'], 8); + } + + // The authorization code + if ( ! isset($authParams['code']) && ! isset($_POST['code'])) { + + throw new OAuthServerClientException(sprintf($this->errors['invalid_request'], 'code'), 0); + + } else { + + $params['code'] = (isset($authParams['code'])) ? $authParams['code'] : $_POST['code']; + + } + + // Verify the authorization code matches the client_id and the + // request_uri + $session = $this->dbcall('validateAuthCode', + $params['client_id'], + $params['redirect_uri'], + $params['code'] + ); + + if ( ! $session) { + + throw new OAuthServerClientException(sprintf($this->errors['invalid_grant'], 'code'), 9); + + } else { + + // A session ID was returned so update it with an access token, + // remove the authorisation code, change the stage to 'granted' + + $accessToken = $this->generateCode(); + + $accessTokenExpires = ($this->config['access_token_ttl'] === null) ? null : time() + $this->config['access_token_ttl']; + + $this->dbcall('updateSession', + $session['id'], + null, + $accessToken, + $accessTokenExpires, + 'granted' + ); + + // Update the session's scopes to reference the access token + $this->dbcall('updateSessionScopeAccessToken', + $session['id'], + $accessToken + ); + + return array( + 'access_token' => $accessToken, + 'token_type' => 'bearer', + 'expires_in' => $this->config['access_token_ttl'] + ); + } + } + + /** + * Generates the redirect uri with appended params + * + * @param string $redirectUri The redirect URI + * @param array $params The parameters to be appended to the URL + * @param string $query_delimeter The query string delimiter (default: ?) + * + * @return string The updated redirect URI + */ + public function redirectUri($redirectUri, $params = array(), $queryDelimeter = '?') + { + + if (strstr($redirectUri, $queryDelimeter)) { + + $redirectUri = $redirectUri . '&' . http_build_query($params); + + } else { + + $redirectUri = $redirectUri . $queryDelimeter . http_build_query($params); + + } + + return $redirectUri; + + } + + /** + * Call database methods from the abstractor + * + * @return mixed The query result + */ + private function dbcall() + { + if ($this->db === null) { + throw new OAuthServerException('No registered database abstractor'); + } + + if ( ! $this->db instanceof Database) { + throw new OAuthServerException('Registered database abstractor is not an instance of Oauth2\Authentication\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); + } +} diff --git a/src/Oauth2/Resource/Database.php b/src/Oauth2/Resource/Database.php new file mode 100644 index 00000000..9c5d1b44 --- /dev/null +++ b/src/Oauth2/Resource/Database.php @@ -0,0 +1,59 @@ +<?php + +namespace Oauth2\Resource; + +interface Database +{ + /** + * Validate an access token and return the session details. + * + * Database query: + * + * <code> + * SELECT id, owner_type, owner_id FROM oauth_sessions WHERE access_token = + * $accessToken AND stage = 'granted' AND + * access_token_expires > UNIX_TIMESTAMP(now()) + * </code> + * + * Response: + * + * <code> + * Array + * ( + * [id] => (int) The session ID + * [owner_type] => (string) The session owner type + * [owner_id] => (string) The session owner's ID + * ) + * </code> + * + * @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: + * + * <code> + * SELECT scope FROM oauth_session_scopes WHERE access_token = + * '291dca1c74900f5f252de351e0105aa3fc91b90b' + * </code> + * + * Response: + * + * <code> + * Array + * ( + * [0] => (string) A scope + * [1] => (string) Another scope + * ... + * ) + * </code> + * + * @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 new file mode 100644 index 00000000..0ec835a5 --- /dev/null +++ b/src/Oauth2/Resource/Server.php @@ -0,0 +1,225 @@ +<?php + +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 + if (function_exists('getallheaders')) { + + $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', $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', $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/src/sql/database.sql b/src/sql/database.sql new file mode 100644 index 00000000..0c218795 --- /dev/null +++ b/src/sql/database.sql @@ -0,0 +1,59 @@ +-- Create syntax for TABLE 'clients' +CREATE TABLE `clients` ( + `id` varchar(40) NOT NULL DEFAULT '', + `secret` varchar(40) NOT NULL DEFAULT '', + `name` varchar(255) NOT NULL DEFAULT '', + `auto_approve` tinyint(1) NOT NULL DEFAULT '0', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +-- Create syntax for TABLE 'client_endpoints' +CREATE TABLE `client_endpoints` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `client_id` varchar(40) NOT NULL DEFAULT '', + `redirect_uri` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `client_id` (`client_id`), + CONSTRAINT `client_endpoints_ibfk_1` FOREIGN KEY (`client_id`) REFERENCES `clients` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +-- Create syntax for TABLE 'oauth_sessions' +CREATE TABLE `oauth_sessions` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `client_id` varchar(32) NOT NULL DEFAULT '', + `redirect_uri` varchar(250) NOT NULL DEFAULT '', + `owner_type` enum('user','client') NOT NULL DEFAULT 'user', + `owner_id` varchar(255) DEFAULT NULL, + `auth_code` varchar(40) DEFAULT '', + `access_token` varchar(40) DEFAULT '', + `access_token_expires` int(10) DEFAULT NULL, + `stage` enum('requested','granted') NOT NULL DEFAULT 'requested', + `first_requested` int(10) unsigned NOT NULL, + `last_updated` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + KEY `client_id` (`client_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +-- Create syntax for TABLE 'scopes' +CREATE TABLE `scopes` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `scope` varchar(255) NOT NULL DEFAULT '', + `name` varchar(255) NOT NULL DEFAULT '', + `description` varchar(255) DEFAULT '', + PRIMARY KEY (`id`), + UNIQUE KEY `scope` (`scope`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +-- Create syntax for TABLE 'oauth_session_scopes' +CREATE TABLE `oauth_session_scopes` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `session_id` int(11) unsigned NOT NULL, + `access_token` varchar(40) NOT NULL DEFAULT '', + `scope` varchar(255) NOT NULL DEFAULT '', + PRIMARY KEY (`id`), + KEY `session_id` (`session_id`), + KEY `access_token` (`access_token`), + KEY `scope` (`scope`), + CONSTRAINT `oauth_session_scopes_ibfk_3` FOREIGN KEY (`scope`) REFERENCES `scopes` (`scope`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `oauth_session_scopes_ibfk_4` FOREIGN KEY (`session_id`) REFERENCES `oauth_sessions` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; \ No newline at end of file diff --git a/src/sql/index.html b/src/sql/index.html new file mode 100644 index 00000000..e69de29b diff --git a/test b/test new file mode 100755 index 00000000..dae89d37 --- /dev/null +++ b/test @@ -0,0 +1 @@ +vendor/bin/phpunit --coverage-text --configuration build/phpunit.xml diff --git a/tests/authentication/database_mock.php b/tests/authentication/database_mock.php new file mode 100644 index 00000000..955035ed --- /dev/null +++ b/tests/authentication/database_mock.php @@ -0,0 +1,191 @@ +<?php + +use Oauth2\Authentication\Database; + +class OAuthdb implements Database +{ + private $sessions = array(); + private $sessions_client_type_id = array(); + private $sessions_code = array(); + private $session_scopes = array(); + + private $clients = array(0 => array( + 'client_id' => 'test', + 'client_secret' => 'test', + 'redirect_uri' => 'http://example.com/test', + 'name' => 'Test Client' + )); + + private $scopes = array('test' => array( + 'id' => 1, + 'scope' => 'test', + 'name' => 'test', + 'description' => 'test' + )); + + public function validateClient( + $clientId, + $clientSecret = null, + $redirectUri = null + ) + { + if ($clientId !== $this->clients[0]['client_id']) + { + return false; + } + + if ($clientSecret !== null && $clientSecret !== $this->clients[0]['client_secret']) + { + return false; + } + + if ($redirectUri !== null && $redirectUri !== $this->clients[0]['redirect_uri']) + { + return false; + } + + return $this->clients[0]; + } + + public function newSession( + $clientId, + $redirectUri, + $type = 'user', + $typeId = null, + $authCode = null, + $accessToken = null, + $accessTokenExpire = null, + $stage = 'requested' + ) + { + $id = count($this->sessions); + + $this->sessions[$id] = array( + 'id' => $id, + 'client_id' => $clientId, + 'redirect_uri' => $redirectUri, + 'owner_type' => $type, + 'owner_id' => $typeId, + 'auth_code' => $authCode, + 'access_token' => $accessToken, + 'access_token_expire' => $accessTokenExpire, + 'stage' => $stage + ); + + $this->sessions_client_type_id[$clientId . ':' . $type . ':' . $typeId] = $id; + $this->sessions_code[$clientId . ':' . $redirectUri . ':' . $authCode] = $id; + + return true; + } + + public function updateSession( + $sessionId, + $authCode = null, + $accessToken = null, + $accessTokenExpire = null, + $stage = 'requested' + ) + { + $this->sessions[$sessionId]['auth_code'] = $authCode; + $this->sessions[$sessionId]['access_token'] = $accessToken; + $this->sessions[$sessionId]['access_token_expire'] = $accessTokenExpire; + $this->sessions[$sessionId]['stage'] = $stage; + + return true; + } + + public function deleteSession( + $clientId, + $type, + $typeId + ) + { + $key = $clientId . ':' . $type . ':' . $typeId; + if (isset($this->sessions_client_type_id[$key])) + { + unset($this->sessions[$this->sessions_client_type_id[$key]]); + } + return true; + } + + public function validateAuthCode( + $clientId, + $redirectUri, + $authCode + ) + { + $key = $clientId . ':' . $redirectUri . ':' . $authCode; + + if (isset($this->sessions_code[$key])) + { + return $this->sessions[$this->sessions_code[$key]]; + } + + return false; + } + + public function hasSession( + $type, + $typeId, + $clientId + ) + { + die('not implemented hasSession'); + } + + public function getAccessToken($sessionId) + { + die('not implemented getAccessToken'); + } + + public function removeAuthCode($sessionId) + { + die('not implemented removeAuthCode'); + } + + public function setAccessToken( + $sessionId, + $accessToken + ) + { + die('not implemented setAccessToken'); + } + + public function addSessionScope( + $sessionId, + $scope + ) + { + if ( ! isset($this->session_scopes[$sessionId])) + { + $this->session_scopes[$sessionId] = array(); + } + + $this->session_scopes[$sessionId][] = $scope; + + return true; + } + + public function getScope($scope) + { + if ( ! isset($this->scopes[$scope])) + { + return false; + } + + return $this->scopes[$scope]; + } + + public function updateSessionScopeAccessToken( + $sessionId, + $accessToken + ) + { + return true; + } + + public function accessTokenScopes($accessToken) + { + die('not implemented accessTokenScopes'); + } +} \ No newline at end of file diff --git a/tests/authentication/server_test.php b/tests/authentication/server_test.php new file mode 100644 index 00000000..6e79e24c --- /dev/null +++ b/tests/authentication/server_test.php @@ -0,0 +1,398 @@ +<?php + +class Authentication_Server_test extends PHPUnit_Framework_TestCase { + + function setUp() + { + $this->oauth = new Oauth2\Authentication\Server(); + + require_once('database_mock.php'); + $this->oauthdb = new OAuthdb(); + $this->assertInstanceOf('Oauth2\Authentication\Database', $this->oauthdb); + $this->oauth->registerDbAbstractor($this->oauthdb); + } + + function test_generateCode() + { + $reflector = new ReflectionClass($this->oauth); + $method = $reflector->getMethod('generateCode'); + $method->setAccessible(true); + + $result = $method->invoke($this->oauth); + $result2 = $method->invoke($this->oauth); + + $this->assertEquals(40, strlen($result)); + $this->assertNotEquals($result, $result2); + } + + function test_redirectUri() + { + $result1 = $this->oauth->redirectUri('http://example.com/foo'); + $result2 = $this->oauth->redirectUri('http://example.com/foo', array('foo' => 'bar')); + $result3 = $this->oauth->redirectUri('http://example.com/foo', array('foo' => 'bar'), '#'); + + $this->assertEquals('http://example.com/foo?', $result1); + $this->assertEquals('http://example.com/foo?foo=bar', $result2); + $this->assertEquals('http://example.com/foo#foo=bar', $result3); + } + + function test_checkClientAuthoriseParams_GET() + { + $_GET['client_id'] = 'test'; + $_GET['redirect_uri'] = 'http://example.com/test'; + $_GET['response_type'] = 'code'; + $_GET['scope'] = 'test'; + + $expect = array( + 'client_id' => 'test', + 'redirect_uri' => 'http://example.com/test', + 'response_type' => 'code', + 'scopes' => array( + 0 => array( + 'id' => 1, + 'scope' => 'test', + 'name' => 'test', + 'description' => 'test' + ) + ) + ); + + $result = $this->oauth->checkClientAuthoriseParams(); + + $this->assertEquals($expect, $result); + } + + function test_checkClientAuthoriseParams_PassedParams() + { + unset($_GET['client_id']); + unset($_GET['redirect_uri']); + unset($_GET['response_type']); + unset($_GET['scope']); + + $params = array( + 'client_id' => 'test', + 'redirect_uri' => 'http://example.com/test', + 'response_type' => 'code', + 'scope' => 'test' + ); + + $this->assertEquals(array( + 'client_id' => 'test', + 'redirect_uri' => 'http://example.com/test', + 'response_type' => 'code', + 'scopes' => array(0 => array( + 'id' => 1, + 'scope' => 'test', + 'name' => 'test', + 'description' => 'test' + )) + ), $this->oauth->checkClientAuthoriseParams($params)); + } + + /** + * @expectedException Oauth2\Authentication\OAuthServerClientException + * @expectedExceptionCode 0 + */ + function test_checkClientAuthoriseParams_missingClientId() + { + $this->oauth->checkClientAuthoriseParams(); + } + + /** + * @expectedException Oauth2\Authentication\OAuthServerClientException + * @expectedExceptionCode 0 + */ + function test_checkClientAuthoriseParams_missingRedirectUri() + { + $_GET['client_id'] = 'test'; + + $this->oauth->checkClientAuthoriseParams(); + } + + /** + * @expectedException Oauth2\Authentication\OAuthServerClientException + * @expectedExceptionCode 0 + */ + function test_checkClientAuthoriseParams_missingResponseType() + { + $_GET['client_id'] = 'test'; + $_GET['redirect_uri'] = 'http://example.com/test'; + + $this->oauth->checkClientAuthoriseParams(); + } + + /** + * @expectedException Oauth2\Authentication\OAuthServerClientException + * @expectedExceptionCode 0 + */ + function test_checkClientAuthoriseParams_missingScopes() + { + $_GET['client_id'] = 'test'; + $_GET['redirect_uri'] = 'http://example.com/test'; + $_GET['response_type'] = 'code'; + $_GET['scope'] = ' '; + + $this->oauth->checkClientAuthoriseParams(); + } + + /** + * @expectedException Oauth2\Authentication\OAuthServerClientException + * @expectedExceptionCode 4 + */ + function test_checkClientAuthoriseParams_invalidScopes() + { + $_GET['client_id'] = 'test'; + $_GET['redirect_uri'] = 'http://example.com/test'; + $_GET['response_type'] = 'code'; + $_GET['scope'] = 'blah'; + + $this->oauth->checkClientAuthoriseParams(); + } + + function test_newAuthoriseRequest() + { + $result = $this->oauth->newAuthoriseRequest('user', '123', array( + 'client_id' => 'test', + 'redirect_uri' => 'http://example.com/test', + 'scopes' => array(array( + 'id' => 1, + 'scope' => 'test', + 'name' => 'test', + 'description' => 'test' + )) + )); + + $this->assertEquals(40, strlen($result)); + } + + function test_newAuthoriseRequest_isUnique() + { + $result1 = $this->oauth->newAuthoriseRequest('user', '123', array( + 'client_id' => 'test', + 'redirect_uri' => 'http://example.com/test', + 'scopes' => array(array( + 'id' => 1, + 'scope' => 'test', + 'name' => 'test', + 'description' => 'test' + )) + )); + + $result2 = $this->oauth->newAuthoriseRequest('user', '123', array( + 'client_id' => 'test', + 'redirect_uri' => 'http://example.com/test', + 'scopes' => array(array( + 'id' => 1, + 'scope' => 'test', + 'name' => 'test', + 'description' => 'test' + )) + )); + + $this->assertNotEquals($result1, $result2); + } + + function test_issueAccessToken_POST() + { + $auth_code = $this->oauth->newAuthoriseRequest('user', '123', array( + 'client_id' => 'test', + 'redirect_uri' => 'http://example.com/test', + 'scopes' => array(array( + 'id' => 1, + 'scope' => 'test', + 'name' => 'test', + 'description' => 'test' + )) + )); + + $_POST['client_id'] = 'test'; + $_POST['client_secret'] = 'test'; + $_POST['redirect_uri'] = 'http://example.com/test'; + $_POST['grant_type'] = 'authorization_code'; + $_POST['code'] = $auth_code; + + $result = $this->oauth->issueAccessToken(); + + $this->assertCount(3, $result); + $this->assertArrayHasKey('access_token', $result); + $this->assertArrayHasKey('token_type', $result); + $this->assertArrayHasKey('expires_in', $result); + } + + function test_issueAccessToken_PassedParams() + { + $auth_code = $this->oauth->newAuthoriseRequest('user', '123', array( + 'client_id' => 'test', + 'redirect_uri' => 'http://example.com/test', + 'scopes' => array(array( + 'id' => 1, + 'scope' => 'test', + 'name' => 'test', + 'description' => 'test' + )) + )); + + $params['client_id'] = 'test'; + $params['client_secret'] = 'test'; + $params['redirect_uri'] = 'http://example.com/test'; + $params['grant_type'] = 'authorization_code'; + $params['code'] = $auth_code; + + $result = $this->oauth->issueAccessToken($params); + + $this->assertCount(3, $result); + $this->assertArrayHasKey('access_token', $result); + $this->assertArrayHasKey('token_type', $result); + $this->assertArrayHasKey('expires_in', $result); + } + + /** + * @expectedException Oauth2\Authentication\OAuthServerClientException + * @expectedExceptionCode 0 + */ + function test_issueAccessToken_missingGrantType() + { + $this->oauth->issueAccessToken(); + } + + /** + * @expectedException Oauth2\Authentication\OAuthServerClientException + * @expectedExceptionCode 7 + */ + function test_issueAccessToken_unsupportedGrantType() + { + $params['grant_type'] = 'blah'; + + $this->oauth->issueAccessToken($params); + } + + /** + * @expectedException Oauth2\Authentication\OAuthServerClientException + * @expectedExceptionCode 0 + */ + function test_completeAuthCodeGrant_missingClientId() + { + $reflector = new ReflectionClass($this->oauth); + $method = $reflector->getMethod('completeAuthCodeGrant'); + $method->setAccessible(true); + + $method->invoke($this->oauth); + } + + /** + * @expectedException Oauth2\Authentication\OAuthServerClientException + * @expectedExceptionCode 0 + */ + function test_completeAuthCodeGrant_missingClientSecret() + { + $reflector = new ReflectionClass($this->oauth); + $method = $reflector->getMethod('completeAuthCodeGrant'); + $method->setAccessible(true); + + $authParams['client_id'] = 'test'; + + $method->invoke($this->oauth, $authParams); + } + + /** + * @expectedException Oauth2\Authentication\OAuthServerClientException + * @expectedExceptionCode 0 + */ + function test_completeAuthCodeGrant_missingRedirectUri() + { + $reflector = new ReflectionClass($this->oauth); + $method = $reflector->getMethod('completeAuthCodeGrant'); + $method->setAccessible(true); + + $authParams['client_id'] = 'test'; + $authParams['client_secret'] = 'test'; + + $method->invoke($this->oauth, $authParams); + } + + /** + * @expectedException Oauth2\Authentication\OAuthServerClientException + * @expectedExceptionCode 8 + */ + function test_completeAuthCodeGrant_invalidClient() + { + $reflector = new ReflectionClass($this->oauth); + $method = $reflector->getMethod('completeAuthCodeGrant'); + $method->setAccessible(true); + + $authParams['client_id'] = 'test'; + $authParams['client_secret'] = 'test123'; + $authParams['redirect_uri'] = 'http://example.com/test'; + + $method->invoke($this->oauth, $authParams); + } + + /** + * @expectedException Oauth2\Authentication\OAuthServerClientException + * @expectedExceptionCode 0 + */ + function test_completeAuthCodeGrant_missingCode() + { + $reflector = new ReflectionClass($this->oauth); + $method = $reflector->getMethod('completeAuthCodeGrant'); + $method->setAccessible(true); + + $authParams['client_id'] = 'test'; + $authParams['client_secret'] = 'test'; + $authParams['redirect_uri'] = 'http://example.com/test'; + + $method->invoke($this->oauth, $authParams); + } + + /** + * @expectedException Oauth2\Authentication\OAuthServerClientException + * @expectedExceptionCode 9 + */ + function test_completeAuthCodeGrant_invalidCode() + { + $reflector = new ReflectionClass($this->oauth); + $method = $reflector->getMethod('completeAuthCodeGrant'); + $method->setAccessible(true); + + $authParams['client_id'] = 'test'; + $authParams['client_secret'] = 'test'; + $authParams['redirect_uri'] = 'http://example.com/test'; + $authParams['code'] = 'blah'; + + $method->invoke($this->oauth, $authParams); + } + + /** + * @expectedException Oauth2\Authentication\OAuthServerException + * @expectedExceptionMessage No registered database abstractor + */ + function test_noRegisteredDatabaseAbstractor() + { + $reflector = new ReflectionClass($this->oauth); + $method = $reflector->getMethod('dbcall'); + $method->setAccessible(true); + + $dbAbstractor = $reflector->getProperty('db'); + $dbAbstractor->setAccessible(true); + $dbAbstractor->setValue($this->oauth, null); + + $result = $method->invoke($this->oauth); + } + + /** + * @expectedException Oauth2\Authentication\OAuthServerException + * @expectedExceptionMessage Registered database abstractor is not an instance of Oauth2\Authentication\Database + */ + function test_invalidRegisteredDatabaseAbstractor() + { + $fake = new stdClass; + $this->oauth->registerDbAbstractor($fake); + + $reflector = new ReflectionClass($this->oauth); + $method = $reflector->getMethod('dbcall'); + $method->setAccessible(true); + + $result = $method->invoke($this->oauth); + } + +} \ 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..52c698d8 --- /dev/null +++ b/tests/resource/database_mock.php @@ -0,0 +1,31 @@ +<?php + +use Oauth2\Resource\Database; + +class ResourceDB implements Database +{ + private $accessTokens = array( + 'test12345' => 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..fdb35be4 --- /dev/null +++ b/tests/resource/server_test.php @@ -0,0 +1,121 @@ +<?php + +class Resource_Server_test extends PHPUnit_Framework_TestCase { + + function setUp() + { + require_once('database_mock.php'); + $this->server = new Oauth2\Resource\Server(); + $this->db = new ResourceDB(); + + $this->assertInstanceOf('Oauth2\Resource\Database', $this->db); + $this->server->registerDbAbstractor($this->db); + } + + function test_init_POST() + { + $_SERVER['REQUEST_METHOD'] = 'POST'; + $_POST['oauth_token'] = 'test12345'; + + $this->server->init(); + + $reflector = new ReflectionClass($this->server); + + $_accessToken = $reflector->getProperty('_accessToken'); + $_accessToken->setAccessible(true); + + $_type = $reflector->getProperty('_type'); + $_type->setAccessible(true); + + $_typeId = $reflector->getProperty('_typeId'); + $_typeId->setAccessible(true); + + $_scopes = $reflector->getProperty('_scopes'); + $_scopes->setAccessible(true); + + $this->assertEquals($_accessToken->getValue($this->server), $_POST['oauth_token']); + $this->assertEquals($_type->getValue($this->server), 'user'); + $this->assertEquals($_typeId->getValue($this->server), 123); + $this->assertEquals($_scopes->getValue($this->server), array('foo', 'bar')); + } + + function test_init_GET() + { + $_GET['oauth_token'] = 'test12345'; + + $this->server->init(); + + $reflector = new ReflectionClass($this->server); + + $_accessToken = $reflector->getProperty('_accessToken'); + $_accessToken->setAccessible(true); + + $_type = $reflector->getProperty('_type'); + $_type->setAccessible(true); + + $_typeId = $reflector->getProperty('_typeId'); + $_typeId->setAccessible(true); + + $_scopes = $reflector->getProperty('_scopes'); + $_scopes->setAccessible(true); + + $this->assertEquals($_accessToken->getValue($this->server), $_GET['oauth_token']); + $this->assertEquals($_type->getValue($this->server), 'user'); + $this->assertEquals($_typeId->getValue($this->server), 123); + $this->assertEquals($_scopes->getValue($this->server), array('foo', 'bar')); + } + + function test_init_header() + { + // Test with authorisation header + $this->markTestIncomplete('Authorisation header test has not been implemented yet.'); + } + + /** + * @expectedException \Oauth2\Resource\OAuthResourceServerException + * @expectedExceptionMessage An access token was not presented with the request + */ + function test_init_missingToken() + { + $this->server->init(); + } + + /** + * @expectedException \Oauth2\Resource\OAuthResourceServerException + * @expectedExceptionMessage The access token is not registered with the resource server + */ + function test_init_wrongToken() + { + $_POST['oauth_token'] = 'blah'; + $_SERVER['REQUEST_METHOD'] = 'POST'; + + $this->server->init(); + } + + function test_hasScope() + { + $_POST['oauth_token'] = 'test12345'; + $_SERVER['REQUEST_METHOD'] = 'POST'; + + $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'; + $_SERVER['REQUEST_METHOD'] = 'POST'; + + $this->server->init(); + + $this->assertEquals(123, $this->server->isUser()); + $this->assertEquals(false, $this->server->isMachine()); + } + +} \ No newline at end of file