mcpField
S
salla-app-auth

salla-app-auth

github.com/sallaappセキュリティ290 回インストール2 週間前に更新
リポジトリ

salla-app-auth について

Salla アプリの OAuth 設定とマーチャントトークンの扱いを案内する、アプリ開発者向けスキルです。Webhook ベースの Easy Mode とコールバック方式の Custom Mode を区別し、永続保存と安全なリフレッシュトークンの更新を重視します。

主な機能

  1. 01OAuth モードの選択
  2. 02スコープとリダイレクト URL の設定
  3. 03マーチャントトークンの受信または交換
  4. 04マーチャント単位でのトークン保存
  5. 05アクセストークンの有効期限管理
  6. 06競合を防ぐ直列リフレッシュ

活用シーン

  1. 01公開アプリのトークン配信設定
  2. 02ローカルまたは Postman での OAuth テスト
  3. 03後続の API 呼び出しに備えたトークン保存
  4. 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.md
custom-mode.md
token-refresh.md

詳細

Salla App 認証フロー

アプリ用の有効なマーチャントトークンを取得して保持します。OAuth モードを選び、アプリを設定し、トークンを受け取り/交換して、安全に更新します。手順を順番に進め、次に進む前に各確認項目を完了してください。ステップ 2 では Salla Partners MCP を使って操作を実行します。トークンの処理は実行時のコードで行います。

アプリを公開しますか? → Easy Mode を標準にしてください。 トークンは app.store.authorize webhook で届くため、OAuth /callback や state の処理は不要です。Custom Mode(/callback での コード交換)は、主にローカル開発や Postman 用です。正当な用途がないまま公開アプリに組み込むと、 Salla の管理者審査で却下される可能性があります。慣れ親しんだ OAuth2 コールバックを習慣で使うのではなく、 Easy Mode を選んでください。

ツールと MCP

ツール操作機能
salla_scopesget / setアプリの OAuth スコープ(スラッグ、無効化フラグ、選択状態)を読み取る、または更新します
salla_appsconnectスコープ、リダイレクト 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 — 確認

  1. このアプリは App Store で公開しますか(→ Easy Mode)、それともローカル/Postman でテストしますか (→ Custom Mode)?
  2. トークンはどこに保存しますか? 有効期限とともに、merchant ID をキーとして実際のデータストア(DB)に永続化してください。 /tmp とメモリ上は保存先ではありません。その理由はステップ 3 で説明します。
  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 で設定してください。

  1. スコープ — salla_scopes action=get と app_id を指定して、スラッグ(およびアプリごとの無効化フラグ)を読み取ります。salla_scopes action=set で更新するか、以下の Connect に含めて設定します(フラットな slug → "read" | "read_write" | "" マップ)。 最小権限をリクエストしてください: アプリが実際に使用するリソースのスラッグだけを許可し、アプリがそのリソースに書き込む場合を除き "read" を優先してください。スラッグ全体に広く "read_write" をデフォルト適用しないでください。

  2. 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"})。

  3. トークンイベントの購読呼び出しは不要です。 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 — トークンを受信または交換する

簡単モード(本番環境)
  1. マーチャントがアプリをインストールすると、Salla が app.store.authorize を Webhook に送信します。
  2. ハンドラーが payload.data から access_token と refresh_token を読み取り、両方を merchant をキーとして保存します。
  3. マーチャントがアプリを更新すると、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 のベース URLhttps://api.salla.dev/admin/v2/
ライブラリ言語リポジトリ
salla/oauth2-merchantPHPhttps://github.com/SallaApp/oauth2-merchant
@salla.sa/passport-strategyJavaScripthttps://github.com/SallaApp/passport-strategy
Laravel starter kitPHP/Laravelhttps://github.com/SallaApp/laravel-starter-kit

コメント