Skip to content

[API Pull] Auth Flow #2424

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions js/src/components/enable-new-product-sync-button.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/
import { __ } from '@wordpress/i18n';
import { addQueryArgs } from '@wordpress/url';
import { useState } from '@wordpress/element';

/**
* Internal dependencies
Expand All @@ -21,16 +22,19 @@ import useDispatchCoreNotices from '.~/hooks/useDispatchCoreNotices';
*/
const EnableNewProductSyncButton = ( params ) => {
const { createNotice } = useDispatchCoreNotices();

const [ loading, setLoading ] = useState( false );
const nextPageName = glaData.mcSetupComplete ? 'settings' : 'setup-mc';
const query = { next_page_name: nextPageName };
const path = addQueryArgs( `${ API_NAMESPACE }/rest-api/authorize`, query );
const [ fetchRestAPIAuthorize ] = useApiFetchCallback( { path } );
const handleEnableClick = async () => {
try {
setLoading( true );
const d = await fetchRestAPIAuthorize();
setLoading( false );
window.location.href = d.auth_url;
} catch ( error ) {
setLoading( false );
createNotice(
'error',
__(
Expand All @@ -42,7 +46,12 @@ const EnableNewProductSyncButton = ( params ) => {
};

return (
<AppButton isSecondary onClick={ handleEnableClick } { ...params } />
<AppButton
isSecondary
loading={ loading }
onClick={ handleEnableClick }
{ ...params }
/>
);
};

Expand Down
3 changes: 2 additions & 1 deletion js/src/components/enable-new-product-sync-notice.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const EnableNewProductSyncNotice = () => {
<Notice status="info" isDismissible={ false }>
{ createInterpolateElement(
__(
'Enable new product sync. <enableButton>Enable</enableButton>',
'<p>We will soon transition to a new and improved method for synchronizing product data with Google.</p><enableButton>Get early access</enableButton>',
'google-listings-and-ads'
),
{
Expand All @@ -45,6 +45,7 @@ const EnableNewProductSyncNotice = () => {
eventProps={ { context: 'banner' } }
/>
),
p: <p />,
}
) }
</Notice>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ const ConnectedGoogleMCAccountCard = ( {
'Disable product data fetch',
'google-listings-and-ads'
) }
eventName="gla_mc_account_disconnect_wpcom_rest_api"
eventName="gla_disable_product_sync_click"
onClick={ openDisableDataFetchModal }
/>
) }
Expand Down
56 changes: 56 additions & 0 deletions src/API/Google/Middleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
use Automattic\WooCommerce\GoogleListingsAndAds\Utility\DateTimeUtility;
use Automattic\WooCommerce\GoogleListingsAndAds\Value\TosAccepted;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\GuzzleHttp\Client;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Psr\Container\ContainerExceptionInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Psr\Container\ContainerInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Psr\Container\NotFoundExceptionInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Psr\Http\Client\ClientExceptionInterface;
use DateTime;
use Exception;
Expand Down Expand Up @@ -482,6 +484,19 @@ protected function get_manager_url( string $name = '' ): string {
return $name ? trailingslashit( $url ) . $name : $url;
}

/**
* Get the Google Shopping Data Integration auth endpoint URL
*
* @return string
*/
public function get_sdi_auth_endpoint(): string {
return $this->container->get( 'connect_server_root' )
. 'google/google-sdi/v1/credentials/partners/WOO_COMMERCE/merchants/'
. $this->strip_url_protocol( $this->get_site_url() )
. '/oauth/redirect:generate'
. '?merchant_id=' . $this->options->get_merchant_id();
}

/**
* Generate a descriptive name for a new account.
* Use site name if available.
Expand Down Expand Up @@ -552,4 +567,45 @@ public function is_subaccount(): bool {
// since transients don't support booleans, we save them as 0/1 and do the conversion here
return boolval( $is_subaccount );
}

/**
* Performs a request to Google Shopping Data Integration (SDI) to get required information in order to form an auth URL.
*
* @return array An array with the JSON response from the WCS server.
* @throws NotFoundExceptionInterface When the container was not found.
* @throws ContainerExceptionInterface When an error happens while retrieving the container.
* @throws Exception When the response status is not successful.
* @see google-sdi in google/services inside WCS
*/
public function get_sdi_auth_params() {
try {
/** @var Client $client */
$client = $this->container->get( Client::class );
$result = $client->get( $this->get_sdi_auth_endpoint() );
$response = json_decode( $result->getBody()->getContents(), true );

if ( 200 !== $result->getStatusCode() ) {
Copy link
Contributor

@jorgemd24 jorgemd24 Jun 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I simulated a response other than 200, but I don't see any error and I get redirected to: /wp-admin/undefined

dHe4yg.gif

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @jorgemd24 How did you simulate the bad response?

When I do so I cannot reproduce the error you mentioned.

Screen.Recording.2024-06-26.at.14.01.27.mov

I did the simulation doing:

$result = new Response(400 );
$response = [];

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah sorry! I realized that my simulation was not correct, because I changed this line from if ( 200 !== $result->getStatusCode() ) { to if ( 200 === $result->getStatusCode() ) { but then the Exception was thrown as 200 and that's why I was getting that result.

do_action(
'woocommerce_gla_partner_app_auth_failure',
[
'error' => 'response',
'response' => $response,
]
);
do_action( 'woocommerce_gla_guzzle_invalid_response', $response, __METHOD__ );
$error = $response['message'] ?? __( 'Invalid response authenticating partner app.', 'google-listings-and-ads' );
throw new Exception( $error, $result->getStatusCode() );
}

return $response;

} catch ( ClientExceptionInterface $e ) {
do_action( 'woocommerce_gla_guzzle_client_exception', $e, __METHOD__ );

throw new Exception(
$this->client_exception_message( $e, __( 'Error authenticating Google Partner APP.', 'google-listings-and-ads' ) ),
$e->getCode()
);
}
}
}
3 changes: 2 additions & 1 deletion src/API/Site/Controllers/RestAPI/AuthController.php
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,8 @@
protected function delete_authorize_callback(): callable {
return function ( Request $request ) {
try {
$this->account_service->reset_wpcom_api_authorization();
$this->account_service->reset_wpcom_api_authorization_data();
$this->oauth_service->revoke_wpcom_api_auth();

Check warning on line 121 in src/API/Site/Controllers/RestAPI/AuthController.php

View check run for this annotation

Codecov / codecov/patch

src/API/Site/Controllers/RestAPI/AuthController.php#L120-L121

Added lines #L120 - L121 were not covered by tests
return $this->prepare_item_for_response( [], $request );
} catch ( Exception $e ) {
return $this->response_from_exception( $e );
Expand Down
69 changes: 60 additions & 9 deletions src/API/WP/OAuthService.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@

namespace Automattic\WooCommerce\GoogleListingsAndAds\API\WP;

use Automattic\Jetpack\Connection\Client;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\Middleware;
use Automattic\WooCommerce\GoogleListingsAndAds\HelperTraits\Utilities as UtilitiesTrait;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Service;
use Automattic\WooCommerce\GoogleListingsAndAds\Internal\ContainerAwareTrait;
use Automattic\WooCommerce\GoogleListingsAndAds\Internal\Interfaces\ContainerAwareInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareTrait;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Exception;
use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Psr\Container\ContainerExceptionInterface;
use Jetpack_Options;

defined( 'ABSPATH' ) || exit;
Expand All @@ -19,12 +25,14 @@
* @since x.x.x
* @package Automattic\WooCommerce\GoogleListingsAndAds\API\WP
*/
class OAuthService implements Service, OptionsAwareInterface {
class OAuthService implements Service, OptionsAwareInterface, ContainerAwareInterface {

use OptionsAwareTrait;
use UtilitiesTrait;
use ContainerAwareTrait;

public const AUTH_URL = 'https://public-api.wordpress.com/oauth2/authorize';
public const WPCOM_API_URL = 'https://public-api.wordpress.com';
public const AUTH_URL = '/oauth2/authorize';
public const RESPONSE_TYPE = 'code';
public const SCOPE = 'wc-partner-access';

Expand Down Expand Up @@ -66,6 +74,7 @@
* @param string $path A URL parameter for the path within GL&A page, which will be added in the merchant redirect URL.
*
* @return string Auth URL.
* @throws ContainerExceptionInterface When get_data_from_google throws an exception.
*/
public function get_auth_url( string $path ): string {
$google_data = $this->get_data_from_google();
Expand All @@ -91,31 +100,73 @@
'scope' => self::SCOPE,
'state' => $state,
],
self::AUTH_URL
$this->get_wpcom_api_url( self::AUTH_URL )
)
);

return $auth_url;
}

/**
* Calls an API by Google to get required information in order to form an auth URL.
* Get a WPCOM REST API URl concatenating the endpoint with the API Domain
*
* TODO: Call an actual API by Google.
* We'd probably need use WCS to communicate with the new API.
* @param string $endpoint The endpoint to get the URL for
*
* @return string The WPCOM endpoint with the domain.
*/
protected function get_wpcom_api_url( string $endpoint ): string {
return self::WPCOM_API_URL . $endpoint;
}

/**
* Calls an API by Google via WCS to get required information in order to form an auth URL.
*
* @return array{client_id: string, redirect_uri: string, nonce: string} An associative array contains required information that is retrived from Google.
* client_id: Google's WPCOM app client ID, will be used to form the authorization URL.
* redirect_uri: A Google's URL that will be redirected to when the merchant approve the app access. Note that it needs to be matched with the Google WPCOM app client settings.
* nonce: A string returned by Google that we will put it in the auth URL and the redirect_uri. Google will use it to verify the call.
* @throws ContainerExceptionInterface When get_sdi_auth_params throws an exception.
*/
protected function get_data_from_google(): array {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_sdi_auth_params can return an exception.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tweaked here

e42f728

$nonce = 'nonce-123';
/** @var Middleware $middleware */
$middleware = $this->container->get( Middleware::class );
$response = $middleware->get_sdi_auth_params();
$nonce = $response['nonce'];
$this->options->update( OptionsInterface::GOOGLE_WPCOM_AUTH_NONCE, $nonce );
return [
'client_id' => '91299',
'redirect_uri' => 'https://woo.com',
'client_id' => $response['clientId'],
'redirect_uri' => $response['redirectUri'],
'nonce' => $nonce,
];
}

/**
* Perform a remote request for revoking OAuth access for the current user.
*
* @return string The body of the response
* @throws Exception If the remote request fails.
*/
public function revoke_wpcom_api_auth(): string {
$args = [
'method' => 'DELETE',
'timeout' => 30,
'url' => $this->get_wpcom_api_url( '/wpcom/v2/sites/' . Jetpack_Options::get_option( 'id' ) . '/wc/partners/google/revoke-token' ),
'user_id' => get_current_user_id(),
];

Check warning on line 155 in src/API/WP/OAuthService.php

View check run for this annotation

Codecov / codecov/patch

src/API/WP/OAuthService.php#L149-L155

Added lines #L149 - L155 were not covered by tests

$request = Client::remote_request( $args );

Check warning on line 157 in src/API/WP/OAuthService.php

View check run for this annotation

Codecov / codecov/patch

src/API/WP/OAuthService.php#L157

Added line #L157 was not covered by tests

if ( is_wp_error( $request ) ) {
throw new Exception( $request->get_error_message(), 400 );

Check warning on line 160 in src/API/WP/OAuthService.php

View check run for this annotation

Codecov / codecov/patch

src/API/WP/OAuthService.php#L159-L160

Added lines #L159 - L160 were not covered by tests
} else {
$body = wp_remote_retrieve_body( $request );
$status = wp_remote_retrieve_response_code( $request );
if ( 200 !== wp_remote_retrieve_response_code( $request ) ) {
$data = json_decode( $body, true );
throw new Exception( $data['message'] ?? 'Error revoking access to WPCOM.', $status );

Check warning on line 166 in src/API/WP/OAuthService.php

View check run for this annotation

Codecov / codecov/patch

src/API/WP/OAuthService.php#L162-L166

Added lines #L162 - L166 were not covered by tests
}

return $body;

Check warning on line 169 in src/API/WP/OAuthService.php

View check run for this annotation

Codecov / codecov/patch

src/API/WP/OAuthService.php#L169

Added line #L169 was not covered by tests
}
}
}
3 changes: 1 addition & 2 deletions src/MerchantCenter/AccountService.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
use Automattic\WooCommerce\GoogleListingsAndAds\Exception\ExceptionWithResponseData;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Service;
use Automattic\WooCommerce\GoogleListingsAndAds\Jobs\CleanupSyncedProducts;
use Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\MerchantCenterService;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\AdsAccountState;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\MerchantAccountState;
use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareInterface;
Expand Down Expand Up @@ -536,7 +535,7 @@ private function prepare_exception( string $message, array $data = [], int $code
*
* @return bool
*/
public function reset_wpcom_api_authorization(): bool {
public function reset_wpcom_api_authorization_data(): bool {
$this->delete_wpcom_api_auth_nonce();
return $this->options->delete( OptionsInterface::WPCOM_REST_API_STATUS );
}
Expand Down
37 changes: 37 additions & 0 deletions tests/Unit/API/Google/MiddlewareTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -388,4 +388,41 @@ public function test_mark_tos_accepted_account_reconnect_exception() {
$this->assertFalse( $tos->accepted() );
$this->assertEquals( 'Please reconnect your Jetpack account.', $tos->message() );
}

public function test_get_sdi_auth_endpoint() {
$this->assertEquals(
$this->middleware->get_sdi_auth_endpoint(),
'https://connect-server.test/google/google-sdi/v1/credentials/partners/WOO_COMMERCE/merchants/example.org/oauth/redirect:generate?merchant_id=0'
);
}

public function test_get_sdi_auth_params() {
$expected_response = [
'clientId' => self::TEST_MERCHANT_ID,
'redirectUri' => 'https://example.com',
'nonce' => '123',
];

$this->generate_request_mock( $expected_response );
$this->assertEquals( $this->middleware->get_sdi_auth_params(), $expected_response );
}

public function test_get_sdi_auth_params_no_success() {
$this->generate_request_mock( [], 'get', 400 );
$this->expectException( Exception::class );
$this->expectExceptionCode( 400 );
$this->expectExceptionMessage( 'Invalid response authenticating partner app.' );
$this->middleware->get_sdi_auth_params();
$this->assertEquals( 1, did_action( 'woocommerce_gla_partner_app_auth_failure' ) );
$this->assertEquals( 1, did_action( 'woocommerce_gla_guzzle_invalid_response' ) );
}

public function test_get_sdi_auth_params_exception() {
$this->generate_request_mock_exception( 'Some exception.' );
$this->expectException( Exception::class );
$this->expectExceptionCode( 400 );
$this->expectExceptionMessage( 'Error authenticating Google Partner APP.' );
$this->middleware->get_sdi_auth_params();
$this->assertEquals( 1, did_action( 'woocommerce_gla_guzzle_client_exception' ) );
}
}
Loading