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