Shopify / shopify-app-template-remix

336 stars 142 forks source link

No admin object when authenticate.webhook(request) #763

Closed FynnMenkIT closed 2 months ago

FynnMenkIT commented 3 months ago

Issue summary

Before opening this issue, I have:

If I trigger my webhook webhooks: { DISCOUNTS_UPDATE: { deliveryMethod: DeliveryMethod.Http, callbackUrl: "/test-endpoint", }, }, I get a empty admin object, only the topic, shop and payload comes back from authenticate.webhook

I called it via CLI, via test-send in backend and also via actually triggering it in the backend

Expected behavior

after I call

export const action = async ({ request }) => {
  const { admin } = await authenticate.webhook(request);
  console.log('webhook_fired', admin);

...

My console log should be defined. But this simple code wont work:

if (admin) {
    const data = await admin.graphql.query<any>({ data: `{shop {name}}` });

    console.log(data.body.data.shop.name);
    const response = await admin.graphql(
            `#graphql
        query webhooks {
            webhookSubscriptions(first: 100) {
                edges {
                    node {
                        id
                        topic
                        endpoint {
                            __typename
                            ... on WebhookHttpEndpoint {
                                callbackUrl
                            }
                            ... on WebhookEventBridgeEndpoint {
                                arn
                            }
                            ... on WebhookPubSubEndpoint {
                                pubSubProject
                                pubSubTopic
                            }
                        }
                    }
                }
            }
        }`
    );
  }

    const responseJson = await response.json();
    console.log(responseJson);

Actual behavior

When I call it with or without the v3_webhookAdminContext flag, I get an empty admin object

export const action = async ({ request }) => {
  const { admin } = await authenticate.webhook(request);
  console.log('webhook_fired', admin);

...

or without the v3_webhookAdminContext

export const action = async ({ request }) => {
  const { admin } = await authenticate.webhook(request);
  console.log('webhook_fired', admin);

...

Steps to reproduce the problem

  1. register a new webhook
  2. try to authenticate the webhook
  3. console.log the admin object after the authenticate

Debug logs

22:01:15 │ remix      │ [shopify-api/DEBUG] webhook request is valid
22:01:15 │ remix      │ webhook_fired DISCOUNTS_UPDATE shop.myshopify.com undefined undefined {
22:01:15 │ remix      │   admin_graphql_api_id: 'gid://shopify/DiscountAutomaticNode/1',
22:01:15 │ remix      │   title: 'Automatic free shipping updated',
22:01:15 │ remix      │   status: 'ACTIVE',
22:01:15 │ remix      │   created_at: '2016-08-29T08:00:00-04:00',
22:01:15 │ remix      │   updated_at: '2016-08-29T08:00:00-04:00'
22:01:15 │ remix      │ }
lizkenyon commented 3 months ago

Hi there 👋

This could happen if we can't find and offline token for this Shop when the event comes in.

Could you verify that the sessions in your database are as expected? And there is a session token for that shop?

FynnMenkIT commented 3 months ago

Hey @lizkenyon and thanks for the quick answer

how do I verify that there is a session token for the shop ? For me it was pretty self explained that i have working connection if the app starts without errors and i got my preview url and a working graphiQL url

For example I also can access the shopInfo where I get the right shopifyDomain in graphiQL via:

query shopInfo {
  shop {
    name
    url
    myshopifyDomain
    plan {
      displayName
      partnerDevelopment
      shopifyPlus
    }
  }
}
mohamedhaddi commented 3 months ago

Having the same issue/questions here.

Update:

The issue on my end turned out to be related to this section of the documentation (https://shopify.dev/docs/apps/launch/deployment/deploy-web-app):

image

If you're deploying to Fly.io and didn't change the default SQLite database of this template, the session data will be cleared from the database after the Fly.io machine restarts, causing the issue.

A volume should be created for the app according to this guide: https://fly.io/docs/apps/volume-storage/#add-volumes-to-an-existing-app, and the datasource block in schema.prisma should be updated accordingly (https://www.prisma.io/docs/orm/overview/databases/sqlite).

For example:

fly.toml:

...
[mounts]
  source="data"
  destination="/data"
...

schema.prisma:

...
datasource db {
  provider = "sqlite"
  url      = "file://data/dev.sqlite"
}
...
FynnMenkIT commented 3 months ago

Thanks @mohamedhaddi for your mention but this problem is not related. We dont deploy to fly.io. I try to run it locally with prisma and sqlite.

When I start my remix app then there is also this log, which shows me that my configuration is working correct:

13:49:49 │                 remix │ Environment variables loaded from .env
13:49:49 │                 remix │ Prisma schema loaded from prisma/schema.prisma
13:49:49 │                 remix │ Datasource "db": SQLite database "dev.sqlite" at "file:dev.sqlite"
13:49:49 │                 remix │
13:49:49 │                 remix │ 1 migration found in prisma/migrations
13:49:49 │                 remix │
13:49:49 │                 remix │
13:49:49 │                 remix │ No pending migrations to apply.

btw this is the migration for the session table:

CREATE TABLE "Session" (
    "id" TEXT NOT NULL PRIMARY KEY,
    "shop" TEXT NOT NULL,
    "state" TEXT NOT NULL,
    "isOnline" BOOLEAN NOT NULL DEFAULT false,
    "scope" TEXT,
    "expires" DATETIME,
    "accessToken" TEXT NOT NULL,
    "userId" BIGINT
);

In addition and to be clear about it, I have a problem with webhook authentication. The authentication via loader in the app is working:

export const loader = async ({ request }) => {
  await authenticate.admin(request);

so only on my registered webhooks I have the problem that I dont get a admin object back with the request I trigger via CLI or via a webhook "send test".

FynnMenkIT commented 3 months ago

I would be glad to hear other answers about my problem.

Maybe its my local app. I will try to host it on a live instance..

mohamedhaddi commented 3 months ago

@FynnMenkIT as @lizkenyon suggested, could you verify if you have a session token for your shop in the database? Try to log it with something like console.log(await db.session.findMany({ where: { shop } })).

Also, do you initially get an admin object with the webhook upon first install of the app, but only stop getting it afterwards on later attempts?

FynnMenkIT commented 2 months ago

No i never got an admin object. Also another coworker has the same problem with our instance.

I have two problems now I guess:

  1. I get an hmac validatione Error on anyhow I fire the webhook. (via Store, via Test trigger, via CLI) example of my CLI trigger:
  2. If I put the console.log before the authenticate.webhook, because obviously I cant put it after the authenticate method. The console.log outprint gives an error: ReferenceError: Cannot access 'shop' before initialization

My next attemps now are :

I am grateful for any new information regarding my problems

github-actions[bot] commented 2 months ago

We are closing this issue because we did not hear back regarding additional details we needed to resolve this issue. If the issue persists and you are able to provide the missing clarification we need, you can respond here or create a new issue.

We appreciate your understanding as we try to manage our number of open issues.

FynnMenkIT commented 2 months ago

Hey unfortunate it´s me again.

So here is my update. I still need to try the v3 and also the webhook subscription( as far as toml config for webhooks only works for the v3). But it has to work with the v2, or here would be a lot more open issues I guess

My action in the test-endpoint.jsx file looks like the following:

export const action = async ( {request}) => {
  console.log('test-endpoint_fired', request);

  const { topic, shop, session, admin, payload } = await authenticate.webhook(request);
  console.log(await db.session.findMany({ where: { shop } }))
  console.log('test-endpoint_admin', admin);
    console.log('test-endpoint_rest', { topic, shop, session, admin, payload });

  if (!admin) {
    throw new Response();
  }
  if (admin) {
    const data = await admin.graphql(
      `#graphql
        query shopInfo {
          shop {
            name
            url
            myshopifyDomain
            plan {
              displayName
              partnerDevelopment
              shopifyPlus
            }
          }
        }
      }`
    );
  }
  ...

I was able to trigger the webhook via cli and it was valid. The key was to add the client-secret in the trigger like this: shopify app webhook trigger --client-secret=xxxxxxxxxx --api-version 2024-07 --delivery-method http --topic discounts/update --address=https://xxxxx/test-endpoint

Now I get the following log with a valid webhook but an empty admin object:

16:55:41 │                 remix │ test-endpoint_fired Request {
16:55:41 │                 remix │   method: 'POST',
16:55:41 │                 remix │   url: 'http://xxxxx/test-endpoint',
16:55:41 │                 remix │   headers: Headers {
16:55:41 │                 remix │     accept: '*/*',
16:55:41 │                 remix │     'accept-encoding': 'gzip',
16:55:41 │                 remix │     'cdn-loop': 'cloudflare; subreqs=1',
16:55:41 │                 remix │     'cf-connecting-ip': 'XXX',
16:55:41 │                 remix │     'cf-ew-via': '15',
16:55:41 │                 remix │     'cf-ipcountry': 'US',
16:55:41 │                 remix │     'cf-ray': 'XXX',
16:55:41 │                 remix │     'cf-visitor': '{"scheme":"https"}',
16:55:41 │                 remix │     'cf-warp-tag-id': 'XXXX',
16:55:41 │                 remix │     'cf-worker': 'trycloudflare.com',
16:55:41 │                 remix │     connection: 'close',
16:55:41 │                 remix │     'content-length': '227',
16:55:41 │                 remix │     'content-type': 'application/json',
16:55:41 │                 remix │     host: 'XXX.trycloudflare.com',
16:55:41 │                 remix │     'user-agent': 'Shopify-Captain-Hook',
16:55:41 │                 remix │     'x-forwarded-for': 'XXXX',
16:55:41 │                 remix │     'x-forwarded-proto': 'https',
16:55:41 │                 remix │     'x-shopify-api-version': '2024-07',
16:55:41 │                 remix │     'x-shopify-hmac-sha256': 'XXXX',
16:55:41 │                 remix │     'x-shopify-shop-domain': 'shop.myshopify.com',
16:55:41 │                 remix │     'x-shopify-test': 'true',
16:55:41 │                 remix │     'x-shopify-topic': 'discounts/update',
16:55:41 │                 remix │     'x-shopify-triggered-at': '2024-07-25T14:55:40.816021832Z',
16:55:41 │                 remix │     'x-shopify-webhook-id': 'XXXX'
16:55:41 │                 remix │   },
16:55:41 │                 remix │   destination: '',
16:55:41 │                 remix │   referrer: 'about:client',
16:55:41 │                 remix │   referrerPolicy: '',
16:55:41 │                 remix │   mode: 'cors',
16:55:41 │                 remix │   credentials: 'same-origin',
16:55:41 │                 remix │   cache: 'default',
16:55:41 │                 remix │   redirect: 'follow',
16:55:41 │                 remix │   integrity: '',
16:55:41 │                 remix │   keepalive: false,
16:55:41 │                 remix │   isReloadNavigation: false,
16:55:41 │                 remix │   isHistoryNavigation: false,
16:55:41 │                 remix │   signal: AbortSignal { aborted: false }
16:55:41 │                 remix │ }
16:55:41 │                 remix │ [shopify-api/DEBUG] webhook request is valid
16:55:41 │                 remix │ []
16:55:41 │                 remix │ test-endpoint_admin undefined
16:55:41 │                 remix │ test-endpoint_rest {
16:55:41 │                 remix │   topic: 'DISCOUNTS_UPDATE',
16:55:41 │                 remix │   shop: 'shop.myshopify.com',
16:55:41 │                 remix │   session: undefined,
16:55:41 │                 remix │   admin: undefined,
16:55:41 │                 remix │   payload: {
16:55:41 │                 remix │     admin_graphql_api_id: 'gid://shopify/DiscountAutomaticNode/1',
16:55:41 │                 remix │     title: 'Automatic free shipping updated',
16:55:41 │                 remix │     status: 'ACTIVE',
16:55:41 │                 remix │     created_at: '2016-08-29T08:00:00-04:00',
16:55:41 │                 remix │     updated_at: '2016-08-29T08:00:00-04:00'
16:55:41 │                 remix │   }
16:55:41 │                 remix │ }

Another problem worth it to mention and maybe its correlating, when I update the discount code via the developers store with actual saving in the development store. I get a invalid webhook.

I feel like I´m doing something fundamental wrong. So I would be glad about some help from the community.

PS: @lizkenyon & @mohamedhaddi When I start my Project in Debug mode and when I open the remix app in shopify. I get the following log, which let me suspect, that I have a valid session token:

15:31:48 │                 remix │ [shopify-app/INFO] Authenticating admin request
15:31:48 │                 remix │ [shopify-app/DEBUG] Attempting to authenticate session token | {sessionToken:
{"search":"XXXXXXXXXXXX}}
15:31:48 │                 remix │ [shopify-app/DEBUG] Validating session token
15:31:48 │                 remix │ [shopify-app/DEBUG] Session token is valid | {payload:
{"iss":"https://XXX.myshopify.com/admin","dest":"https://XXX.myshopify.com","aud":"XXX
432","sub":"109226852661","exp":XXX,"nbf":XXX,"iat":XXX,"jti":"XXX","sid":"XXX","sig":"XXX"}}
15:31:48 │                 remix │ [shopify-app/DEBUG] Session token is valid | {shop:XXX.myshopify.com, payload: [object
 Object]}
15:31:48 │                 remix │ [shopify-app/DEBUG] Session token is valid | {shop: XXX.myshopify.com, payload: [object
 Object]}
15:31:48 │                 remix │ [shopify-app/DEBUG] Loading session from storage | {sessionId:
offline_XXX.myshopify.com}