From d7c1c5026982d2a103d89854a08e6661f23946ff Mon Sep 17 00:00:00 2001 From: Woody Gilk Date: Sat, 31 May 2014 15:34:12 -0500 Subject: [PATCH 1/4] Throw MissingAccessTokenException in the Resource server when no token exists --- .../Exception/MissingAccessTokenException.php | 20 +++++++++++++++++++ src/League/OAuth2/Server/Resource.php | 5 +++-- tests/resource/ResourceServerTest.php | 4 ++-- 3 files changed, 25 insertions(+), 4 deletions(-) create mode 100644 src/League/OAuth2/Server/Exception/MissingAccessTokenException.php diff --git a/src/League/OAuth2/Server/Exception/MissingAccessTokenException.php b/src/League/OAuth2/Server/Exception/MissingAccessTokenException.php new file mode 100644 index 00000000..7bc3fe83 --- /dev/null +++ b/src/League/OAuth2/Server/Exception/MissingAccessTokenException.php @@ -0,0 +1,20 @@ + + * @copyright Copyright (c) 2014 PHP League of Extraordinary Packages + * @license http://mit-license.org/ + * @link http://github.com/php-loep/oauth2-server + */ + +namespace League\OAuth2\Server\Exception; + +/** + * MissingAccessToken Exception + */ +class MissingAccessTokenException extends OAuth2Exception +{ + +} diff --git a/src/League/OAuth2/Server/Resource.php b/src/League/OAuth2/Server/Resource.php index 3485315a..eb48ac5e 100644 --- a/src/League/OAuth2/Server/Resource.php +++ b/src/League/OAuth2/Server/Resource.php @@ -4,7 +4,8 @@ * * @package php-loep/oauth2-server * @author Alex Bilbie - * @copyright Copyright (c) 2013 PHP League of Extraordinary Packages + * @author Woody Gilk + * @copyright Copyright (c) 2013-2014 PHP League of Extraordinary Packages * @license http://mit-license.org/ * @link http://github.com/php-loep/oauth2-server */ @@ -274,7 +275,7 @@ class Resource } if (empty($accessToken)) { - throw new Exception\InvalidAccessTokenException('Access token is missing'); + throw new Exception\MissingAccessTokenException('Access token is missing'); } return $accessToken; diff --git a/tests/resource/ResourceServerTest.php b/tests/resource/ResourceServerTest.php index aee81b52..b25b89be 100644 --- a/tests/resource/ResourceServerTest.php +++ b/tests/resource/ResourceServerTest.php @@ -66,7 +66,7 @@ class Resource_Server_test extends PHPUnit_Framework_TestCase } /** - * @expectedException League\OAuth2\Server\Exception\InvalidAccessTokenException + * @expectedException League\OAuth2\Server\Exception\MissingAccessTokenException */ public function test_determineAccessToken_missingToken() { @@ -84,7 +84,7 @@ class Resource_Server_test extends PHPUnit_Framework_TestCase } /** - * @expectedException League\OAuth2\Server\Exception\InvalidAccessTokenException + * @expectedException League\OAuth2\Server\Exception\MissingAccessTokenException */ public function test_determineAccessToken_brokenCurlRequest() { From e61782975ae014a8d75d9bc61c84115a8aca4efe Mon Sep 17 00:00:00 2001 From: Woody Gilk Date: Sat, 31 May 2014 15:46:10 -0500 Subject: [PATCH 2/4] Copy getExceptionType(), getExceptionMessage(), and getExceptionHttpHeaders() to Resource server --- src/League/OAuth2/Server/Resource.php | 115 +++++++++++++++++++++++++- tests/resource/ResourceServerTest.php | 26 ++++++ 2 files changed, 139 insertions(+), 2 deletions(-) diff --git a/src/League/OAuth2/Server/Resource.php b/src/League/OAuth2/Server/Resource.php index eb48ac5e..4ba64b13 100644 --- a/src/League/OAuth2/Server/Resource.php +++ b/src/League/OAuth2/Server/Resource.php @@ -76,6 +76,117 @@ class Resource */ protected $clientId = null; + /** + * Exception error codes + * @var array + */ + protected static $exceptionCodes = array( + 0 => 'invalid_request', + 1 => 'invalid_token', + 2 => 'insufficient_scope', + ); + + /** + * Exception error messages + * @var array + */ + protected static $exceptionMessages = 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.', + 'invalid_token' => 'The access token provided is expired, revoked, malformed, or invalid for other reasons.', + 'insufficient_scope' => 'The request requires higher privileges than provided by the access token.', + ); + + /** + * Exception error HTTP status codes + * @var array + * + * RFC 6750, section 3.1: + * When a request fails, the resource server responds using the + * appropriate HTTP status code (typically, 400, 401, 403, or 405) and + * includes one of the following error codes in the response: + */ + protected static $exceptionHttpStatusCodes = array( + 'invalid_request' => 400, + 'invalid_token' => 401, + 'insufficient_scope' => 403, + ); + + /** + * Get an exception message + * + * @param string $error The error message key + * @return string The error message + */ + public static function getExceptionMessage($error = '') + { + return self::$exceptionMessages[$error]; + } + + /** + * Get an exception code + * + * @param integer $code The exception code + * @return string The exception code type + */ + public static function getExceptionType($code = 0) + { + return self::$exceptionCodes[$code]; + } + + /** + * Get all headers that have to be send with the error response + * + * @param string $error The error message key + * @return array Array with header values + */ + public static function getExceptionHttpHeaders($error) + { + $headers = array(); + switch (self::$exceptionHttpStatusCodes[$error]) { + case 401: + $headers[] = 'HTTP/1.1 401 Unauthorized'; + break; + case 403: + $headers[] = 'HTTP/1.1 403 Forbidden'; + break; + case 400: + default: + $headers[] = 'HTTP/1.1 400 Bad Request'; + } + + // Add "WWW-Authenticate" header + // + // RFC 6749, section 5.2.: + // "If the client attempted to authenticate via the 'Authorization' + // request header field, the authorization server MUST + // respond with an HTTP 401 (Unauthorized) status code and + // include the "WWW-Authenticate" response header field + // matching the authentication scheme used by the client. + // @codeCoverageIgnoreStart + if ($error === 'insufficient_scope') { + $authScheme = null; + $request = new Request(); + if ($request->server('PHP_AUTH_USER') !== null) { + $authScheme = 'Basic'; + } else { + $authHeader = $request->header('Authorization'); + if ($authHeader !== null) { + if (strpos($authHeader, 'Bearer') === 0) { + $authScheme = 'Bearer'; + } elseif (strpos($authHeader, 'Basic') === 0) { + $authScheme = 'Basic'; + } + } + } + if ($authScheme !== null) { + $headers[] = 'WWW-Authenticate: '.$authScheme.' realm=""'; + } + } + // @codeCoverageIgnoreEnd + + return $headers; + } + /** * Sets up the Resource * @@ -187,7 +298,7 @@ class Resource $result = $this->storages['session']->validateAccessToken($accessToken); if (! $result) { - throw new Exception\InvalidAccessTokenException('Access token is not valid'); + throw new Exception\InvalidAccessTokenException(self::$exceptionMessages['invalid_token'], 1); } $this->accessToken = $accessToken; @@ -275,7 +386,7 @@ class Resource } if (empty($accessToken)) { - throw new Exception\MissingAccessTokenException('Access token is missing'); + throw new Exception\MissingAccessTokenException(self::$exceptionMessages['invalid_request'], 0); } return $accessToken; diff --git a/tests/resource/ResourceServerTest.php b/tests/resource/ResourceServerTest.php index b25b89be..9658cba5 100644 --- a/tests/resource/ResourceServerTest.php +++ b/tests/resource/ResourceServerTest.php @@ -16,6 +16,32 @@ class Resource_Server_test extends PHPUnit_Framework_TestCase return new League\OAuth2\Server\Resource($this->session); } + public function test_getExceptionMessage() + { + $m = League\OAuth2\Server\Resource::getExceptionMessage('invalid_request'); + + $reflector = new ReflectionClass($this->returnDefault()); + $exceptionMessages = $reflector->getProperty('exceptionMessages'); + $exceptionMessages->setAccessible(true); + $v = $exceptionMessages->getValue(); + + $this->assertEquals($v['invalid_request'], $m); + } + + public function test_getExceptionCode() + { + $this->assertEquals('invalid_request', League\OAuth2\Server\Resource::getExceptionType(0)); + $this->assertEquals('invalid_token', League\OAuth2\Server\Resource::getExceptionType(1)); + $this->assertEquals('insufficient_scope', League\OAuth2\Server\Resource::getExceptionType(2)); + } + + public function test_getExceptionHttpHeaders() + { + $this->assertEquals(array('HTTP/1.1 400 Bad Request'), League\OAuth2\Server\Resource::getExceptionHttpHeaders('invalid_request')); + $this->assertEquals(array('HTTP/1.1 401 Unauthorized'), League\OAuth2\Server\Resource::getExceptionHttpHeaders('invalid_token')); + $this->assertContains('HTTP/1.1 403 Forbidden', League\OAuth2\Server\Resource::getExceptionHttpHeaders('insufficient_scope')); + } + public function test_setRequest() { $s = $this->returnDefault(); From 33f4f5b7ab07c0b67149dd42ddef28af5c3cd16b Mon Sep 17 00:00:00 2001 From: Woody Gilk Date: Sat, 31 May 2014 16:11:00 -0500 Subject: [PATCH 3/4] Add $required parameter to hasScope(), triggers InsufficientScopeException --- .../Exception/InsufficientScopeException.php | 20 +++++++++++ src/League/OAuth2/Server/Resource.php | 31 ++++++++--------- tests/resource/ResourceServerTest.php | 33 +++++++++++++++++++ 3 files changed, 69 insertions(+), 15 deletions(-) create mode 100644 src/League/OAuth2/Server/Exception/InsufficientScopeException.php diff --git a/src/League/OAuth2/Server/Exception/InsufficientScopeException.php b/src/League/OAuth2/Server/Exception/InsufficientScopeException.php new file mode 100644 index 00000000..04f66848 --- /dev/null +++ b/src/League/OAuth2/Server/Exception/InsufficientScopeException.php @@ -0,0 +1,20 @@ + + * @copyright Copyright (c) 2014 PHP League of Extraordinary Packages + * @license http://mit-license.org/ + * @link http://github.com/php-loep/oauth2-server + */ + +namespace League\OAuth2\Server\Exception; + +/** + * InsufficientScope Exception + */ +class InsufficientScopeException extends OAuth2Exception +{ + +} diff --git a/src/League/OAuth2/Server/Resource.php b/src/League/OAuth2/Server/Resource.php index 4ba64b13..158d9087 100644 --- a/src/League/OAuth2/Server/Resource.php +++ b/src/League/OAuth2/Server/Resource.php @@ -93,7 +93,7 @@ class Resource protected static $exceptionMessages = 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.', 'invalid_token' => 'The access token provided is expired, revoked, malformed, or invalid for other reasons.', - 'insufficient_scope' => 'The request requires higher privileges than provided by the access token.', + 'insufficient_scope' => 'The request requires higher privileges than provided by the access token. Required scopes are: %s.', ); /** @@ -328,25 +328,26 @@ class Resource * Checks if the presented access token has the given scope(s). * * @param array|string An array of scopes or a single scope as a string + * @param bool If scopes are required, missing scope will trigger an exception + * @throws Exception\InsufficientScopeException Thrown if the any of the given scopes are not in the session * @return bool Returns bool if all scopes are found, false if any fail */ - public function hasScope($scopes) + public function hasScope($scopes, $required = false) { - if (is_string($scopes)) { - if (in_array($scopes, $this->sessionScopes)) { - return true; - } - return false; - } elseif (is_array($scopes)) { - foreach ($scopes as $scope) { - if (! in_array($scope, $this->sessionScopes)) { - return false; - } - } - return true; + if (!is_array($scopes)) { + $scopes = array($scopes); } - return false; + $missing = array_diff($scopes, $this->sessionScopes); + + if ($missing) { + if ($required) { + $missing = implode(', ', $missing); + throw new Exception\InsufficientScopeException(sprintf(self::$exceptionMessages['insufficient_scope'], $missing), 3); + } + return false; + } + return true; } /** diff --git a/tests/resource/ResourceServerTest.php b/tests/resource/ResourceServerTest.php index 9658cba5..3c459294 100644 --- a/tests/resource/ResourceServerTest.php +++ b/tests/resource/ResourceServerTest.php @@ -192,6 +192,39 @@ class Resource_Server_test extends PHPUnit_Framework_TestCase $this->assertEquals('abcdef', $result); } + public function test_hasScope_isRequired() + { + $s = $this->returnDefault(); + + $reflector = new ReflectionClass($s); + $param = $reflector->getProperty('sessionScopes'); + $param->setAccessible(true); + $param->setValue($s, array( + 'a', 'b', 'c' + )); + + $result = $s->hasScope(array('a', 'b'), true); + + $this->assertEquals(true, $result); + } + + /** + * @expectedException League\OAuth2\Server\Exception\InsufficientScopeException + */ + public function test_hasScope_isRequiredFailure() + { + $s = $this->returnDefault(); + + $reflector = new ReflectionClass($s); + $param = $reflector->getProperty('sessionScopes'); + $param->setAccessible(true); + $param->setValue($s, array( + 'a', 'b', 'c' + )); + + $s->hasScope('d', true); + } + /** * @expectedException League\OAuth2\Server\Exception\InvalidAccessTokenException */ From 44db2b295f4563776b45e85c3e226c80678ab87f Mon Sep 17 00:00:00 2001 From: Woody Gilk Date: Sat, 31 May 2014 17:28:48 -0500 Subject: [PATCH 4/4] Stop testing stdClass as an argument to hasScope, it is pointless --- tests/resource/ResourceServerTest.php | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/resource/ResourceServerTest.php b/tests/resource/ResourceServerTest.php index 3c459294..326f3f03 100644 --- a/tests/resource/ResourceServerTest.php +++ b/tests/resource/ResourceServerTest.php @@ -275,11 +275,10 @@ class Resource_Server_test extends PHPUnit_Framework_TestCase $this->assertEquals('user', $s->getOwnerType()); $this->assertEquals('abcdef', $s->getAccessToken()); $this->assertEquals('testapp', $s->getClientId()); - $this->assertTrue($s->hasScope('foo')); - $this->assertTrue($s->hasScope('bar')); - $this->assertTrue($s->hasScope(array('foo', 'bar'))); - $this->assertFalse($s->hasScope(array('foobar'))); - $this->assertFalse($s->hasScope('foobar')); - $this->assertFalse($s->hasScope(new StdClass)); + $this->assertTrue($s->hasScope('foo')); + $this->assertTrue($s->hasScope('bar')); + $this->assertTrue($s->hasScope(array('foo', 'bar'))); + $this->assertFalse($s->hasScope(array('foobar'))); + $this->assertFalse($s->hasScope('foobar')); } }