はじめに
Reactアプリ単体であれば、Clerkの認証実装はかなり楽です。<ClerkProvider> で囲んで、<SignIn /> を置けばほぼ完成。
ところが今回は Chrome拡張機能(ポップアップUI)とWebアプリ(ad4n.dev)の両方でClerkを使う という構成を選んだのですが、これが思いのほかしんどかったです。詰まったポイントが時系列順に3つあったので、まとめておきます。
詰まり① Production Keyで拡張機能からClerkを叩けない
何が起きたか
ローカル開発中は特に問題なかったのに、Production Keyに切り替えた途端にこんなエラーが出ました。
Clerk: Production Keys are only allowed for domain "ad4n.dev".
API Error: The Request HTTP Origin header must be equal to or a subdomain of the requesting URL.
ClerkのProduction Keyは、登録したプライマリドメイン(今回は ad4n.dev)以外からのリクエストを拒否します。Chrome拡張機能のOriginは chrome-extension://... になるので、当然弾かれます。
解決策:syncHost(セッション共有方式)
直接Clerk APIを叩くのをやめ、WebアプリとCookieを共有する 方式にしました。
Chrome拡張機能
↕ Cookie同期(syncHost)
clerk.ad4n.dev ← Clerk Frontend API
↕ HTTPS
Clerk サーバー
拡張機能が自分でClerk APIを叩くのではなく、clerk.ad4n.dev のCookieを読んでセッションを借用するイメージです。
変更点1:ClerkProviderにsyncHostを追加
// src/popup/index.tsx
const SYNC_HOST = process.env.VITE_CLERK_SYNC_HOST
<ClerkProvider publishableKey={PUBLISHABLE_KEY} syncHost={SYNC_HOST}>
# .env.production
VITE_CLERK_SYNC_HOST=https://clerk.ad4n.dev
syncHost に設定するのは WebアプリのURL(ad4n.dev)ではなく、Clerk Frontend APIのURL(clerk.ad4n.dev) です。Clerk Dashboard の API Keys から確認できます。
変更点2:manifest.config.tsにcookiesパーミッションを追加
permissions: [
'storage',
'activeTab',
'cookies' // 追加
],
host_permissions: [
'https://note.com/*',
'https://clerk.ad4n.dev/*' // 追加
],
プロジェクトのルートに manifest.json があっても、@crxjs/vite-plugin を使っている場合は manifest.config.ts が実際のビルドに使われます。最初そこを勘違いしてハマりました。
変更点3:SignInButton / SignOutButton を廃止
syncHostモードでは、ログイン・ログアウトAPIを拡張機能内から呼ぶと OriginヘッダーとAuthorizationヘッダーが同時に送信されてClerkに拒否されます。
only one of 'Origin' and 'Authorization' headers should be provided
なので、拡張機能内にサインインUIを置くのをやめ、Webアプリのログインページに誘導する方式 にしました。
// ログイン
const onClickSignIn = () => {
void chrome.tabs.create({ url: `${process.env.VITE_WEB_APP_URL}/login` })
}
<Button size="lg" onClick={onClickSignIn}>ログイン</Button>
// ログアウト(Header.tsx)
<Menu.Item
leftSection={<MdLogout />}
onClick={() => chrome.tabs.create({ url: `${process.env.VITE_WEB_APP_URL}/logout` })}
>
ログアウト
</Menu.Item>
UXとしては「ログインボタン → 新規タブでWebアプリが開く → Webアプリでログイン → タブを閉じる → 拡張機能のポップアップを開き直す → ログイン済みになってる」という流れになります。
詰まり② ログインしようとしたら /client-trust に飛ばされてフローが止まる
何が起きたか
Webアプリ側の <SignIn /> でパスワードを入力してログインしようとすると、/login/client-trust というURLに遷移してフォームが消える、という現象が起きました。
原因:Client Trust機能
Clerkには Client Trust という、クレデンシャルスタッフィング攻撃(不正なパスワード総当たり)への対策機能があります。2025年11月14日以降に作成されたClerkアプリではデフォルトで有効になっています。
以下の条件をすべて満たすと、サインイン結果のステータスが needs_client_trust になり、メール/SMSコードでの追加確認が必要になります。
- 正しいパスワードを入力した
- MFAを設定していない
- 新しいデバイス(クライアント)からのサインインである
Client Trust自体は問題ないんですが、問題はルーティングでした。
NG: 専用ルートを割り当ててしまった
<SignIn routing="path" path="/login" /> のパスベースルーティングでは、内部ステップの遷移がURLの変化として表現されます(/login/client-trust など)。
最初、これに気づかず以下のようなルーティングを書いてしまいました。
// NG例
{
path: "/login",
element: <LoginPage />,
},
{
path: "/login/client-trust",
element: <ClientTrustPage />, // 空のプレースホルダー
},
こうすると <SignIn /> が /login/client-trust に遷移しようとした瞬間に <LoginPage /> がアンマウントされ、確認コード入力フォームが表示されないまま終わります。
暫定対応:ワイルドカードルート
// OK例(ただし後で別の問題が起きる)
{
path: "/login/*",
element: <LoginPage />,
},
詰まり③ 確認メールが2通届く(二重送信問題)
何が起きたか
ワイルドカードルートで対応したら Client Trust の遷移はできるようになったんですが、今度はパスワード認証後に 確認メールが2通届く という問題が発生しました。
開発環境・本番ビルドの両方で再現。
最初は React 18 の StrictMode によるエフェクトの二重実行を疑いましたが(Clerkの <SignUp/> で類似のバグ報告あり)、本番ビルドでも再現するので StrictMode の問題ではないと判断しました。
原因の推測
routing="path" では、ステップ遷移(パスワード確認 → Client Trust確認コード入力)がブラウザの実URLの変化として表現されます。ステップが切り替わるたびに新しいURLへのナビゲーションが発生し、それに紐づく副作用(確認コード送信)が遷移前後で重複して実行されてしまっていた、と考えられます。
解決策:routing="hash" に変更
hash ルーティングでは、ステップの状態はURLの フラグメント部分(#以降)で管理され、pathname は変化しません。フラグメントの変更はReact Routerにとってはルート変更ではないので、<SignIn /> が常に同一インスタンスとしてマウントされ続けます。
// LoginPage.tsx
// path propはhashルーティングでは無視されるため削除
<SignIn routing="hash" fallbackRedirectUrl="/dashboard" />
// Router.tsx
// ワイルドカードも専用ルートも不要になり、シンプルな形に戻せる
{
path: "/login",
element: <LoginPage />,
},
これで二重送信が解消しました。ClientTrustPage.tsx も不要になったので削除。
routing方式の比較
| 方式 | URLの変化 | リロード耐性 | 戻る/進む | 備考 |
|---|---|---|---|---|
path |
pathnameが変わる(例: /login/factor-two) |
◎ | ◎ | ルーター側でサブパスを潰さない設計が必要。今回の二重送信の原因 |
hash |
フラグメントのみ変わる(例: /login#/factor-two) |
◎ | 〇 | ルーター側の追加設定不要。今回はこれで解消 |
virtual |
URLは変化しない | × | × | モーダル向け。@clerk/react の <SignIn /> は非対応 |
まとめ・学んだこと
-
Chrome拡張機能 + ClerkのProduction認証は
syncHost方式で解決できる。ただしログイン・ログアウトのUIは拡張機能内に置けないので、Webアプリへ誘導する設計にする必要がある -
<SignIn routing="path">を使うときは、内部的に遷移するサブパスをルーター側で潰さないこと。ワイルドカードルートで回避できるが、副作用の重複実行リスクがある -
routing="hash"はrouting="path"より単純で、React Router との相性も良い。@clerk/react(素のReact)でのデフォルトはhashなので、特別な理由がなければhashを選んで良い - 「本番ビルドでも再現するか」の確認は StrictMode起因かどうかの切り分けに必須
-
routingprop のデフォルト値・選択肢はSDKによって異なる。Next.js用のドキュメントをReactプロジェクトに流用しないこと