From 7500915353d2d158af9dcc812365595a19a41c08 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Tim=20D=C3=BCsterhus?= Date: Tue, 27 Jul 2021 16:32:27 +0200 Subject: [PATCH] Add PKCE support to AbstractOauth2Action --- .../lib/action/AbstractOauth2Action.class.php | 39 ++++++++++++++++--- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/wcfsetup/install/files/lib/action/AbstractOauth2Action.class.php b/wcfsetup/install/files/lib/action/AbstractOauth2Action.class.php index f4adc8bd96..5ac9397644 100644 --- a/wcfsetup/install/files/lib/action/AbstractOauth2Action.class.php +++ b/wcfsetup/install/files/lib/action/AbstractOauth2Action.class.php @@ -4,6 +4,7 @@ namespace wcf\action; use GuzzleHttp\ClientInterface; use GuzzleHttp\Psr7\Request; +use ParagonIE\ConstantTime\Base64UrlSafe; use ParagonIE\ConstantTime\Hex; use Psr\Http\Client\ClientExceptionInterface; use wcf\system\exception\NamedUserException; @@ -28,6 +29,8 @@ abstract class AbstractOauth2Action extends AbstractAction { private const STATE = self::class . "\0state_parameter"; + private const PKCE = self::class . "\0pkce"; + /** * @var ClientInterface */ @@ -96,6 +99,14 @@ abstract class AbstractOauth2Action extends AbstractAction */ abstract protected function supportsState(): bool; + /** + * Whether to use PKCE (RFC 7636). Defaults to 'false'. + */ + protected function usePkce(): bool + { + return false; + } + /** * Turns the access token response into an oauth user. */ @@ -129,16 +140,26 @@ abstract class AbstractOauth2Action extends AbstractAction */ protected function codeToAccessToken(string $code): array { - $request = new Request('POST', $this->getTokenEndpoint(), [ - 'Accept' => 'application/json', - 'Content-Type' => 'application/x-www-form-urlencoded', - ], \http_build_query([ + $payload = [ 'grant_type' => 'authorization_code', 'client_id' => $this->getClientId(), 'client_secret' => $this->getClientSecret(), 'redirect_uri' => $this->getCallbackUrl(), 'code' => $_GET['code'], - ], '', '&', \PHP_QUERY_RFC1738)); + ]; + + if ($this->usePkce()) { + if (!($verifier = WCF::getSession()->getVar(self::PKCE))) { + throw new StateValidationException('Missing PKCE verifier in session'); + } + + $payload['code_verifier'] = $verifier; + } + + $request = new Request('POST', $this->getTokenEndpoint(), [ + 'Accept' => 'application/json', + 'Content-Type' => 'application/x-www-form-urlencoded', + ], \http_build_query($payload, '', '&', \PHP_QUERY_RFC1738)); try { $response = $this->getHttpClient()->send($request); @@ -193,6 +214,14 @@ abstract class AbstractOauth2Action extends AbstractAction $parameters['state'] = $token; } + if ($this->usePkce()) { + $verifier = Hex::encode(\random_bytes(32)); + WCF::getSession()->register(self::PKCE, $verifier); + + $parameters['code_challenge'] = Base64UrlSafe::encodeUnpadded(\hash('sha256', $verifier, true)); + $parameters['code_challenge_method'] = 'S256'; + } + $url = $this->getAuthorizeUrl() . '?' . \http_build_query($parameters, '', '&'); HeaderUtil::redirect($url); -- 2.20.1