openrouter-oauth について
openrouter-oauthは、WebアプリにOpenRouterログインを追加するためのスキルです。クライアント登録やバックエンドを必要としないPKCEフローでAPIキーを取得する方法と、ログインボタンの実装を説明します。
主な機能
- 01OAuth PKCEフローの実装
- 02認可コードからAPIキーへの交換
- 03検証子とキーの保存管理
- 04タブ間でのログイン状態の同期
- 05複数スタイルのログインボタン作成
活用シーン
- 01WebアプリへのOpenRouterログイン追加
- 02ログインUIなしでの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 を実行し、callModel/ストリーミングについては openrouter-typescript-sdk スキルを参照してください |
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:リダイレクトから戻ったときに処理する
ユーザーは ?code= が付加された状態で callback_url に戻ります。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に保存しますhistory.replaceState({}, "", location.pathname)で URL をクリーンアップし、?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 スキルを参照してください。
リソース
- OAuth PKCE ガイド — パラメーターの完全なリファレンスとキー管理
- 認証ガイド — API キーの使用と Bearer トークンの設定
- ライブデモ — インタラクティブなボタンのプレイグラウンド
- OpenRouter TypeScript SDK — 補完とストリーミングのための
callModelパターン