From 1effb8fe9bc1ca528102104b7c50c1e986a13310 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Wed, 19 Jun 2024 10:10:37 +0800 Subject: [PATCH 1/5] Possible implementation of Base64URL encoded `state` parameter for dynamic redirects --- src/class-convertkit-api-v4.php | 34 +++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/class-convertkit-api-v4.php b/src/class-convertkit-api-v4.php index b7dd61f..6b2e859 100644 --- a/src/class-convertkit-api-v4.php +++ b/src/class-convertkit-api-v4.php @@ -265,17 +265,19 @@ private function generate_and_store_code_verifier() { } /** - * Base64URL the given code verifier, as PHP has no built in function for this. + * Base64URL encodes the given string, as PHP has no built in function for this. + * + * Used for both `code_challenge` and `state` arguments * * @since 2.0.0 * - * @param string $code_verifier Code Verifier. - * @return string Code Challenge. + * @param string $str String to Base64URL encode. + * @return string Base64URL encoded string */ - public function generate_code_challenge( $code_verifier ) { + public function base64_urlencode( $str ) { // Hash using S256. - $code_challenge = hash( 'sha256', $code_verifier, true ); + $code_challenge = hash( 'sha256', $str, true ); // Encode to Base64 string. $code_challenge = base64_encode( $code_challenge ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions @@ -322,14 +324,22 @@ private function delete_code_verifier() { * * @since 2.0.0 * - * @param bool|string $state Optional state parameter to include in OAuth request. - * @return string OAuth URL + * @param $return_url Return URL after users grants/denies OAuth application. + * @return string OAuth URL */ - public function get_oauth_url( $state = false ) { + public function get_oauth_url( $return_url ) { // Generate and store code verifier and challenge. $code_verifier = $this->generate_and_store_code_verifier(); - $code_challenge = $this->generate_code_challenge( $code_verifier ); + $code_challenge = $this->base64_urlencode( $code_verifier ); + $state = $this->base64_urlencode( + wp_json_encode( + array( + 'return_to' => $return_url, + 'app_id' => $this->client_id, + ) + ) + ); // Build args. $args = array( @@ -338,13 +348,9 @@ public function get_oauth_url( $state = false ) { 'redirect_uri' => rawurlencode( $this->redirect_uri ), 'code_challenge' => $code_challenge, 'code_challenge_method' => 'S256', + 'state' => $state, ); - // If a state parameter needs to be included, add it now. - if ( $state ) { - $args['state'] = rawurlencode( $state ); - } - // Return OAuth URL. return add_query_arg( $args, From 7dae4fe068e931f1e156c59889ecca8e7616afa3 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Wed, 26 Jun 2024 10:18:40 +0800 Subject: [PATCH 2/5] Add support for state --- src/class-convertkit-api-v4.php | 85 +++++++++++++++++++++------------ 1 file changed, 54 insertions(+), 31 deletions(-) diff --git a/src/class-convertkit-api-v4.php b/src/class-convertkit-api-v4.php index 6b2e859..f542eda 100644 --- a/src/class-convertkit-api-v4.php +++ b/src/class-convertkit-api-v4.php @@ -248,13 +248,7 @@ private function generate_and_store_code_verifier() { $code_verifier = random_bytes( 64 ); // Encode to Base64 string. - $code_verifier = base64_encode( $code_verifier ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions - - // Convert Base64 to Base64URL by replacing “+” with “-” and “/” with “_”. - $code_verifier = strtr( $code_verifier, '+/', '-_' ); - - // Remove padding character from the end of line. - $code_verifier = rtrim( $code_verifier, '=' ); + $code_verifier = $this->base64_urlencode( $code_verifier ); // Store in database for later use. update_option( 'ck_code_verifier', $code_verifier ); @@ -265,19 +259,17 @@ private function generate_and_store_code_verifier() { } /** - * Base64URL encodes the given string, as PHP has no built in function for this. - * - * Used for both `code_challenge` and `state` arguments + * Base64URL the given code verifier, as PHP has no built in function for this. * * @since 2.0.0 * - * @param string $str String to Base64URL encode. - * @return string Base64URL encoded string + * @param string $code_verifier Code Verifier. + * @return string Code Challenge. */ - public function base64_urlencode( $str ) { + public function generate_code_challenge( $code_verifier ) { // Hash using S256. - $code_challenge = hash( 'sha256', $str, true ); + $code_challenge = hash( 'sha256', $code_verifier, true ); // Encode to Base64 string. $code_challenge = base64_encode( $code_challenge ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions @@ -319,27 +311,42 @@ private function delete_code_verifier() { } + /** + * Base64URL encode the given string. + * + * @since 2.0.0 + * + * @param string $str String to encode. + * @return Encoded string. + */ + private function base64_urlencode( $str ) { + + // Encode to Base64 string. + $str = base64_encode( $str ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions + + // Convert Base64 to Base64URL by replacing “+” with “-” and “/” with “_”. + $str = strtr( $str, '+/', '-_' ); + + // Remove padding character from the end of line. + $str = rtrim( $str, '=' ); + + return $str; + + } + /** * Returns the URL used to begin the OAuth process * * @since 2.0.0 * - * @param $return_url Return URL after users grants/denies OAuth application. - * @return string OAuth URL + * @param string $state State. + * @return string OAuth URL */ - public function get_oauth_url( $return_url ) { + public function get_oauth_url( $state = false ) { // Generate and store code verifier and challenge. $code_verifier = $this->generate_and_store_code_verifier(); - $code_challenge = $this->base64_urlencode( $code_verifier ); - $state = $this->base64_urlencode( - wp_json_encode( - array( - 'return_to' => $return_url, - 'app_id' => $this->client_id, - ) - ) - ); + $code_challenge = $this->generate_code_challenge( $code_verifier ); // Build args. $args = array( @@ -348,9 +355,19 @@ public function get_oauth_url( $return_url ) { 'redirect_uri' => rawurlencode( $this->redirect_uri ), 'code_challenge' => $code_challenge, 'code_challenge_method' => 'S256', - 'state' => $state, ); + if ( $state ) { + $args['state'] = $this->base64_urlencode( + wp_json_encode( + array( + 'return_to' => $state, + 'client_id' => $this->client_id, + ) + ) + ); + } + // Return OAuth URL. return add_query_arg( $args, @@ -398,7 +415,7 @@ public function get_access_token( $authorization_code ) { * @since 2.0.0 * * @param array $result Access Token, Refresh Token, Expiry, Bearer and Scope. - * @param string $client_id OAUth Client ID. + * @param string $client_id OAuth Client ID. */ do_action( 'convertkit_api_get_access_token', $result, $this->client_id ); @@ -429,6 +446,10 @@ public function refresh_token() { return $result; } + // Store existing access and refresh tokens. + $previous_access_token = $this->access_token; + $previous_refresh_token = $this->refresh_token; + // Update the access and refresh tokens in this class. $this->access_token = $result['access_token']; $this->refresh_token = $result['refresh_token']; @@ -438,10 +459,12 @@ public function refresh_token() { * * @since 2.0.0 * - * @param array $result Access Token, Refresh Token, Expiry, Bearer and Scope. - * @param string $client_id OAUth Client ID. + * @param array $result New Access Token, Refresh Token, Expiry, Bearer and Scope. + * @param string $client_id OAuth Client ID. + * @param string $previous_access_token Existing Access Token. + * @param string $previous_refresh_token Existing Refresh Token. */ - do_action( 'convertkit_api_refresh_token', $result, $this->client_id ); + do_action( 'convertkit_api_refresh_token', $result, $this->client_id, $previous_access_token, $previous_refresh_token ); // Return. return $result; From 56c7547fadf5ee44dbbe8c51bff5e0968b9ae1b9 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Wed, 26 Jun 2024 10:26:29 +0800 Subject: [PATCH 3/5] Updated tests --- src/class-convertkit-api-v4.php | 12 ++++++------ tests/wpunit/APITest.php | 11 +++++++++-- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/class-convertkit-api-v4.php b/src/class-convertkit-api-v4.php index f542eda..8bd7ebe 100644 --- a/src/class-convertkit-api-v4.php +++ b/src/class-convertkit-api-v4.php @@ -319,7 +319,7 @@ private function delete_code_verifier() { * @param string $str String to encode. * @return Encoded string. */ - private function base64_urlencode( $str ) { + public function base64_urlencode( $str ) { // Encode to Base64 string. $str = base64_encode( $str ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions @@ -339,10 +339,10 @@ private function base64_urlencode( $str ) { * * @since 2.0.0 * - * @param string $state State. - * @return string OAuth URL + * @param string $return_url Return URL. + * @return string OAuth URL */ - public function get_oauth_url( $state = false ) { + public function get_oauth_url( $return_url = false ) { // Generate and store code verifier and challenge. $code_verifier = $this->generate_and_store_code_verifier(); @@ -357,11 +357,11 @@ public function get_oauth_url( $state = false ) { 'code_challenge_method' => 'S256', ); - if ( $state ) { + if ( $return_url ) { $args['state'] = $this->base64_urlencode( wp_json_encode( array( - 'return_to' => $state, + 'return_to' => $return_url, 'client_id' => $this->client_id, ) ) diff --git a/tests/wpunit/APITest.php b/tests/wpunit/APITest.php index 4316f86..f14af87 100644 --- a/tests/wpunit/APITest.php +++ b/tests/wpunit/APITest.php @@ -351,7 +351,7 @@ public function testGetOAuthURLWithState() { // Confirm the OAuth URL returned is correct. $this->assertEquals( - $this->api->get_oauth_url( 'an-example-state' ), + $this->api->get_oauth_url( 'https://example.com' ), 'https://app.convertkit.com/oauth/authorize?' . http_build_query( [ 'client_id' => $_ENV['CONVERTKIT_OAUTH_CLIENT_ID'], @@ -359,7 +359,14 @@ public function testGetOAuthURLWithState() 'redirect_uri' => $_ENV['CONVERTKIT_OAUTH_REDIRECT_URI'], 'code_challenge' => $this->api->generate_code_challenge( $this->api->get_code_verifier() ), 'code_challenge_method' => 'S256', - 'state' => 'an-example-state', + 'state' => $this->api->base64_urlencode( + wp_json_encode( + array( + 'return_to' => 'https://example.com', + 'client_id' => $_ENV['CONVERTKIT_OAUTH_CLIENT_ID'], + ) + ) + ), ] ) ); From eda0117d1ac41eedfd9670f7493eb7629d92a3f3 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Wed, 26 Jun 2024 18:56:32 +0800 Subject: [PATCH 4/5] Coding standards --- src/class-convertkit-api-v4.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/class-convertkit-api-v4.php b/src/class-convertkit-api-v4.php index 8bd7ebe..3fb157e 100644 --- a/src/class-convertkit-api-v4.php +++ b/src/class-convertkit-api-v4.php @@ -317,7 +317,7 @@ private function delete_code_verifier() { * @since 2.0.0 * * @param string $str String to encode. - * @return Encoded string. + * @return string Encoded string. */ public function base64_urlencode( $str ) { From 5d1485fa5edd2ec15c5c1a9e5ef9edb8c8ad4db3 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Wed, 26 Jun 2024 19:17:02 +0800 Subject: [PATCH 5/5] PHPStan compat --- src/class-convertkit-api-v4.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/class-convertkit-api-v4.php b/src/class-convertkit-api-v4.php index 3fb157e..3b14c52 100644 --- a/src/class-convertkit-api-v4.php +++ b/src/class-convertkit-api-v4.php @@ -339,8 +339,8 @@ public function base64_urlencode( $str ) { * * @since 2.0.0 * - * @param string $return_url Return URL. - * @return string OAuth URL + * @param bool|string $return_url Return URL. + * @return string OAuth URL */ public function get_oauth_url( $return_url = false ) {