diff --git a/src/OAuth2/Controller/AuthorizeController.php b/src/OAuth2/Controller/AuthorizeController.php index 4bafb1d24..b2e12bb5d 100644 --- a/src/OAuth2/Controller/AuthorizeController.php +++ b/src/OAuth2/Controller/AuthorizeController.php @@ -87,6 +87,7 @@ public function __construct(ClientInterface $clientStorage, array $responseTypes 'enforce_state' => true, 'require_exact_redirect_uri' => true, 'redirect_status_code' => 302, + 'enforce_pkce' => false, ), $config); if (is_null($scopeUtil)) { diff --git a/src/OAuth2/GrantType/AuthorizationCode.php b/src/OAuth2/GrantType/AuthorizationCode.php index 784f6b3a3..5bcb4f253 100644 --- a/src/OAuth2/GrantType/AuthorizationCode.php +++ b/src/OAuth2/GrantType/AuthorizationCode.php @@ -84,6 +84,41 @@ public function validateRequest(RequestInterface $request, ResponseInterface $re return false; } + if (isset($authCode['code_challenge']) && $authCode['code_challenge']) { + if (!($code_verifier = $request->request('code_verifier'))) { + $response->setError(400, 'code_verifier_missing', "The PKCE code verifier parameter is required."); + + return false; + } + // Validate code_verifier according to RFC-7636 + // @see: https://tools.ietf.org/html/rfc7636#section-4.1 + if (preg_match('/^[A-Za-z0-9-._~]{43,128}$/', $code_verifier) !== 1) { + $response->setError(400, 'code_verifier_invalid', "The PKCE code verifier parameter is invalid."); + + return false; + } + $code_verifier = $request->request('code_verifier'); + switch ($authCode['code_challenge_method']) { + case 'S256': + $code_verifier_hashed = strtr(rtrim(base64_encode(hash('sha256', $code_verifier, true)), '='), '+/', '-_'); + break; + + case 'plain': + $code_verifier_hashed = $code_verifier; + break; + + default: + $response->setError(400, 'code_challenge_method_invalid', "Unknown PKCE code challenge method."); + + return FALSE; + } + if ($code_verifier_hashed !== $authCode['code_challenge']) { + $response->setError(400, 'code_verifier_mismatch', "The PKCE code verifier parameter does not match the code challenge."); + + return FALSE; + } + } + if (!isset($authCode['code'])) { $authCode['code'] = $code; // used to expire the code after the access token is granted } diff --git a/src/OAuth2/OpenID/Controller/AuthorizeController.php b/src/OAuth2/OpenID/Controller/AuthorizeController.php index 54c5f9a63..52e183bb3 100644 --- a/src/OAuth2/OpenID/Controller/AuthorizeController.php +++ b/src/OAuth2/OpenID/Controller/AuthorizeController.php @@ -16,6 +16,16 @@ class AuthorizeController extends BaseAuthorizeController implements AuthorizeCo */ private $nonce; + /** + * @var mixed + */ + protected $code_challenge; + + /** + * @var mixed + */ + protected $code_challenge_method; + /** * Set not authorized response * @@ -65,6 +75,10 @@ protected function buildAuthorizeParameters($request, $response, $user_id) // add the nonce to return with the redirect URI $params['nonce'] = $this->nonce; + // Add PKCE code challenge. + $params['code_challenge'] = $this->code_challenge; + $params['code_challenge_method'] = $this->code_challenge_method; + return $params; } @@ -90,6 +104,32 @@ public function validateAuthorizeRequest(RequestInterface $request, ResponseInte $this->nonce = $nonce; + $code_challenge = $request->query('code_challenge'); + $code_challenge_method = $request->query('code_challenge_method'); + + if ($this->config['enforce_pkce']) { + if (!$code_challenge) { + $response->setError(400, 'missing_code_challenge', 'This application requires you provide a PKCE code challenge'); + + return false; + } + + if (preg_match('/^[A-Za-z0-9-._~]{43,128}$/', $code_challenge) !== 1) { + $response->setError(400, 'invalid_code_challenge', 'The PKCE code challenge supplied is invalid'); + + return false; + } + + if (!in_array($code_challenge_method, array('plain', 'S256'), true)) { + $response->setError(400, 'missing_code_challenge_method', 'This application requires you specify a PKCE code challenge method'); + + return false; + } + } + + $this->code_challenge = $code_challenge; + $this->code_challenge_method = $code_challenge_method; + return true; } diff --git a/src/OAuth2/OpenID/ResponseType/AuthorizationCode.php b/src/OAuth2/OpenID/ResponseType/AuthorizationCode.php index b8ad41ffb..19e04104d 100644 --- a/src/OAuth2/OpenID/ResponseType/AuthorizationCode.php +++ b/src/OAuth2/OpenID/ResponseType/AuthorizationCode.php @@ -31,9 +31,9 @@ public function getAuthorizeResponse($params, $user_id = null) // build the URL to redirect to $result = array('query' => array()); - $params += array('scope' => null, 'state' => null, 'id_token' => null); + $params += array('scope' => null, 'state' => null, 'id_token' => null, 'code_challenge' => null, 'code_challenge_method' => null); - $result['query']['code'] = $this->createAuthorizationCode($params['client_id'], $user_id, $params['redirect_uri'], $params['scope'], $params['id_token']); + $result['query']['code'] = $this->createAuthorizationCode($params['client_id'], $user_id, $params['redirect_uri'], $params['scope'], $params['id_token'], $params['code_challenge'], $params['code_challenge_method']); if (isset($params['state'])) { $result['query']['state'] = $params['state']; @@ -56,10 +56,10 @@ public function getAuthorizeResponse($params, $user_id = null) * @see http://tools.ietf.org/html/rfc6749#section-4 * @ingroup oauth2_section_4 */ - public function createAuthorizationCode($client_id, $user_id, $redirect_uri, $scope = null, $id_token = null) + public function createAuthorizationCode($client_id, $user_id, $redirect_uri, $scope = null, $id_token = null, $code_challenge = null, $code_challenge_method = null) { $code = $this->generateAuthorizationCode(); - $this->storage->setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, time() + $this->config['auth_code_lifetime'], $scope, $id_token); + $this->storage->setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, time() + $this->config['auth_code_lifetime'], $scope, $id_token, $code_challenge, $code_challenge_method); return $code; } diff --git a/src/OAuth2/OpenID/Storage/AuthorizationCodeInterface.php b/src/OAuth2/OpenID/Storage/AuthorizationCodeInterface.php index 446cec928..8e0988ff4 100644 --- a/src/OAuth2/OpenID/Storage/AuthorizationCodeInterface.php +++ b/src/OAuth2/OpenID/Storage/AuthorizationCodeInterface.php @@ -33,5 +33,5 @@ interface AuthorizationCodeInterface extends BaseAuthorizationCodeInterface * * @ingroup oauth2_section_4 */ - public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null); + public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null, $code_challenge = null, $code_challenge_method = null); } diff --git a/src/OAuth2/ResponseType/AuthorizationCode.php b/src/OAuth2/ResponseType/AuthorizationCode.php index b92c73cda..7b33f7ce1 100644 --- a/src/OAuth2/ResponseType/AuthorizationCode.php +++ b/src/OAuth2/ResponseType/AuthorizationCode.php @@ -26,9 +26,9 @@ public function getAuthorizeResponse($params, $user_id = null) // build the URL to redirect to $result = array('query' => array()); - $params += array('scope' => null, 'state' => null); + $params += array('scope' => null, 'state' => null, 'code_challenge' => null, 'code_challenge_method' => null); - $result['query']['code'] = $this->createAuthorizationCode($params['client_id'], $user_id, $params['redirect_uri'], $params['scope']); + $result['query']['code'] = $this->createAuthorizationCode($params['client_id'], $user_id, $params['redirect_uri'], $params['scope'], $params['code_challenge'], $params['code_challenge_method']); if (isset($params['state'])) { $result['query']['state'] = $params['state']; @@ -53,10 +53,10 @@ public function getAuthorizeResponse($params, $user_id = null) * @see http://tools.ietf.org/html/rfc6749#section-4 * @ingroup oauth2_section_4 */ - public function createAuthorizationCode($client_id, $user_id, $redirect_uri, $scope = null) + public function createAuthorizationCode($client_id, $user_id, $redirect_uri, $scope = null, $code_challenge = null, $code_challenge_method = null) { $code = $this->generateAuthorizationCode(); - $this->storage->setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, time() + $this->config['auth_code_lifetime'], $scope); + $this->storage->setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, time() + $this->config['auth_code_lifetime'], $scope, $code_challenge, $code_challenge_method); return $code; } diff --git a/src/OAuth2/Server.php b/src/OAuth2/Server.php index cf040c2bc..1fbc6666d 100644 --- a/src/OAuth2/Server.php +++ b/src/OAuth2/Server.php @@ -172,6 +172,7 @@ public function __construct($storage = array(), array $config = array(), array $ 'enforce_state' => true, 'require_exact_redirect_uri' => true, 'allow_implicit' => false, + 'enforce_pkce' => false, 'allow_credentials_in_request_body' => true, 'allow_public_clients' => true, 'always_issue_new_refresh_token' => false, @@ -577,7 +578,7 @@ protected function createDefaultAuthorizeController() } } - $config = array_intersect_key($this->config, array_flip(explode(' ', 'allow_implicit enforce_state require_exact_redirect_uri'))); + $config = array_intersect_key($this->config, array_flip(explode(' ', 'allow_implicit enforce_state require_exact_redirect_uri enforce_pkce'))); if ($this->config['use_openid_connect']) { return new OpenIDAuthorizeController($this->storages['client'], $this->responseTypes, $config, $this->getScopeUtil()); diff --git a/src/OAuth2/Storage/Cassandra.php b/src/OAuth2/Storage/Cassandra.php index e60e9d3ad..3a138bb52 100644 --- a/src/OAuth2/Storage/Cassandra.php +++ b/src/OAuth2/Storage/Cassandra.php @@ -191,11 +191,11 @@ public function getAuthorizationCode($code) * @param string $id_token * @return bool */ - public function setAuthorizationCode($authorization_code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) + public function setAuthorizationCode($authorization_code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null, $code_challenge = null, $code_challenge_method = null) { return $this->setValue( $this->config['code_key'] . $authorization_code, - compact('authorization_code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token'), + compact('authorization_code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token', 'code_challenge', 'code_challenge_method'), $expires ); } diff --git a/src/OAuth2/Storage/CouchbaseDB.php b/src/OAuth2/Storage/CouchbaseDB.php index 9e8148b6b..5c0d7b423 100755 --- a/src/OAuth2/Storage/CouchbaseDB.php +++ b/src/OAuth2/Storage/CouchbaseDB.php @@ -173,7 +173,7 @@ public function getAuthorizationCode($code) return is_null($code) ? false : $code; } - public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) + public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null, $code_challenge = null, $code_challenge_method = null) { // if it exists, update it. if ($this->getAuthorizationCode($code)) { @@ -185,6 +185,8 @@ public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, 'expires' => $expires, 'scope' => $scope, 'id_token' => $id_token, + 'code_challenge' => $code_challenge, + 'code_challenge_method' => $code_challenge_method, )); } else { $this->setObjectByType('code_table',$code,array( @@ -195,6 +197,8 @@ public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, 'expires' => $expires, 'scope' => $scope, 'id_token' => $id_token, + 'code_challenge' => $code_challenge, + 'code_challenge_method' => $code_challenge_method, )); } diff --git a/src/OAuth2/Storage/DynamoDB.php b/src/OAuth2/Storage/DynamoDB.php index a54cb3712..713189d23 100644 --- a/src/OAuth2/Storage/DynamoDB.php +++ b/src/OAuth2/Storage/DynamoDB.php @@ -213,12 +213,12 @@ public function getAuthorizationCode($code) } - public function setAuthorizationCode($authorization_code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) + public function setAuthorizationCode($authorization_code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null, $code_challenge = null, $code_challenge_method = null) { // convert expires to datestring $expires = date('Y-m-d H:i:s', $expires); - $clientData = compact('authorization_code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'id_token', 'scope'); + $clientData = compact('authorization_code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token', 'code_challenge', 'code_challenge_method'); $clientData = array_filter($clientData, 'self::isNotEmpty'); $result = $this->client->putItem(array( diff --git a/src/OAuth2/Storage/Memory.php b/src/OAuth2/Storage/Memory.php index 2c60b71ce..c33bd0ebb 100644 --- a/src/OAuth2/Storage/Memory.php +++ b/src/OAuth2/Storage/Memory.php @@ -74,9 +74,9 @@ public function getAuthorizationCode($code) ), $this->authorizationCodes[$code]); } - public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) + public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null, $code_challenge = null, $code_challenge_method = null) { - $this->authorizationCodes[$code] = compact('code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token'); + $this->authorizationCodes[$code] = compact('code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token', 'code_challenge', 'code_challenge_method'); return true; } diff --git a/src/OAuth2/Storage/Mongo.php b/src/OAuth2/Storage/Mongo.php index eea06e315..92f93d5b2 100644 --- a/src/OAuth2/Storage/Mongo.php +++ b/src/OAuth2/Storage/Mongo.php @@ -179,7 +179,7 @@ public function getAuthorizationCode($code) return is_null($code) ? false : $code; } - public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) + public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null, $code_challenge = null, $code_challenge_method = null) { // if it exists, update it. if ($this->getAuthorizationCode($code)) { @@ -192,6 +192,8 @@ public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, 'expires' => $expires, 'scope' => $scope, 'id_token' => $id_token, + 'code_challenge' => $code_challenge, + 'code_challenge_method' => $code_challenge_method, )) ); } else { @@ -203,6 +205,8 @@ public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, 'expires' => $expires, 'scope' => $scope, 'id_token' => $id_token, + 'code_challenge' => $code_challenge, + 'code_challenge_method' => $code_challenge_method, ); $this->collection('code_table')->insert($token); } diff --git a/src/OAuth2/Storage/MongoDB.php b/src/OAuth2/Storage/MongoDB.php index 64f740fc1..ef302ae57 100644 --- a/src/OAuth2/Storage/MongoDB.php +++ b/src/OAuth2/Storage/MongoDB.php @@ -167,7 +167,7 @@ public function getAuthorizationCode($code) return is_null($code) ? false : $code; } - public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) + public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null, $code_challenge = null, $code_challenge_method = null) { // if it exists, update it. if ($this->getAuthorizationCode($code)) { @@ -180,6 +180,8 @@ public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, 'expires' => $expires, 'scope' => $scope, 'id_token' => $id_token, + 'code_challenge' => $code_challenge, + 'code_challenge_method' => $code_challenge_method, )) ); return $result->getMatchedCount() > 0; @@ -192,6 +194,8 @@ public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, 'expires' => $expires, 'scope' => $scope, 'id_token' => $id_token, + 'code_challenge' => $code_challenge, + 'code_challenge_method' => $code_challenge_method, ); $result = $this->collection('code_table')->insertOne($token); return $result->getInsertedCount() > 0; diff --git a/src/OAuth2/Storage/Pdo.php b/src/OAuth2/Storage/Pdo.php index 074cee447..46c873359 100644 --- a/src/OAuth2/Storage/Pdo.php +++ b/src/OAuth2/Storage/Pdo.php @@ -247,7 +247,7 @@ public function getAuthorizationCode($code) * @param string $id_token * @return bool|mixed */ - public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) + public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null, $code_challenge = null, $code_challenge_method = null) { if (func_num_args() > 6) { // we are calling with an id token @@ -259,12 +259,12 @@ public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, // if it exists, update it. if ($this->getAuthorizationCode($code)) { - $stmt = $this->db->prepare($sql = sprintf('UPDATE %s SET client_id=:client_id, user_id=:user_id, redirect_uri=:redirect_uri, expires=:expires, scope=:scope where authorization_code=:code', $this->config['code_table'])); + $stmt = $this->db->prepare($sql = sprintf('UPDATE %s SET client_id=:client_id, user_id=:user_id, redirect_uri=:redirect_uri, expires=:expires, scope=:scope, code_challenge=:code_challenge, code_challenge_method=:code_challenge_method where authorization_code=:code', $this->config['code_table'])); } else { - $stmt = $this->db->prepare(sprintf('INSERT INTO %s (authorization_code, client_id, user_id, redirect_uri, expires, scope) VALUES (:code, :client_id, :user_id, :redirect_uri, :expires, :scope)', $this->config['code_table'])); + $stmt = $this->db->prepare(sprintf('INSERT INTO %s (authorization_code, client_id, user_id, redirect_uri, expires, scope, code_challenge, code_challenge_method) VALUES (:code, :client_id, :user_id, :redirect_uri, :expires, :scope, :code_challenge, :code_challenge_method)', $this->config['code_table'])); } - return $stmt->execute(compact('code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope')); + return $stmt->execute(compact('code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'code_challenge', 'code_challenge_method')); } /** @@ -277,19 +277,19 @@ public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, * @param string $id_token * @return bool */ - private function setAuthorizationCodeWithIdToken($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) + private function setAuthorizationCodeWithIdToken($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null, $code_challenge = null, $code_challenge_method = null) { // convert expires to datestring $expires = date('Y-m-d H:i:s', $expires); // if it exists, update it. if ($this->getAuthorizationCode($code)) { - $stmt = $this->db->prepare($sql = sprintf('UPDATE %s SET client_id=:client_id, user_id=:user_id, redirect_uri=:redirect_uri, expires=:expires, scope=:scope, id_token =:id_token where authorization_code=:code', $this->config['code_table'])); + $stmt = $this->db->prepare($sql = sprintf('UPDATE %s SET client_id=:client_id, user_id=:user_id, redirect_uri=:redirect_uri, expires=:expires, scope=:scope, id_token =:id_token, code_challenge=:code_challenge, code_challenge_method=:code_challenge_method where authorization_code=:code', $this->config['code_table'])); } else { - $stmt = $this->db->prepare(sprintf('INSERT INTO %s (authorization_code, client_id, user_id, redirect_uri, expires, scope, id_token) VALUES (:code, :client_id, :user_id, :redirect_uri, :expires, :scope, :id_token)', $this->config['code_table'])); + $stmt = $this->db->prepare(sprintf('INSERT INTO %s (authorization_code, client_id, user_id, redirect_uri, expires, scope, id_token, code_challenge, code_challenge_method) VALUES (:code, :client_id, :user_id, :redirect_uri, :expires, :scope, :id_token, :code_challenge, :code_challenge_method)', $this->config['code_table'])); } - return $stmt->execute(compact('code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token')); + return $stmt->execute(compact('code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token', 'code_challenge', 'code_challenge_method')); } /** @@ -676,6 +676,8 @@ public function getBuildSql($dbName = 'oauth2_server_php') expires TIMESTAMP NOT NULL, scope VARCHAR(4000), id_token VARCHAR(1000), + code_challenge VARCHAR(1000), + code_challenge_method VARCHAR(20), PRIMARY KEY (authorization_code) ); diff --git a/src/OAuth2/Storage/Redis.php b/src/OAuth2/Storage/Redis.php index e6294e22d..5a41dfc22 100644 --- a/src/OAuth2/Storage/Redis.php +++ b/src/OAuth2/Storage/Redis.php @@ -95,11 +95,11 @@ public function getAuthorizationCode($code) return $this->getValue($this->config['code_key'] . $code); } - public function setAuthorizationCode($authorization_code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) + public function setAuthorizationCode($authorization_code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null, $code_challenge = null, $code_challenge_method = null) { return $this->setValue( $this->config['code_key'] . $authorization_code, - compact('authorization_code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token'), + compact('authorization_code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token', 'code_challenge', 'code_challenge_method'), $expires ); }