关于 openrouter-oauth
openrouter-oauth 是为 Web 应用添加 OpenRouter 登录的 skill。它介绍无需客户端注册或后端的 PKCE 流程来获取 API 密钥,并提供登录按钮实现指南。
主要功能
- 01实现 OAuth PKCE 流程
- 02用授权码交换 API 密钥
- 03管理验证器和密钥存储
- 04在多个标签页同步登录状态
- 05构建不同样式的登录按钮
适用场景
- 01为 Web 应用添加 OpenRouter 登录
- 02无登录界面地获取 API 密钥
- 03将认证接入 OpenRouter API 调用
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ openrouterteam/ skills --skill openrouter-oauth安装后在 Claude Code 中输入/openrouter-oauth确认已加载。
附带文件README.md
详细介绍
使用 OpenRouter 登录
为任意 Web 应用添加 OAuth 登录。用户在 OpenRouter 上授权后,你的应用会收到一个 API 密钥——无需注册客户端、无需后端,也无需密钥。适用于任何框架。
在线演示:openrouterteam.github.io/sign-in-with-openrouter
决策树
| 用户想要…… | 操作 |
|---|---|
| 为 Web 应用添加注册/登录功能 | 按照下方完整的 PKCE 流程和按钮指南操作 |
| 以编程方式获取 API 密钥(无 UI) | 只实现 PKCE 流程,跳过按钮部分 |
| 在认证后使用 OpenRouter SDK | 在此处执行 PKCE 流程以获取密钥,然后参阅 openrouter-typescript-sdk 技能,了解 callModel/流式传输 |
OAuth PKCE 流程
无需客户端 ID 或密钥——PKCE 挑战是唯一的身份凭据。
步骤 1:生成验证器和挑战
code_verifier = base64url(32 random bytes)
code_challenge = base64url(SHA-256(code_verifier))
- 使用
crypto.getRandomValues(new Uint8Array(32))生成随机字节 - base64url 编码:先使用标准 base64 编码,再将
+替换为-、将/替换为_,并去掉末尾的= - 将
code_verifier存储在sessionStorage(而不是localStorage)中,这样标签页关闭后验证器就不会继续保留,也不会泄漏到其他标签页(安全提示:验证器是一次性密钥)
步骤 2:重定向到 OpenRouter
https://openrouter.ai/auth?callback_url={url}&code_challenge={challenge}&code_challenge_method=S256
| 参数 | 值 |
|---|---|
callback_url | 你的应用 URL(用户完成认证后返回的位置) |
code_challenge | 步骤 1 中的 S256 挑战 |
code_challenge_method | 始终为 S256 |
步骤 3:处理重定向返回
用户会返回到你的 callback_url,并附带 ?code=。提取 code 查询参数。
重要: 在处理 ?code= 之前,检查 sessionStorage 中是否存在 code_verifier。其他路由或第三方代码可能会将 ?code= 查询参数用于无关用途——使用 hasOAuthCallbackPending() 守卫,确保只使用属于你的 OAuth 流程的代码。
步骤 4:将代码兑换为 API 密钥
POST https://openrouter.ai/api/v1/auth/keys
Content-Type: application/json
{
"code": "<code from query param>",
"code_verifier": "<verifier from sessionStorage>",
"code_challenge_method": "S256"
}
→ { "key": "sk-or-..." }
在兑换之前或之后,从 sessionStorage 中移除验证器。
步骤 5:存储密钥并清理
- 将
key存储在localStorage中 - 清理 URL:使用
history.replaceState({}, "", location.pathname)移除?code= - 跨标签页同步: 监听 API 密钥对应的
localStorage条目上的storage事件,以便用户登录或退出时其他标签页也能更新
认证模块参考
可直接使用的模块,实现了完整的 PKCE 流程。降低 base64url 编码、sessionStorage 处理或密钥兑换出错的风险。
// lib/openrouter-auth.ts
const STORAGE_KEY = "openrouter_api_key";
const VERIFIER_KEY = "openrouter_code_verifier";
type AuthListener = () => void;
const listeners = new Set<AuthListener>();
export const onAuthChange = (fn: AuthListener) => { listeners.add(fn); return () => listeners.delete(fn); };
const notify = () => listeners.forEach((fn) => fn());
// Cross-tab sync: other tabs update when user signs in/out
if (typeof window !== "undefined") {
window.addEventListener("storage", (e) => { if (e.key === STORAGE_KEY) notify(); });
}
export const getApiKey = (): string | null =>
typeof window !== "undefined" ? localStorage.getItem(STORAGE_KEY) : null;
export const setApiKey = (key: string) => { localStorage.setItem(STORAGE_KEY, key); notify(); };
export const clearApiKey = () => { localStorage.removeItem(STORAGE_KEY); notify(); };
// Guard: only process ?code= if we initiated an OAuth flow in this tab
export const hasOAuthCallbackPending = (): boolean =>
typeof window !== "undefined" && sessionStorage.getItem(VERIFIER_KEY) !== null;
function generateCodeVerifier(): string {
const bytes = new Uint8Array(32);
crypto.getRandomValues(bytes);
return btoa(String.fromCharCode(...bytes))
.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
async function computeS256Challenge(verifier: string): Promise<string> {
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
return btoa(String.fromCharCode(...new Uint8Array(digest)))
.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
export async function initiateOAuth(callbackUrl?: string): Promise<void> {
const verifier = generateCodeVerifier();
sessionStorage.setItem(VERIFIER_KEY, verifier);
const challenge = await computeS256Challenge(verifier);
const url = callbackUrl ?? window.location.origin + window.location.pathname;
window.location.href = `https://openrouter.ai/auth?${new URLSearchParams({
callback_url: url, code_challenge: challenge, code_challenge_method: "S256",
})}`;
}
export async function handleOAuthCallback(code: string): Promise<void> {
const verifier = sessionStorage.getItem(VERIFIER_KEY);
if (!verifier) throw new Error("Missing code verifier");
sessionStorage.removeItem(VERIFIER_KEY);
const res = await fetch("https://openrouter.ai/api/v1/auth/keys", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ code, code_verifier: verifier, code_challenge_method: "S256" }),
});
if (!res.ok) throw new Error(`Key exchange failed (${res.status})`);
const { key } = await res.json();
setApiKey(key);
}
登录按钮
构建一个点击时调用 initiateOAuth() 的按钮组件。添加 OpenRouter 徽标,并提供多种视觉样式。
OpenRouter 徽标 SVG
<svg viewBox="0 0 401.4 293.7" fill="currentColor">
<path d="M303.9475,17.19926c42.79734,0,77.48933,34.69327,77.48933,77.48933s-34.69199,77.48933-77.48933,77.48933l76.86166,76.86244c9.76367,9.76313,2.84903,26.45667-10.95697,26.45667h-220.88335c-71.32686,0-129.14889-57.82202-129.14889-129.14889S77.64197,17.19926,148.96884,17.19926h154.97866ZM148.96884,68.85881c-42.79607,0-77.48933,34.69327-77.48933,77.48933s34.69327,77.48933,77.48933,77.48933,77.48933-34.69327,77.48933-77.48933-34.69327-77.48933-77.48933-77.48933Z"/>
</svg>
fill="currentColor" 会继承按钮的文字颜色。若要改用官方品牌色,请应用浅色模式下的紫色和深色模式下的荧光绿:
<svg viewBox="0 0 401.4 293.7" class="fill-[#7624F4] dark:fill-[#C8FF00]">...</svg>
| 模式 | 填充色 |
|---|---|
| 浅色背景 | #7624F4 |
| 深色背景 | #C8FF00 |
样式变体(Tailwind)
建议使用以下类,以确保视觉效果与参考实现一致:
| 变体 | 类 |
|---|---|
default | rounded-lg border border-neutral-300 bg-white text-neutral-900 shadow-sm hover:bg-neutral-50 |
minimal | text-neutral-700 underline-offset-4 hover:underline |
branded | rounded-lg bg-neutral-900 text-white shadow hover:bg-neutral-800 |
icon | 与 default 相同,另加 aspect-square(仅徽标,无文字) |
cta | rounded-xl bg-neutral-900 text-white shadow-lg hover:bg-neutral-800 hover:scale-[1.02] active:scale-[0.98] |
尺寸
| 尺寸 | 类 |
|---|---|
sm | h-8 px-3 text-xs |
default | h-10 px-5 text-sm |
lg | h-12 px-8 text-base |
xl | h-14 px-10 text-lg |
所有变体均使用:inline-flex items-center justify-center gap-2 font-medium transition-all cursor-pointer disabled:opacity-50
密钥兑换期间显示加载指示器。默认标签:“使用 OpenRouter 登录”。
暗黑模式
若要支持暗黑模式,请添加暗黑变体:将浅色背景切换为深色背景(dark:bg-neutral-900 dark:text-white),并对 branded/cta 反向处理(dark:bg-white dark:text-neutral-900)。
使用 API 密钥
const response = await fetch("https://openrouter.ai/api/v1/responses", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "openai/gpt-4o-mini",
input: [{ type: "message", role: "user", content: "Hello!" }],
}),
});
若要采用类型安全的 SDK 方案(callModel、流式传输、工具使用),请参阅 openrouter-typescript-sdk skill。
资源
- OAuth PKCE 指南 — 完整的参数参考和密钥管理
- 身份验证指南 — API 密钥用法和 Bearer 令牌设置
- 在线演示 — 交互式按钮体验场
- OpenRouter TypeScript SDK — 用于补全和流式传输的
callModel模式