search
LoginSignup
11

More than 1 year has passed since last update.

posted at

updated at

Organization

ShopifyアプリでOAuthを学ぶ

OPNELOGI Advent Calendarの12日目です。
先日玄関のドアを開けたらドアが取れてしまった大島が担当します。

今回はOPENLOGIもパートナーとしてアプリを出させていただいているShopifyで
アプリ(OAuthクライアント)を実装してみて理解を深めたいと思います。

実装する言語、環境は下記を想定しています。

  • PHP7.3
  • Laravel 8.x
  • ngrok 2.3

全体像の把握

はじめにShopifyにおけるOAuthの全体像を把握します。

OAuthとはサービス(Shopify)のリソースに外部アプリからアクセスしたいときに、
ユーザーがアプリに対して、どの権限を使っていいよ、というのを承認する仕組みです。

OAuth2.0自体の流れについては下記の記事が大変勉強になりました。
この記事で記載している各エンドポイントの名称もこちらを参考にさせていただいています。
https://qiita.com/TakahikoKawasaki/items/200951e5b5929f840a1f

Shopifyの実装は公式のチュートリアルで丁寧に説明されています。
https://shopify.dev/tutorials/authenticate-with-oauth
image.png

すごいざっくり意訳すると、登場人物としては下記の三者です

名前 役割
ストアオーナー Shopifyで商品を販売するユーザー
アプリ Shopifyのリソースを使いたい外部アプリ(OAuthクライアント)。ストアに対してインストールする形になります
SHOPIFY 管理画面とユーザーの商品や注文(リソース)を管理します

流れは下記のようになると思います
2020_12_07_wiki.png

実装

LaravelでOAuthクライアントを実装してみて流れを追っていきます

追記:実際にアプリを作成する場合、NodeかRailsであれば
Shopify App CLI を利用すると↓の内容が一瞬でできてしまいます

アプリの準備

まずはShopifyの開発用アカウントとテストアプリを用意します。

下記から開発者アカウントを作成し
https://www.shopify.jp/partners

アプリ管理 にてアプリを作成します。作成する際にいくつかURLが要求されます。

アプリ設定_-_sample_app_-_Shopify_パートナー.png

これから詳しく見ていきますが、下記のように使われます。

  • アプリURL はインストールボタンを押したときにリクエストされ、認可画面へのリダイレクトをするエンドポイントです。
    • 認可画面とは「この権限でアクセストークン発行(アプリインストール)していい?」ってユーザーに承認を求める画面です。
  • リダイレクトURLの許可 には認可画面の承認後にリダイレクトされ、アクセストークンを要求する処理をするエンドポイントのホワイトリストです。
    • 実際のURL指定はアプリURL のレスポンスとして指定します。

インストール操作

公開されているアプリであれば、Shopifyのアプリストアからインストールすることができます。
https://apps.shopify.com

今回は非公開のテストアプリなので、アプリの管理画面にて開発ストアに対してテストするという操作でインストールします。
Shopify_パートナー.png

下記、それぞれのアクション毎に流れを追っていきます。

ユーザーがインストールボタンを押した〜認可画面の表示

  • ユーザーがインストールボタンを押すと、アプリの アプリURL として設定したURLにリダイレクトされます。

アプリ設定_-_sample_app_-_Shopify_パートナー.png

このときアプリが受け取るリクエストパラメータ:

パラメータ名 意味
hmac パラメータ改ざんの検証に利用
shop ショップ名
timestamp タイムスタンプ。hmacの検証に利用

アプリの実装としては、hmacをもとに渡されたパラメータが正しいことを検証し、(後述)
ユーザーに権限を承認してもらうための確認画面(認可画面)を出すようにリダイレクトします。

このリダイレクトのリクエストを認可リクエスト、エンドポイントを認可エンドポイントと呼びます

認可エンドポイント:
GET: https://{shop}.myshopify.com/admin/oauth/authorize

渡すパラメータ:

名前 意味
client_id アプリのID
scope 認可するスコープ
redirect_uri インストール後にリダイレクトするURI
state リクエスト毎にユニークなランダムな文字列。

実装:

route/api.php
Route::get('/shopify/auth', [ShopifyController::class, 'auth']);
ShopifyController.php
    public function auth(Request $request)
    {
        if (!$request->has(['shop', 'timestamp', 'hmac'])) {
            return 'パラメータが不正です';
        }

        $shop = $request->get('shop');
        $scope = 'read_orders';

        // 認可後にリダイレクトされ、トークンを要求するエンドポイント
        // ngrokでローカルのエンドポイントを公開しています
        $redirectUri = 'https://ab8c0a13fc81.ngrok.io/api/shopify/token';

        $url = "https://{$shop}/admin/oauth/authorize?".http_build_query([
                'client_id' => env('SHOPIFY_API_KEY'),
                'scope' => $scope,
                'redirect_uri' => $redirectUri,
                'state' => bin2hex(random_bytes(16)),
            ]);

        if (!$this->verifyHmac($request->all())) {
            throw new InvalidArgumentException('パラメータが不正です');
        }


        return redirect($url);
    }

認可画面の表示

Shopifyは認可エンドポイントへリクエストがあるとユーザーへ 認可画面 を表示します。

macaron-apptest___sample_appを承認___Shopify.png

ユーザーがインストールボタンをクリックする〜トークン発行

ユーザーが承認ボタンを押すと、まず認可決定エンドポイントへリクエストが送信されます。

POST: https://{shop}.myshopify.com/admin/oauth/grant

このリクエストでは、先ほど認可リクエストでパラメータを渡したredirect_uriのURLに認可コードと一緒にリダイレクトされます。
(あらかじめアプリ設定でホワイトリストに登録しておく必要があります。)

redirect_uri に渡されるリクエストパラメータ:

パラメータ名 意味
code ユーザーが認可したことを示す認可コード。一回しか使えない
hmac パラメータ改ざん検知に利用
shop ショップ名。
state リクエスト毎にユニークなランダムな文字列
timestamp タイムスタンプ。hmacの検証に利用

下記を検証する必要があります

  • stateが先ほど送ったものと一致する。
  • hmacが正しい
  • shop名が正しい(myshopify.comで終わっている。)

OKならShopifyの トークンエンドポイント にアクセストークンを要求するリクエストを送ります

POST https://{shop}.myshopify.com/admin/oauth/access_token

パラメータは下記:

パラメータ名 意味
client_id アプリのid
client_secret アプリのsecret(秘密鍵)
code: 認証コード

実装:

route/api.php
Route::get('/shopify/token', [ShopifyController::class, 'token']);
ShopifyController.php
    public function token(Request $request)
    {
        [
            'code' => $authCode,
            'shop' => $shop,
        ] = $request->all();

        if (!preg_match('/[a-zA-Z0-9][a-zA-Z0-9\-]*\.myshopify\.com[\/]?/', $shop)) {
            throw new InvalidArgumentException('hostnameが不正です');
        }
        if (!$this->verifyHmac($request->all())) {
            throw new InvalidArgumentException('パラメータが不正です');
        }

        $response = Http::post("https://{$shop}/admin/oauth/access_token", [
            'client_id' => env('SHOPIFY_API_KEY'),
            'client_secret' => env('SHOPIFY_API_SECRET'),
            'code' => $authCode,
        ]);

        // responseからaccess tokenを保存する
    }

このレスポンスとしてついにアクセストークンを取得することができました。

{"access_token":"shpat_1b144b80b6***************","scope":"read_orders"}  

以降はアクセストークンを提示してAPIアクセスを行うことで必要なリソースを取得することができます。

リクエストパラメータの検証について

Shopifyからのリクエストには常にhmacパラメータが付与されており、
受け取る側(アプリ)ではそれを利用してパラメータが改ざんされていないことを検証する必要があります。

パラメータからhmacを除いたクエリ文字列と事前に共有済みの秘密鍵(APIシークレットキー
)をsha256でハッシュ化した文字列が、hmac と一致することでパラメータが正当であるとします。

実装例

    private function verifyHmac(array $params): bool
    {
        // キーの辞書順にソートしておきます
        $parameters = collect($params)->sortBy(function ($_, $key) {
            return $key;
        })->toArray();

        $hmac = $parameters->pull('hmac');
        if (!$hmac) {
            return false;
        }
        // hmacを除いたクエリ文字列。
        // 例: shop=macaron-apptest.myshopify.com&timestamp=1607315468
        $queryString = http_build_query($parameters);

        $hash = hash_hmac(
            'sha256',
            $queryString,
            env('SHOPIFY_API_SECRET')
        );

        return $hash === $hmac;
    }

全体のリクエストの流れ

リクエストベースで見るとこんな流れになりました。

2020_12_07_-_OPENLOGI_wiki.png

まとめ

ShopifyアプリとしてOAuth2.0の実装を行うことで理解を深めることができました。
Refresh tokenなど触れていない概念もあるので今後見ていきたいと思います。

参考

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
11