アプリ(Remix)でFlow actionからのリクエストを認証する方法はありますか?

Topic summary

Flow actionからのリクエストを認証する方法について、質問者が解決策を見つけました。

課題:

  • RemixアプリでFlow action拡張機能を追加し、GraphQLを使用した処理を実行したい
  • 管理画面からのリクエストはshopify.authenticated.admin(request)で認証できるが、Flow actionからのリクエストの認証方法が不明

解決方法:

  • unauthenticatedを使用してHMAC検証を実装
  • cryptoモジュールでSHA256ハッシュを生成し、リクエストヘッダーのX-Shopify-Hmac-SHA256と照合
  • 検証後、unauthenticated.admin(data.shop_domain)でGraphQL APIにアクセス

重要な注意点:

  • 処理のルートは必ずapp配下に配置する必要がある(例:app-directory/app/routes/app/api/action.js
  • これを守らないと「Invalid API key or access token」エラーが発生し、GraphQLが認証されない

コードサンプルも提供されており、質問者自身が解決策を共有する形で完結しています。

Summarized with AI on November 14. AI used: claude-sonnet-4-5-20250929.

やりたいこと

アプリにFlow actionの拡張機能を追加し、呼び出された際にgraghqlを使用した処理を行いたいです。

前提条件

アプリのテンプレート:Remix

不明な点

管理画面からリクエストした場合、下記コードで認証する形になります。

このとき取得したadminを使用してgraphqlを使用できるようになると思います。


const { admin } = await shopify.authenticated.admin(request);

上記のようなFlow actionからのリクエストを認証するメソッドはありますでしょうか。

詳しい方おられましたら教えていただきたいです。

https://shopify.dev/docs/apps/flow/actions/endpoints#verifying-requests

以下のコードでできました。注意点としては、呼び出す処理のルートはapp配下にすることです。

例)app-directory/app/routes/app/api/action.js

そうしなければGraphqlが認証されず、"[API] Invalid API key or access token (unrecognized login or wrong password)"のエラーになります。

import { unauthenticated } from "~/shopify.server";
// Includes crypto module
const crypto = require("crypto");

export const action = async ({ request }) => {
  // HMAC検証
  const rawBody = await request.text();
  const hmacHeader = request.headers.get("X-Shopify-Hmac-SHA256");
  const hmac = await crypto.createHmac("sha256", process?.env?.SHOPIFY_API_SECRET);
  const hash = await hmac.update(rawBody, "utf8").digest("base64");
  // console.log("signature match : " + (hash == hmacHeader));
  if(hash !== hmacHeader) {
    return new Response(undefined, {
      status: 401,
      statusText: 'Unauthorized'
    });
  }

  const data = JSON.parse(rawBody);
  if(data.handle !== "handle") {
    return new Response(undefined, {
      status: 405,
      statusText: 'Method Not Allowed'
    });
  }

  const { admin } = await unauthenticated.admin(data.shopify_domain);
  const response = await admin.graphql(
   `#graphql
      query {
        shop{
         name
        }
      }
  }`);
  const responseJson = await response.json();
  console.log(responseJson);
  return null;
};