From 1a9a034a8d3ad4a347d108ce58c8ab126405c9c4 Mon Sep 17 00:00:00 2001 From: Spomky Date: Mon, 21 Oct 2013 21:41:39 +0200 Subject: [PATCH 01/13] Added Scope Policy functionality --- lib/OAuth2/OAuth2.php | 48 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/lib/OAuth2/OAuth2.php b/lib/OAuth2/OAuth2.php index 9765634..d276e8b 100644 --- a/lib/OAuth2/OAuth2.php +++ b/lib/OAuth2/OAuth2.php @@ -95,6 +95,8 @@ class OAuth2 { const CONFIG_REFRESH_LIFETIME = 'refresh_token_lifetime'; // The lifetime of refresh token in seconds. const CONFIG_AUTH_LIFETIME = 'auth_code_lifetime'; // The lifetime of auth code in seconds. const CONFIG_SUPPORTED_SCOPES = 'supported_scopes'; // Array of scopes you want to support + const CONFIG_SCOPES_POLICY = 'scopes_policy'; // Policy if no scope is set. Values can be "error" or "default". + const CONFIG_DEFAULT_SCOPES = 'default_scopes'; // If scope policy is set to "default", this array of scopes will be used. const CONFIG_TOKEN_TYPE = 'token_type'; // Token type to respond with. Currently only "Bearer" supported. const CONFIG_WWW_REALM = 'realm'; const CONFIG_ENFORCE_INPUT_REDIRECT = 'enforce_redirect'; // Set to true to enforce redirect_uri on input for both authorize and token steps. @@ -307,6 +309,14 @@ class OAuth2 { */ const ERROR_INVALID_SCOPE = 'invalid_scope'; + /** + * The requested scope policy is invalid, unknown, or malformed. + * Must be either "error" or "default" + * + * @see https://tools.ietf.org/html/rfc6749#section-3.3 + */ + const ERROR_INVALID_SCOPE_POLICY = 'invalid_scope_policy'; + /** * The provided authorization grant is invalid, expired, * revoked, does not match the redirection URI used in the @@ -371,7 +381,9 @@ protected function setDefaultOptions() { self::CONFIG_ENFORCE_INPUT_REDIRECT => TRUE, self::CONFIG_ENFORCE_STATE => FALSE, - self::CONFIG_SUPPORTED_SCOPES => null, // This is expected to be passed in on construction. Scopes can be an aribitrary string. + self::CONFIG_SUPPORTED_SCOPES => null, // This is expected to be passed in on construction. Scopes can be an arbitrary string. + self::CONFIG_SCOPES_POLICY => "default", // This is expected to be passed in on construction. Scopes policy can be "default" or "error". + self::CONFIG_DEFAULT_SCOPES => null, // This is expected to be passed in on construction. Default scopes can be an arbitrary string. ); } @@ -403,6 +415,8 @@ public function getVariable($name, $default = NULL) { public function setVariable($name, $value) { $name = strtolower($name); + if( self::CONFIG_SCOPES_POLICY === $name && !in_array($value, array("error","default")) ) + throw new OAuth2ServerException(self::HTTP_BAD_REQUEST, self::ERROR_INVALID_SCOPE_POLICY, 'The policy must be either "error" or "default"'); $this->conf[$name] = $value; return $this; } @@ -651,6 +665,32 @@ protected function checkScope($required_scope, $available_scope) { return (count(array_diff($required_scope, $available_scope)) == 0); } + /** + * Checks whether the scope policy is respected + * + * @param string $scope + * The scopes to check. + * + * @return + * The scopes or throw an exception + * + * @see https://tools.ietf.org/html/rfc6749#section-3.3 + * + * @ingroup oauth2_section_3.3 + */ + protected function checkScopePolicy($scope) { + // If Scopes Policy is set to "error" and no scope is input, then throws an error + if (!$scope && "error" === $this->getVariable(self::CONFIG_SCOPES_POLICY, "default") ) { + throw new OAuth2ServerException(self::HTTP_BAD_REQUEST, self::ERROR_INVALID_SCOPE, 'No scope was requested.'); + } + + // If Scopes Policy is set to "default" and no scope is input, then application defaults are set + if (!$scope && "default" === $this->getVariable(self::CONFIG_SCOPES_POLICY, "default") ) { + return $this->getVariable(self::CONFIG_DEFAULT_SCOPES, null); + } + return $scope; + } + // Access token granting (Section 4). /** @@ -701,6 +741,8 @@ public function grantAccessToken(Request $request = NULL) { throw new OAuth2ServerException(self::HTTP_BAD_REQUEST, self::ERROR_INVALID_REQUEST, 'Invalid grant_type parameter or parameter missing'); } + $input["scope"] = $this->checkScopePolicy($input["scope"]); + // Authorize the client $clientCreds = $this->getClientCredentials($inputData, $authHeaders); @@ -753,7 +795,7 @@ public function grantAccessToken(Request $request = NULL) { throw new OAuth2ServerException(self::HTTP_BAD_REQUEST, self::ERROR_INVALID_SCOPE, 'An unsupported scope was requested.'); } - $token = $this->createAccessToken($client, $stored['data'], $stored['scope']); + $token = $this->createAccessToken($client, $stored['data'], $input['scope']); return new Response(json_encode($token), 200, $this->getJsonHeaders()); } @@ -1073,6 +1115,8 @@ public function finishClientAuthorization($is_authorized, $data = NULL, Request $result["query"]["state"] = $params["state"]; } + $scope = $this->checkScopePolicy($scope); + if ($is_authorized === FALSE) { throw new OAuth2RedirectException($params["redirect_uri"], self::ERROR_USER_DENIED, "The user denied access to your application", $params["state"]); } From 522747c47776539cf74f11d72bf95d9718c513ce Mon Sep 17 00:00:00 2001 From: Spomky Date: Mon, 21 Oct 2013 22:51:45 +0200 Subject: [PATCH 02/13] Fix test error The requested scope is only "scope1"in this test, so the access token only contains "scope1" and not "scope1 scope2" as before. --- tests/OAuth2Test.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/OAuth2Test.php b/tests/OAuth2Test.php index 44cc680..6df61a1 100644 --- a/tests/OAuth2Test.php +++ b/tests/OAuth2Test.php @@ -467,7 +467,7 @@ public function testGrantAccessTokenWithGrantUserWithScope() { array('date' => null) )); - $this->assertRegExp('{"access_token":"[^"]+","expires_in":3600,"token_type":"bearer","scope":"scope1 scope2"}', $response->getContent()); + $this->assertRegExp('{"access_token":"[^"]+","expires_in":3600,"token_type":"bearer","scope":"scope1"}', $response->getContent()); $token = $stub->getLastAccessToken(); $this->assertSame('cid', $token->getClientId()); From 31a7b2ea35824d311d1edba38ac98c63f58db1dc Mon Sep 17 00:00:00 2001 From: Spomky Date: Mon, 21 Oct 2013 23:25:17 +0200 Subject: [PATCH 03/13] Fix test error --- tests/OAuth2Test.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/OAuth2Test.php b/tests/OAuth2Test.php index 6df61a1..c1fcd40 100644 --- a/tests/OAuth2Test.php +++ b/tests/OAuth2Test.php @@ -505,7 +505,7 @@ public function testGrantAccessTokenWithGrantUserWithReducedScope() { $token = $stub->getLastAccessToken(); $this->assertSame('cid', $token->getClientId()); - $this->assertSame('scope1 scope2', $token->getScope()); + $this->assertSame('scope1', $token->getScope()); } /** From a97e6b67765441339eb30349bf82ce0959c1a476 Mon Sep 17 00:00:00 2001 From: Spomky Date: Tue, 22 Oct 2013 12:41:45 +0200 Subject: [PATCH 04/13] Function doc and tests Function documentation modified according to Stof's coments. Tests added. --- lib/OAuth2/OAuth2.php | 6 +- tests/OAuth2Test.php | 147 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 151 insertions(+), 2 deletions(-) diff --git a/lib/OAuth2/OAuth2.php b/lib/OAuth2/OAuth2.php index d276e8b..b6d58bf 100644 --- a/lib/OAuth2/OAuth2.php +++ b/lib/OAuth2/OAuth2.php @@ -671,8 +671,10 @@ protected function checkScope($required_scope, $available_scope) { * @param string $scope * The scopes to check. * - * @return - * The scopes or throw an exception + * @throws OAuth2ServerException + * + * @return string + * The modified scopes according to the policy. * * @see https://tools.ietf.org/html/rfc6749#section-3.3 * diff --git a/tests/OAuth2Test.php b/tests/OAuth2Test.php index c1fcd40..e7a9ce8 100644 --- a/tests/OAuth2Test.php +++ b/tests/OAuth2Test.php @@ -839,6 +839,153 @@ public function testFinishClientAuthorizationThrowsErrorIfUnauthorized() { } } + public function testScopePolicyWithErrorModeWithoutScopeInRequest() { + + $stub = new OAuth2GrantCodeStub; + $stub->addClient(new OAuth2Client('blah', 'foo', array('http://www.example.com/'))); + $oauth2 = new OAuth2($stub, array( + OAuth2::CONFIG_SUPPORTED_SCOPES => "scope1 scope2 scope3 scope4 scope5 scope6 scope7", + OAuth2::CONFIG_SCOPES_POLICY => "error", + )); + + $data = new \stdClass; + $request = new Request(array( + 'client_id' => 'blah', + 'redirect_uri' => 'http://www.example.com/?foo=bar', + 'response_type' => 'code', + 'state' => '42', + )); + + try { + $response = $oauth2->finishClientAuthorization(true, $data, $request); + $this->fail('The expected exception OAuth2ServerException was not thrown'); + } catch(OAuth2ServerException $e) { + $this->assertSame('invalid_scope', $e->getMessage()); + } + } + + public function testScopePolicyWithErrorModeWithScopesInRequest() { + + $stub = new OAuth2GrantCodeStub; + $stub->addClient(new OAuth2Client('blah', 'foo', array('http://www.example.com/'))); + $oauth2 = new OAuth2($stub, array( + OAuth2::CONFIG_SUPPORTED_SCOPES => "scope1 scope2 scope3 scope4 scope5 scope6 scope7", + OAuth2::CONFIG_SCOPES_POLICY => "error", + )); + + $data = new \stdClass; + $request = new Request(array( + 'client_id' => 'blah', + 'redirect_uri' => 'http://www.example.com/?foo=bar', + 'response_type' => 'code', + 'state' => '42', + )); + + $response = $oauth2->finishClientAuthorization(true, $data, $request, "scope1 scope2"); + + $code = $stub->getLastAuthCode(); + + $this->assertSame("scope1 scope2", $code->getScope()); + } + + public function testScopePolicyWithDefaultModeAndNoDefaultScopesWithoutScopeInRequest() { + + $stub = new OAuth2GrantCodeStub; + $stub->addClient(new OAuth2Client('blah', 'foo', array('http://www.example.com/'))); + $oauth2 = new OAuth2($stub, array( + OAuth2::CONFIG_SUPPORTED_SCOPES => "scope1 scope2 scope3 scope4 scope5 scope6 scope7", + OAuth2::CONFIG_SCOPES_POLICY => "default", + )); + + $data = new \stdClass; + $request = new Request(array( + 'client_id' => 'blah', + 'redirect_uri' => 'http://www.example.com/?foo=bar', + 'response_type' => 'code', + 'state' => '42', + )); + + $response = $oauth2->finishClientAuthorization(true, $data, $request, null); + + $code = $stub->getLastAuthCode(); + + $this->assertSame(null, $code->getScope()); + } + + public function testScopePolicyWithDefaultModeAndNoDefaultScopesWithScopesInRequest() { + + $stub = new OAuth2GrantCodeStub; + $stub->addClient(new OAuth2Client('blah', 'foo', array('http://www.example.com/'))); + $oauth2 = new OAuth2($stub, array( + OAuth2::CONFIG_SUPPORTED_SCOPES => "scope1 scope2 scope3 scope4 scope5 scope6 scope7", + OAuth2::CONFIG_SCOPES_POLICY => "default", + )); + + $data = new \stdClass; + $request = new Request(array( + 'client_id' => 'blah', + 'redirect_uri' => 'http://www.example.com/?foo=bar', + 'response_type' => 'code', + 'state' => '42', + )); + + $response = $oauth2->finishClientAuthorization(true, $data, $request, "scope1 scope2"); + + $code = $stub->getLastAuthCode(); + + $this->assertSame("scope1 scope2", $code->getScope()); + } + + public function testScopePolicyWithDefaultModeAndDefaultScopesWithoutScopeInRequest() { + + $stub = new OAuth2GrantCodeStub; + $stub->addClient(new OAuth2Client('blah', 'foo', array('http://www.example.com/'))); + $oauth2 = new OAuth2($stub, array( + OAuth2::CONFIG_SUPPORTED_SCOPES => "scope1 scope2 scope3 scope4 scope5 scope6 scope7", + OAuth2::CONFIG_SCOPES_POLICY => "default", + OAuth2::CONFIG_DEFAULT_SCOPES => "scope3 scope5 scope7", + )); + + $data = new \stdClass; + $request = new Request(array( + 'client_id' => 'blah', + 'redirect_uri' => 'http://www.example.com/?foo=bar', + 'response_type' => 'code', + 'state' => '42', + )); + + $response = $oauth2->finishClientAuthorization(true, $data, $request); + + $code = $stub->getLastAuthCode(); + + $this->assertSame("scope3 scope5 scope7", $code->getScope()); + } + + public function testScopePolicyWithDefaultModeAndDefaultScopesWithScopesInRequest() { + + $stub = new OAuth2GrantCodeStub; + $stub->addClient(new OAuth2Client('blah', 'foo', array('http://www.example.com/'))); + $oauth2 = new OAuth2($stub, array( + OAuth2::CONFIG_SUPPORTED_SCOPES => "scope1 scope2 scope3 scope4 scope5 scope6 scope7", + OAuth2::CONFIG_SCOPES_POLICY => "default", + OAuth2::CONFIG_DEFAULT_SCOPES => "scope3 scope5 scope7", + )); + + $data = new \stdClass; + $request = new Request(array( + 'client_id' => 'blah', + 'redirect_uri' => 'http://www.example.com/?foo=bar', + 'response_type' => 'code', + 'state' => '42', + )); + + $response = $oauth2->finishClientAuthorization(true, $data, $request, "scope1 scope2"); + + $code = $stub->getLastAuthCode(); + + $this->assertSame("scope1 scope2", $code->getScope()); + } + /** * @dataProvider getTestGetBearerTokenData */ From eaf17ddd86512a26eb760e10fb6bbe343faf22bf Mon Sep 17 00:00:00 2001 From: Spomky Date: Tue, 22 Oct 2013 18:39:31 +0200 Subject: [PATCH 05/13] Removed hardcoded policy values Hardcoded policy values are now constants --- lib/OAuth2/OAuth2.php | 26 +++++++++++++++++++------- tests/OAuth2Test.php | 12 ++++++------ 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/lib/OAuth2/OAuth2.php b/lib/OAuth2/OAuth2.php index b6d58bf..e4f55f5 100644 --- a/lib/OAuth2/OAuth2.php +++ b/lib/OAuth2/OAuth2.php @@ -86,6 +86,18 @@ class OAuth2 { const DEFAULT_AUTH_CODE_LIFETIME = 30; const DEFAULT_WWW_REALM = 'Service'; + /** + * Available scope policies. + * + * @var string + */ + const POLICY_MODE_ERROR = 'error'; + const POLICY_MODE_DEFAULT = 'default'; + static function supportedPolicies() + { + return array(self::POLICY_MODE_DEFAULT, self::POLICY_MODE_ERROR); + } + /** * Configurable options. * @@ -311,7 +323,7 @@ class OAuth2 { /** * The requested scope policy is invalid, unknown, or malformed. - * Must be either "error" or "default" + * See constants for supported policies * * @see https://tools.ietf.org/html/rfc6749#section-3.3 */ @@ -382,7 +394,7 @@ protected function setDefaultOptions() { self::CONFIG_ENFORCE_STATE => FALSE, self::CONFIG_SUPPORTED_SCOPES => null, // This is expected to be passed in on construction. Scopes can be an arbitrary string. - self::CONFIG_SCOPES_POLICY => "default", // This is expected to be passed in on construction. Scopes policy can be "default" or "error". + self::CONFIG_SCOPES_POLICY => self::POLICY_MODE_DEFAULT, // This is expected to be passed in on construction. See constants for supported policies. self::CONFIG_DEFAULT_SCOPES => null, // This is expected to be passed in on construction. Default scopes can be an arbitrary string. ); } @@ -415,8 +427,8 @@ public function getVariable($name, $default = NULL) { public function setVariable($name, $value) { $name = strtolower($name); - if( self::CONFIG_SCOPES_POLICY === $name && !in_array($value, array("error","default")) ) - throw new OAuth2ServerException(self::HTTP_BAD_REQUEST, self::ERROR_INVALID_SCOPE_POLICY, 'The policy must be either "error" or "default"'); + if( self::CONFIG_SCOPES_POLICY === $name && !in_array($value, self::supportedPolicies()) ) + throw new OAuth2ServerException(self::HTTP_BAD_REQUEST, self::ERROR_INVALID_SCOPE_POLICY, 'The policy must be one of these values: '.json_encode(self::supportedPolicies() )); $this->conf[$name] = $value; return $this; } @@ -666,7 +678,7 @@ protected function checkScope($required_scope, $available_scope) { } /** - * Checks whether the scope policy is respected + * Checks whether the scope policy is respected. * * @param string $scope * The scopes to check. @@ -682,12 +694,12 @@ protected function checkScope($required_scope, $available_scope) { */ protected function checkScopePolicy($scope) { // If Scopes Policy is set to "error" and no scope is input, then throws an error - if (!$scope && "error" === $this->getVariable(self::CONFIG_SCOPES_POLICY, "default") ) { + if (!$scope && self::POLICY_MODE_ERROR === $this->getVariable(self::CONFIG_SCOPES_POLICY, self::POLICY_MODE_DEFAULT) ) { throw new OAuth2ServerException(self::HTTP_BAD_REQUEST, self::ERROR_INVALID_SCOPE, 'No scope was requested.'); } // If Scopes Policy is set to "default" and no scope is input, then application defaults are set - if (!$scope && "default" === $this->getVariable(self::CONFIG_SCOPES_POLICY, "default") ) { + if (!$scope && self::POLICY_MODE_DEFAULT === $this->getVariable(self::CONFIG_SCOPES_POLICY, self::POLICY_MODE_DEFAULT) ) { return $this->getVariable(self::CONFIG_DEFAULT_SCOPES, null); } return $scope; diff --git a/tests/OAuth2Test.php b/tests/OAuth2Test.php index e7a9ce8..4bc5fa2 100644 --- a/tests/OAuth2Test.php +++ b/tests/OAuth2Test.php @@ -845,7 +845,7 @@ public function testScopePolicyWithErrorModeWithoutScopeInRequest() { $stub->addClient(new OAuth2Client('blah', 'foo', array('http://www.example.com/'))); $oauth2 = new OAuth2($stub, array( OAuth2::CONFIG_SUPPORTED_SCOPES => "scope1 scope2 scope3 scope4 scope5 scope6 scope7", - OAuth2::CONFIG_SCOPES_POLICY => "error", + OAuth2::CONFIG_SCOPES_POLICY => OAuth2::POLICY_MODE_ERROR, )); $data = new \stdClass; @@ -870,7 +870,7 @@ public function testScopePolicyWithErrorModeWithScopesInRequest() { $stub->addClient(new OAuth2Client('blah', 'foo', array('http://www.example.com/'))); $oauth2 = new OAuth2($stub, array( OAuth2::CONFIG_SUPPORTED_SCOPES => "scope1 scope2 scope3 scope4 scope5 scope6 scope7", - OAuth2::CONFIG_SCOPES_POLICY => "error", + OAuth2::CONFIG_SCOPES_POLICY => OAuth2::POLICY_MODE_ERROR, )); $data = new \stdClass; @@ -894,7 +894,7 @@ public function testScopePolicyWithDefaultModeAndNoDefaultScopesWithoutScopeInRe $stub->addClient(new OAuth2Client('blah', 'foo', array('http://www.example.com/'))); $oauth2 = new OAuth2($stub, array( OAuth2::CONFIG_SUPPORTED_SCOPES => "scope1 scope2 scope3 scope4 scope5 scope6 scope7", - OAuth2::CONFIG_SCOPES_POLICY => "default", + OAuth2::CONFIG_SCOPES_POLICY => OAuth2::POLICY_MODE_DEFAULT, )); $data = new \stdClass; @@ -918,7 +918,7 @@ public function testScopePolicyWithDefaultModeAndNoDefaultScopesWithScopesInRequ $stub->addClient(new OAuth2Client('blah', 'foo', array('http://www.example.com/'))); $oauth2 = new OAuth2($stub, array( OAuth2::CONFIG_SUPPORTED_SCOPES => "scope1 scope2 scope3 scope4 scope5 scope6 scope7", - OAuth2::CONFIG_SCOPES_POLICY => "default", + OAuth2::CONFIG_SCOPES_POLICY => OAuth2::POLICY_MODE_DEFAULT, )); $data = new \stdClass; @@ -942,7 +942,7 @@ public function testScopePolicyWithDefaultModeAndDefaultScopesWithoutScopeInRequ $stub->addClient(new OAuth2Client('blah', 'foo', array('http://www.example.com/'))); $oauth2 = new OAuth2($stub, array( OAuth2::CONFIG_SUPPORTED_SCOPES => "scope1 scope2 scope3 scope4 scope5 scope6 scope7", - OAuth2::CONFIG_SCOPES_POLICY => "default", + OAuth2::CONFIG_SCOPES_POLICY => OAuth2::POLICY_MODE_DEFAULT, OAuth2::CONFIG_DEFAULT_SCOPES => "scope3 scope5 scope7", )); @@ -967,7 +967,7 @@ public function testScopePolicyWithDefaultModeAndDefaultScopesWithScopesInReques $stub->addClient(new OAuth2Client('blah', 'foo', array('http://www.example.com/'))); $oauth2 = new OAuth2($stub, array( OAuth2::CONFIG_SUPPORTED_SCOPES => "scope1 scope2 scope3 scope4 scope5 scope6 scope7", - OAuth2::CONFIG_SCOPES_POLICY => "default", + OAuth2::CONFIG_SCOPES_POLICY => OAuth2::POLICY_MODE_DEFAULT, OAuth2::CONFIG_DEFAULT_SCOPES => "scope3 scope5 scope7", )); From a363448d6a3f4449c8c1caf46a23166d81d26a68 Mon Sep 17 00:00:00 2001 From: Spomky Date: Tue, 22 Oct 2013 18:55:25 +0200 Subject: [PATCH 06/13] New test added Unknown scope policy test added --- tests/OAuth2Test.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/OAuth2Test.php b/tests/OAuth2Test.php index 4bc5fa2..b4b2664 100644 --- a/tests/OAuth2Test.php +++ b/tests/OAuth2Test.php @@ -839,6 +839,22 @@ public function testFinishClientAuthorizationThrowsErrorIfUnauthorized() { } } + public function testScopePolicyWithUnknownMode() { + + $stub = new OAuth2GrantCodeStub; + $stub->addClient(new OAuth2Client('blah', 'foo', array('http://www.example.com/'))); + + try { + $oauth2 = new OAuth2($stub, array( + OAuth2::CONFIG_SCOPES_POLICY => 'custom_mode', + )); + + $this->fail('The expected exception OAuth2ServerException was not thrown'); + } catch(OAuth2ServerException $e) { + $this->assertSame('invalid_scope_policy', $e->getMessage()); + } + } + public function testScopePolicyWithErrorModeWithoutScopeInRequest() { $stub = new OAuth2GrantCodeStub; From 24006e308e7aed11b7cb3ab039c947999ec7e876 Mon Sep 17 00:00:00 2001 From: Spomky Date: Thu, 24 Oct 2013 17:16:03 +0200 Subject: [PATCH 07/13] invalid_scope_policy replaced by invalid_scope invalid_scope_policy is not part of the RFC6749 --- lib/OAuth2/OAuth2.php | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/lib/OAuth2/OAuth2.php b/lib/OAuth2/OAuth2.php index e4f55f5..14330a5 100644 --- a/lib/OAuth2/OAuth2.php +++ b/lib/OAuth2/OAuth2.php @@ -321,14 +321,6 @@ static function supportedPolicies() */ const ERROR_INVALID_SCOPE = 'invalid_scope'; - /** - * The requested scope policy is invalid, unknown, or malformed. - * See constants for supported policies - * - * @see https://tools.ietf.org/html/rfc6749#section-3.3 - */ - const ERROR_INVALID_SCOPE_POLICY = 'invalid_scope_policy'; - /** * The provided authorization grant is invalid, expired, * revoked, does not match the redirection URI used in the @@ -428,7 +420,7 @@ public function setVariable($name, $value) { $name = strtolower($name); if( self::CONFIG_SCOPES_POLICY === $name && !in_array($value, self::supportedPolicies()) ) - throw new OAuth2ServerException(self::HTTP_BAD_REQUEST, self::ERROR_INVALID_SCOPE_POLICY, 'The policy must be one of these values: '.json_encode(self::supportedPolicies() )); + throw new OAuth2ServerException(self::HTTP_BAD_REQUEST, self::ERROR_INVALID_SCOPE, 'The policy must be one of these values: '.json_encode(self::supportedPolicies() )); $this->conf[$name] = $value; return $this; } From 0e23c14cd024c9a11fdb986e33a638c2e0280c04 Mon Sep 17 00:00:00 2001 From: Spomky Date: Fri, 25 Oct 2013 15:06:48 +0200 Subject: [PATCH 08/13] Default policy is set as default config value --- lib/OAuth2/OAuth2.php | 3 ++- tests/OAuth2Test.php | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/OAuth2/OAuth2.php b/lib/OAuth2/OAuth2.php index 14330a5..42789f7 100644 --- a/lib/OAuth2/OAuth2.php +++ b/lib/OAuth2/OAuth2.php @@ -85,6 +85,7 @@ class OAuth2 { const DEFAULT_REFRESH_TOKEN_LIFETIME = 1209600; const DEFAULT_AUTH_CODE_LIFETIME = 30; const DEFAULT_WWW_REALM = 'Service'; + const DEFAULT_SCOPE_POLICY = self::POLICY_MODE_DEFAULT; /** * Available scope policies. @@ -386,7 +387,7 @@ protected function setDefaultOptions() { self::CONFIG_ENFORCE_STATE => FALSE, self::CONFIG_SUPPORTED_SCOPES => null, // This is expected to be passed in on construction. Scopes can be an arbitrary string. - self::CONFIG_SCOPES_POLICY => self::POLICY_MODE_DEFAULT, // This is expected to be passed in on construction. See constants for supported policies. + self::CONFIG_SCOPES_POLICY => self::DEFAULT_SCOPE_POLICY, // This is expected to be passed in on construction. See constants for supported policies. self::CONFIG_DEFAULT_SCOPES => null, // This is expected to be passed in on construction. Default scopes can be an arbitrary string. ); } diff --git a/tests/OAuth2Test.php b/tests/OAuth2Test.php index b4b2664..e75f5c9 100644 --- a/tests/OAuth2Test.php +++ b/tests/OAuth2Test.php @@ -851,7 +851,7 @@ public function testScopePolicyWithUnknownMode() { $this->fail('The expected exception OAuth2ServerException was not thrown'); } catch(OAuth2ServerException $e) { - $this->assertSame('invalid_scope_policy', $e->getMessage()); + $this->assertSame(Oauth2::ERROR_INVALID_SCOPE, $e->getMessage()); } } From 91b3a947d3a3f3c4d6a81815d833f62d38a55b1c Mon Sep 17 00:00:00 2001 From: Spomky Date: Mon, 4 Nov 2013 18:06:03 +0100 Subject: [PATCH 09/13] Fixes scopes missing using auth code This commit fixes the issue and adds a new test. --- lib/OAuth2/OAuth2.php | 3 +++ tests/OAuth2Test.php | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/lib/OAuth2/OAuth2.php b/lib/OAuth2/OAuth2.php index 42789f7..2b93013 100644 --- a/lib/OAuth2/OAuth2.php +++ b/lib/OAuth2/OAuth2.php @@ -771,6 +771,9 @@ public function grantAccessToken(Request $request = NULL) { switch ($input["grant_type"]) { case self::GRANT_TYPE_AUTH_CODE: $stored = $this->grantAccessTokenAuthCode($client, $input); // returns array('data' => data, 'scope' => scope) + if( isset($stored['scope'])) { + $input["scope"] = $stored["scope"]; + } break; case self::GRANT_TYPE_USER_CREDENTIALS: $stored = $this->grantAccessTokenUserCredentials($client, $input); // returns: true || array('scope' => scope) diff --git a/tests/OAuth2Test.php b/tests/OAuth2Test.php index e75f5c9..2e9c988 100644 --- a/tests/OAuth2Test.php +++ b/tests/OAuth2Test.php @@ -600,6 +600,44 @@ public function testFinishClientAuthorization() { $this->assertSame(null, $code->getScope()); $this->assertSame($data, $code->getData()); } + + /** + * Tests OAuth2->finishClientAuthorization() + */ + public function testFinishClientAuthorizationWithScopes() { + + $stub = new OAuth2GrantCodeStub; + $stub->addClient(new OAuth2Client('blah', 'foo', array('http://www.example.com/'))); + $stub->setAllowedGrantTypes(array('authorization_code')); + $oauth2 = new OAuth2($stub, array( + OAuth2::CONFIG_SUPPORTED_SCOPES => "scope1 scope2 scope3 scope4 scope5 scope6 scope7", + )); + + $data = new \stdClass; + + $response = $oauth2->finishClientAuthorization(true, $data, new Request(array( + 'client_id' => 'blah', + 'redirect_uri' => 'http://www.example.com/?foo=bar', + 'response_type' => 'code', + 'state' => '42', + )), + 'scope1 scope3 scope5' + ); + + $this->assertSame(302, $response->getStatusCode()); + $this->assertRegexp('#^http://www\.example\.com/\?foo=bar&state=42&code=#', $response->headers->get('location')); + + $code = $stub->getLastAuthCode(); + $this->assertSame('blah', $code->getClientId()); + $this->assertSame('scope1 scope3 scope5', $code->getScope()); + $this->assertSame($data, $code->getData()); + + $inputData = array('grant_type' => OAuth2::GRANT_TYPE_AUTH_CODE, 'client_id' => 'blah', 'client_secret' => 'foo', 'redirect_uri' => 'http://www.example.com/?foo=bars', 'code'=> $code->getToken()); + $request = $this->createRequest($inputData); + + $response = $oauth2->grantAccessToken($request); + $this->assertRegexp('{"access_token":"[^"]+","expires_in":3600,"token_type":"bearer","scope":"scope1 scope3 scope5"}', $response->getContent()); + } public function testFinishClientAuthorizationThrowsErrorIfClientIdMissing() { From 6b2c1de002fdad799bda78910a330d18255f22e0 Mon Sep 17 00:00:00 2001 From: Spomky Date: Tue, 24 Dec 2013 12:14:48 +0100 Subject: [PATCH 10/13] Default socpes and policy handled by clients * Clients can override the default scopes and scope policy defined by the application * Tests updated --- lib/OAuth2/Model/IOAuth2Client.php | 3 + lib/OAuth2/Model/OAuth2Client.php | 25 +- lib/OAuth2/OAuth2.php | 54 ++++- tests/OAuth2Test.php | 361 +++++++++++++++++------------ 4 files changed, 291 insertions(+), 152 deletions(-) diff --git a/lib/OAuth2/Model/IOAuth2Client.php b/lib/OAuth2/Model/IOAuth2Client.php index 20e3323..326eada 100644 --- a/lib/OAuth2/Model/IOAuth2Client.php +++ b/lib/OAuth2/Model/IOAuth2Client.php @@ -6,5 +6,8 @@ interface IOAuth2Client { public function getPublicId(); public function getRedirectUris(); + + public function getDefaultScopes(); + public function getScopePolicy(); } diff --git a/lib/OAuth2/Model/OAuth2Client.php b/lib/OAuth2/Model/OAuth2Client.php index c356dd8..c8df090 100644 --- a/lib/OAuth2/Model/OAuth2Client.php +++ b/lib/OAuth2/Model/OAuth2Client.php @@ -8,10 +8,16 @@ class OAuth2Client implements IOAuth2Client { private $redirectUris; private $secret; - public function __construct($id, $secret = NULL, array $redirectUris = array()) { + private $default_scopes; + private $scope_policy; + + public function __construct($id, $secret = NULL, array $redirectUris = array(), $scope_policy = null, $default_scopes = null) { $this->setPublicId($id); $this->setSecret($secret); $this->setRedirectUris($redirectUris); + + $this->setScopePolicy($scope_policy); + $this->setDefaultScopes($default_scopes); } public function setPublicId($id) { @@ -37,5 +43,22 @@ public function setRedirectUris(array $redirectUris) { public function getRedirectUris() { return $this->redirectUris; } + + + public function setDefaultScopes($default_scopes) { + $this->default_scopes = $default_scopes; + } + + public function getDefaultScopes() { + return $this->default_scopes; + } + + public function setScopePolicy($scope_policy) { + $this->scope_policy = $scope_policy; + } + + public function getScopePolicy() { + return $this->scope_policy; + } } diff --git a/lib/OAuth2/OAuth2.php b/lib/OAuth2/OAuth2.php index 2b93013..8986707 100644 --- a/lib/OAuth2/OAuth2.php +++ b/lib/OAuth2/OAuth2.php @@ -673,31 +673,68 @@ protected function checkScope($required_scope, $available_scope) { /** * Checks whether the scope policy is respected. * + * @param IOAuth2Client $client + * The client. + * * @param string $scope * The scopes to check. * * @throws OAuth2ServerException * * @return string - * The modified scopes according to the policy. + * The modified scopes according to the policy of the client or the server. * * @see https://tools.ietf.org/html/rfc6749#section-3.3 * * @ingroup oauth2_section_3.3 */ - protected function checkScopePolicy($scope) { + protected function checkScopePolicy(IOAuth2Client $client, $scope) { // If Scopes Policy is set to "error" and no scope is input, then throws an error - if (!$scope && self::POLICY_MODE_ERROR === $this->getVariable(self::CONFIG_SCOPES_POLICY, self::POLICY_MODE_DEFAULT) ) { + if (!$scope && self::POLICY_MODE_ERROR === $this->getScopePolicy($client) ) { throw new OAuth2ServerException(self::HTTP_BAD_REQUEST, self::ERROR_INVALID_SCOPE, 'No scope was requested.'); } - // If Scopes Policy is set to "default" and no scope is input, then application defaults are set - if (!$scope && self::POLICY_MODE_DEFAULT === $this->getVariable(self::CONFIG_SCOPES_POLICY, self::POLICY_MODE_DEFAULT) ) { - return $this->getVariable(self::CONFIG_DEFAULT_SCOPES, null); + // If Scopes Policy is set to "default" and no scope is input, then application or client defaults are set + if (!$scope && self::POLICY_MODE_DEFAULT === $this->getScopePolicy($client) ) { + return $this->getDefaultScopes($client); } return $scope; } + /** + * Get the scope policy. + * + * @param IOAuth2Client $client + * The client. + * + * @return string + * The scope policy depending on the client and the server. + * + * @see https://tools.ietf.org/html/rfc6749#section-3.3 + * + * @ingroup oauth2_section_3.3 + */ + protected function getScopePolicy(IOAuth2Client $client) { + return $client->getScopePolicy()?:$this->getVariable(self::CONFIG_SCOPES_POLICY, self::POLICY_MODE_DEFAULT); + } + + /** + * Get the default scopes. + * + * @param IOAuth2Client $client + * The client. + * + * @return string + * The default scopes depending on the client and the server. + * + * @see https://tools.ietf.org/html/rfc6749#section-3.3 + * + * @ingroup oauth2_section_3.3 + */ + protected function getDefaultScopes(IOAuth2Client $client) { + return $client->getDefaultScopes()?:$this->getVariable(self::CONFIG_DEFAULT_SCOPES, null); + } + // Access token granting (Section 4). /** @@ -748,7 +785,6 @@ public function grantAccessToken(Request $request = NULL) { throw new OAuth2ServerException(self::HTTP_BAD_REQUEST, self::ERROR_INVALID_REQUEST, 'Invalid grant_type parameter or parameter missing'); } - $input["scope"] = $this->checkScopePolicy($input["scope"]); // Authorize the client $clientCreds = $this->getClientCredentials($inputData, $authHeaders); @@ -767,6 +803,8 @@ public function grantAccessToken(Request $request = NULL) { throw new OAuth2ServerException(self::HTTP_BAD_REQUEST, self::ERROR_UNAUTHORIZED_CLIENT, 'The grant type is unauthorized for this client_id'); } + $input["scope"] = $this->checkScopePolicy($client, $input["scope"]); + // Do the granting switch ($input["grant_type"]) { case self::GRANT_TYPE_AUTH_CODE: @@ -1125,7 +1163,7 @@ public function finishClientAuthorization($is_authorized, $data = NULL, Request $result["query"]["state"] = $params["state"]; } - $scope = $this->checkScopePolicy($scope); + $scope = $this->checkScopePolicy($params["client"], $scope); if ($is_authorized === FALSE) { throw new OAuth2RedirectException($params["redirect_uri"], self::ERROR_USER_DENIED, "The user denied access to your application", $params["state"]); diff --git a/tests/OAuth2Test.php b/tests/OAuth2Test.php index 2e9c988..3fec361 100644 --- a/tests/OAuth2Test.php +++ b/tests/OAuth2Test.php @@ -877,167 +877,242 @@ public function testFinishClientAuthorizationThrowsErrorIfUnauthorized() { } } - public function testScopePolicyWithUnknownMode() { + /** + * Test for scope policies and default scopes + * @dataProvider getTestScopePolicyData + */ + public function testScopePolicy(OAuth2Client $client, array $server_options, Request $request = null, $exception = null, $exceptionMessage = null, $requested_scopes = null, $expected_scopes = null) { $stub = new OAuth2GrantCodeStub; - $stub->addClient(new OAuth2Client('blah', 'foo', array('http://www.example.com/'))); + $stub->addClient( $client ); try { - $oauth2 = new OAuth2($stub, array( - OAuth2::CONFIG_SCOPES_POLICY => 'custom_mode', - )); - - $this->fail('The expected exception OAuth2ServerException was not thrown'); - } catch(OAuth2ServerException $e) { - $this->assertSame(Oauth2::ERROR_INVALID_SCOPE, $e->getMessage()); - } - } - - public function testScopePolicyWithErrorModeWithoutScopeInRequest() { - - $stub = new OAuth2GrantCodeStub; - $stub->addClient(new OAuth2Client('blah', 'foo', array('http://www.example.com/'))); - $oauth2 = new OAuth2($stub, array( - OAuth2::CONFIG_SUPPORTED_SCOPES => "scope1 scope2 scope3 scope4 scope5 scope6 scope7", - OAuth2::CONFIG_SCOPES_POLICY => OAuth2::POLICY_MODE_ERROR, - )); - - $data = new \stdClass; - $request = new Request(array( - 'client_id' => 'blah', - 'redirect_uri' => 'http://www.example.com/?foo=bar', - 'response_type' => 'code', - 'state' => '42', - )); + $oauth2 = new OAuth2($stub, $server_options); + + if( $request !== null ) { + $data = new \stdClass; + $response = $oauth2->finishClientAuthorization(true, $data, $request, $requested_scopes); + + $code = $stub->getLastAuthCode(); + $this->assertSame($expected_scopes, $code->getScope()); + } - try { - $response = $oauth2->finishClientAuthorization(true, $data, $request); - $this->fail('The expected exception OAuth2ServerException was not thrown'); - } catch(OAuth2ServerException $e) { - $this->assertSame('invalid_scope', $e->getMessage()); + if( $exception !== null ) { + $this->fail('The expected exception OAuth2ServerException was not thrown'); + } + } catch(\Exception $e) { + if (!$exception || !($e instanceof $exception)) { + throw $e; + } + $this->assertSame($exceptionMessage, $e->getMessage()); } } - public function testScopePolicyWithErrorModeWithScopesInRequest() { - - $stub = new OAuth2GrantCodeStub; - $stub->addClient(new OAuth2Client('blah', 'foo', array('http://www.example.com/'))); - $oauth2 = new OAuth2($stub, array( - OAuth2::CONFIG_SUPPORTED_SCOPES => "scope1 scope2 scope3 scope4 scope5 scope6 scope7", - OAuth2::CONFIG_SCOPES_POLICY => OAuth2::POLICY_MODE_ERROR, - )); - - $data = new \stdClass; - $request = new Request(array( - 'client_id' => 'blah', - 'redirect_uri' => 'http://www.example.com/?foo=bar', - 'response_type' => 'code', - 'state' => '42', - )); - - $response = $oauth2->finishClientAuthorization(true, $data, $request, "scope1 scope2"); - - $code = $stub->getLastAuthCode(); - - $this->assertSame("scope1 scope2", $code->getScope()); - } - - public function testScopePolicyWithDefaultModeAndNoDefaultScopesWithoutScopeInRequest() { - - $stub = new OAuth2GrantCodeStub; - $stub->addClient(new OAuth2Client('blah', 'foo', array('http://www.example.com/'))); - $oauth2 = new OAuth2($stub, array( - OAuth2::CONFIG_SUPPORTED_SCOPES => "scope1 scope2 scope3 scope4 scope5 scope6 scope7", - OAuth2::CONFIG_SCOPES_POLICY => OAuth2::POLICY_MODE_DEFAULT, - )); - - $data = new \stdClass; - $request = new Request(array( - 'client_id' => 'blah', - 'redirect_uri' => 'http://www.example.com/?foo=bar', - 'response_type' => 'code', - 'state' => '42', - )); - - $response = $oauth2->finishClientAuthorization(true, $data, $request, null); - - $code = $stub->getLastAuthCode(); - - $this->assertSame(null, $code->getScope()); - } - - public function testScopePolicyWithDefaultModeAndNoDefaultScopesWithScopesInRequest() { - - $stub = new OAuth2GrantCodeStub; - $stub->addClient(new OAuth2Client('blah', 'foo', array('http://www.example.com/'))); - $oauth2 = new OAuth2($stub, array( - OAuth2::CONFIG_SUPPORTED_SCOPES => "scope1 scope2 scope3 scope4 scope5 scope6 scope7", - OAuth2::CONFIG_SCOPES_POLICY => OAuth2::POLICY_MODE_DEFAULT, - )); - - $data = new \stdClass; - $request = new Request(array( - 'client_id' => 'blah', - 'redirect_uri' => 'http://www.example.com/?foo=bar', - 'response_type' => 'code', - 'state' => '42', - )); - - $response = $oauth2->finishClientAuthorization(true, $data, $request, "scope1 scope2"); - - $code = $stub->getLastAuthCode(); - - $this->assertSame("scope1 scope2", $code->getScope()); - } - - public function testScopePolicyWithDefaultModeAndDefaultScopesWithoutScopeInRequest() { - - $stub = new OAuth2GrantCodeStub; - $stub->addClient(new OAuth2Client('blah', 'foo', array('http://www.example.com/'))); - $oauth2 = new OAuth2($stub, array( - OAuth2::CONFIG_SUPPORTED_SCOPES => "scope1 scope2 scope3 scope4 scope5 scope6 scope7", - OAuth2::CONFIG_SCOPES_POLICY => OAuth2::POLICY_MODE_DEFAULT, - OAuth2::CONFIG_DEFAULT_SCOPES => "scope3 scope5 scope7", - )); + public function getTestScopePolicyData() { + return array( + /** Scope policy and default scopes defined by the application **/ + // Unknown scope policy + array( + new OAuth2Client('blah', 'foo', array('http://www.example.com/')), + array( + OAuth2::CONFIG_SCOPES_POLICY => 'custom_mode', + ), + null, + 'OAuth2\OAuth2ServerException', + Oauth2::ERROR_INVALID_SCOPE, + ), - $data = new \stdClass; - $request = new Request(array( - 'client_id' => 'blah', - 'redirect_uri' => 'http://www.example.com/?foo=bar', - 'response_type' => 'code', - 'state' => '42', - )); + // Error policy without scope in request + array( + new OAuth2Client('blah', 'foo', array('http://www.example.com/')), + array( + OAuth2::CONFIG_SUPPORTED_SCOPES => "scope1 scope2 scope3 scope4 scope5 scope6 scope7", + OAuth2::CONFIG_SCOPES_POLICY => OAuth2::POLICY_MODE_ERROR, + ), + new Request(array( + 'client_id' => 'blah', + 'redirect_uri' => 'http://www.example.com/?foo=bar', + 'response_type' => 'code', + 'state' => '42', + )), + 'OAuth2\OAuth2ServerException', + Oauth2::ERROR_INVALID_SCOPE, + ), - $response = $oauth2->finishClientAuthorization(true, $data, $request); + // Error policy with scopes in request + array( + new OAuth2Client('blah', 'foo', array('http://www.example.com/')), + array( + OAuth2::CONFIG_SUPPORTED_SCOPES => "scope1 scope2 scope3 scope4 scope5 scope6 scope7", + OAuth2::CONFIG_SCOPES_POLICY => OAuth2::POLICY_MODE_ERROR, + ), + new Request(array( + 'client_id' => 'blah', + 'redirect_uri' => 'http://www.example.com/?foo=bar', + 'response_type' => 'code', + 'state' => '42', + )), + null, + null, + 'scope1 scope2', + 'scope1 scope2', + ), - $code = $stub->getLastAuthCode(); + // Default policy, no default scopes and no scope in request + array( + new OAuth2Client('blah', 'foo', array('http://www.example.com/')), + array( + OAuth2::CONFIG_SUPPORTED_SCOPES => "scope1 scope2 scope3 scope4 scope5 scope6 scope7", + OAuth2::CONFIG_SCOPES_POLICY => OAuth2::POLICY_MODE_DEFAULT, + ), + new Request(array( + 'client_id' => 'blah', + 'redirect_uri' => 'http://www.example.com/?foo=bar', + 'response_type' => 'code', + 'state' => '42', + )), + ), - $this->assertSame("scope3 scope5 scope7", $code->getScope()); - } + // Default policy, no default scopes and scopes in request + array( + new OAuth2Client('blah', 'foo', array('http://www.example.com/')), + array( + OAuth2::CONFIG_SUPPORTED_SCOPES => "scope1 scope2 scope3 scope4 scope5 scope6 scope7", + OAuth2::CONFIG_SCOPES_POLICY => OAuth2::POLICY_MODE_DEFAULT, + ), + new Request(array( + 'client_id' => 'blah', + 'redirect_uri' => 'http://www.example.com/?foo=bar', + 'response_type' => 'code', + 'state' => '42', + )), + null, + null, + 'scope1 scope2', + 'scope1 scope2', + ), - public function testScopePolicyWithDefaultModeAndDefaultScopesWithScopesInRequest() { + // Default policy, default scopes and scopes in request + array( + new OAuth2Client('blah', 'foo', array('http://www.example.com/')), + array( + OAuth2::CONFIG_SUPPORTED_SCOPES => "scope1 scope2 scope3 scope4 scope5 scope6 scope7", + OAuth2::CONFIG_SCOPES_POLICY => OAuth2::POLICY_MODE_DEFAULT, + OAuth2::CONFIG_DEFAULT_SCOPES => "scope3 scope5 scope7", + ), + new Request(array( + 'client_id' => 'blah', + 'redirect_uri' => 'http://www.example.com/?foo=bar', + 'response_type' => 'code', + 'state' => '42', + )), + null, + null, + 'scope1 scope2', + 'scope1 scope2', + ), - $stub = new OAuth2GrantCodeStub; - $stub->addClient(new OAuth2Client('blah', 'foo', array('http://www.example.com/'))); - $oauth2 = new OAuth2($stub, array( - OAuth2::CONFIG_SUPPORTED_SCOPES => "scope1 scope2 scope3 scope4 scope5 scope6 scope7", - OAuth2::CONFIG_SCOPES_POLICY => OAuth2::POLICY_MODE_DEFAULT, - OAuth2::CONFIG_DEFAULT_SCOPES => "scope3 scope5 scope7", - )); + /** Scope policy and default scopes defined by the client **/ + // Error policy and no scope in request + array( + new OAuth2Client('blah', 'foo', array('http://www.example.com/'), OAuth2::POLICY_MODE_ERROR), + array( + OAuth2::CONFIG_SUPPORTED_SCOPES => "scope1 scope2 scope3 scope4 scope5 scope6 scope7", + OAuth2::CONFIG_SCOPES_POLICY => OAuth2::POLICY_MODE_DEFAULT, + OAuth2::CONFIG_DEFAULT_SCOPES => "scope3 scope5 scope7", + ), + new Request(array( + 'client_id' => 'blah', + 'redirect_uri' => 'http://www.example.com/?foo=bar', + 'response_type' => 'code', + 'state' => '42', + )), + 'OAuth2\OAuth2ServerException', + Oauth2::ERROR_INVALID_SCOPE, + null, + null, + ), - $data = new \stdClass; - $request = new Request(array( - 'client_id' => 'blah', - 'redirect_uri' => 'http://www.example.com/?foo=bar', - 'response_type' => 'code', - 'state' => '42', - )); + // Error policy and scopes in request + array( + new OAuth2Client('blah', 'foo', array('http://www.example.com/'), OAuth2::POLICY_MODE_ERROR), + array( + OAuth2::CONFIG_SUPPORTED_SCOPES => "scope1 scope2 scope3 scope4 scope5 scope6 scope7", + OAuth2::CONFIG_SCOPES_POLICY => OAuth2::POLICY_MODE_DEFAULT, + OAuth2::CONFIG_DEFAULT_SCOPES => "scope3 scope5 scope7", + ), + new Request(array( + 'client_id' => 'blah', + 'redirect_uri' => 'http://www.example.com/?foo=bar', + 'response_type' => 'code', + 'state' => '42', + )), + null, + null, + 'scope1 scope2', + 'scope1 scope2', + ), - $response = $oauth2->finishClientAuthorization(true, $data, $request, "scope1 scope2"); + // Default policy, no default scopes in client and no scope in request + array( + new OAuth2Client('blah', 'foo', array('http://www.example.com/'), OAuth2::POLICY_MODE_DEFAULT), + array( + OAuth2::CONFIG_SUPPORTED_SCOPES => "scope1 scope2 scope3 scope4 scope5 scope6 scope7", + OAuth2::CONFIG_SCOPES_POLICY => OAuth2::POLICY_MODE_ERROR, + OAuth2::CONFIG_DEFAULT_SCOPES => "scope3 scope5 scope7", + ), + new Request(array( + 'client_id' => 'blah', + 'redirect_uri' => 'http://www.example.com/?foo=bar', + 'response_type' => 'code', + 'state' => '42', + )), + null, + null, + null, + 'scope3 scope5 scope7', + ), - $code = $stub->getLastAuthCode(); + // Default policy, default scopes in client and no scope in request + array( + new OAuth2Client('blah', 'foo', array('http://www.example.com/'), OAuth2::POLICY_MODE_DEFAULT, 'scope4 scope6'), + array( + OAuth2::CONFIG_SUPPORTED_SCOPES => "scope1 scope2 scope3 scope4 scope5 scope6 scope7", + OAuth2::CONFIG_SCOPES_POLICY => OAuth2::POLICY_MODE_ERROR, + OAuth2::CONFIG_DEFAULT_SCOPES => "scope3 scope5 scope7", + ), + new Request(array( + 'client_id' => 'blah', + 'redirect_uri' => 'http://www.example.com/?foo=bar', + 'response_type' => 'code', + 'state' => '42', + )), + null, + null, + null, + 'scope4 scope6', + ), - $this->assertSame("scope1 scope2", $code->getScope()); + // Default policy, default scopes in client and scopes in request + array( + new OAuth2Client('blah', 'foo', array('http://www.example.com/'), OAuth2::POLICY_MODE_DEFAULT, 'scope4 scope6'), + array( + OAuth2::CONFIG_SUPPORTED_SCOPES => "scope1 scope2 scope3 scope4 scope5 scope6 scope7", + OAuth2::CONFIG_SCOPES_POLICY => OAuth2::POLICY_MODE_ERROR, + OAuth2::CONFIG_DEFAULT_SCOPES => "scope3 scope5 scope7", + ), + new Request(array( + 'client_id' => 'blah', + 'redirect_uri' => 'http://www.example.com/?foo=bar', + 'response_type' => 'code', + 'state' => '42', + )), + null, + null, + 'scope1 scope2', + 'scope1 scope2', + ), + ); } /** From 1784261eb79c66679729342204be52ad31739627 Mon Sep 17 00:00:00 2001 From: Spomky Date: Tue, 24 Dec 2013 12:29:38 +0100 Subject: [PATCH 11/13] Bug fixed Policy of clients was not checked. Test added --- lib/OAuth2/OAuth2.php | 11 +++++++---- tests/OAuth2Test.php | 43 ++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/lib/OAuth2/OAuth2.php b/lib/OAuth2/OAuth2.php index 8986707..1652280 100644 --- a/lib/OAuth2/OAuth2.php +++ b/lib/OAuth2/OAuth2.php @@ -420,8 +420,6 @@ public function getVariable($name, $default = NULL) { public function setVariable($name, $value) { $name = strtolower($name); - if( self::CONFIG_SCOPES_POLICY === $name && !in_array($value, self::supportedPolicies()) ) - throw new OAuth2ServerException(self::HTTP_BAD_REQUEST, self::ERROR_INVALID_SCOPE, 'The policy must be one of these values: '.json_encode(self::supportedPolicies() )); $this->conf[$name] = $value; return $this; } @@ -689,13 +687,18 @@ protected function checkScope($required_scope, $available_scope) { * @ingroup oauth2_section_3.3 */ protected function checkScopePolicy(IOAuth2Client $client, $scope) { + + $policy = $this->getScopePolicy($client); + if( !in_array($policy, self::supportedPolicies()) ) + throw new OAuth2ServerException(self::HTTP_BAD_REQUEST, self::ERROR_INVALID_SCOPE, 'The policy must be one of these values: '.json_encode(self::supportedPolicies() )); + // If Scopes Policy is set to "error" and no scope is input, then throws an error - if (!$scope && self::POLICY_MODE_ERROR === $this->getScopePolicy($client) ) { + if (!$scope && self::POLICY_MODE_ERROR === $policy ) { throw new OAuth2ServerException(self::HTTP_BAD_REQUEST, self::ERROR_INVALID_SCOPE, 'No scope was requested.'); } // If Scopes Policy is set to "default" and no scope is input, then application or client defaults are set - if (!$scope && self::POLICY_MODE_DEFAULT === $this->getScopePolicy($client) ) { + if (!$scope && self::POLICY_MODE_DEFAULT === $policy ) { return $this->getDefaultScopes($client); } return $scope; diff --git a/tests/OAuth2Test.php b/tests/OAuth2Test.php index 3fec361..417954c 100644 --- a/tests/OAuth2Test.php +++ b/tests/OAuth2Test.php @@ -881,7 +881,7 @@ public function testFinishClientAuthorizationThrowsErrorIfUnauthorized() { * Test for scope policies and default scopes * @dataProvider getTestScopePolicyData */ - public function testScopePolicy(OAuth2Client $client, array $server_options, Request $request = null, $exception = null, $exceptionMessage = null, $requested_scopes = null, $expected_scopes = null) { + public function testScopePolicy(OAuth2Client $client, array $server_options, Request $request = null, $exception = null, $exceptionMessage = null, $exceptionDescription = null, $requested_scopes = null, $expected_scopes = null) { $stub = new OAuth2GrantCodeStub; $stub->addClient( $client ); @@ -898,13 +898,14 @@ public function testScopePolicy(OAuth2Client $client, array $server_options, Req } if( $exception !== null ) { - $this->fail('The expected exception OAuth2ServerException was not thrown'); + $this->fail('The expected exception was not thrown'); } } catch(\Exception $e) { if (!$exception || !($e instanceof $exception)) { throw $e; } $this->assertSame($exceptionMessage, $e->getMessage()); + $this->assertSame($exceptionDescription, $e->getDescription()); } } @@ -917,9 +918,15 @@ public function getTestScopePolicyData() { array( OAuth2::CONFIG_SCOPES_POLICY => 'custom_mode', ), - null, + new Request(array( + 'client_id' => 'blah', + 'redirect_uri' => 'http://www.example.com/?foo=bar', + 'response_type' => 'code', + 'state' => '42', + )), 'OAuth2\OAuth2ServerException', Oauth2::ERROR_INVALID_SCOPE, + 'The policy must be one of these values: '.json_encode(OAuth2::supportedPolicies()), ), // Error policy without scope in request @@ -937,6 +944,7 @@ public function getTestScopePolicyData() { )), 'OAuth2\OAuth2ServerException', Oauth2::ERROR_INVALID_SCOPE, + 'No scope was requested.', ), // Error policy with scopes in request @@ -954,6 +962,7 @@ public function getTestScopePolicyData() { )), null, null, + null, 'scope1 scope2', 'scope1 scope2', ), @@ -988,6 +997,7 @@ public function getTestScopePolicyData() { )), null, null, + null, 'scope1 scope2', 'scope1 scope2', ), @@ -1008,11 +1018,33 @@ public function getTestScopePolicyData() { )), null, null, + null, 'scope1 scope2', 'scope1 scope2', ), /** Scope policy and default scopes defined by the client **/ + // Unknown policy + array( + new OAuth2Client('blah', 'foo', array('http://www.example.com/'), 'unknown_policy'), + array( + OAuth2::CONFIG_SUPPORTED_SCOPES => "scope1 scope2 scope3 scope4 scope5 scope6 scope7", + OAuth2::CONFIG_SCOPES_POLICY => OAuth2::POLICY_MODE_DEFAULT, + OAuth2::CONFIG_DEFAULT_SCOPES => "scope3 scope5 scope7", + ), + new Request(array( + 'client_id' => 'blah', + 'redirect_uri' => 'http://www.example.com/?foo=bar', + 'response_type' => 'code', + 'state' => '42', + )), + 'OAuth2\OAuth2ServerException', + Oauth2::ERROR_INVALID_SCOPE, + 'The policy must be one of these values: '.json_encode(OAuth2::supportedPolicies()), + null, + null, + ), + // Error policy and no scope in request array( new OAuth2Client('blah', 'foo', array('http://www.example.com/'), OAuth2::POLICY_MODE_ERROR), @@ -1029,6 +1061,7 @@ public function getTestScopePolicyData() { )), 'OAuth2\OAuth2ServerException', Oauth2::ERROR_INVALID_SCOPE, + 'No scope was requested.', null, null, ), @@ -1049,6 +1082,7 @@ public function getTestScopePolicyData() { )), null, null, + null, 'scope1 scope2', 'scope1 scope2', ), @@ -1070,6 +1104,7 @@ public function getTestScopePolicyData() { null, null, null, + null, 'scope3 scope5 scope7', ), @@ -1090,6 +1125,7 @@ public function getTestScopePolicyData() { null, null, null, + null, 'scope4 scope6', ), @@ -1109,6 +1145,7 @@ public function getTestScopePolicyData() { )), null, null, + null, 'scope1 scope2', 'scope1 scope2', ), From 426d1424c5692af6469a2b616f24c0faa223294b Mon Sep 17 00:00:00 2001 From: Spomky Date: Wed, 25 Dec 2013 22:02:48 +0100 Subject: [PATCH 12/13] Supoorted scopes defined by clients Supported scopes can be defined by clients (eg: limitation depending on the client type). --- lib/OAuth2/Model/IOAuth2Client.php | 1 + lib/OAuth2/Model/OAuth2Client.php | 13 ++- lib/OAuth2/OAuth2.php | 26 +++++- tests/OAuth2Test.php | 123 +++++++++++++++++++++++++++++ 4 files changed, 160 insertions(+), 3 deletions(-) diff --git a/lib/OAuth2/Model/IOAuth2Client.php b/lib/OAuth2/Model/IOAuth2Client.php index 326eada..2af1ee8 100644 --- a/lib/OAuth2/Model/IOAuth2Client.php +++ b/lib/OAuth2/Model/IOAuth2Client.php @@ -7,6 +7,7 @@ interface IOAuth2Client { public function getPublicId(); public function getRedirectUris(); + public function getSupportedScopes(); public function getDefaultScopes(); public function getScopePolicy(); } diff --git a/lib/OAuth2/Model/OAuth2Client.php b/lib/OAuth2/Model/OAuth2Client.php index c8df090..f3152d3 100644 --- a/lib/OAuth2/Model/OAuth2Client.php +++ b/lib/OAuth2/Model/OAuth2Client.php @@ -8,16 +8,18 @@ class OAuth2Client implements IOAuth2Client { private $redirectUris; private $secret; + private $supported_scopes; private $default_scopes; private $scope_policy; - public function __construct($id, $secret = NULL, array $redirectUris = array(), $scope_policy = null, $default_scopes = null) { + public function __construct($id, $secret = NULL, array $redirectUris = array(), $scope_policy = null, $default_scopes = null, $supported_scopes = null) { $this->setPublicId($id); $this->setSecret($secret); $this->setRedirectUris($redirectUris); $this->setScopePolicy($scope_policy); $this->setDefaultScopes($default_scopes); + $this->setSupportedScopes($supported_scopes); } public function setPublicId($id) { @@ -45,6 +47,15 @@ public function getRedirectUris() { } + public function setSupportedScopes($supported_scopes) { + $this->supported_scopes = $supported_scopes; + } + + public function getSupportedScopes() { + return $this->supported_scopes; + } + + public function setDefaultScopes($default_scopes) { $this->default_scopes = $default_scopes; } diff --git a/lib/OAuth2/OAuth2.php b/lib/OAuth2/OAuth2.php index 1652280..d474d7c 100644 --- a/lib/OAuth2/OAuth2.php +++ b/lib/OAuth2/OAuth2.php @@ -738,6 +738,23 @@ protected function getDefaultScopes(IOAuth2Client $client) { return $client->getDefaultScopes()?:$this->getVariable(self::CONFIG_DEFAULT_SCOPES, null); } + /** + * Get the available scopes. + * + * @param IOAuth2Client $client + * The client. + * + * @return string + * The available scopes depending on the client and the server. + * + * @see https://tools.ietf.org/html/rfc6749#section-3.3 + * + * @ingroup oauth2_section_3.3 + */ + protected function getSupportedScopes(IOAuth2Client $client) { + return $client->getSupportedScopes()?:$this->getVariable(self::CONFIG_SUPPORTED_SCOPES, null); + } + // Access token granting (Section 4). /** @@ -839,7 +856,7 @@ public function grantAccessToken(Request $request = NULL) { // if no scope provided to check against $input['scope'] then application defaults are set // if no data is provided than null is set - $stored += array('scope' => $this->getVariable(self::CONFIG_SUPPORTED_SCOPES, null), 'data' => null); + $stored += array('scope' => $this->getSupportedScopes($client), 'data' => null); // Check scope, if provided if ($input["scope"] && (!isset($stored["scope"]) || !$this->checkScope($input["scope"], $stored["scope"]))) { @@ -1073,7 +1090,7 @@ protected function getAuthorizeParams(Request $request = NULL) { } // Validate that the requested scope is supported - if ($input["scope"] && !$this->checkScope($input["scope"], $this->getVariable(self::CONFIG_SUPPORTED_SCOPES))) { + if ($input["scope"] && !$this->checkScope($input["scope"], $this->getSupportedScopes($client))) { throw new OAuth2RedirectException($input["redirect_uri"], self::ERROR_INVALID_SCOPE, 'An unsupported scope was requested.', $input["state"]); } @@ -1166,6 +1183,11 @@ public function finishClientAuthorization($is_authorized, $data = NULL, Request $result["query"]["state"] = $params["state"]; } + // Validate that the requested scope is supported + if ($scope && !$this->checkScope($scope, $this->getSupportedScopes($params["client"]))) { + throw new OAuth2RedirectException($params["redirect_uri"], self::ERROR_INVALID_SCOPE, 'An unsupported scope was requested.', $params["state"]); + } + $scope = $this->checkScopePolicy($params["client"], $scope); if ($is_authorized === FALSE) { diff --git a/tests/OAuth2Test.php b/tests/OAuth2Test.php index 417954c..d637f05 100644 --- a/tests/OAuth2Test.php +++ b/tests/OAuth2Test.php @@ -1149,6 +1149,129 @@ public function getTestScopePolicyData() { 'scope1 scope2', 'scope1 scope2', ), + + /* Scope policy with supported scope defined by the client */ + // No scope are requested and default scopes should be set + array( + new OAuth2Client('blah', 'foo', array('http://www.example.com/'), OAuth2::POLICY_MODE_DEFAULT, 'scope6', 'scope4 scope5 scope6'), + array( + OAuth2::CONFIG_SUPPORTED_SCOPES => "scope1 scope2 scope3", + OAuth2::CONFIG_SCOPES_POLICY => OAuth2::POLICY_MODE_ERROR, + OAuth2::CONFIG_DEFAULT_SCOPES => "scope2", + ), + new Request(array( + 'client_id' => 'blah', + 'redirect_uri' => 'http://www.example.com/?foo=bar', + 'response_type' => 'code', + 'state' => '42', + )), + null, + null, + null, + null, + 'scope6', + ), + + // No scope are requested and error mode is set + array( + new OAuth2Client('blah', 'foo', array('http://www.example.com/'), OAuth2::POLICY_MODE_ERROR, null, 'scope4 scope5 scope6'), + array( + OAuth2::CONFIG_SUPPORTED_SCOPES => "scope1 scope2 scope3", + OAuth2::CONFIG_SCOPES_POLICY => OAuth2::POLICY_MODE_ERROR, + OAuth2::CONFIG_DEFAULT_SCOPES => "scope2", + ), + new Request(array( + 'client_id' => 'blah', + 'redirect_uri' => 'http://www.example.com/?foo=bar', + 'response_type' => 'code', + 'state' => '42', + )), + 'OAuth2\OAuth2ServerException', + Oauth2::ERROR_INVALID_SCOPE, + 'No scope was requested.', + ), + + // No scope are requested and error mode is set + array( + new OAuth2Client('blah', 'foo', array('http://www.example.com/'), OAuth2::POLICY_MODE_ERROR, null, 'scope4 scope5 scope6'), + array( + OAuth2::CONFIG_SUPPORTED_SCOPES => "scope1 scope2 scope3", + OAuth2::CONFIG_SCOPES_POLICY => OAuth2::POLICY_MODE_ERROR, + OAuth2::CONFIG_DEFAULT_SCOPES => "scope2", + ), + new Request(array( + 'client_id' => 'blah', + 'redirect_uri' => 'http://www.example.com/?foo=bar', + 'response_type' => 'code', + 'state' => '42', + )), + null, + null, + null, + 'scope6', + 'scope6' + ), + + // Scopes are requested and should be accepted + array( + new OAuth2Client('blah', 'foo', array('http://www.example.com/'), OAuth2::POLICY_MODE_DEFAULT, 'scope6', 'scope4 scope5 scope6'), + array( + OAuth2::CONFIG_SUPPORTED_SCOPES => "scope1 scope2 scope3", + OAuth2::CONFIG_SCOPES_POLICY => OAuth2::POLICY_MODE_ERROR, + OAuth2::CONFIG_DEFAULT_SCOPES => "scope2", + ), + new Request(array( + 'client_id' => 'blah', + 'redirect_uri' => 'http://www.example.com/?foo=bar', + 'response_type' => 'code', + 'state' => '42', + )), + null, + null, + null, + 'scope4 scope5', + 'scope4 scope5', + ), + + // Scopes are requested but are not supported by the client + array( + new OAuth2Client('blah', 'foo', array('http://www.example.com/'), OAuth2::POLICY_MODE_DEFAULT, 'scope6', 'scope4 scope5 scope6'), + array( + OAuth2::CONFIG_SUPPORTED_SCOPES => "scope1 scope2 scope3", + OAuth2::CONFIG_SCOPES_POLICY => OAuth2::POLICY_MODE_ERROR, + OAuth2::CONFIG_DEFAULT_SCOPES => "scope2", + ), + new Request(array( + 'client_id' => 'blah', + 'redirect_uri' => 'http://www.example.com/?foo=bar', + 'response_type' => 'code', + 'state' => '42', + )), + 'OAuth2\OAuth2RedirectException', + Oauth2::ERROR_INVALID_SCOPE, + 'An unsupported scope was requested.', + 'scope1 scope2', + ), + + // Scopes are requested but are not supported by the client + array( + new OAuth2Client('blah', 'foo', array('http://www.example.com/'), OAuth2::POLICY_MODE_ERROR, null, 'scope4 scope5 scope6'), + array( + OAuth2::CONFIG_SUPPORTED_SCOPES => "scope1 scope2 scope3", + OAuth2::CONFIG_SCOPES_POLICY => OAuth2::POLICY_MODE_ERROR, + OAuth2::CONFIG_DEFAULT_SCOPES => "scope2", + ), + new Request(array( + 'client_id' => 'blah', + 'redirect_uri' => 'http://www.example.com/?foo=bar', + 'response_type' => 'code', + 'state' => '42', + )), + 'OAuth2\OAuth2RedirectException', + Oauth2::ERROR_INVALID_SCOPE, + 'An unsupported scope was requested.', + 'scope1 scope2', + ), ); } From 61fe1bf12da41cf255153cd51db6e17cb93f3337 Mon Sep 17 00:00:00 2001 From: Spomky Date: Thu, 26 Dec 2013 11:07:18 +0100 Subject: [PATCH 13/13] Test fixed --- tests/OAuth2Test.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/OAuth2Test.php b/tests/OAuth2Test.php index d637f05..e97246e 100644 --- a/tests/OAuth2Test.php +++ b/tests/OAuth2Test.php @@ -467,7 +467,7 @@ public function testGrantAccessTokenWithGrantUserWithScope() { array('date' => null) )); - $this->assertRegExp('{"access_token":"[^"]+","expires_in":3600,"token_type":"bearer","scope":"scope1"}', $response->getContent()); + $this->assertRegExp('{"access_token":"[^"]+","expires_in":3600,"token_type":"bearer","scope":"scope1 scope2"}', $response->getContent()); $token = $stub->getLastAccessToken(); $this->assertSame('cid', $token->getClientId()); @@ -501,7 +501,7 @@ public function testGrantAccessTokenWithGrantUserWithReducedScope() { array('date' => null) )); - $this->assertRegExp('{"access_token":"[^"]+","expires_in":3600,"token_type":"bearer","scope":"scope1 scope2"}', $response->getContent()); + $this->assertRegExp('{"access_token":"[^"]+","expires_in":3600,"token_type":"bearer","scope":"scope1"}', $response->getContent()); $token = $stub->getLastAccessToken(); $this->assertSame('cid', $token->getClientId());