mcpField
O
openrouter-oauth

openrouter-oauth

github.com/openrouterteam开发309302 次安装1 天前更新
代码仓库

关于 openrouter-oauth

openrouter-oauth 是为 Web 应用添加 OpenRouter 登录的 skill。它介绍无需客户端注册或后端的 PKCE 流程来获取 API 密钥,并提供登录按钮实现指南。

主要功能

  1. 01实现 OAuth PKCE 流程
  2. 02用授权码交换 API 密钥
  3. 03管理验证器和密钥存储
  4. 04在多个标签页同步登录状态
  5. 05构建不同样式的登录按钮

适用场景

  1. 01为 Web 应用添加 OpenRouter 登录
  2. 02无登录界面地获取 API 密钥
  3. 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)

建议使用以下类,以确保视觉效果与参考实现一致:

变体类
defaultrounded-lg border border-neutral-300 bg-white text-neutral-900 shadow-sm hover:bg-neutral-50
minimaltext-neutral-700 underline-offset-4 hover:underline
brandedrounded-lg bg-neutral-900 text-white shadow hover:bg-neutral-800
icon与 default 相同,另加 aspect-square(仅徽标,无文字)
ctarounded-xl bg-neutral-900 text-white shadow-lg hover:bg-neutral-800 hover:scale-[1.02] active:scale-[0.98]
尺寸
尺寸类
smh-8 px-3 text-xs
defaulth-10 px-5 text-sm
lgh-12 px-8 text-base
xlh-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。


资源

评论

支持 Markdown