Skip to content

Commit

Permalink
feat: add support of PKCE (#1045)
Browse files Browse the repository at this point in the history
  • Loading branch information
thaarok committed Jun 12, 2023
1 parent d788c44 commit 894f672
Show file tree
Hide file tree
Showing 15 changed files with 120 additions and 29 deletions.
1 change: 1 addition & 0 deletions src/OAuth2/Controller/AuthorizeController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
35 changes: 35 additions & 0 deletions src/OAuth2/GrantType/AuthorizationCode.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
40 changes: 40 additions & 0 deletions src/OAuth2/OpenID/Controller/AuthorizeController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down Expand Up @@ -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;
}

Expand All @@ -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;
}

Expand Down
8 changes: 4 additions & 4 deletions src/OAuth2/OpenID/ResponseType/AuthorizationCode.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Expand All @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion src/OAuth2/OpenID/Storage/AuthorizationCodeInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
8 changes: 4 additions & 4 deletions src/OAuth2/ResponseType/AuthorizationCode.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Expand All @@ -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;
}
Expand Down
3 changes: 2 additions & 1 deletion src/OAuth2/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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());
Expand Down
4 changes: 2 additions & 2 deletions src/OAuth2/Storage/Cassandra.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
}
Expand Down
6 changes: 5 additions & 1 deletion src/OAuth2/Storage/CouchbaseDB.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand All @@ -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(
Expand All @@ -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,
));
}

Expand Down
4 changes: 2 additions & 2 deletions src/OAuth2/Storage/DynamoDB.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
4 changes: 2 additions & 2 deletions src/OAuth2/Storage/Memory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
6 changes: 5 additions & 1 deletion src/OAuth2/Storage/Mongo.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand All @@ -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 {
Expand All @@ -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);
}
Expand Down
6 changes: 5 additions & 1 deletion src/OAuth2/Storage/MongoDB.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand All @@ -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;
Expand All @@ -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;
Expand Down
Loading

0 comments on commit 894f672

Please sign in to comment.