Skip to content

Commit 894f672

Browse files
authored
feat: add support of PKCE (#1045)
1 parent d788c44 commit 894f672

File tree

15 files changed

+120
-29
lines changed

15 files changed

+120
-29
lines changed

src/OAuth2/Controller/AuthorizeController.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ public function __construct(ClientInterface $clientStorage, array $responseTypes
8787
'enforce_state' => true,
8888
'require_exact_redirect_uri' => true,
8989
'redirect_status_code' => 302,
90+
'enforce_pkce' => false,
9091
), $config);
9192

9293
if (is_null($scopeUtil)) {

src/OAuth2/GrantType/AuthorizationCode.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,41 @@ public function validateRequest(RequestInterface $request, ResponseInterface $re
8484
return false;
8585
}
8686

87+
if (isset($authCode['code_challenge']) && $authCode['code_challenge']) {
88+
if (!($code_verifier = $request->request('code_verifier'))) {
89+
$response->setError(400, 'code_verifier_missing', "The PKCE code verifier parameter is required.");
90+
91+
return false;
92+
}
93+
// Validate code_verifier according to RFC-7636
94+
// @see: https://tools.ietf.org/html/rfc7636#section-4.1
95+
if (preg_match('/^[A-Za-z0-9-._~]{43,128}$/', $code_verifier) !== 1) {
96+
$response->setError(400, 'code_verifier_invalid', "The PKCE code verifier parameter is invalid.");
97+
98+
return false;
99+
}
100+
$code_verifier = $request->request('code_verifier');
101+
switch ($authCode['code_challenge_method']) {
102+
case 'S256':
103+
$code_verifier_hashed = strtr(rtrim(base64_encode(hash('sha256', $code_verifier, true)), '='), '+/', '-_');
104+
break;
105+
106+
case 'plain':
107+
$code_verifier_hashed = $code_verifier;
108+
break;
109+
110+
default:
111+
$response->setError(400, 'code_challenge_method_invalid', "Unknown PKCE code challenge method.");
112+
113+
return FALSE;
114+
}
115+
if ($code_verifier_hashed !== $authCode['code_challenge']) {
116+
$response->setError(400, 'code_verifier_mismatch', "The PKCE code verifier parameter does not match the code challenge.");
117+
118+
return FALSE;
119+
}
120+
}
121+
87122
if (!isset($authCode['code'])) {
88123
$authCode['code'] = $code; // used to expire the code after the access token is granted
89124
}

src/OAuth2/OpenID/Controller/AuthorizeController.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,16 @@ class AuthorizeController extends BaseAuthorizeController implements AuthorizeCo
1616
*/
1717
private $nonce;
1818

19+
/**
20+
* @var mixed
21+
*/
22+
protected $code_challenge;
23+
24+
/**
25+
* @var mixed
26+
*/
27+
protected $code_challenge_method;
28+
1929
/**
2030
* Set not authorized response
2131
*
@@ -65,6 +75,10 @@ protected function buildAuthorizeParameters($request, $response, $user_id)
6575
// add the nonce to return with the redirect URI
6676
$params['nonce'] = $this->nonce;
6777

78+
// Add PKCE code challenge.
79+
$params['code_challenge'] = $this->code_challenge;
80+
$params['code_challenge_method'] = $this->code_challenge_method;
81+
6882
return $params;
6983
}
7084

@@ -90,6 +104,32 @@ public function validateAuthorizeRequest(RequestInterface $request, ResponseInte
90104

91105
$this->nonce = $nonce;
92106

107+
$code_challenge = $request->query('code_challenge');
108+
$code_challenge_method = $request->query('code_challenge_method');
109+
110+
if ($this->config['enforce_pkce']) {
111+
if (!$code_challenge) {
112+
$response->setError(400, 'missing_code_challenge', 'This application requires you provide a PKCE code challenge');
113+
114+
return false;
115+
}
116+
117+
if (preg_match('/^[A-Za-z0-9-._~]{43,128}$/', $code_challenge) !== 1) {
118+
$response->setError(400, 'invalid_code_challenge', 'The PKCE code challenge supplied is invalid');
119+
120+
return false;
121+
}
122+
123+
if (!in_array($code_challenge_method, array('plain', 'S256'), true)) {
124+
$response->setError(400, 'missing_code_challenge_method', 'This application requires you specify a PKCE code challenge method');
125+
126+
return false;
127+
}
128+
}
129+
130+
$this->code_challenge = $code_challenge;
131+
$this->code_challenge_method = $code_challenge_method;
132+
93133
return true;
94134
}
95135

src/OAuth2/OpenID/ResponseType/AuthorizationCode.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ public function getAuthorizeResponse($params, $user_id = null)
3131
// build the URL to redirect to
3232
$result = array('query' => array());
3333

34-
$params += array('scope' => null, 'state' => null, 'id_token' => null);
34+
$params += array('scope' => null, 'state' => null, 'id_token' => null, 'code_challenge' => null, 'code_challenge_method' => null);
3535

36-
$result['query']['code'] = $this->createAuthorizationCode($params['client_id'], $user_id, $params['redirect_uri'], $params['scope'], $params['id_token']);
36+
$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']);
3737

3838
if (isset($params['state'])) {
3939
$result['query']['state'] = $params['state'];
@@ -56,10 +56,10 @@ public function getAuthorizeResponse($params, $user_id = null)
5656
* @see http://tools.ietf.org/html/rfc6749#section-4
5757
* @ingroup oauth2_section_4
5858
*/
59-
public function createAuthorizationCode($client_id, $user_id, $redirect_uri, $scope = null, $id_token = null)
59+
public function createAuthorizationCode($client_id, $user_id, $redirect_uri, $scope = null, $id_token = null, $code_challenge = null, $code_challenge_method = null)
6060
{
6161
$code = $this->generateAuthorizationCode();
62-
$this->storage->setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, time() + $this->config['auth_code_lifetime'], $scope, $id_token);
62+
$this->storage->setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, time() + $this->config['auth_code_lifetime'], $scope, $id_token, $code_challenge, $code_challenge_method);
6363

6464
return $code;
6565
}

src/OAuth2/OpenID/Storage/AuthorizationCodeInterface.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,5 @@ interface AuthorizationCodeInterface extends BaseAuthorizationCodeInterface
3333
*
3434
* @ingroup oauth2_section_4
3535
*/
36-
public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null);
36+
public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null, $code_challenge = null, $code_challenge_method = null);
3737
}

src/OAuth2/ResponseType/AuthorizationCode.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ public function getAuthorizeResponse($params, $user_id = null)
2626
// build the URL to redirect to
2727
$result = array('query' => array());
2828

29-
$params += array('scope' => null, 'state' => null);
29+
$params += array('scope' => null, 'state' => null, 'code_challenge' => null, 'code_challenge_method' => null);
3030

31-
$result['query']['code'] = $this->createAuthorizationCode($params['client_id'], $user_id, $params['redirect_uri'], $params['scope']);
31+
$result['query']['code'] = $this->createAuthorizationCode($params['client_id'], $user_id, $params['redirect_uri'], $params['scope'], $params['code_challenge'], $params['code_challenge_method']);
3232

3333
if (isset($params['state'])) {
3434
$result['query']['state'] = $params['state'];
@@ -53,10 +53,10 @@ public function getAuthorizeResponse($params, $user_id = null)
5353
* @see http://tools.ietf.org/html/rfc6749#section-4
5454
* @ingroup oauth2_section_4
5555
*/
56-
public function createAuthorizationCode($client_id, $user_id, $redirect_uri, $scope = null)
56+
public function createAuthorizationCode($client_id, $user_id, $redirect_uri, $scope = null, $code_challenge = null, $code_challenge_method = null)
5757
{
5858
$code = $this->generateAuthorizationCode();
59-
$this->storage->setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, time() + $this->config['auth_code_lifetime'], $scope);
59+
$this->storage->setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, time() + $this->config['auth_code_lifetime'], $scope, $code_challenge, $code_challenge_method);
6060

6161
return $code;
6262
}

src/OAuth2/Server.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ public function __construct($storage = array(), array $config = array(), array $
172172
'enforce_state' => true,
173173
'require_exact_redirect_uri' => true,
174174
'allow_implicit' => false,
175+
'enforce_pkce' => false,
175176
'allow_credentials_in_request_body' => true,
176177
'allow_public_clients' => true,
177178
'always_issue_new_refresh_token' => false,
@@ -577,7 +578,7 @@ protected function createDefaultAuthorizeController()
577578
}
578579
}
579580

580-
$config = array_intersect_key($this->config, array_flip(explode(' ', 'allow_implicit enforce_state require_exact_redirect_uri')));
581+
$config = array_intersect_key($this->config, array_flip(explode(' ', 'allow_implicit enforce_state require_exact_redirect_uri enforce_pkce')));
581582

582583
if ($this->config['use_openid_connect']) {
583584
return new OpenIDAuthorizeController($this->storages['client'], $this->responseTypes, $config, $this->getScopeUtil());

src/OAuth2/Storage/Cassandra.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,11 +191,11 @@ public function getAuthorizationCode($code)
191191
* @param string $id_token
192192
* @return bool
193193
*/
194-
public function setAuthorizationCode($authorization_code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null)
194+
public function setAuthorizationCode($authorization_code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null, $code_challenge = null, $code_challenge_method = null)
195195
{
196196
return $this->setValue(
197197
$this->config['code_key'] . $authorization_code,
198-
compact('authorization_code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token'),
198+
compact('authorization_code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token', 'code_challenge', 'code_challenge_method'),
199199
$expires
200200
);
201201
}

src/OAuth2/Storage/CouchbaseDB.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ public function getAuthorizationCode($code)
173173
return is_null($code) ? false : $code;
174174
}
175175

176-
public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null)
176+
public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null, $code_challenge = null, $code_challenge_method = null)
177177
{
178178
// if it exists, update it.
179179
if ($this->getAuthorizationCode($code)) {
@@ -185,6 +185,8 @@ public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri,
185185
'expires' => $expires,
186186
'scope' => $scope,
187187
'id_token' => $id_token,
188+
'code_challenge' => $code_challenge,
189+
'code_challenge_method' => $code_challenge_method,
188190
));
189191
} else {
190192
$this->setObjectByType('code_table',$code,array(
@@ -195,6 +197,8 @@ public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri,
195197
'expires' => $expires,
196198
'scope' => $scope,
197199
'id_token' => $id_token,
200+
'code_challenge' => $code_challenge,
201+
'code_challenge_method' => $code_challenge_method,
198202
));
199203
}
200204

src/OAuth2/Storage/DynamoDB.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,12 +213,12 @@ public function getAuthorizationCode($code)
213213

214214
}
215215

216-
public function setAuthorizationCode($authorization_code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null)
216+
public function setAuthorizationCode($authorization_code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null, $code_challenge = null, $code_challenge_method = null)
217217
{
218218
// convert expires to datestring
219219
$expires = date('Y-m-d H:i:s', $expires);
220220

221-
$clientData = compact('authorization_code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'id_token', 'scope');
221+
$clientData = compact('authorization_code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token', 'code_challenge', 'code_challenge_method');
222222
$clientData = array_filter($clientData, 'self::isNotEmpty');
223223

224224
$result = $this->client->putItem(array(

0 commit comments

Comments
 (0)