OPNELOGI Advent Calendar2021の11日目です。
昨年に続いてShopifyアプリの認証周りの内容です。
Shopifyアプリとは
ShopifyアプリとはOauth認証によってShopifyリソースを操作できるwebアプリのことで、
パブリックなものはShopify App Storeに公開されており、ストアにインストール(Oauth認証)することで利用可能になります。
https://apps.shopify.com/?locale=ja
Shopifyアプリには大きく分類すると下記の2つががあります。
- 外部サイトで動作するもの(OPENLOGIはこちらです)
- Shopifyの管理画面内に表示されるもの(埋め込みアプリと呼ばれます)
埋め込みアプリの課題
埋め込みアプリは別ドメインのアプリをiframeで表示しているため、アプリ内で発行されるCookieが3rd Party Cookieの扱いになり、現状は一部ブラウザでブロックされるようになっています。
3rd Party CookieとはURLが表示されているサイトと別ドメインのサイトから発行されたCookieのことで、プライバシーの観点から各ブラウザで規制が進められています。
規制の背景としてはGDPR, ePR, CCPAといったプライバシー保護に関する法律が成立し、Cookieを個人情報とした上で、暗黙的に利用されることの多い3rd Party Cookieがプライバシー上の課題とみなされているようです。
参考:
各ブラウザの制限状況(2021/12)
- Safari: デフォルトでブロック
- Firefox: デフォルトでブロック
- Chrome: 2023年後半から
- https://www.itmedia.co.jp/news/articles/2106/25/news067.html
- 少し前まで2022年対応としていましたが延期されているようです。
Session Tokenへの移行
このようにCookieベース認証の埋め込みアプリはブラウザによって動かなかったりするため、アプリのフロント・バックエンド間の認証は代替のものを使う必要があります。
ここでShopifyが提示しているのがSession Token(JWT)です。
https://jwt.io/
大きく異なるのはCookieがステートフル、Session Tokenはステートレスという点です。
Cookieはサーバー側でセッション情報を管理し、クライアントから送信されるセッションIDを認証しているのに対して、Session Tokenはそれ自体が認証が成功したという情報を共通の秘密鍵で署名したものになっています。
ここで課題になるのがステートレスのためサーバー側で無効化ができないという点ですが、これは有効期限を1分にすることで担保しています。
なおjwtはBASE64エンコードされているデータなので簡単にデコードすることができます。
詳細については開発時は気にすることはほとんどないのですが、簡単に中身を見ておくと、payloadは下記のようなものになっていて
{
"iss": "https://adcale-test-store.myshopify.com/admin",
"dest": "https://adcale-test-store.myshopify.com",
"aud": "a40e3cb05b3164b1e2fd42ca9694abbe",
"sub": "80117104852",
"exp": 1638616612,
"nbf": 1638616552,
"iat": 1638616552,
"jti": "49f36960-c052-419b-b124-49923792dd76",
"sid": "1c6432f2ba56be01b2d7bdb77f856eeb65a7a6e81350a2457f8ef1e4219f9551"
}
それぞれ下記を意味しています。
iss: adminのURL
dest: ショップのドメイン
aud: アプリのAPIキー(Shopifyから発行され、アプリの環境変数に保持しておく)
sub: ユーザーID(利用ショップ)
exp: 有効期限
nbf: 有効化の日時
iat: 発行日時
jti: ランダムな値
sid: アプリ・ユーザーでユニークなセッションID
Session Tokenを使った認証フローの実装
フロー
公式ドキュメントから抜粋
AppBridgeとはShopify公式が提供されているJSライブラリで、埋め込み画面上で呼び出すとShopifyの管理画面と通信していい感じにSessionTokenを発行してくれます(ここの詳細は非公開のようです)
フローの要点をピックアップすると下記のようになっています。
- Shopifyがアプリバックエンドにリクエストを送る(埋め込み画面の表示)
- バックエンドはAppBridgeクライアントを含んだ空のページを返す
- フロントエンドはAppBridgeクライアントを呼び出し、Sesson Tokenを取得。サーバーへのリクエスト時にそれをクエリパラメータとして付与する
- バックエンドは渡されたtokenを検証し、レスポンスを返す
これを説明したすごくゆるい公式動画もあります
SessionToken発行
これを簡易なものですがPHP/Laravelで実装して追ってみたいと思います。(OAuthの部分は省略し、インストール済みの前提で進めます)
-
Shopifyのアプリ一覧からアプリ遷移すると、インストール済みアプリの場合、上記で設定したURLにパラメータを含めて遷移します
ここで渡されるリクエストパラメータは下記のようになってます
hmac: bd266ca557c2013342f94616d0476f509b218737208d8cb064f428cba67c4379
host: bWFjYXJvbi1hcHB0ZXN0Lm15c2hvcGlmeS5jb20vYWRtaW4
session: e3d33dac674357d49a3a9f5d37d3d1b90477f7e34850c60964bfb5951932f12e
shop: macaron-apptest.myshopify.com
timestamp: 1637269721
重要なのがhost
で、フロントでApp Bridgeを初期化するために必要なパラメータになります。
実態はshopの値をエンコードしたものらしいです。
ShopifyのアプリURLに設定したルートの実装です。
今回はそのままblade(HTML)を返しちゃいます。(本来ここでhmac値の検証が必要です)
Route::get('/home', fn () => view('skeleton'));
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>skeleton appbridge page</title>
<p>{{ json_encode(Request::all()) }}</p>
</head>
<body>
<div class="app-wrapper">
<div class="app-content">
<main role="main">
skeleton..
</main>
</div>
</div>
<script src="https://unpkg.com/@shopify/app-bridge"></script>
<script src="https://unpkg.com/@shopify/app-bridge-utils"></script>
<script>
// create app bridge instance..
var AppBridge = window['app-bridge'];
var actions = AppBridge.actions;
var utils = window['app-bridge-utils'];
var createApp = AppBridge.default;
var apiKey = "{{ env('SHOPIFY_API_KEY') }}";
var shop = "{{ Request::get('shop') }}";
var host = "{{ Request::get('host') }}";
var app = createApp({
apiKey: apiKey,
shopOrigin: shop,
host: host,
forceRedirect: true,
});
</script>
<script>
utils.getSessionToken(app).then((token) => {
// ここでsession tokenを取得できるので、それをクエリパラメータとして適当な適当なページにリダイレクトする
window.location.href = `{!! {{アプリの適当なURL}} !!}?token=${token}`;
});
</script>
</body>
</html>
やっていることは
- scriptとしてAppBridgeを読み込み、apiKey/shop/hostパラメータを渡して初期化を行っています。
- AppBridgeクライアントからsession Tokenを取得できたら、それをクエリパラメータとして任意のパスにリダイレクトします。
AppBridgeがSession Tokenを生成する詳細は確認できないのですが、Shopifyに対して下記のパラメータで生成リクエストを送っているようです。Shopifyとアプリで共有しているSECRET KEYを使ってHS256アルゴリズムで署名されています。
{
"action": "APP::SESSION_TOKEN::REQUEST",
"app_id": 6068***,
"client_interface_name": "@shopify/app-bridge [CDN Compatibility]",
"client_interface_version": "2.0.5",
"interface_name": "@shopify/app-bridge",
"interface_version": "2.0.5",
"locale": "ja-JP",
"shop_id": 45928546457,
"shopify_employee": false,
"tab_id": "d7fdd757-26b6-4202-9e77-39fb851818bd",
"user_id": 61187391641,
"schema_id": "app_bridge_actions/2.0"
}
検証
バックエンドは受け取ったtokenを検証します。このあたりに従って検証すればOKです(雑)
https://shopify.dev/apps/auth/session-tokens/authenticate-an-embedded-app-using-session-tokens#verify-the-signature
- Take the <header>.<payload> portion of the string and hash it with SHA-256.
- Sign the string using the HS256 algorithm by using the app’s secret as the signing key.
- Base64url-encode the result.
- Verify that the result is the same as the signature that was sent with the session token.
終わりに
認証周りはライブラリがいい感じにやってくれて開発者はあまり気にしなくていいことが多いですが、
普段背景などを細かく追っていけなかったりするので、雰囲気を掴む良い機会になりました。
話がずれますが、ShopifyのドキュメントはAPI周りだけでなくアプリ設計・デザインなどについても提供されており、Web技術者として勉強になるものが多いので、ぜひ読んでみてください。