ShopifyのOAuthについて理解する
はじめに
StoreHero でバックエンドエンジニアをしている美野です。普段は弊社のプロダクトである StoreHero の開発しています。
直近で Shopify の OAuth の実装を担当したのですが、実装を通して学びが多くありました。
この記事では、ShopifyOAuth のフローのステップを一つずつ紐解いて解説していきたいと思います。
また Shopify の OAuth フローの実装は Shopify 開発者向けドキュメント にあるように OAuth2.0 の仕様に従って実装する必要があります。
そのため Shopify の OAuth を知ることは、OAuth2.0 を知ることにも繋がります。本記事では OAuth2.0 の基本的な用語なども併せて紹介していきたいと思います。
※以降は特に断りがなければ出てくる用語は OAuth2.0 の仕様であり、OAuth フローについては OAuth2.0 の仕様に則った Shopify のフローという前提で進めていきます。
ちなみに Shopify では既に OAuth が実装された アプリテンプレート というのがあります。それを使うと簡単にアプリを作成でき、これから解説する OAuth の実装をする必要もないため、特に理由がなければ使用を検討してみるのも良いかと思います。
対象読者
- Shopify の OAuth フローについて理解したい
- 実際に Shopify の OAuth フローを実装する予定がある
記事を読んでわかること
- Shopify の OAuth について
- ShopifyOAuth フローの流れ
- OAuth2.0 について
話さないこと
- OAuth2.0 のセキュリティ
- OAuth1.0 と 2.0 の違い
- OIDC(OpenID Connect)について
使用言語とライブラリ
言語:go v1.20
ライブラリ:go-shopify v4.1.0
実装する際は言語毎に用意されている ShopifyAPI のライブラリ を使用することをお勧めします。
ShopifyOAuth フロー全体像
まずは ShopifyOAuth フローの全体像を把握します。
この ShopifyOAuth フローのゴールは、「ShopifyAPI への限定したアクセスを可能にするアクセストークン」の発行です。
OAuth2.0 は、そのアクセストークンの発行をリソースオーナーがクライアントに対して承認する権限委譲のプロトコルになります。
もう少し詳細なシーケンス図にすると次のようになります。
今回の ShopifyOAuth フローでは 4 つのロールが登場します。もちろんこれは OAuth2.0 の仕様 で 4 つのロールとして定義されています。
リソースオーナー(Merchant)
リソースとは Shopify のストアが持っている情報のことを指し、その所有者がリソースオーナーになります。つまりストアのオーナーであるマーチャントが該当します。
クライアント(App)
リソースサーバーにアクセスしようとしているアプリケ-ションのことを指します。
認可サーバー(Shopify)
アクセストークンを発行したり、アクセス権を委譲するための意思を確認する画面を表示したりするサーバーです。ここでは Shopify の認可サーバーのことを指します。これは Shopify 側で提供されるサーバーになります。
リソースサーバー(ShopifyAPI)
リソースがストアのデータであるのに対して、リソースサーバーはそのリソースを提供するサービスであり、一般的には API のような形になります。そのため、ここでのリソースサーバーは ShopifyAPI のことを指します。
実装に必要な準備
実装にあたって 4 つの準備が必要になります。
1.アプリを作成する
Shopify パートナーの管理画面にログインして、アプリを作成します。
※「アプリを手動で作成する」を選択してください。
2.「アプリ URL」「許可されたリダイレクト URL」の値を設定する
作成したアプリの管理画面からそれぞれ任意の URL を設定します。
アプリ URL 例: https://xxxxx.com/api/shopify/oauth
リダイレクト URL 例:https://xxxxx.com/api/shopify/oauth/redirect
- アプリ URLとはマーチャントがアプリのインストールボタンを押したとき(OAuth の開始)にリクエストする URL です
- リダイレクト URLとは、許可画面でマーチャントが権限委譲について承認した後にリダイレクトする URL です
3.アプリの管理画面からクライアント ID/クライアントシークレットを控えておく
実装で必要なので値をコピーしておきます。
4.ShopifyAPI に対して「どのリソース」を「どの権限」で操作したいかのスコープを決める
スコープとはアクセストークンに紐づくアクセス権を細かくコントールするための仕組みです。
アクセストークンが流失してしまった時の影響範囲のことも考えると、設定するスコープは最小限の権限にする必要があります。
権限一覧は Shopify 開発者向けドキュメント にあります。
ステップ 1:アプリのインストールボタンを押下 〜 認可画面の表示
※一連の流れは Shopify 開発者向けドキュメント でも説明されているので、併せて読むことをお勧めします。
シーケンス図だと赤枠内の部分がこのステップに該当する箇所です。
アプリのインストールボタンを押すと、アプリ URL を経由して認可サーバーが提供している認可エンドポイントへリダイレクトします。
それではアプリ URL の内部で、どのような処理を実装して認可エンドポイントへリダイレクトするのかを一つずつ見ていきたいと思います。
1.リクエストの有効性の検証
まずはリクエストされた時に付与されているパラメーターを使ってリクエストの有効性(インストールボタンを押してフローを開始しているか)を検証します。
付与されているパラメーター | 内容 |
---|---|
shop | ショップ名で {shop}.myshop.com の形式 |
timestamp | インストールした日時 |
hmac | リクエストが改ざんされていないことを検証するために必要な値 |
ここでキーになるのが、hmac です。
hmac とは送受信する情報(メッセージ)が改ざんされていないことを検証するために付与されるコードの一つで、このコードはメッセージと共通鍵をハッシュ関数に掛けることで算出します。
今回の場合だと、次のように置き換えることができます。
メッセージ → hmac 以外のパラメータ(shop,timestamp)
共通鍵 → アプリ管理画面にあるクライアントシークレット
ハッシュ関数 → SHA-256
アプリ URL では、次の流れで hmac の算出と検証をする必要があります。
- hmac 以外のパラメーター(shop,timestamp)を抽出する
- 1 のパラメーターとクライアントシークレットをハッシュ関数にかけて hmac を算出する
- 2 で算出した hmac とパラメーターの hmac が完全に一致するかどうかを比較する
本来であれば、この検証を実装する必要があるのですが ShopifyAPI のライブラリ を使うと、既に実装済のメソッドが使用できるためそちらを利用することをお勧めします。
例えば go-shopify では VerifyAuthorizationURL()
というメソッドが用意されており、URL を渡すと先ほどの手順で有効性を検証してくれて結果を返してくれます。(もし興味があれば内部のコードを読んでみてください)
// 最初に設定する必要がある
app := goShopify.App{
ApiKey: "管理画面のAPIキー", // 直接貼り付けない
ApiSecret: "管理画面のクライアントシークレット", // 直接貼り付けない
RedirectUrl: "https://xxxxx.com/api/shopify/oauth/redirect", // 管理画面で設定したリダイレクトURL
Scope: "read_products,read_customer", //スコープ(複数ある場合はカンマで繋げる)
}
// リクエストの有効性を検証する
ok, err := app.VerifyAuthorizationURL(url)
if err != nil || !ok {
// エラーハンドリング
}
2.state を生成する
こちらはCSRF 対策として、認可画面で承認した後にリダイレクトする URL 内で使用するパラメータになります。仮に state パラメーターを使用しなかった場合、アプリ URL とリダイレクト URL の一連の流れが同一のセッションかどうかを検証できないため CSRF の脆弱性が発生してしまいます。
state の値はここでは自前で設定する必要があり、リクエスト毎にユニークな値の文字列にする必要があります。
state に関しての詳しい情報は state の仕様 にも書かれているので参考にすることをお勧めします。
state := "ランダムな文字列"
Shopify の OAuth では、state はフロー内で引き回せるパラメーター(=アプリ URL からリダイレクト URL に引き渡される)になります。
そのため何かしらのメタ情報を載せて、リダイレクト URL の方でその情報を使うことも可能です。
例えば次のように構造体で、必要なメタ情報を定義して文字列にすることで state パラメーターとして扱うことができます。(リダイレクト URL 側ではデコードすることでメタ情報を取り出す)
type State struct {
RandomString string `json:"random_string"`
UserType string `json:"user_id"`
}
state := State{
RandomString: "ランダムな文字列",
UserType: "owner",
}
bytes, _ := json.Marshal(state)
encodeState := base64.StdEncoding.EncodeToString([]byte(bytes))
// encodeState → eyJyYW5kb21fc3Rxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=
state を使ってメタ情報を付与する手法については Shopify の 公式コミュニティでも回答 されています。
※こちらの手法については OAuth2.0 の仕様では特に明記されていなかったため、Shopify の OAuth フローに限定して言及しています
3.認可エンドポイントへリダイレクトする
リダイレクトする認可エンドポイントは決まっています。
GET: https://{shopifyHandle}.myshopify.com/admin/oauth/authorize
ここで付与しなければいけないパラメーターは 4 つです。
付与するパラメーター | 内容 |
---|---|
state | 生成したランダムな文字列 |
redirect_uri | 認可画面で承認した後にリダイレクトする URL |
scopes | API へアクセスした際の操作権限 |
client_id | アプリの管理画面にあるクライアント ID |
ここもライブラリを使うことで必要なパラメーターを設定した状態の認可エンドポイント URL を発行してくれます。
go-shopify だと次のようになります。
// 認可エンドポイントの発行
url, err := app.AuthorizeUrl(shop, state)
if err != nil {
// エラーハンドリング
}
// 認可エンドポイントへリダイレクトする
http.Redirect(http.ResponseWrite, *http.Request, url, http.StatusFound)
リダイレクトした認可エンドポイントは「クライアントが(指定した権限内で)ShopifyAPI へのアクセスできる、アクセストークンを発行するけど良いですか?」とマーチャントに同意を求める許可画面をアプリ上に表示してくれます。
ここで「インストール」を押下することで、権限委譲について同意したと判断され、リダイレクト URL で設定した URL にリダイレクトします。
ここまでのアプリ URL で行う実装のコードを貼っておきます。
コード全文(あくまでも一例)
package shopify
import (
goShopify "github.com/bold-commerce/go-shopify/v4"
)
// (/api/shopify/oauth) にリクエストされた時に実行する関数と想定
func ShopifyOAuth(w http.ResponseWriter, r *http.Request) {
// APIキーやシークレットなどをDBもしくはその他保存しているところから取得する処理
config, err := GetShopifyOAuthConfig()
if err != nil {
// エラーハンドリング
}
// app設定
app := goShopify.App{
ApiKey: config.apiKey, // 直接貼り付けない
ApiSecret: config.apiSecret, // 直接貼り付けない
RedirectUrl: "https://xxxxx.com/api/shopify/oauth/redirect", // 管理画面で設定したURL
Scope: "read_products,read_customer", //スコープ(複数ある場合はカンマで繋げる)
}
// リクエストの有効性を検証する
ok, err := app.VerifyAuthorizationURL(r.URL)
if err != nil || !ok {
// エラーハンドリング
}
state := "リクエスト毎にユニークな文字列"
// 認可エンドポイントを構築する
url, err := app.AuthorizeUrl(shop, state)
if err != nil {
// エラーハンドリング
}
// 認可エンドポイントへリダイレクトする
http.Redirect(w, r, url, http.StatusFound)
}
ステップ 2:マーチャントが認可画面で承認 〜 アクセストークンの発行
ステップ 1 ではインストールボタンを押下してから認可画面を表示するところまでを確認してきました。ステップ 2 では、リソースオーナーが認可画面で承認してからアクセストークンを発行するところまでを見ていきます。
シーケンス図では赤枠内がステップ 2 に該当します。
マーチャントが認可画面で承認をすると、アプリ管理画面で設定したリダイレクト URL(今回であれば /api/shopify/oauth/redirect
)にリダイレクトします。
リダイレクト URL でどのようにアクセストークンの発行する処理を実装していくのかを一つずつ見ていきたいと思います。
1. リクエストの有効性を検証
この部分はアプリ URL と変わらない部分になります。(ライブラリで使うメソッドも同じです)
ここで付与されるパラメータは次のようになります。
たとえばアプリ URL で設定した state とパラメータの state の値が違うと、hamc の値が完全一致しないので有効なリクエストではないと判断されます。
付与されているパラメーター | 内容 |
---|---|
code | 認可コード |
redirect_uri | 認可画面で承認した後にリダイレクトする URL |
host | ショップ名を base64 でエンコードした文字列 |
shop | ショップ名で {shop}.myshop.com の形式 |
state | アプリ URL 内の実装で設定した値 |
timestamp | 認可画面で承認した日時 |
ここで覚えておきたいパラメーターはcodeです。
認可コードと呼ばれ、「リソースオーナーがクライアントへの権限委譲に同意した証」(先ほどの認可画面でマーチャントが承認した証)として認可エンドポイントが発行するコードです。
この認可コードはアクセストークンの要求をする際に利用されます。
2. アクセストークンの発行を要求
リクエストの有効であることを確認できたあとは、認可サーバーが提供しているトークンエンドポイントにリクエストすることで、アクセストークンを発行できます。
トークンエンドポイントも決まっています。
POST: https://{shop}.myshopify.com/admin/oauth/access_token
また付与するパラメーターは下記になります。
付与するパラメーター | 内容 |
---|---|
client_id | アプリの管理画面にあるクライアント ID |
client_secret | アプリの管理画面にあるクライアントシークレット |
code | 認可コード |
トークンエンドポイントへのリクエストもライブラリを使うと実装済みのメソッドが使えます。
go-shopify では GetAccessToken()
を使うことができます。
// アクセストークンを取得する
ctx := context.Background()
token, err := app.GetAccessToken(ctx, shop, code)
if err != nil {
// エラーハンドリング
}
ちなみに認可コードが利用できるのは一度だけです。同じ認可コードを使って、複数回トー
クンを要求できません。
アクセストークンを取得しなおしたい場合などは再度 OAuth フローを通る必要があります。
ここまでのリダイレクト URL で行う実装のコードを貼っておきます。
コード全文(あくまでも一例)
package shopify
import (
goShopify "github.com/bold-commerce/go-shopify/v4"
)
// (/api/shopify/oauth/redirect) にリダイレクトされた時に実行する関数と想定
func ShopifyOAuthRedirect(w http.ResponseWriter, r *http.Request) {
// APIキーやシークレットなどをDBもしくはその他保存しているところから取得する処理
config, err := GetShopifyOAuthConfig()
if err != nil {
// エラーハンドリング
}
// app設定
app := goShopify.App{
ApiKey: config.apiKey, // 直接貼り付けない
ApiSecret: config.apiSecret, // 直接貼り付けない
RedirectUrl: "https://xxxxx.com/api/shopify/oauth/redirect", // 管理画面で設定したURL
Scope: "read_products,read_customer", //スコープ(複数ある場合はカンマで繋げる)
}
// リクエストの有効性を検証する
ok, err := app.VerifyAuthorizationURL(r.URL)
if err != nil || !ok {
// エラーハンドリング
}
// アクセストークンを取得する
ctx := context.Background()
token, err := app.GetAccessToken(ctx, shop, code)
if err != nil {
// エラーハンドリング
}
url := "任意のページのURL"
// フローが終了したら任意のページにリダイレクトする
http.Redirect(w, r, url, http.StatusFound)
}
おまけ ①:アクセストークンについて
アクセストークン取得後はトークンを使ってクライアントからリソースサーバーに対するアクセスで利用できます。
ここではアクセストークンについて 2 点情報を補足します。トークン漏洩時のリスクなどを理解してトークンの管理ができるようにしましょう!
- ShopifyOAuth で発行したアクセストークンはBearer トークンである
- Bearer トークンとは、持参人トークン・署名なしトークンとも呼ばれており、「トークンを所有している」という条件を満たしさえすればそのトークンを利用できる
- そのためリソースサーバー(ShopifyAPI)はアクセストークンの送信元を確認しない。アクセストークンが漏洩した場合などは、誰でも API へアクセスできてしまう状態になってしまう。(そのため必要最小限のスコープにとどめておきましょう)
- ShopifyOAuth フローで取得したアクセストークンには、オンラインアクセストークンとオフラインアクセストークンの 2 種類が存在する
- 二つの違いは「有効期限」を持っているかどうか。オンラインアクセストークンが有効期限を持ったトークン。有効期限を過ぎるとリソースサーバーにアクセスしても 401 エラーが返ってくる
- オンラインアクセストークンを発行したい場合は Shopify 開発者向けドキュメント にも書かれているが明示的に指定する必要がある。(本記事ではオフラインアクセストークンを発行)
おまけ ②:「OAuth」と「OAuth 認証」は違う
最後に OAuth で混同しやすい「OAuth」と「OAuth 認証」の違いについて理解できるようにしたいと思います。
まず前提として「OAuth」と「OAuth 認証」は別のプロトコルになります。
(そのため本記事でも「OAuth フロー」と書いています)
本記事で取り上げたのは「OAuth」であり、クライアントが特定の権限でリソースを操作できるようなるための認可プロトコルでした。
このように「OAuth」は基本的に権限委譲のために使われるべきですが、時に認証としても使われることがありその場合は「OAuth 認証」として実装する必要があります。
「OAuth 認証」は、例えば「クライアントアプリに Google アカウントでログインする」のようにアプリの認証で使われます。
ここで詳しい言及は避けますが、取得したアクセストークンを使ってリソースサーバーからユーザーの情報を取得することで認証し、アプリにログインします。
まとめ
ここまでで ShopifyOAuth フローについて、ステップを分けて一つずつ確認しました。
全体を通して主に次の 2 点を知ることができたと思います。
- ShopifyOAuth フローの流れとどのような実装が必要か
- OAuth2.0 の基本的な用語と解説、登場するロール、ロールの役割
特に用語やロールなどを知っておくことは、今後 Shopify 以外のサービスで OAuth を実装する際にも役に立つものだと思うので、ぜひこの記事で覚えていただきたいです!
参考記事
雰囲気で OAuth2.0 を使っているエンジニアが OAuth2.0 を整理して、手を動かしながら学べる本
※図を用いてとてもわかりやすく説明してくれています。
OAuth2.0 の基本仕様
ShopifyOAuth
ShopifyAPI アクセススコープ
Bearer トークンの仕様
Shopify アプリで OAuth を学ぶ
Discussion