clerk-chrome-extension-patterns

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Chrome Extension Patterns

Chrome扩展开发模式

CRITICAL RULES

核心规则

  1. OAuth (Google, GitHub, etc.) and SAML are NOT supported in popups or side panels -- use
    syncHost
    to delegate auth to your web app
  2. Email links (magic links) don't work in popups -- the popup closes when the user clicks outside, resetting sign-in state
  3. Side panels don't auto-refresh auth state -- users must close and reopen the side panel after signing in via the web app
  4. Service workers and content scripts have NO access to Clerk React hooks -- use
    createClerkClient()
    or message passing
  5. Extension URLs use
    chrome-extension://
    not
    http://
    -- all redirect URLs must use
    chrome.runtime.getURL('.')
  6. Without a stable CRX ID, every rebuild breaks auth -- configure
    key
    in manifest BEFORE deploying
  7. Content scripts cannot use Clerk directly due to origin restrictions -- Clerk enforces strict allowed origins
  8. Bot protection must be DISABLED in Clerk Dashboard -- Cloudflare bot detection is not supported in extension environments
  1. 弹窗或侧边面板不支持OAuth(Google、GitHub等)和SAML——使用
    syncHost
    将身份验证委托给你的web应用
  2. 邮件链接(魔法链接)在弹窗中无法正常工作——用户点击弹窗外部时弹窗会关闭,登录状态会被重置
  3. 侧边面板不会自动刷新身份验证状态——用户通过web应用登录后,必须关闭并重新打开侧边面板才能同步状态
  4. Service Worker和内容脚本无法访问Clerk React钩子——使用
    createClerkClient()
    或消息传递机制
  5. 扩展URL使用
    chrome-extension://
    而非
    http://
    ——所有重定向URL必须使用
    chrome.runtime.getURL('.')
    生成
  6. 没有稳定的CRX ID时,每次重新构建都会导致身份验证失效——部署前请先在manifest中配置
    key
  7. 由于源限制,内容脚本无法直接使用Clerk——Clerk会严格校验允许的来源
  8. 必须在Clerk管理后台关闭机器人保护——扩展环境不支持Cloudflare机器人检测

Authentication Options

身份验证选项

MethodPopupSide PanelsyncHost (with web app)
Email + OTPYesYesYes
Email + LinkNoNoYes
Email + PasswordYesYesYes
Username + PasswordYesYesYes
SMS + OTPYesYesYes
OAuth (Google, GitHub, etc.)NONOYES
SAMLNONOYES
PasskeysYesYesYes
Google One TapNoNoYes
Web3NoNoYes
方法弹窗侧边面板syncHost(配合web应用)
邮箱+OTP支持支持支持
邮箱+链接不支持不支持支持
邮箱+密码支持支持支持
用户名+密码支持支持支持
短信+OTP支持支持支持
OAuth(Google、GitHub等)不支持不支持支持
SAML不支持不支持支持
通行密钥(Passkeys)支持支持支持
Google One Tap不支持不支持支持
Web3不支持不支持支持

Quick Start (Plasmo)

快速开始(Plasmo)

bash
npx create-plasmo --with-tailwindcss --with-src my-extension
cd my-extension
npm install @clerk/chrome-extension
Enable Native API in Clerk Dashboard under Native applications. Required for all extension integrations.
.env.development
:
PLASMO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_FRONTEND_API=https://your-app.clerk.accounts.dev
src/popup.tsx
:
tsx
import { ClerkProvider, Show, SignInButton, SignUpButton, UserButton } from '@clerk/chrome-extension'

const PUBLISHABLE_KEY = process.env.PLASMO_PUBLIC_CLERK_PUBLISHABLE_KEY
const EXTENSION_URL = chrome.runtime.getURL('.')

if (!PUBLISHABLE_KEY) {
  throw new Error('Missing PLASMO_PUBLIC_CLERK_PUBLISHABLE_KEY')
}

function IndexPopup() {
  return (
    <ClerkProvider
      publishableKey={PUBLISHABLE_KEY}
      afterSignOutUrl={`${EXTENSION_URL}/popup.html`}
      signInFallbackRedirectUrl={`${EXTENSION_URL}/popup.html`}
      signUpFallbackRedirectUrl={`${EXTENSION_URL}/popup.html`}
    >
      <Show when="signed-out">
        <SignInButton mode="modal" />
        <SignUpButton mode="modal" />
      </Show>
      <Show when="signed-in">
        <UserButton />
      </Show>
    </ClerkProvider>
  )
}

export default IndexPopup
Use
mode="modal"
for
SignInButton
-- navigating to a separate page breaks the popup flow.
bash
npx create-plasmo --with-tailwindcss --with-src my-extension
cd my-extension
npm install @clerk/chrome-extension
在Clerk管理后台的原生应用设置中启用Native API,所有扩展集成都需要开启该配置。
.env.development
:
PLASMO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_FRONTEND_API=https://your-app.clerk.accounts.dev
src/popup.tsx
:
tsx
import { ClerkProvider, Show, SignInButton, SignUpButton, UserButton } from '@clerk/chrome-extension'

const PUBLISHABLE_KEY = process.env.PLASMO_PUBLIC_CLERK_PUBLISHABLE_KEY
const EXTENSION_URL = chrome.runtime.getURL('.')

if (!PUBLISHABLE_KEY) {
  throw new Error('Missing PLASMO_PUBLIC_CLERK_PUBLISHABLE_KEY')
}

function IndexPopup() {
  return (
    <ClerkProvider
      publishableKey={PUBLISHABLE_KEY}
      afterSignOutUrl={`${EXTENSION_URL}/popup.html`}
      signInFallbackRedirectUrl={`${EXTENSION_URL}/popup.html`}
      signUpFallbackRedirectUrl={`${EXTENSION_URL}/popup.html`}
    >
      <Show when="signed-out">
        <SignInButton mode="modal" />
        <SignUpButton mode="modal" />
      </Show>
      <Show when="signed-in">
        <UserButton />
      </Show>
    </ClerkProvider>
  )
}

export default IndexPopup
SignInButton
请使用
mode="modal"
——跳转到单独页面会破坏弹窗流程。

syncHost -- Sync Auth with Web App

syncHost -- 与Web应用同步身份验证状态

Use this when you need OAuth, SAML, or want the extension to reflect sign-in from your web app.
How it works: The extension reads the Clerk session cookie from your web app's domain via
host_permissions
.
Step 1 -- Environment variables:
.env.development
:
PLASMO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_FRONTEND_API=https://your-app.clerk.accounts.dev
PLASMO_PUBLIC_CLERK_SYNC_HOST=http://localhost
.env.production
:
PLASMO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_...
CLERK_FRONTEND_API=https://clerk.your-domain.com
PLASMO_PUBLIC_CLERK_SYNC_HOST=https://clerk.your-domain.com
Step 2 -- Add
syncHost
prop:
tsx
const SYNC_HOST = process.env.PLASMO_PUBLIC_CLERK_SYNC_HOST

<ClerkProvider
  publishableKey={PUBLISHABLE_KEY}
  syncHost={SYNC_HOST}
  afterSignOutUrl="/"
  routerPush={(to) => navigate(to)}
  routerReplace={(to) => navigate(to, { replace: true })}
>
Step 3 -- Configure
host_permissions
in
package.json
:
json
{
  "manifest": {
    "key": "$CRX_PUBLIC_KEY",
    "permissions": ["cookies", "storage"],
    "host_permissions": [
      "$PLASMO_PUBLIC_CLERK_SYNC_HOST/*",
      "$CLERK_FRONTEND_API/*"
    ]
  }
}
Step 4 -- Add extension ID to web app's allowed origins via Clerk API:
bash
curl -X PATCH https://api.clerk.com/v1/instance \
  -H "Authorization: Bearer YOUR_SECRET_KEY" \
  -H "Content-type: application/json" \
  -d '{"allowed_origins": ["chrome-extension://YOUR_EXTENSION_ID"]}'
Hide unsupported auth methods in popup when using syncHost:
tsx
<SignIn
  appearance={{
    elements: {
      socialButtonsRoot: 'plasmo-hidden',
      dividerRow: 'plasmo-hidden',
    },
  }}
/>
Full guide:
references/sync-host.md
当你需要使用OAuth、SAML,或者希望扩展同步web应用的登录状态时使用该方案。
工作原理:扩展通过
host_permissions
读取web应用域名下的Clerk会话Cookie。
步骤1 -- 配置环境变量:
.env.development
:
PLASMO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_FRONTEND_API=https://your-app.clerk.accounts.dev
PLASMO_PUBLIC_CLERK_SYNC_HOST=http://localhost
.env.production
:
PLASMO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_...
CLERK_FRONTEND_API=https://clerk.your-domain.com
PLASMO_PUBLIC_CLERK_SYNC_HOST=https://clerk.your-domain.com
步骤2 -- 添加
syncHost
属性:
tsx
const SYNC_HOST = process.env.PLASMO_PUBLIC_CLERK_SYNC_HOST

<ClerkProvider
  publishableKey={PUBLISHABLE_KEY}
  syncHost={SYNC_HOST}
  afterSignOutUrl="/"
  routerPush={(to) => navigate(to)}
  routerReplace={(to) => navigate(to, { replace: true })}
>
步骤3 -- 在
package.json
中配置
host_permissions
:
json
{
  "manifest": {
    "key": "$CRX_PUBLIC_KEY",
    "permissions": ["cookies", "storage"],
    "host_permissions": [
      "$PLASMO_PUBLIC_CLERK_SYNC_HOST/*",
      "$CLERK_FRONTEND_API/*"
    ]
  }
}
步骤4 -- 通过Clerk API将扩展ID添加到web应用的允许来源列表:
bash
curl -X PATCH https://api.clerk.com/v1/instance \
  -H "Authorization: Bearer YOUR_SECRET_KEY" \
  -H "Content-type: application/json" \
  -d '{"allowed_origins": ["chrome-extension://YOUR_EXTENSION_ID"]}'
使用syncHost时在弹窗中隐藏不支持的验证方式:
tsx
<SignIn
  appearance={{
    elements: {
      socialButtonsRoot: 'plasmo-hidden',
      dividerRow: 'plasmo-hidden',
    },
  }}
/>
完整指南:
references/sync-host.md

createClerkClient() for Vanilla JS / Service Workers

适用于原生JS/Service Worker的createClerkClient()

Import from
@clerk/chrome-extension/client
(not
@clerk/chrome-extension
).
Background service worker (
src/background/index.ts
):
typescript
import { createClerkClient } from '@clerk/chrome-extension/client'

const publishableKey = process.env.PLASMO_PUBLIC_CLERK_PUBLISHABLE_KEY

async function getToken(): Promise<string | null> {
  const clerk = await createClerkClient({
    publishableKey,
    background: true,
  })
  if (!clerk.session) return null
  return await clerk.session.getToken()
}

chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  getToken()
    .then((token) => sendResponse({ token }))
    .catch((error) => {
      console.error('[Background] Error:', JSON.stringify(error))
      sendResponse({ token: null })
    })
  return true
})
The
background: true
flag keeps sessions fresh even when popup/sidepanel is closed. Without it, tokens expire after 60 seconds.
Popup with vanilla JS (
src/popup.ts
):
typescript
import { createClerkClient } from '@clerk/chrome-extension/client'

const EXTENSION_URL = chrome.runtime.getURL('.')
const POPUP_URL = `${EXTENSION_URL}popup.html`

const clerk = createClerkClient({ publishableKey })

clerk.load({
  afterSignOutUrl: POPUP_URL,
  signInForceRedirectUrl: POPUP_URL,
  signUpForceRedirectUrl: POPUP_URL,
  allowedRedirectProtocols: ['chrome-extension:'],
}).then(() => {
  clerk.addListener(render)
  render()
})
Full guide:
references/create-clerk-client.md
@clerk/chrome-extension/client
导入(不要从
@clerk/chrome-extension
导入)。
后台Service Worker (
src/background/index.ts
):
typescript
import { createClerkClient } from '@clerk/chrome-extension/client'

const publishableKey = process.env.PLASMO_PUBLIC_CLERK_PUBLISHABLE_KEY

async function getToken(): Promise<string | null> {
  const clerk = await createClerkClient({
    publishableKey,
    background: true,
  })
  if (!clerk.session) return null
  return await clerk.session.getToken()
}

chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  getToken()
    .then((token) => sendResponse({ token }))
    .catch((error) => {
      console.error('[Background] Error:', JSON.stringify(error))
      sendResponse({ token: null })
    })
  return true
})
background: true
标识会保持会话有效,即使弹窗/侧边面板关闭。没有该配置的话,令牌会在60秒后过期。
原生JS实现的弹窗 (
src/popup.ts
):
typescript
import { createClerkClient } from '@clerk/chrome-extension/client'

const EXTENSION_URL = chrome.runtime.getURL('.')
const POPUP_URL = `${EXTENSION_URL}popup.html`

const clerk = createClerkClient({ publishableKey })

clerk.load({
  afterSignOutUrl: POPUP_URL,
  signInForceRedirectUrl: POPUP_URL,
  signUpForceRedirectUrl: POPUP_URL,
  allowedRedirectProtocols: ['chrome-extension:'],
}).then(() => {
  clerk.addListener(render)
  render()
})
完整指南:
references/create-clerk-client.md

Headless Extension (no popup, no side panel)

无头扩展(无弹窗、无边侧面板)

For extensions that run entirely in the background and sync with a web app.
Uses
syncHost
+
createClerkClient
with
background: true
to read auth state from the web app's cookies.
typescript
import { createClerkClient } from '@clerk/chrome-extension/client'

const publishableKey = process.env.PLASMO_PUBLIC_CLERK_PUBLISHABLE_KEY
const syncHost = process.env.PLASMO_PUBLIC_CLERK_SYNC_HOST

async function getAuthenticatedUser() {
  const clerk = await createClerkClient({
    publishableKey,
    syncHost,
    background: true,
  })
  return clerk.user
}
Requires
host_permissions
for the sync host domain in
package.json
.
Full guide:
references/headless-extension.md
适用于完全在后台运行、与web应用同步状态的扩展。
使用
syncHost
+ 配置了
background: true
createClerkClient
来读取web应用Cookie中的身份验证状态。
typescript
import { createClerkClient } from '@clerk/chrome-extension/client'

const publishableKey = process.env.PLASMO_PUBLIC_CLERK_PUBLISHABLE_KEY
const syncHost = process.env.PLASMO_PUBLIC_CLERK_SYNC_HOST

async function getAuthenticatedUser() {
  const clerk = await createClerkClient({
    publishableKey,
    syncHost,
    background: true,
  })
  return clerk.user
}
需要在
package.json
中配置syncHost域名的
host_permissions
完整指南:
references/headless-extension.md

Content Scripts

内容脚本

Content scripts run in an isolated JavaScript world injected into web pages. Clerk cannot be used directly -- origin restrictions prevent it.
Use message passing to request auth state from the background service worker:
typescript
// content.ts
async function getToken(): Promise<string | null> {
  return new Promise((resolve) => {
    chrome.runtime.sendMessage({ type: 'GET_TOKEN' }, (response) => {
      resolve(response?.token ?? null)
    })
  })
}

async function main() {
  const token = await getToken()
  if (!token) return
  // use token for authenticated API calls
}

main()
Full guide:
references/content-scripts.md
内容脚本运行在注入到网页的独立JavaScript环境中,无法直接使用Clerk——源限制会阻止访问。
使用消息传递机制向后台Service Worker请求身份验证状态:
typescript
// content.ts
async function getToken(): Promise<string | null> {
  return new Promise((resolve) => {
    chrome.runtime.sendMessage({ type: 'GET_TOKEN' }, (response) => {
      resolve(response?.token ?? null)
    })
  })
}

async function main() {
  const token = await getToken()
  if (!token) return
  // 使用token发起需要身份验证的API请求
}

main()
完整指南:
references/content-scripts.md

Stable CRX ID

稳定CRX ID

Without a pinned key, Chrome derives the CRX ID from a random key at build time. This rotates every rebuild, breaking allowed origins.
Option A -- Plasmo Itero (recommended):
  1. Visit Plasmo Itero Generate Keypairs
  2. Click "Generate KeyPairs" -- save Private Key securely, copy Public Key and CRX ID
Option B -- OpenSSL:
bash
openssl genrsa -out key.pem 2048
如果没有固定密钥,Chrome会在构建时根据随机密钥生成CRX ID,每次重新构建都会变动,导致允许来源配置失效。
选项A -- Plasmo Itero(推荐):
  1. 访问 Plasmo Itero Generate Keypairs
  2. 点击"Generate KeyPairs"——安全保存私钥,复制公钥和CRX ID
选项B -- OpenSSL:
bash
openssl genrsa -out key.pem 2048

Use Plasmo Itero to convert or extract the public key in correct format

使用Plasmo Itero转换或提取正确格式的公钥


**`.env.chrome`:**
CRX_PUBLIC_KEY="<PUBLIC KEY from Itero>"

**`package.json`:**
```json
{
  "manifest": {
    "key": "$CRX_PUBLIC_KEY",
    "permissions": ["cookies", "storage"],
    "host_permissions": [
      "http://localhost/*",
      "$CLERK_FRONTEND_API/*"
    ]
  }
}
Add
chrome-extension://YOUR_STABLE_CRX_ID
to Clerk Dashboard > Allowed Origins.

**`.env.chrome`:**
CRX_PUBLIC_KEY="<PUBLIC KEY from Itero>"

**`package.json`:**
```json
{
  "manifest": {
    "key": "$CRX_PUBLIC_KEY",
    "permissions": ["cookies", "storage"],
    "host_permissions": [
      "http://localhost/*",
      "$CLERK_FRONTEND_API/*"
    ]
  }
}
chrome-extension://YOUR_STABLE_CRX_ID
添加到Clerk管理后台的允许来源列表中。

Token Cache (persist across popup closes)

令牌缓存(跨弹窗关闭持久化)

tsx
const tokenCache = {
  async getToken(key: string) {
    const result = await chrome.storage.local.get(key)
    return result[key] ?? null
  },
  async saveToken(key: string, token: string) {
    await chrome.storage.local.set({ [key]: token })
  },
  async clearToken(key: string) {
    await chrome.storage.local.remove(key)
  },
}

<ClerkProvider publishableKey={PUBLISHABLE_KEY} tokenCache={tokenCache}>
Storage typeScopeClears on
chrome.storage.local
DeviceUninstall or manual clear
chrome.storage.session
SessionBrowser close
chrome.storage.sync
All devicesUninstall (size-limited, 8KB)
localStorage
Popup onlyPopup close -- do not use for auth
tsx
const tokenCache = {
  async getToken(key: string) {
    const result = await chrome.storage.local.get(key)
    return result[key] ?? null
  },
  async saveToken(key: string, token: string) {
    await chrome.storage.local.set({ [key]: token })
  },
  async clearToken(key: string) {
    await chrome.storage.local.remove(key)
  },
}

<ClerkProvider publishableKey={PUBLISHABLE_KEY} tokenCache={tokenCache}>
存储类型作用范围清除条件
chrome.storage.local
设备卸载扩展或手动清除
chrome.storage.session
会话浏览器关闭
chrome.storage.sync
所有同步设备卸载扩展(大小限制8KB)
localStorage
仅弹窗弹窗关闭——不要用于身份验证存储

Common Pitfalls

常见问题

SymptomCauseFix
Redirect loop on sign-inMissing CRX URL in ClerkProvider propsSet
afterSignOutUrl
,
signInFallbackRedirectUrl
OAuth button not workingOAuth not supported in popupUse
syncHost
to delegate to web app
Auth state stale after web app sign-in
syncHost
not configured
Add
syncHost
prop +
host_permissions
Side panel shows signed-out after web sign-inKnown limitationUser must close and reopen the side panel
Background can't get token after 60sSession expired, no background refreshUse
createClerkClient({ background: true })
Content script can't access ClerkIsolated world + origin restrictionsUse message passing to background service worker
Auth breaks after rebuildCRX ID rotatedConfigure stable key via
.env.chrome
PLASMO_PUBLIC_
var undefined
Wrong env fileUse
.env.development
, not
.env
Bot protection errorsCloudflare not supported in extensionsDisable bot protection in Clerk Dashboard
Token cache not persistingUsing
localStorage
in popup
Use
chrome.storage.local
or pass
tokenCache
prop
现象原因解决方法
登录时重定向循环ClerkProvider属性中缺少CRX URL配置设置
afterSignOutUrl
signInFallbackRedirectUrl
OAuth按钮无法工作弹窗不支持OAuth使用
syncHost
将验证委托给web应用
web应用登录后扩展身份验证状态未更新未配置
syncHost
添加
syncHost
属性 +
host_permissions
配置
web端登录后边侧面板仍显示未登录已知限制用户需关闭并重新打开侧边面板
60秒后后台无法获取令牌会话过期,无后台刷新机制使用
createClerkClient({ background: true })
内容脚本无法访问Clerk独立环境+源限制使用消息传递机制调用后台Service Worker
重新构建后身份验证失效CRX ID变动通过
.env.chrome
配置稳定密钥
PLASMO_PUBLIC_
开头的变量未定义
环境文件错误使用
.env.development
,不要用
.env
机器人保护报错扩展不支持Cloudflare在Clerk管理后台关闭机器人保护
令牌缓存未持久化弹窗中使用了
localStorage
使用
chrome.storage.local
或传入
tokenCache
属性

Plan Requirements

套餐要求

FeaturePlan
Basic popup auth (email/password, OTP)Free
PasskeysFree
syncHostRequires Pro (custom domain)
OAuth through syncHostPro + OAuth configured on web app
SAML through syncHostEnterprise
Bot protectionN/A -- must be disabled for extensions
功能所需套餐
基础弹窗验证(邮箱/密码、OTP)免费版
通行密钥(Passkeys)免费版
syncHost专业版(自定义域名)
通过syncHost使用OAuth专业版 + web端已配置OAuth
通过syncHost使用SAML企业版
机器人保护不适用——扩展必须关闭该功能

See Also

相关内容

  • clerk-setup
    - Initial Clerk install
  • clerk-custom-ui
    - Custom flows & appearance
  • clerk-setup
    - Clerk初始安装
  • clerk-custom-ui
    - 自定义流程与外观