salla-app-auth について
Salla アプリの OAuth 設定とマーチャントトークンの扱いを案内する、アプリ開発者向けスキルです。Webhook ベースの Easy Mode とコールバック方式の Custom Mode を区別し、永続保存と安全なリフレッシュトークンの更新を重視します。
主な機能
- 01OAuth モードの選択
- 02スコープとリダイレクト URL の設定
- 03マーチャントトークンの受信または交換
- 04マーチャント単位でのトークン保存
- 05アクセストークンの有効期限管理
- 06競合を防ぐ直列リフレッシュ
活用シーン
- 01公開アプリのトークン配信設定
- 02ローカルまたは Postman での OAuth テスト
- 03後続の API 呼び出しに備えたトークン保存
- 04リフレッシュトークンの並行再利用の回避
MCP 対応
このスキルは Salla Partners MCP のツールを使い、OAuth スコープの参照・設定とアプリ設定の接続を行います。トークンの受信や交換を開発者に案内しますが、トークン処理自体は実行時コードで行います。
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ sallaapp/ salla-partners-agent-kit --skill salla-app-authインストール後、Claude Code で/salla-app-authを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/app-events.mdcustom-mode.mdtoken-refresh.md詳細
Salla App 認証フロー
アプリ用の有効なマーチャントトークンを取得して保持します。OAuth モードを選び、アプリを設定し、トークンを受け取り/交換して、安全に更新します。手順を順番に進め、次に進む前に各確認項目を完了してください。ステップ 2 では Salla Partners MCP を使って操作を実行します。トークンの処理は実行時のコードで行います。
アプリを公開しますか? → Easy Mode を標準にしてください。 トークンは
app.store.authorizewebhook で届くため、OAuth/callbackやstateの処理は不要です。Custom Mode(/callbackでの コード交換)は、主にローカル開発や Postman 用です。正当な用途がないまま公開アプリに組み込むと、 Salla の管理者審査で却下される可能性があります。慣れ親しんだ OAuth2 コールバックを習慣で使うのではなく、 Easy Mode を選んでください。
ツールと MCP
| ツール | 操作 | 機能 |
|---|---|---|
salla_scopes | get / set | アプリの OAuth スコープ(スラッグ、無効化フラグ、選択状態)を読み取る、または更新します |
salla_apps | connect | スコープ、リダイレクト URL、webhook_url(app.store.authorize を自動的に配信)を 1 回の呼び出しで設定します |
app.store.authorizeはアプリイベントです。購読処理は不要で、自動的に配信されます。 アプリはデフォルトで 自身のアプリイベントを購読するため、webhook_urlを設定するとトークンイベント (app.store.authorize)や、その他すべてのapp.*ライフサイクルイベントが自動的に届きます。salla_events action=subscribeを呼び出す必要はありません。この操作はストアイベント (order.*、product.*など)専用で、→ salla-webhooks で扱います。
ドキュメント:https://docs.salla.dev/421118m0.md · アプリイベント:https://docs.salla.dev/421413m0.md · API ヘッダー:
Authorization: Bearer <access_token>。
ステップ 0 — 確認
- このアプリは App Store で公開しますか(→ Easy Mode)、それともローカル/Postman でテストしますか (→ Custom Mode)?
- トークンはどこに保存しますか? 有効期限とともに、
merchantID をキーとして実際のデータストア(DB)に永続化してください。/tmpとメモリ上は保存先ではありません。その理由はステップ 3 で説明します。 - トークン更新時の同時実行制御方法は決まっていますか?(必要になります。ステップ 5 を参照してください)
ステップ 1 — OAuth モードを選択する
| Easy Mode ✅ | Custom Mode | |
|---|---|---|
| トークンの受け取り方法 | app.store.authorize webhook payload 経由 | /oauth/callback でのコード交換 |
| コールバック URL が必要? | いいえ | はい |
| 公開アプリの場合 | 推奨(デフォルト) | 正当なユースケースがあれば許可 — 審査で却下される場合があります |
| テストでの使用 | はい | はい(Postman、ローカル開発) |
| 推奨事項 | デフォルト — すべてのアプリに推奨 | 開発時のみ。本当のユースケースが必要 |
| トークンの処理 | Salla がすべて処理します。お客様は保存するだけです | 交換処理全体を実装します |
選択ルール — デフォルトでは Easy Mode を選びます。 Easy Mode はすべてのアプリに推奨されるデフォルトです。より信頼性が高く、分かりやすく、ほとんどのユースケースで最も簡単に実装できます。Salla は app.store.authorize webhook 経由でトークンを配信するため、構築、保護、保守が必要なコールバックや state フローはありません。具体的な技術的理由で利用できない場合を除き、Easy Mode を使用してください。Custom Mode は開発中のローカル開発/Postman 用です。本番環境で本当に必要な場合は、ユースケースを説明できるようにしてください。実際のユースケースがないまま Custom Mode を使用する公開アプリは、審査で却下される場合があります。
確認事項:「Easy Mode をデフォルトにしましたか(または Custom Mode を選ぶ、審査可能な正当な理由がありますか)?」
ステップ 2 — アプリを設定する(Partners MCP)
トークンの受け渡しに必要な OAuth と webhook の設定を行います。Partners MCP で設定してください。
-
スコープ —
salla_scopes action=getとapp_idを指定して、スラッグ(およびアプリごとの無効化フラグ)を読み取ります。salla_scopes action=setで更新するか、以下の Connect に含めて設定します(フラットなslug → "read" | "read_write" | ""マップ)。 最小権限をリクエストしてください: アプリが実際に使用するリソースのスラッグだけを許可し、アプリがそのリソースに書き込む場合を除き"read"を優先してください。スラッグ全体に広く"read_write"をデフォルト適用しないでください。 -
Connect —
salla_apps action=connectとapp_idを指定し、scopesを設定します({ "<slug>": "read" | "read_write" }— スラッグとアクセスレベルは別々のキーです。例:{"orders": "read_write"})。redirect_urlsは URL の登録だけではなく、認証モードを選択する設定です:- Easy Mode —
redirect_urlsに、アプリのeasy_redirect_urlを設定します(Portal が Salla 管理のコールバック URL を計算します。salla_apps action=getで確認してください)。本番環境では["https://accounts.salla.sa/callback/{app_id}"]です。OAuth のredirect_uriを Salla 独自のコールバックに指定することで、Salla が交換処理を担います。webhook_urlとwebhook_security_strategy: "signature"を組み合わせてください。 - Custom Mode —
redirect_urlsに独自のコールバック URL を設定します。ここに Salla 以外の URL を指定すると、Custom Mode が有効になります。Salla はコード交換を想定して、マーチャントをお客様のコールバックにリダイレクトします。つまり、「コールバックを構築しない」(Easy Mode)ことと「redirect_urlsに何を指定するか」は同じ判断です。Easy Mode では、redirect_urlsにアプリではなく Salla のコールバックを指定します。
(信頼済み IP もここで設定します — 下記の「パート:IP ホワイトリスト設定」を参照してください。)
MCP が署名シークレットを生成したりローテーションしたりすることはありません。デプロイ前にライブで読み取ってください。
connectは Webhook 署名シークレットに触れません。https://portal.salla.partners/apps/{app_id}の Partner Portal で、手動で作成またはローテーションしてください。アプリの{app_id}プレースホルダーではなく、実際の ID を含むリンクをユーザーに提供してください(例:https://portal.salla.partners/apps/1234567)。 そこでローテーションすると、以前の値は無効になります。MCP はこの値に対して読み取り専用です。デプロイ前に必ずsalla_apps action=get(webhook_secretフィールド)で現在のシークレットを読み取り、検証用環境変数にその値を正確に設定してください。以前のセッションやコンテキスト圧縮前に記憶したシークレットを決して信用しないでください。Portal でローテーションされていれば、その値は古くなっており、すべてのX-Salla-Signatureチェックに失敗し、トークンは届きません。 (署名検証について → salla-webhooks。)offline_accessをconnectのスコープマップに含めてはいけません。 これはリフレッシュトークンを有効にする OAuth2 トークンスコープであり、認可 URL のみに指定します(スペース区切り。例:scope=offline_access orders.read_write)。connectマップに指定できるのはリソーススラッグのみです(例:{"orders": "read_write"})。 - Easy Mode —
-
トークンイベントの購読呼び出しは不要です。
app.store.authorizeはアプリイベントです。手順 2 でwebhook_urlを設定すると、ほかのすべてのapp.*ライフサイクルイベントとともに自動配信されます。(アプリが反応するストアイベントに対してのみsalla_events action=subscribeを使用してください → salla-webhooks。)
手動での代替手順: Partners Portal → App Keys / Webhooks / App Scope。
確認事項:「リソーススコープが適用されており、redirect_urls が意図したモードと一致していますか? — Easy Mode → redirect_urls = アプリの easy_redirect_url(本番環境:
["https://accounts.salla.sa/callback/{app_id}"])かつ webhook_url と
webhook_security_strategy が設定済み(app.store.authorize はそこに自動配信されるため、購読呼び出しは不要です。webhook_url がないとイベントの配信先がなく、トークンは届きません);
Custom Mode → 独自のコールバック URL ですか?」
確認事項:「デプロイ前に、salla_apps action=get で現在の webhook_secret を読み取り、その値を検証環境に正確に設定しましたか?以前のセッションから持ち越したシークレットではありませんか(ローテーションされている可能性があります)?」
ステップ 3 — トークンを受信または交換する
簡単モード(本番環境)
- マーチャントがアプリをインストールすると、Salla が
app.store.authorizeを Webhook に送信します。 - ハンドラーが
payload.dataからaccess_tokenとrefresh_tokenを読み取り、両方をmerchantをキーとして保存します。 - マーチャントがアプリを更新すると、Salla は
app.updatedの後に再びapp.store.authorizeを送信し、同じハンドラーが新しいトークンを受け取ります。
ハンドラーの構成:まず署名を検証し(salla-webhooks)、次に app.store.authorize で merchant をキーとして access_token/refresh_token/expires * 1000 をアップサートし、すぐに 200 を返します。ハンドラーの完全なコード:references/app-events.md。
トークンは実際のデータストアに永続化してください。
/tmpやメモリ上は保存先ではありません。merchantをキーとして、アクセストークンとリフレッシュトークンを永続性のある DB(Postgres、MySQL、DynamoDB、永続化設定済みの Redis など)に書き込みます。サーバーレス環境/Vercel では、ファイルシステム(/tmp)やモジュールレベルの変数はすべて一時的なもので、コールドスタートのたびに消去されます。トークンが消えると、次の API 呼び出しは 403 になり、実際にはアンインストールされていないのに、マーチャントにはアプリがアンインストールされたように見えます。app.store.authorizeはインストール/更新ごとに 1 回だけ送信されるため、失われたトークンは再送されません。復旧するには再インストールが必要です。最初のコミットから実際のストレージを使用してください。
シークレットの取り扱い(両モード共通): アクセストークン/リフレッシュトークンとクライアントシークレットはシークレットです。保存時には暗号化し、ログ、エラー、診断情報には決して書き込まないでください。リダイレクト URL と Webhook URL には HTTPS のみを使用します。アプリへのアクセスは、既知のサーバー IP に制限してください(下記の IP 許可リストを参照)。
簡単モードのチェックリスト:Webhook URL が設定されている(ステップ 2)・app.store.authorize が自動的に送信される(アプリイベントなのでサブスクライブ呼び出しは不要)・ペイロード内の許可された data.scope に offline_access が含まれている(リフレッシュトークンが発行される)・DB がマーチャントごとに access_token/refresh_token/token_expires_at を保存する・ハンドラーが挿入ではなくアップサートを行う。
カスタムモード(テスト/ローカル開発)
認可コードフローです。認可リクエスト(offline_access が必須。redirect_uri は Portal と完全に一致する必要があります)→ コールバック(code と state)→ POST /oauth2/token による交換 → 両方のトークンを永続化します。このフローには、Salla 固有のコールバックに関する注意点がいくつかあります(登録前にコールバックをデプロイする、Salla が開始するインストールでは独自の state が送られる、state からハイフンが除去される、code は 1 回限り、Next.js でリダイレクト時に Cookie が失われる問題)。
認可 URL、トークン交換、コールバックのルール、PHP/Laravel のコードを含む完全なフローは、references/custom-mode.md を参照してください。
確認事項:「トークンはマーチャントごとに永続化され、expires から算出した tokenExpiresAt が保存されていますか?」
ステップ 4 — トークンのライフサイクルを理解する
| トークン | 有効期間 | 備考 |
|---|---|---|
| アクセストークン | expires フィールドに従う(固定の数値なし) | app.store.authorize ペイロードの expires が信頼できる情報源です。これは Unix タイムスタンプです。固定の期間を想定しないでください |
| リフレッシュトークン | 常に有効(有効期限なし) | リフレッシュのたびに 1 回限り使用できます。各リフレッシュで新しいリフレッシュトークンが返されるため、保存してください。トークンチェーン自体に有効期限はありません |
expires は期間ではなく、絶対値の Unix タイムスタンプ(秒)です。固定値をハードコードせず、これを基準に有効期限を判定してください。保存前に変換します(出典:https://docs.salla.dev/421413m0.md)。
// ✅ expires is an absolute Unix timestamp (seconds)
const expiresAt = new Date(payload.data.expires * 1000); // ms
// ❌ it is NOT a duration — never do: new Date(Date.now() + expires * 1000)
リフレッシュトークンは、スコープに offline_access が含まれている場合にのみ発行されます。必ず offline_access を含めてください。含めないとリフレッシュトークンが発行されず、expires の経過後にアクセストークンを更新できないため、マーチャントはアプリを再インストールする必要があります。
確認事項:「両方のトークンと、変換済みの expiresAt が保存され、スコープに offline_access が含まれていますか?」
ステップ 5 — トークンを安全に更新する(要注意)
更新は使い捨てです。新しいリフレッシュトークンが返され、以前のトークンは無効になります。 同じリフレッシュトークンを 2 回使うと(並列更新による競合)、Salla の OAuth サーバーは トークンの連鎖が侵害されたと判断します。その結果、連鎖全体が無効になり、マーチャントはアプリを再インストールする必要があります。この状態からは復旧できません。マーチャントごとにロックを使って更新を直列化し、新しいトークンが永続化されるまで、リフレッシュトークンがロックの外に出ないようにしてください。
必須:マーチャントごとの分散ミューテックス。 トークンエンドポイントを呼び出す前に、マーチャントごとのロックを取得してください。別のプロセスがすでにロックを保持している場合は、短時間待ってから DB の更新済みトークンを再読み込みしてください。更新を再試行してはいけません。所有者トークンの管理とアトミックなロック解放を任せられる、実績のある分散ロックライブラリ(Redis 用の redlock や、DB のアドバイザリロックなど)を使用してください。有効期限切れ後に 401 を受けてからではなく、事前に更新してください(例:expiresAt の約 1 日前)。また、ロックを解放する前に、新しいアクセストークンとリフレッシュトークンの両方を必ず永続化してください。呼び出しが返された時点で、古いリフレッシュトークンは無効になります。
実行可能なコード一式(TS の refreshTokenSafe と getValidToken、および PHP 版)については、references/token-refresh.md を参照してください。
確認事項:「更新は分散ロックで保護され、新しいトークンの両方を保存し、有効期限前に事前実行されますか?」
ステップ 6 — マーチャント情報を取得して保存する
トークンの取得後(および app.store.authorize の実行後は毎回)、マーチャントの詳細情報を更新してください。
GET https://accounts.salla.sa/oauth2/user/info
Authorization: Bearer <access_token>
{
"id": 1771165749,
"name": "Test User",
"email": "[email protected]",
"merchant": {
"id": 1803665367,
"username": "dev-store-name",
"name": "My Store",
"plan": "special",
"status": "active",
"domain": "https://salla.sa/my-store"
}
}
ストア ID は merchant.id です。レスポンスの最上位にあり、data の中ではありません。 user/info
には data のエンベロープがありません(Webhook/API のレスポンスとは異なります)。安全に値を取り出してください。
// ✅ user/info shape: { id, name, email, merchant: { id, ... } }
const merchantId = info?.merchant?.id; // number, top level — NOT info.data.merchant.id
if (!merchantId) throw new Error("user/info: missing merchant.id"); // guard BEFORE stringify
const storeId = String(merchantId);
// ❌ String(info?.merchant?.id ?? "") — turns a missing id into "" / a 0 id into "0", both
// truthy after String(), so the guard silently passes and a bad value reaches the DB
確認事項:「マーチャント ID とストアの詳細情報は、トークンと一緒に保存されていますか?」
危険信号
個別に考えるともっともらしくても、本番環境の Salla アプリを壊す考え方です。次のどれかを考えていることに気づいたら、立ち止まって、指定されたステップを読み直してください。
| つい考えてしまうこと | それが間違っている理由 |
|---|---|
「OAuth はわかっているので、/callback フローを作ればいい」 | それはカスタムモードです。正当なユースケースがないまま公開アプリに組み込むと、審査で却下される可能性があります。既定ではイージーモードを選んでください(ステップ 1)。 |
| 「加盟店ごとのリフレッシュ用ミューテックスは過剰だ」 | リフレッシュトークンは単回使用です。並行して二重使用するとトークンチェーン全体が無効になり、加盟店は再インストールが必要になります。これは必須です(ステップ 5)。 |
| 「分散ロックは後で追加しよう/開発環境では省略しよう」 | 開発時の習慣は本番環境にも持ち込まれます。また、この競合状態は実際の並行処理でのみ発生します。つまり本番環境の実際の加盟店で発生します。今すぐ一度追加してください(ステップ 5)。 |
| 「リフレッシュに成功したので、新しいアクセストークンを保存しよう」 | 新しいトークンを両方とも保存する必要があります。古いリフレッシュトークンはすでに無効です。新しいトークンを破棄すると、次のリフレッシュが失敗します(ステップ 5)。 |
「offline_access はほかのリソーススコープと同じだ」 | これは OAuth トークンスコープであり、認可 URL にのみ指定します。connect マップには含めません。省略すると、リフレッシュトークンが発行されません(ステップ 4)。 |
「expires はトークンの有効期間を秒数で表したものだ」 | これは絶対 Unix タイムスタンプです。期間として扱うと、有効期限が数十年先に設定され、トークンは気付かないうちに失効します(ステップ 4)。 |
「ひとまずトークンを /tmp に(またはモジュール変数に)置いておこう」 | サーバーレス環境では、コールドスタートのたびにファイルシステムとメモリが消去されます。トークンが失われ、次の呼び出しは403 となり、加盟店が未インストールのように見えてしまいます。最初のコミットから、merchant をキーにして実際の DB に永続化してください(ステップ 3)。 |
「デプロイ前に connect を再実行して、Webhook シークレットを(再)発行しよう」 | connect は署名シークレットを発行もローテーションもしません。MCP はシークレットに対して読み取り専用です。Portal(https://portal.salla.partners/apps/{app_id})で作成/ローテーションしてください。デプロイ前に salla_apps action=get で現在の webhook_secret を読み取り、その値をそのまま使用してください(ステップ 2)。 |
| 「以前メモした Webhook シークレットを再利用しよう」 | Portal でシークレットをローテーションすると、古い値は無効になります。そのため、以前のセッションやコンパクション前に記憶したシークレットは古くなっている可能性があり、検証がすべて失敗します。デプロイ前に salla_apps action=get で最新の値を読み取ってください(ステップ 2)。 |
| 「デバッグ用ならトークンをログに出しても問題ない」 | アクセストークン、リフレッシュトークン、クライアントシークレットは秘密情報です。保存時には暗号化し、決してログに記録しないでください(ステップ 3)。 |
リファレンス
OAuth スコープ
スコープのコンテキストは 2 種類あり、混同しないでください。
1. salla_apps action=connect のスコープマップ — リソーススコープのみを指定します。スラッグとレベルは別々のフィールドに指定します。
{ "orders": "read_write", "products": "read", "customers": "read_write" }
2. OAuth 認可 URL — スペース区切りのドット形式の文字列です。ここには offline_access を含めてください(リソーススコープではなく、リフレッシュトークンを有効にする OAuth トークンスコープです)。
scope=offline_access orders.read_write products.read customers.read_write
salla_scopes action=get を使用して、アプリで有効なリソーススラッグ(およびアプリごとの無効化フラグ)を確認してください。
orders products customers branches
settings webhooks payments taxes
specialoffers categories brands metadata
アプリイベント
| イベント | タイミング | アクション |
|---|---|---|
app.store.authorize | アプリのインストール時または更新時(トークンの更新時には発生しません。その場合は grant_type=refresh_token を呼び出します) | 両方のトークンと有効期限を保存/更新 |
app.installed | 初回インストール時 | リソースをプロビジョニングする |
app.uninstalled | マーチャントがアプリを削除したとき | マーチャントのデータをクリーンアップし、保存済みトークンを失効させる |
ペイロードの全体構造については、references/app-events.md を参照してください。ライフサイクル処理については、salla-app-lifecycle を参照してください。
IP のホワイトリスト登録
Partners Portal → My Apps → Your App → App Trusted IPs で既知のサーバー IP にアプリを制限します(または salla_apps action=connect で trusted_ips を渡します)。本番アプリの攻撃対象領域を縮小できます。
主なエンドポイントとライブラリ
| 用途 | URL |
|---|---|
| 認可 | https://accounts.salla.sa/oauth2/auth |
| トークン交換と更新 | https://accounts.salla.sa/oauth2/token |
| ユーザー情報 | https://accounts.salla.sa/oauth2/user/info |
| 直接インストール | https://s.salla.sa/apps/install/{app-id} |
| Salla API のベース URL | https://api.salla.dev/admin/v2/ |
| ライブラリ | 言語 | リポジトリ |
|---|---|---|
salla/oauth2-merchant | PHP | https://github.com/SallaApp/oauth2-merchant |
@salla.sa/passport-strategy | JavaScript | https://github.com/SallaApp/passport-strategy |
| Laravel starter kit | PHP/Laravel | https://github.com/SallaApp/laravel-starter-kit |