Add PKCE support to AbstractOauth2Action
authorTim Düsterhus <duesterhus@woltlab.com>
Tue, 27 Jul 2021 14:32:27 +0000 (16:32 +0200)
committerTim Düsterhus <duesterhus@woltlab.com>
Tue, 27 Jul 2021 14:32:27 +0000 (16:32 +0200)
wcfsetup/install/files/lib/action/AbstractOauth2Action.class.php

index f4adc8bd964a7e5c0a1bc37fa6ccfc8a58cecdde..5ac9397644681de05234faff70a6de689a7230d7 100644 (file)
@@ -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);