コンテンツにスキップ

Auth Denied

plasma における認証失敗には、3 つの異なる種類があります。どれに当たっているかで 対処が決まります。

種類 1 — リクエストレベルの 401

Section titled “種類 1 — リクエストレベルの 401”

症状:

  • /sync/push または /sync/pull で HTTP 401
  • リトライ枯渇後に client.onError{ kind: "push-http", status: 401, url } または { kind: "pull-http", status: 401, url } で発火する

原因: SyncHandlerOptions.auth(req){ ok: false } を返しました。

対処: auth 関数が何をしているか確認します。

auth: async (req) => {
const token = req.headers.get("authorization")?.replace("Bearer ", "")
console.log("auth token:", token?.slice(0, 8)) // sanity log
if (!token) return { ok: false, reason: "no token" }
const user = await verifyJWT(token)
if (!user) return { ok: false, reason: "bad token" }
return {
ok: true,
clientGroupID: user.id,
clientID: req.headers.get("x-client") ?? "unknown",
ctx: { userId: user.id },
}
}

よくある原因:

  • クライアントが Authorization ヘッダーを送っていない。 PlasmaClientOptions.authHeaders が配線されているか確認してください。
    createPlasmaClient({
    ...,
    authHeaders: async () => ({
    "authorization": `Bearer ${await getToken()}`,
    }),
    })
  • トークンがセッション途中で期限切れになった。authHeaders はすべての リクエストで呼ばれるので、積極的にリフレッシュするトークン関数が対処になります。

種類 2 — mutator レベルの PlasmaAuthorizationError

Section titled “種類 2 — mutator レベルの PlasmaAuthorizationError”

症状:

  • push は 200 OK を返した (ワイヤ上で { ok: true })。
  • SyncClientError は発火しない。
  • ユーザーの optimistic apply は一瞬成功したように見えたが、次の pull で 行が静かに消えた。

原因: あるテーブルの auth.write(ctx, row)false を返しました。 サーバー側の mutator が PlasmaAuthorizationError を throw し、トランザクションが ロールバックされ、last_mutation_id は依然として進み (poison がリトライされない ように)、change log 行は書かれませんでした。次の pull でクライアントの dropConfirmed が outbox エントリを削除し、rebuildOptimistic が (その書き込みを 見ていない) base ストアの上に生き残った outbox をリプレイし、optimistic ビューが revert します。

対処: テーブルの auth.write predicate を確認します。よくある原因:

  • サーバー上の ctx の値が、期待したフィールドを持っていない。auth()ctx: { userId: user.id } を返すのに、行の userId が異なる形式 (email か UUID か) を使っている場合、predicate は決して一致しません。
  • クライアントが、現在のユーザーに属さない userId を持つ行を optimistic に 書き込んだ — おそらくユーザー切り替え前の古いフォームの値です。
  • 更新している行が別のユーザーによって作成されたもので、UI が「編集」ボタンを 所有権でゲートしていなかった。

UI での応答: mutate() を呼ぶ前にコンポーネントで権限を事前チェックします。 クライアントはサーバー側の PlasmaAuthorizationError を直接観測できないため、 目に見える失敗モードは「行が戻ってきた」ことです。所有権を反映するように ボタンの状態を設計してください。

<button
disabled={row.userId !== ctx.userId}
onClick={() => update.mutate({ ... })}
>
Edit
</button>

症状:

  • エラーイベントは一切発火しない。
  • しかしユーザーが見えることを期待している行が、決して現れない。
  • Devtools パネルは、サーバー上の _plasma_changes にはそれらがあると表示するが、 クライアントの IDB にはない。

原因: テーブルの auth.read(ctx, row) が、pull 時にそれらの行に対して false を返しました。plasma は認可されていない行を pull レスポンスから静かに フィルタリングし、ユーザーが許可されていない行を決して見ないようにします。

対処: 種類 2 と同じ predicate の診断です。確認事項:

  • pull 時に ctx.userId (または predicate が使うフィールド) が期待どおりか? auth() ハンドラでログを取ってください。
  • 行自身のフィールド (row.userId) が期待どおりか? サーバー DB を直接 クエリします。
    Terminal window
    wrangler d1 execute my-app-prod --command "SELECT id, userId FROM todos WHERE ..."
  • 集合メンバーシップのつもりで、うっかり厳密等価を使っていないか? (ユーザーが多くのチームのメンバーなのに row.teamId === ctx.userId としている等)。

すべての auth 判断は、まずあなたの auth() 関数を通ります。そこでログを取り、 tail を監視します。

auth: async (req) => {
const result = await realAuthFunction(req)
console.log("auth:", req.url, result.ok, result.reason)
return result
}
Terminal window
pnpm wrangler tail

すべてのリクエストが 1 行ログを出します。クライアントの失敗した mutation を サーバーの拒否と相関付けられます。

  • UX が有利になる箇所ではクライアントで事前チェックする。 よくあるケースで revert に頼らないでください。
  • 現実的な ctx 値でテストする。 auth.writefalse を返すケースを 行使し、トランザクションがロールバックされ (change log に一致する行がない) 、 last_mutation_id が poison を落とすために進んだことをアサートする認証テストを、 サーバー側のテストスイートに含めてください。
  • 拒否を集計してログに残す。 本番では auth() の戻り値をテレメトリに hook し、パターンを見つけられるようにしてください (「バージョン X のユーザーが 全員 401 になっている」— おそらくトークン形式の変更)。