diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/OAuth2/ClientAssertionType/HttpBasic.php | 29 | ||||
| -rw-r--r-- | src/OAuth2/Controller/TokenController.php | 1 | ||||
| -rw-r--r-- | src/OAuth2/Encryption/Jwt.php | 4 | ||||
| -rw-r--r-- | src/OAuth2/GrantType/ClientCredentials.php | 16 | ||||
| -rw-r--r-- | src/OAuth2/Server.php | 6 | ||||
| -rw-r--r-- | src/OAuth2/Storage/AccessTokenInterface.php | 1 | ||||
| -rw-r--r-- | src/OAuth2/Storage/ClientCredentialsInterface.php | 18 | ||||
| -rw-r--r-- | src/OAuth2/Storage/Memory.php | 10 | ||||
| -rw-r--r-- | src/OAuth2/Storage/Mongo.php | 12 | ||||
| -rw-r--r-- | src/OAuth2/Storage/Pdo.php | 31 | ||||
| -rw-r--r-- | src/OAuth2/Storage/Redis.php | 40 | ||||
| -rw-r--r-- | src/OAuth2/TokenType/Bearer.php | 16 |
12 files changed, 155 insertions, 29 deletions
diff --git a/src/OAuth2/ClientAssertionType/HttpBasic.php b/src/OAuth2/ClientAssertionType/HttpBasic.php index 2e8c89d..d755e1b 100644 --- a/src/OAuth2/ClientAssertionType/HttpBasic.php +++ b/src/OAuth2/ClientAssertionType/HttpBasic.php @@ -22,7 +22,8 @@ class HttpBasic implements ClientAssertionTypeInterface { $this->storage = $storage; $this->config = array_merge(array( - 'allow_credentials_in_request_body' => true + 'allow_credentials_in_request_body' => true, + 'allow_public_clients' => true, ), $config); } @@ -32,11 +33,23 @@ class HttpBasic implements ClientAssertionTypeInterface return false; } - if (!isset($clientData['client_id']) || !isset($clientData['client_secret'])) { - throw new \LogicException('the clientData array must have "client_id" and "client_secret" values set.'); + if (!isset($clientData['client_id'])) { + throw new \LogicException('the clientData array must have "client_id" set'); } - if ($this->storage->checkClientCredentials($clientData['client_id'], $clientData['client_secret']) === false) { + if (!isset($clientData['client_secret']) || $clientData['client_secret'] == '') { + if (!$this->config['allow_public_clients']) { + $response->setError(400, 'invalid_client', 'client credentials are required'); + + return false; + } + + if (!$this->storage->isPublicClient($clientData['client_id'])) { + $response->setError(400, 'invalid_client', 'This client is invalid or must authenticate using a client secret'); + + return false; + } + } elseif ($this->storage->checkClientCredentials($clientData['client_id'], $clientData['client_secret']) === false) { $response->setError(400, 'invalid_client', 'The client credentials are invalid'); return false; @@ -70,7 +83,7 @@ class HttpBasic implements ClientAssertionTypeInterface * @code * return array( * "client_id" => CLIENT_ID, // REQUIRED the client id - * "client_secret" => CLIENT_SECRET, // REQUIRED the client secret + * "client_secret" => CLIENT_SECRET, // OPTIONAL the client secret (may be omitted for public clients) * ); * @endcode * @@ -91,13 +104,13 @@ class HttpBasic implements ClientAssertionTypeInterface * client_secret can be null if the client's password is an empty string * @see http://tools.ietf.org/html/rfc6749#section-2.3.1 */ - - return array('client_id' => $request->request('client_id'), 'client_secret' => $request->request('client_secret', '')); + return array('client_id' => $request->request('client_id'), 'client_secret' => $request->request('client_secret')); } } if ($response) { - $response->setError(400, 'invalid_client', 'Client credentials were not found in the headers or body'); + $message = $this->config['allow_credentials_in_request_body'] ? ' or body' : ''; + $response->setError(400, 'invalid_client', 'Client credentials were not found in the headers'.$message); } return null; diff --git a/src/OAuth2/Controller/TokenController.php b/src/OAuth2/Controller/TokenController.php index a615354..277b477 100644 --- a/src/OAuth2/Controller/TokenController.php +++ b/src/OAuth2/Controller/TokenController.php @@ -86,6 +86,7 @@ class TokenController implements TokenControllerInterface return null; } + if (!isset($this->grantTypes[$grantTypeIdentifier])) { /* TODO: If this is an OAuth2 supported grant type that we have chosen not to implement, throw a 501 Not Implemented instead */ $response->setError(400, 'unsupported_grant_type', sprintf('Grant type "%s" not supported', $grantTypeIdentifier)); diff --git a/src/OAuth2/Encryption/Jwt.php b/src/OAuth2/Encryption/Jwt.php index 641a2e9..6bde68e 100644 --- a/src/OAuth2/Encryption/Jwt.php +++ b/src/OAuth2/Encryption/Jwt.php @@ -27,6 +27,10 @@ class Jwt implements EncryptionInterface public function decode($jwt, $key = null, $verify = true) { + if (!strpos($jwt, '.')) { + return false; + } + $tks = explode('.', $jwt); if (count($tks) != 3) { diff --git a/src/OAuth2/GrantType/ClientCredentials.php b/src/OAuth2/GrantType/ClientCredentials.php index 0a73e53..d18bb8c 100644 --- a/src/OAuth2/GrantType/ClientCredentials.php +++ b/src/OAuth2/GrantType/ClientCredentials.php @@ -4,6 +4,7 @@ namespace OAuth2\GrantType; use OAuth2\ClientAssertionType\HttpBasic; use OAuth2\ResponseType\AccessTokenInterface; +use OAuth2\Storage\ClientCredentialsInterface; /** * @author Brent Shaffer <bshafs at gmail dot com> @@ -14,6 +15,18 @@ class ClientCredentials extends HttpBasic implements GrantTypeInterface { private $clientData; + public function __construct(ClientCredentialsInterface $storage, array $config = array()) + { + /** + * The client credentials grant type MUST only be used by confidential clients + * + * @see http://tools.ietf.org/html/rfc6749#section-4.4 + */ + $config['allow_public_clients'] = false; + + parent::__construct($storage, $config); + } + public function getQuerystringIdentifier() { return 'client_credentials'; @@ -35,8 +48,9 @@ class ClientCredentials extends HttpBasic implements GrantTypeInterface public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope) { - /* + /** * Client Credentials Grant does NOT include a refresh token + * * @see http://tools.ietf.org/html/rfc6749#section-4.4.3 */ $includeRefreshToken = false; diff --git a/src/OAuth2/Server.php b/src/OAuth2/Server.php index 2d12c5b..12dfd3d 100644 --- a/src/OAuth2/Server.php +++ b/src/OAuth2/Server.php @@ -103,6 +103,7 @@ class Server implements ResourceControllerInterface, 'require_exact_redirect_uri' => true, 'allow_implicit' => false, 'allow_credentials_in_request_body' => true, + 'allow_public_clients' => true, 'always_issue_new_refresh_token' => false, ), $config); @@ -400,7 +401,7 @@ class Server implements ResourceControllerInterface, if (!isset($this->storages['client_credentials'])) { throw new \LogicException("You must supply a storage object implementing OAuth2\Storage\ClientCredentialsInterface to use the token server"); } - $config = array_intersect_key($this->config, array('allow_credentials_in_request_body' => '')); + $config = array_intersect_key($this->config, array_flip(explode(' ', 'allow_credentials_in_request_body allow_public_clients'))); $this->clientAssertionType = new HttpBasic($this->storages['client_credentials'], $config); break; } @@ -459,7 +460,8 @@ class Server implements ResourceControllerInterface, } if (isset($this->storages['client_credentials'])) { - $grantTypes['client_credentials'] = new ClientCredentials($this->storages['client_credentials']); + $config = array_intersect_key($this->config, array('allow_credentials_in_request_body' => '')); + $grantTypes['client_credentials'] = new ClientCredentials($this->storages['client_credentials'], $config); } if (isset($this->storages['refresh_token'])) { diff --git a/src/OAuth2/Storage/AccessTokenInterface.php b/src/OAuth2/Storage/AccessTokenInterface.php index b538ad2..dd687e4 100644 --- a/src/OAuth2/Storage/AccessTokenInterface.php +++ b/src/OAuth2/Storage/AccessTokenInterface.php @@ -23,6 +23,7 @@ interface AccessTokenInterface * is invalid: * - client_id: Stored client identifier. * - expires: Stored expiration in unix timestamp. + * - user_id: (optional) Stored user identifier. * - scope: (optional) Stored scope values in space-separated string. * * @ingroup oauth2_section_7 diff --git a/src/OAuth2/Storage/ClientCredentialsInterface.php b/src/OAuth2/Storage/ClientCredentialsInterface.php index 530157c..3318c69 100644 --- a/src/OAuth2/Storage/ClientCredentialsInterface.php +++ b/src/OAuth2/Storage/ClientCredentialsInterface.php @@ -28,4 +28,22 @@ interface ClientCredentialsInterface extends ClientInterface * @ingroup oauth2_section_3 */ public function checkClientCredentials($client_id, $client_secret = null); + + /** + * Determine if the client is a "public" client, and therefore + * does not require passing credentials for certain grant types + * + * @param $client_id + * Client identifier to be check with. + * + * @return + * TRUE if the client is public, and FALSE if it isn't. + * @endcode + * + * @see http://tools.ietf.org/html/rfc6749#section-2.3 + * @see https://github.com/bshaffer/oauth2-server-php/issues/257 + * + * @ingroup oauth2_section_2 + */ + public function isPublicClient($client_id); } diff --git a/src/OAuth2/Storage/Memory.php b/src/OAuth2/Storage/Memory.php index 7f5a0c7..ee59703 100644 --- a/src/OAuth2/Storage/Memory.php +++ b/src/OAuth2/Storage/Memory.php @@ -131,6 +131,16 @@ class Memory implements AuthorizationCodeInterface, return isset($this->clientCredentials[$client_id]['client_secret']) && $this->clientCredentials[$client_id]['client_secret'] === $client_secret; } + public function isPublicClient($client_id) + { + if (!isset($this->clientCredentials[$client_id])) { + return false; + } + + return empty($this->clientCredentials[$client_id]['client_secret']); + } + + /* ClientInterface */ public function getClientDetails($client_id) { if (!isset($this->clientCredentials[$client_id])) { diff --git a/src/OAuth2/Storage/Mongo.php b/src/OAuth2/Storage/Mongo.php index 3cc70c6..e198dfc 100644 --- a/src/OAuth2/Storage/Mongo.php +++ b/src/OAuth2/Storage/Mongo.php @@ -67,6 +67,16 @@ class Mongo implements AuthorizationCodeInterface, return false; } + public function isPublicClient($client_id) + { + if (!$result = $this->collection('client_table')->findOne(array('client_id' => $client_id))) { + return false; + } + + return empty($result['client_secret']); + } + + /* ClientInterface */ public function getClientDetails($client_id) { $result = $this->collection('client_table')->findOne(array('client_id' => $client_id)); @@ -304,6 +314,6 @@ class Mongo implements AuthorizationCodeInterface, public function setJti($client_id, $subject, $audience, $expiration, $jti) { //TODO: Needs mongodb implementation. - throw new \Exception('setJti() for the MongoDB driver is currently unimplemented.'); + throw new \Exception('setJti() for the MongoDB driver is currently unimplemented.'); } } diff --git a/src/OAuth2/Storage/Pdo.php b/src/OAuth2/Storage/Pdo.php index cd19b4c..1c2b522 100644 --- a/src/OAuth2/Storage/Pdo.php +++ b/src/OAuth2/Storage/Pdo.php @@ -59,7 +59,7 @@ class Pdo implements AuthorizationCodeInterface, ), $config); } - /* OAuth2_Storage_ClientCredentialsInterface */ + /* OAuth2\Storage\ClientCredentialsInterface */ public function checkClientCredentials($client_id, $client_secret = null) { $stmt = $this->db->prepare(sprintf('SELECT * from %s where client_id = :client_id', $this->config['client_table'])); @@ -70,6 +70,19 @@ class Pdo implements AuthorizationCodeInterface, return $result && $result['client_secret'] == $client_secret; } + public function isPublicClient($client_id) + { + $stmt = $this->db->prepare(sprintf('SELECT * from %s where client_id = :client_id', $this->config['client_table'])); + $stmt->execute(compact('client_id')); + + if (!$result = $stmt->fetch()) { + return false; + } + + return empty($result['client_secret']);; + } + + /* OAuth2\Storage\ClientInterface */ public function getClientDetails($client_id) { $stmt = $this->db->prepare(sprintf('SELECT * from %s where client_id = :client_id', $this->config['client_table'])); @@ -103,7 +116,7 @@ class Pdo implements AuthorizationCodeInterface, return true; } - /* OAuth2_Storage_AccessTokenInterface */ + /* OAuth2\Storage\AccessTokenInterface */ public function getAccessToken($access_token) { $stmt = $this->db->prepare(sprintf('SELECT * from %s where access_token = :access_token', $this->config['access_token_table'])); @@ -132,7 +145,7 @@ class Pdo implements AuthorizationCodeInterface, return $stmt->execute(compact('access_token', 'client_id', 'user_id', 'expires', 'scope')); } - /* OAuth2_Storage_AuthorizationCodeInterface */ + /* OAuth2\Storage\AuthorizationCodeInterface */ public function getAuthorizationCode($code) { $stmt = $this->db->prepare(sprintf('SELECT * from %s where authorization_code = :code', $this->config['code_table'])); @@ -168,7 +181,7 @@ class Pdo implements AuthorizationCodeInterface, return $stmt->execute(compact('code')); } - /* OAuth2_Storage_UserCredentialsInterface */ + /* OAuth2\Storage\UserCredentialsInterface */ public function checkUserCredentials($username, $password) { if ($user = $this->getUser($username)) { @@ -183,7 +196,7 @@ class Pdo implements AuthorizationCodeInterface, return $this->getUser($username); } - /* OAuth2_Storage_RefreshTokenInterface */ + /* OAuth2\Storage\RefreshTokenInterface */ public function getRefreshToken($refresh_token) { $stmt = $this->db->prepare(sprintf('SELECT * FROM %s WHERE refresh_token = :refresh_token', $this->config['refresh_token_table'])); @@ -254,8 +267,8 @@ class Pdo implements AuthorizationCodeInterface, public function scopeExists($scope, $client_id = null) { $scope = explode(' ', $scope); - $stmt = $this->db->prepare($sql = sprintf('SELECT scope FROM %s WHERE type="supported" AND (client_id=:client_id OR client_id IS NULL) ORDER BY client_id IS NOT NULL DESC', $this->config['scope_table'])); - $stmt->execute(compact('client_id')); + $stmt = $this->db->prepare($sql = sprintf('SELECT scope FROM %s WHERE type=:type AND (client_id=:client_id OR client_id IS NULL) ORDER BY client_id IS NOT NULL DESC', $this->config['scope_table'])); + $stmt->execute(array('client_id' => $client_id, 'type'=> 'supported')); if ($result = $stmt->fetch()) { $supportedScope = explode(' ', $result['scope']); @@ -268,8 +281,8 @@ class Pdo implements AuthorizationCodeInterface, public function getDefaultScope($client_id = null) { - $stmt = $this->db->prepare($sql = sprintf('SELECT scope FROM %s WHERE type="default" AND (client_id=:client_id OR client_id IS NULL) ORDER BY client_id IS NOT NULL DESC', $this->config['scope_table'])); - $stmt->execute(compact('client_id')); + $stmt = $this->db->prepare($sql = sprintf('SELECT scope FROM %s WHERE type=:type AND (client_id=:client_id OR client_id IS NULL) ORDER BY client_id IS NOT NULL DESC', $this->config['scope_table'])); + $stmt->execute(array('client_id' => $client_id, 'type' => 'default')); if ($result = $stmt->fetch()) { return $result['scope']; diff --git a/src/OAuth2/Storage/Redis.php b/src/OAuth2/Storage/Redis.php index 55d300c..0b2badc 100644 --- a/src/OAuth2/Storage/Redis.php +++ b/src/OAuth2/Storage/Redis.php @@ -67,18 +67,21 @@ class Redis implements AuthorizationCodeInterface, $str = json_encode($value); if ($expire > 0) { $seconds = $expire - time(); - - return $this->redis->setex($key, $seconds, $str); + $ret = $this->redis->setex($key, $seconds, $str); } else { - return $this->redis->set($key, $str); + $ret = $this->redis->set($key, $str); } + + // check that the key was set properly + // if this fails, an exception will usually thrown, so this step isn't strictly necessary + return $ret->getPayload() == 'OK'; } protected function expireValue($key) { unset($this->cache[$key]); - return $this->redis->expire($key); + return $this->redis->delete($key); } /* AuthorizationCodeInterface */ @@ -148,6 +151,16 @@ class Redis implements AuthorizationCodeInterface, && $client['client_secret'] == $client_secret; } + public function isPublicClient($client_id) + { + if (!$client = $this->getClientDetails($client_id)) { + return false; + } + + return empty($result['client_secret']);; + } + + /* ClientInterface */ public function getClientDetails($client_id) { return $this->getValue($this->config['client_key'] . $client_id); @@ -230,6 +243,21 @@ class Redis implements AuthorizationCodeInterface, return $result; } + public function setScope($scope, $client_id = null, $type = 'supported') + { + if (!in_array($type, array('default', 'supported'))) { + throw new \InvalidArgumentException('"$type" must be one of "default", "supported"'); + } + + if (is_null($client_id)) { + $key = $this->config['scope_key'].$type.':global'; + } else { + $key = $this->config['scope_key'].$type.':'.$client_id; + } + + return $this->setValue($key, $scope); + } + /*JWTBearerInterface */ public function getClientKey($client_id, $subject) { @@ -244,12 +272,12 @@ class Redis implements AuthorizationCodeInterface, public function getJti($client_id, $subject, $audience, $expiration, $jti) { //TODO: Needs redis implementation. - throw new \Exception('getJti() for the Redis driver is currently unimplemented.'); + throw new \Exception('getJti() for the Redis driver is currently unimplemented.'); } public function setJti($client_id, $subject, $audience, $expiration, $jti) { //TODO: Needs redis implementation. - throw new \Exception('setJti() for the Redis driver is currently unimplemented.'); + throw new \Exception('setJti() for the Redis driver is currently unimplemented.'); } } diff --git a/src/OAuth2/TokenType/Bearer.php b/src/OAuth2/TokenType/Bearer.php index 4355275..f437a0b 100644 --- a/src/OAuth2/TokenType/Bearer.php +++ b/src/OAuth2/TokenType/Bearer.php @@ -51,13 +51,25 @@ class Bearer implements TokenTypeInterface { $headers = $request->headers('AUTHORIZATION'); - // Check that exactly one method was used - $methodsUsed = !empty($headers) + !is_null($request->query($this->config['token_param_name'])) + !is_null($request->request($this->config['token_param_name'])); + /** + * Ensure more than one method is not used for including an + * access token + * + * @see http://tools.ietf.org/html/rfc6750#section-3.1 + */ + $methodsUsed = !empty($headers) + (bool)($request->query($this->config['token_param_name'])) + (bool)($request->request($this->config['token_param_name'])); if ($methodsUsed > 1) { $response->setError(400, 'invalid_request', 'Only one method may be used to authenticate at a time (Auth header, GET or POST)'); return null; } + + /** + * If no authentication is provided, set the status code + * to 401 and return no other error information + * + * @see http://tools.ietf.org/html/rfc6750#section-3.1 + */ if ($methodsUsed == 0) { $response->setStatusCode(401); |
