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
すごいざっくり意訳すると、登場人物としては下記の三者です
名前 | 役割 |
---|---|
ストアオーナー | Shopifyで商品を販売するユーザー |
アプリ | Shopifyのリソースを使いたい外部アプリ(OAuthクライアント)。ストアに対してインストールする形になります |
SHOPIFY | 管理画面とユーザーの商品や注文(リソース)を管理します |
実装
LaravelでOAuthクライアントを実装してみて流れを追っていきます
追記:実際にアプリを作成する場合、NodeかRailsであれば
Shopify App CLI
を利用すると↓の内容が一瞬でできてしまいます
アプリの準備
まずはShopifyの開発用アカウントとテストアプリを用意します。
下記から開発者アカウントを作成し
https://www.shopify.jp/partners
アプリ管理
にてアプリを作成します。作成する際にいくつかURLが要求されます。
これから詳しく見ていきますが、下記のように使われます。
-
アプリURL
はインストールボタンを押したときにリクエストされ、認可画面へのリダイレクトをするエンドポイントです。- 認可画面とは「この権限でアクセストークン発行(アプリインストール)していい?」ってユーザーに承認を求める画面です。
-
リダイレクトURLの許可
には認可画面の承認後にリダイレクトされ、アクセストークンを要求する処理をするエンドポイントのホワイトリストです。- 実際のURL指定は
アプリURL
のレスポンスとして指定します。
- 実際のURL指定は
インストール操作
公開されているアプリであれば、Shopifyのアプリストアからインストールすることができます。
https://apps.shopify.com
今回は非公開のテストアプリなので、アプリの管理画面にて開発ストアに対してテストする
という操作でインストールします。
下記、それぞれのアクション毎に流れを追っていきます。
ユーザーがインストール
ボタンを押した〜認可画面の表示
- ユーザーがインストールボタンを押すと、アプリの
アプリURL
として設定したURLにリダイレクトされます。
このときアプリが受け取るリクエストパラメータ:
パラメータ名 | 意味 |
---|---|
hmac | パラメータ改ざんの検証に利用 |
shop | ショップ名 |
timestamp | タイムスタンプ。hmacの検証に利用 |
アプリの実装としては、hmacをもとに渡されたパラメータが正しいことを検証し、(後述)
ユーザーに権限を承認してもらうための確認画面(認可画面)を出すようにリダイレクトします。
このリダイレクトのリクエストを認可リクエスト、エンドポイントを認可エンドポイントと呼びます
認可エンドポイント:
GET: https://{shop}.myshopify.com/admin/oauth/authorize
渡すパラメータ:
名前 | 意味 |
---|---|
client_id | アプリのID |
scope | 認可するスコープ |
redirect_uri | インストール後にリダイレクトするURI |
state | リクエスト毎にユニークなランダムな文字列。 |
実装:
Route::get('/shopify/auth', [ShopifyController::class, 'auth']);
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は認可エンドポイントへリクエストがあるとユーザーへ 認可画面 を表示します。
ユーザーがインストールボタンをクリックする〜トークン発行
ユーザーが承認ボタンを押すと、まず認可決定エンドポイントへリクエストが送信されます。
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::get('/shopify/token', [ShopifyController::class, 'token']);
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×tamp=1607315468
$queryString = http_build_query($parameters);
$hash = hash_hmac(
'sha256',
$queryString,
env('SHOPIFY_API_SECRET')
);
return $hash === $hmac;
}
全体のリクエストの流れ
リクエストベースで見るとこんな流れになりました。
まとめ
ShopifyアプリとしてOAuth2.0の実装を行うことで理解を深めることができました。
Refresh tokenなど触れていない概念もあるので今後見ていきたいと思います。
参考
- Authenticate with OAuth
- OAuth 2.0 全フローの図解と動画
- OAuth 2.0 の勉強のために認可サーバーを自作する
- この記事に貼った内容がほとんどですが、実装したコードです