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_tokenPOSTリクエストを出してアクセストークンを取得します。

    データは以下のような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でのフローを理解できた気はします。

    他のサービスでも似たような実装をすることがあるかと思いますので、また何か試した時は記事にまとめます。

    参考にした書籍・記事

    広告ここから
    広告ここまで
    Home
    Search
    Bookmark