verbb / consume

A Craft CMS plugin to create HTTP requests in your Twig templates to consume JSON, XML or CSV content.
Other
4 stars 1 forks source link

Connecting to PayPal results in ”invalid client_id or redirect_uri“ error page #2

Closed jannisborgers closed 1 year ago

jannisborgers commented 1 year ago

Describe the bug

I wanted to connect to PayPal’s REST API for processing orders using the steps below.

After clicking the Connect button on the client page, Consume redirects to a URL in this form:

https://www.paypal.com/connect/?approval_prompt=auto&client_id=[CLIENT_ID]&forwardFrom=idsw&redirect_uri=[REDIRECT_URI]&response_type=code&scope=openid&state=[SOME HASH]#provider

It shows an error page telling me “invalid client_id or redirect_url”. I double checked, it’s all set up correctly:

consume-paypal-error-invalid-client-id-or-redirect-url

I found a lot of threads online saying that if the redirect url isn’t an exact match, the Auth fails. I waited overnight because the PayPal app settings page says to wait 3 hours for changes to take effect. Still, nothing. My return uri is a .test domain because I work with Laravel Valet locally.

Next, I tried following the PayPal docs and set up a Postman account (see Authentication Docs) to try and get an access token that way. Postman has a UI for that, and it seems to work with my client ID and secret. I copied the access token and used it in an ad-hoc Guzzle client with Consume, which works. Of course this would require me to renew the token manually…

Steps to reproduce

    • Set up a PayPal client in Consume
    • Create a sandbox business account
    • Create a new app there
    • In the app settings, check ”Log in with PayPal“ in the features section, click ”Advanced Settings“, add the Return URL that is shown in Consume, chose Full Name, Email, Account verification status, PayPal account ID (payer ID), and save the app.
    • Add client ID and client secret from the app into Consume’s client fields (via .env variables)
    • Save client
    • Click Connect button

Craft CMS version

4.4.10.1

Plugin version

1.0.2

Multi-site?

No

Additional context

I don’t know if this is even an error with Consume or PayPal.

engram-design commented 1 year ago

So we do use different endpoints that those docs suggest:

https://api.paypal.com/webapps/auth/protocol/openidconnect/v1/authorize
https://api.paypal.com/v1/identity/openidconnect/tokenservice

Which are the authorization and token endpoints respectively. It's possible this may be out of date, so I'll need to test. It's also possible it may not like .test as a TLD, but I'll double check that as well.

jannisborgers commented 1 year ago

Could it be that Consume needs to distinguish between Sandbox and Live API? The domain used in your post and the one in the docs is the most obvious difference here.

engram-design commented 1 year ago

I did just post the production endpoints for brevity. Using the sandbox will change those TLDs to api.sandbox.paypal.com, but you're right that there's no setting in Consume for toggling that. I've just pushed that for the next release.

I'm still investigating what the issue is here

engram-design commented 1 year ago

Okay, so looks like it just took a good couple of hours for my redirect to work. I can confirm that .test domains work. But it's likely that the issue was that I was trying to use the sandbox, but the integration wasn't enabled to allow that.

To get this early, run composer require verbb/consume:"dev-craft-4 as 1.0.2"

jannisborgers commented 1 year ago

Hi @engram-design — thanks a lot! I can successfully connect to the API, and the sandbox toggle is very helpful.

Is it possible that the sandbox toggle only affects the authentication call, but not the API calls itself? When I try to use the client, the consume() call returns null. The log shows the following, which indicates that it connects to the live/production API (see POST url):

2023-05-17 05:30:30 [ERROR] Unable to fetch data: “Client error: `POST https://api.paypal.com/v2/checkout/orders` resulted in a `401 Unauthorized` response:
{"error":"invalid_token","error_description":"The token passed in was not found in the system"}
engram-design commented 1 year ago

Just pushed this to the auth module, so a composer update or composer require verbb/auth should get the latest.

jannisborgers commented 1 year ago

I can confirm that the authentication works and I can connect the PayPal client. Thank you, Josh!

jannisborgers commented 1 year ago

I’m sorry @engram-design, but there still seems to be an issue with the PayPal provider. Since I updated, I at least got a different response from the PayPal REST API than what I got when I opened this issue, but now I get a 403 Forbidden error PERMISSION_DENIED with descriptions Authorization failed due to insufficient permissions. and detail description You do not have permission to access or perform operations on this resource.

I tried back and forth, even created a whole new sandbox account and REST app in the paypal backend, but I can only make it work with a template-side client:

This works:

{% set paypal_client = {
  base_uri: "https://api-m.sandbox.paypal.com",
  headers: {
    "Content-Type": "application/json",
    "Accept": "application/json",
    "Authorization": "Bearer [Access token manually generated with Postman]",
  }
} %}

{% set paypal_query = {
  purchase_units: paypal_data.purchase_units,
  intent: "CAPTURE",
  application_context: {
    return_url: getenv("PRIMARY_SITE_URL") ~ "shop/paypal/success",
    cancel_url: getenv("PRIMARY_SITE_URL") ~ "shop/paypal/cancel",
  }
} %}

{% set paypal_order = consume(paypal_client, "POST", "/v2/checkout/orders", {
  json: paypal_query
}) %}

This doesn’t work:

{# PayPal Provider client set up in Consume named with handle "paypal_client" #}

{% set paypal_order = consume("paypal_client", "POST", "/v2/checkout/orders", {
  json: paypal_query
}) %}

Also, between these error messages, I also got a couple of expired token errors and had to manually disconnect and reconnect the client for that error to go away. The Consume docs say that providers that support refresh tokens are automatically renewed. Is this the case with the PayPal provider type? Otherwise, the whole auth process wouldn't work without me manually regenerating the token and saving it somewhere… (via a cron?)…

engram-design commented 1 year ago

It looks like PayPal doesn't support refresh tokens (you can see in the auth_oauth_tokens db table, refreshToken is empty as none is reported back). However, looking at their postman example (I can't find reference in their docs) refresh tokens should be possible. Looks like the client is still using old endpoints, so I'm going to update them all to the current docs.

This is all on the auth module. Run composer require verbb/auth:"dev-craft-4 as 1.0.5" just so I can ensure that's working for you.

jannisborgers commented 1 year ago

@engram-design That was it, works as expected now!

The database also shows a refreshToken value now. Am I right to assume that refreshing the token should work via Consume? PayPal’s tokens only work for a couple of hours, so not having to code a token sync mechanism would be a real time saver.

engram-design commented 1 year ago

Correct, as long as a refreshToken exists, Consume can check when it expires and request a new token.

jannisborgers commented 1 year ago

That's wonderful. Thanks Josh!