1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ClerkをChrome拡張機能と組み合わせたらProduction認証が3段階で詰まった話

1
Posted at

はじめに

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起因かどうかの切り分けに必須
  • routing prop のデフォルト値・選択肢はSDKによって異なる。Next.js用のドキュメントをReactプロジェクトに流用しないこと

参考リンク

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?