clerk-chrome-extension-patterns
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseChrome Extension Patterns
Chrome扩展开发模式
CRITICAL RULES
核心规则
- OAuth (Google, GitHub, etc.) and SAML are NOT supported in popups or side panels -- use to delegate auth to your web app
syncHost - Email links (magic links) don't work in popups -- the popup closes when the user clicks outside, resetting sign-in state
- Side panels don't auto-refresh auth state -- users must close and reopen the side panel after signing in via the web app
- Service workers and content scripts have NO access to Clerk React hooks -- use or message passing
createClerkClient() - Extension URLs use not
chrome-extension://-- all redirect URLs must usehttp://chrome.runtime.getURL('.') - Without a stable CRX ID, every rebuild breaks auth -- configure in manifest BEFORE deploying
key - Content scripts cannot use Clerk directly due to origin restrictions -- Clerk enforces strict allowed origins
- Bot protection must be DISABLED in Clerk Dashboard -- Cloudflare bot detection is not supported in extension environments
- 弹窗或侧边面板不支持OAuth(Google、GitHub等)和SAML——使用将身份验证委托给你的web应用
syncHost - 邮件链接(魔法链接)在弹窗中无法正常工作——用户点击弹窗外部时弹窗会关闭,登录状态会被重置
- 侧边面板不会自动刷新身份验证状态——用户通过web应用登录后,必须关闭并重新打开侧边面板才能同步状态
- Service Worker和内容脚本无法访问Clerk React钩子——使用或消息传递机制
createClerkClient() - 扩展URL使用而非
chrome-extension://——所有重定向URL必须使用http://生成chrome.runtime.getURL('.') - 没有稳定的CRX ID时,每次重新构建都会导致身份验证失效——部署前请先在manifest中配置
key - 由于源限制,内容脚本无法直接使用Clerk——Clerk会严格校验允许的来源
- 必须在Clerk管理后台关闭机器人保护——扩展环境不支持Cloudflare机器人检测
Authentication Options
身份验证选项
| Method | Popup | Side Panel | syncHost (with web app) |
|---|---|---|---|
| Email + OTP | Yes | Yes | Yes |
| Email + Link | No | No | Yes |
| Email + Password | Yes | Yes | Yes |
| Username + Password | Yes | Yes | Yes |
| SMS + OTP | Yes | Yes | Yes |
| OAuth (Google, GitHub, etc.) | NO | NO | YES |
| SAML | NO | NO | YES |
| Passkeys | Yes | Yes | Yes |
| Google One Tap | No | No | Yes |
| Web3 | No | No | Yes |
| 方法 | 弹窗 | 侧边面板 | 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-extensionEnable Native API in Clerk Dashboard under Native applications. Required for all extension integrations.
.env.developmentPLASMO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_FRONTEND_API=https://your-app.clerk.accounts.devsrc/popup.tsxtsx
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 IndexPopupUse for -- navigating to a separate page breaks the popup flow.
mode="modal"SignInButtonbash
npx create-plasmo --with-tailwindcss --with-src my-extension
cd my-extension
npm install @clerk/chrome-extension在Clerk管理后台的原生应用设置中启用Native API,所有扩展集成都需要开启该配置。
.env.developmentPLASMO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_FRONTEND_API=https://your-app.clerk.accounts.devsrc/popup.tsxtsx
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 IndexPopupSignInButtonmode="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_permissionsStep 1 -- Environment variables:
.env.developmentPLASMO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_FRONTEND_API=https://your-app.clerk.accounts.dev
PLASMO_PUBLIC_CLERK_SYNC_HOST=http://localhost.env.productionPLASMO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_...
CLERK_FRONTEND_API=https://clerk.your-domain.com
PLASMO_PUBLIC_CLERK_SYNC_HOST=https://clerk.your-domain.comStep 2 -- Add prop:
syncHosttsx
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 in :
host_permissionspackage.jsonjson
{
"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应用的登录状态时使用该方案。
工作原理:扩展通过读取web应用域名下的Clerk会话Cookie。
host_permissions步骤1 -- 配置环境变量:
.env.developmentPLASMO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_FRONTEND_API=https://your-app.clerk.accounts.dev
PLASMO_PUBLIC_CLERK_SYNC_HOST=http://localhost.env.productionPLASMO_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 -- 添加属性:
syncHosttsx
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.jsonhost_permissionsjson
{
"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.mdcreateClerkClient() for Vanilla JS / Service Workers
适用于原生JS/Service Worker的createClerkClient()
Import from (not ).
@clerk/chrome-extension/client@clerk/chrome-extensionBackground service worker ():
src/background/index.tstypescript
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 flag keeps sessions fresh even when popup/sidepanel is closed. Without it, tokens expire after 60 seconds.
background: truePopup with vanilla JS ():
src/popup.tstypescript
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.tstypescript
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原生JS实现的弹窗 ():
src/popup.tstypescript
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.mdHeadless Extension (no popup, no side panel)
无头扩展(无弹窗、无边侧面板)
For extensions that run entirely in the background and sync with a web app.
Uses + with to read auth state from the web app's cookies.
syncHostcreateClerkClientbackground: truetypescript
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 for the sync host domain in .
host_permissionspackage.jsonFull guide:
references/headless-extension.md适用于完全在后台运行、与web应用同步状态的扩展。
使用 + 配置了的来读取web应用Cookie中的身份验证状态。
syncHostbackground: truecreateClerkClienttypescript
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
}需要在中配置syncHost域名的。
package.jsonhost_permissions完整指南:
references/headless-extension.mdContent 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.mdStable 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):
- Visit Plasmo Itero Generate Keypairs
- 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(推荐):
- 访问 Plasmo Itero Generate Keypairs
- 点击"Generate KeyPairs"——安全保存私钥,复制公钥和CRX ID
选项B -- OpenSSL:
bash
openssl genrsa -out key.pem 2048Use 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 to Clerk Dashboard > Allowed Origins.
chrome-extension://YOUR_STABLE_CRX_ID
**`.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/*"
]
}
}将添加到Clerk管理后台的允许来源列表中。
chrome-extension://YOUR_STABLE_CRX_IDToken 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 type | Scope | Clears on |
|---|---|---|
| Device | Uninstall or manual clear |
| Session | Browser close |
| All devices | Uninstall (size-limited, 8KB) |
| Popup only | Popup 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}>| 存储类型 | 作用范围 | 清除条件 |
|---|---|---|
| 设备 | 卸载扩展或手动清除 |
| 会话 | 浏览器关闭 |
| 所有同步设备 | 卸载扩展(大小限制8KB) |
| 仅弹窗 | 弹窗关闭——不要用于身份验证存储 |
Common Pitfalls
常见问题
| Symptom | Cause | Fix |
|---|---|---|
| Redirect loop on sign-in | Missing CRX URL in ClerkProvider props | Set |
| OAuth button not working | OAuth not supported in popup | Use |
| Auth state stale after web app sign-in | | Add |
| Side panel shows signed-out after web sign-in | Known limitation | User must close and reopen the side panel |
| Background can't get token after 60s | Session expired, no background refresh | Use |
| Content script can't access Clerk | Isolated world + origin restrictions | Use message passing to background service worker |
| Auth breaks after rebuild | CRX ID rotated | Configure stable key via |
| Wrong env file | Use |
| Bot protection errors | Cloudflare not supported in extensions | Disable bot protection in Clerk Dashboard |
| Token cache not persisting | Using | Use |
| 现象 | 原因 | 解决方法 |
|---|---|---|
| 登录时重定向循环 | ClerkProvider属性中缺少CRX URL配置 | 设置 |
| OAuth按钮无法工作 | 弹窗不支持OAuth | 使用 |
| web应用登录后扩展身份验证状态未更新 | 未配置 | 添加 |
| web端登录后边侧面板仍显示未登录 | 已知限制 | 用户需关闭并重新打开侧边面板 |
| 60秒后后台无法获取令牌 | 会话过期,无后台刷新机制 | 使用 |
| 内容脚本无法访问Clerk | 独立环境+源限制 | 使用消息传递机制调用后台Service Worker |
| 重新构建后身份验证失效 | CRX ID变动 | 通过 |
| 环境文件错误 | 使用 |
| 机器人保护报错 | 扩展不支持Cloudflare | 在Clerk管理后台关闭机器人保护 |
| 令牌缓存未持久化 | 弹窗中使用了 | 使用 |
Plan Requirements
套餐要求
| Feature | Plan |
|---|---|
| Basic popup auth (email/password, OTP) | Free |
| Passkeys | Free |
| syncHost | Requires Pro (custom domain) |
| OAuth through syncHost | Pro + OAuth configured on web app |
| SAML through syncHost | Enterprise |
| Bot protection | N/A -- must be disabled for extensions |
| 功能 | 所需套餐 |
|---|---|
| 基础弹窗验证(邮箱/密码、OTP) | 免费版 |
| 通行密钥(Passkeys) | 免费版 |
| syncHost | 专业版(自定义域名) |
| 通过syncHost使用OAuth | 专业版 + web端已配置OAuth |
| 通过syncHost使用SAML | 企业版 |
| 机器人保护 | 不适用——扩展必须关闭该功能 |
See Also
相关内容
- - Initial Clerk install
clerk-setup - - Custom flows & appearance
clerk-custom-ui
- - Clerk初始安装
clerk-setup - - 自定义流程与外观
clerk-custom-ui