ShopifyアプリのOAuthをExpress + fetchで自前実装してみる
Shopifyアプリを作るために避けて通れないのが、OAuthによる認証です。 https://shopify.dev/apps/auth/oauth SDKでヘルパーが提供されていたり、スターターテンプレートが用意され […]
目次
Shopifyアプリを作るために避けて通れないのが、OAuthによる認証です。
https://shopify.dev/apps/auth/oauth
SDKでヘルパーが提供されていたり、スターターテンプレートが用意されていたりするので、本来自分でコードを書く必要はありません。
が、OAuthでの認証の流れや、どんなリクエストを飛ばしているのかを調べたかったので、あえてやってみました。
注意: production環境では使わないでください
勉強のために書いたコードのため、セキュリティ系などの必要な処理が不足している可能性があります。
アプリを開発される際などは、公式のSDKやテンプレートをお使いください。
ライブラリインストール
ExpressでAPIを用意するので、ライブラリをインストールします。
npm i express node-fetch@2 crypto
定数とヘルパー関数を用意する
アプリ用のクライアントIDなどを設定します。本来は環境変数に設定すべきですが、サンプルなので省いています。
const SHOPIFY_CLIENT_ID = 'cxxxxxxx'
const SHOPIFY_CLIENT_SECRET = 'shpss_xxxxxx'
const TMP_REQUEST_STATE = 'DO_NOT_USE_THIS_FOR_PRODUCTION'
続いてHMACの検証用関数を作ります。
import { createHmac } from 'crypto'
import * as express from 'express'
const verifyHmac = ({query}: Pick<express.Request, 'query'>) => {
const hmac = createHmac('sha256', SHOPIFY_CLIENT_SECRET)
const message = Object.entries(query)
.filter(([key]) => key !== 'hmac')
.map(([key, value])=> {
return `${key}=${value}`
})
.join("&")
hmac.update(message)
const result = hmac.digest()
const generatedHmac = result.toString('hex')
const givenHmac = Array.isArray(query.hmac) ? query.hmac[0] : query.hmac
if (givenHmac !== generatedHmac) throw new Error("hmac verification failed")
}
認証用のAPIを作る
まずはアプリインストール時に呼び出されるAPIを作ります。
app.get('/', (req, res) => {
const {shop} = req.query as {[key: string]: string}
verifyHmac(req)
const redirectUrl = [
shop,
'admin',
'oauth',
'authorize'
].join('/')
const queryString = Object.entries({
client_id: SHOPIFY_CLIENT_ID,
scope: 'read_customers,read_orders',
redirect_uri: 'https://XXXX.jp.ngrok.io/callback',
state: TMP_REQUEST_STATE,
}).map(([key, value]) => `${key}=${value}`).join('&')
const redirectTo = `https://${redirectUrl}?${queryString}`
res.redirect(redirectTo)
})
このAPIにリダイレクトされた後、HMACによる検証をパスできれば、admin/oauth/authorize
ページにリダイレクトします。
scope
に設定した権限が表示されますので、利用したい権限のScopeはもれなく追加しましょう。
[アプリをインストールする]をクリックすると、redirect_uri
で指定したURLに移動します。
インストール後のアクセストークン取得APIを用意する
インストールボタンがクリックされると、アクセストークンが取得できるようになります。
app.get('/callback', async (req, res) => {
const {params, query, body, method} = req
// 実際にはRedisなどでセッション管理する
if (!query.state || query.state !== TMP_REQUEST_STATE) {
res.status(400).end()
return
}
verifyHmac(req)
const code = Array.isArray(query.code) ? query.code[0] : query.code
const param = {
client_id: SHOPIFY_CLIENT_ID,
client_secret: SHOPIFY_CLIENT_SECRET,
code
}
const resp = await fetch(
`https://${query.shop}/admin/oauth/access_token?${Object.entries(param).map(([k,v]) => `${k}=${v}`).join('&')}`,
{
method: 'POST',
}
).then(async data => {
if (data.ok) return data.json()
return null
}).catch(console.log)
// DBに暗号化して保存する
console.log(resp)
// アプリ管理画面を表示 or リダイレクト
res.json({
message: "callback"
})
})
HMACとstateによる認証を行います。その後、admin/oauth/access_token
にPOST
リクエストを出してアクセストークンを取得します。
データは以下のようなJSONで取得できますので、暗号化してRDBやNoSQL DBに保存しましょう。
{
access_token: 'shpca_cxxxx',
scope: 'read_customers,read_orders'
}
記憶が正しければ、アプリ削除時にトークン削除用のWebhook APIが必要です。
その際にどのサイトのアクセストークンを削除すべきかを判別しますので、req.query.shop
のストアURL(xxxx.myshopify.com
)をキーにするとよさそうです。
なお、admin/oauth/access_token
に2回目のリクエストを投げるとHTTP400が返ってきます。
複数回呼ばれる可能性がある場合は、DBにアクセストークンがすでにあるかをチェックするようにしましょう。
終わりに
ざっと動かすことをゴールに試しましたが、なんとなくOAuthでのフローを理解できた気はします。
他のサービスでも似たような実装をすることがあるかと思いますので、また何か試した時は記事にまとめます。