openrouter-oauth

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Sign In with OpenRouter

通过OpenRouter登录

Add OAuth login to any web app. Users authorize on OpenRouter and your app receives an API key — no client registration, no backend, no secrets. Works with any framework.
为任意Web应用添加OAuth登录功能。用户在OpenRouter完成授权后,你的应用将获得一个API密钥——无需客户端注册、无需后端、无需密钥。支持任意框架。

Decision Tree

决策树

User wants to…Do this
Add sign-in / login to a web appFollow the full PKCE flow + button guidance below
Get an API key programmatically (no UI)Just implement the PKCE flow — skip the button section
Use the OpenRouter SDK after authDo PKCE here for the key, then see
openrouter-typescript-sdk
skill for
callModel
/streaming

用户需求…操作方式
为Web应用添加登录功能遵循下方完整的PKCE流程 + 按钮实现指南
以编程方式获取API密钥(无UI)仅实现PKCE流程——跳过按钮部分
认证后使用OpenRouter SDK在此处通过PKCE获取密钥,然后查看
openrouter-typescript-sdk
技能了解
callModel
/流式传输相关内容

OAuth PKCE Flow

OAuth PKCE流程

No client ID or secret — the PKCE challenge is the only proof of identity.
无需客户端ID或密钥——PKCE挑战码是唯一的身份凭证。

Step 1: Generate verifier and challenge

步骤1:生成验证器和挑战码

code_verifier  = base64url(32 random bytes)
code_challenge = base64url(SHA-256(code_verifier))
  • Use
    crypto.getRandomValues(new Uint8Array(32))
    for the random bytes
  • base64url encoding: standard base64, then replace
    +
    -
    ,
    /
    _
    , strip trailing
    =
  • Store
    code_verifier
    in
    sessionStorage
    (not
    localStorage
    ) — so the verifier doesn't persist after the tab closes or leak to other tabs (security: the verifier is a one-time secret)
code_verifier  = base64url(32 random bytes)
code_challenge = base64url(SHA-256(code_verifier))
  • 使用
    crypto.getRandomValues(new Uint8Array(32))
    生成随机字节
  • base64url编码:标准base64编码后,将
    +
    替换为
    -
    /
    替换为
    _
    ,移除末尾的
    =
  • code_verifier
    存储在**
    sessionStorage
    **中(而非
    localStorage
    )——这样验证器会在标签页关闭后消失,也不会泄露到其他标签页(安全提示:验证器是一次性密钥)

Step 2: Redirect to OpenRouter

步骤2:重定向到OpenRouter

https://openrouter.ai/auth?callback_url={url}&code_challenge={challenge}&code_challenge_method=S256
ParamValue
callback_url
Your app's URL (where the user returns after auth)
code_challenge
The S256 challenge from Step 1
code_challenge_method
Always
S256
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

Step 3: Handle the redirect back

步骤3:处理重定向返回

User returns to your
callback_url
with
?code=
appended. Extract the
code
query parameter.
Important: Before processing
?code=
, check that a
code_verifier
exists in
sessionStorage
. Other routes or third-party code might use
?code=
query params for unrelated purposes — a
hasOAuthCallbackPending()
guard ensures you only consume codes that belong to your OAuth flow.
用户会携带
?code=
参数返回到你的
callback_url
。提取该
code
查询参数。
**重要提示:**在处理
?code=
之前,检查
sessionStorage
中是否存在
code_verifier
。其他路由或第三方代码可能会使用
?code=
参数用于无关目的——
hasOAuthCallbackPending()
守卫确保你只处理属于你的OAuth流程的代码。

Step 4: Exchange code for API key

步骤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-..." }
Remove the verifier from
sessionStorage
before or after the exchange.
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
中移除验证器。

Step 5: Store the key and clean up

步骤5:存储密钥并清理

  • Store
    key
    in
    localStorage
  • Clean the URL:
    history.replaceState({}, "", location.pathname)
    to remove
    ?code=
  • Cross-tab sync: Listen for
    storage
    events on the API key's
    localStorage
    entry so other tabs update when the user signs in or out

  • key
    存储在
    localStorage
  • 清理URL:使用
    history.replaceState({}, "", location.pathname)
    移除
    ?code=
    参数
  • **跨标签页同步:**监听API密钥在
    localStorage
    中的
    storage
    事件,以便用户登录或退出时其他标签页同步更新

Auth Module Reference

认证模块参考

Drop-in module implementing the full PKCE flow. Reduces risk of getting base64url encoding, sessionStorage handling, or the key exchange wrong.
typescript
// 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);
}

可直接嵌入的模块,实现完整的PKCE流程。降低base64url编码、sessionStorage处理或密钥交换出错的风险。
typescript
// 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);
}

Sign-in Button

登录按钮

Build a button component that calls
initiateOAuth()
on click. Include the OpenRouter logo and provide multiple visual variants.
构建一个点击时调用
initiateOAuth()
的按钮组件。包含OpenRouter标志,并提供多种视觉变体。

OpenRouter Logo SVG

OpenRouter标志SVG

svg
<svg viewBox="0 0 512 512" fill="currentColor" stroke="currentColor">
  <path d="M3 248.945C18 248.945 76 236 106 219C136 202 136 202 198 158C276.497 102.293 332 120.945 423 120.945" stroke-width="90"/>
  <path d="M511 121.5L357.25 210.268L357.25 32.7324L511 121.5Z"/>
  <path d="M0 249C15 249 73 261.945 103 278.945C133 295.945 133 295.945 195 339.945C273.497 395.652 329 377 420 377" stroke-width="90"/>
  <path d="M508 376.445L354.25 287.678L354.25 465.213L508 376.445Z"/>
</svg>
svg
<svg viewBox="0 0 512 512" fill="currentColor" stroke="currentColor">
  <path d="M3 248.945C18 248.945 76 236 106 219C136 202 136 202 198 158C276.497 102.293 332 120.945 423 120.945" stroke-width="90"/>
  <path d="M511 121.5L357.25 210.268L357.25 32.7324L511 121.5Z"/>
  <path d="M0 249C15 249 73 261.945 103 278.945C133 295.945 133 295.945 195 339.945C273.497 395.652 329 377 420 377" stroke-width="90"/>
  <path d="M508 376.445L354.25 287.678L354.25 465.213L508 376.445Z"/>
</svg>

Variants (Tailwind)

变体(Tailwind)

Recommended classes for visual consistency with the reference implementation:
VariantClasses
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
Same as
default
+
aspect-square
(logo only, no text)
cta
rounded-xl bg-neutral-900 text-white shadow-lg hover:bg-neutral-800 hover:scale-[1.02] active:scale-[0.98]
为了与参考实现保持视觉一致性,推荐使用以下类名:
变体类名
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]

Sizes

尺寸

SizeClasses
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
All variants use:
inline-flex items-center justify-center gap-2 font-medium transition-all cursor-pointer disabled:opacity-50
Show a loading indicator while the key exchange is in progress. Default label: "Sign in with OpenRouter".
尺寸类名
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 mode

深色模式

For dark mode support, add dark variants: swap light backgrounds to dark (
dark:bg-neutral-900 dark:text-white
) and vice versa for
branded
/
cta
(
dark:bg-white dark:text-neutral-900
).

如需支持深色模式,添加深色变体类名:将浅色背景替换为深色(
dark:bg-neutral-900 dark:text-white
),
branded
/
cta
变体则相反(
dark:bg-white dark:text-neutral-900
)。

Using the API Key

使用API密钥

typescript
const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${apiKey}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    model: "openai/gpt-4o-mini",
    messages: [{ role: "user", content: "Hello!" }],
  }),
});
For the type-safe SDK approach (
callModel
, streaming, tool use), see the
openrouter-typescript-sdk
skill.

typescript
const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${apiKey}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    model: "openai/gpt-4o-mini",
    messages: [{ role: "user", content: "Hello!" }],
  }),
});
如需使用类型安全的SDK方式(
callModel
、流式传输、工具调用),请查看
openrouter-typescript-sdk
技能。

Resources

资源