openrouter-oauth
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSign 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 app | Follow 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 auth | Do PKCE here for the key, then see |
| 用户需求… | 操作方式 |
|---|---|
| 为Web应用添加登录功能 | 遵循下方完整的PKCE流程 + 按钮实现指南 |
| 以编程方式获取API密钥(无UI) | 仅实现PKCE流程——跳过按钮部分 |
| 认证后使用OpenRouter SDK | 在此处通过PKCE获取密钥,然后查看 |
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 for the random bytes
crypto.getRandomValues(new Uint8Array(32)) - base64url encoding: standard base64, then replace →
+,-→/, strip trailing_= - Store in
code_verifier(notsessionStorage) — so the verifier doesn't persist after the tab closes or leak to other tabs (security: the verifier is a one-time secret)localStorage
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| Param | Value |
|---|---|
| Your app's URL (where the user returns after auth) |
| The S256 challenge from Step 1 |
| Always |
https://openrouter.ai/auth?callback_url={url}&code_challenge={challenge}&code_challenge_method=S256| 参数 | 值 |
|---|---|
| 你的应用URL(用户完成认证后返回的地址) |
| 步骤1中生成的S256挑战码 |
| 固定为 |
Step 3: Handle the redirect back
步骤3:处理重定向返回
User returns to your with appended. Extract the query parameter.
callback_url?code=codeImportant: Before processing , check that a exists in . Other routes or third-party code might use query params for unrelated purposes — a guard ensures you only consume codes that belong to your OAuth flow.
?code=code_verifiersessionStorage?code=hasOAuthCallbackPending()用户会携带参数返回到你的。提取该查询参数。
?code=callback_urlcode**重要提示:**在处理之前,检查中是否存在。其他路由或第三方代码可能会使用参数用于无关目的——守卫确保你只处理属于你的OAuth流程的代码。
?code=sessionStoragecode_verifier?code=hasOAuthCallbackPending()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 before or after the exchange.
sessionStoragePOST 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-..." }在交换之前或之后,从中移除验证器。
sessionStorageStep 5: Store the key and clean up
步骤5:存储密钥并清理
- Store in
keylocalStorage - Clean the URL: to remove
history.replaceState({}, "", location.pathname)?code= - Cross-tab sync: Listen for events on the API key's
storageentry so other tabs update when the user signs in or outlocalStorage
- 将存储在
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 on click. Include the OpenRouter logo and provide multiple visual variants.
initiateOAuth()构建一个点击时调用的按钮组件。包含OpenRouter标志,并提供多种视觉变体。
initiateOAuth()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:
| Variant | Classes |
|---|---|
| |
| |
| |
| Same as |
| |
为了与参考实现保持视觉一致性,推荐使用以下类名:
| 变体 | 类名 |
|---|---|
| |
| |
| |
| 与 |
| |
Sizes
尺寸
| Size | Classes |
|---|---|
| |
| |
| |
| |
All variants use:
inline-flex items-center justify-center gap-2 font-medium transition-all cursor-pointer disabled:opacity-50Show a loading indicator while the key exchange is in progress. Default label: "Sign in with OpenRouter".
| 尺寸 | 类名 |
|---|---|
| |
| |
| |
| |
所有变体均需添加:
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 () and vice versa for / ().
dark:bg-neutral-900 dark:text-whitebrandedctadark:bg-white dark:text-neutral-900如需支持深色模式,添加深色变体类名:将浅色背景替换为深色(),/变体则相反()。
dark:bg-neutral-900 dark:text-whitebrandedctadark:bg-white dark:text-neutral-900Using 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 (, streaming, tool use), see the skill.
callModelopenrouter-typescript-sdktypescript
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方式(、流式传输、工具调用),请查看技能。
callModelopenrouter-typescript-sdkResources
资源
- OAuth PKCE guide — full parameter reference and key management
- Live demo — interactive button playground
- OpenRouter TypeScript SDK — pattern for completions and streaming
callModel
- OAuth PKCE指南 — 完整的参数参考和密钥管理说明
- 在线演示 — 交互式按钮体验 playground
- OpenRouter TypeScript SDK — 用于补全和流式传输的模式
callModel