@moikapy/openrouter-auth
v0.1.0
Published
OAuth PKCE + AES-256-GCM encryption for OpenRouter — framework-agnostic, Cloudflare Workers compatible, with React components
Downloads
17
Maintainers
Readme
@moikapy/openrouter-auth
OAuth PKCE + AES-256-GCM encryption for OpenRouter — framework-agnostic, Cloudflare Workers compatible, with React components.
Handles the full OpenRouter OAuth flow: user clicks "Connect" → PKCE redirect → callback exchange → encrypted API key stored in httpOnly cookie.
Security
- AES-256-GCM authenticated encryption — can't tamper without detection
- Key rotation — encrypt with current key, decrypt with current or previous keys. Transparent re-encryption on read.
- Session expiry — timestamp embedded inside ciphertext (can't be forged). Default 30-day expiry enforced server-side.
- PKCE S256 — verifier never sent over the wire
- httpOnly + Secure + SameSite=Lax — JS can't read cookie, CSRF-protected
- HTTPS validation — rejects non-HTTPS auth URLs (localhost exempt)
Install
bun add @moikapy/openrouter-authPrerequisites
- OpenRouter OAuth app — register at openrouter.ai to get a
client_id - Encryption key — generate:
openssl rand -hex 32 - Environment — set
OPENROUTER_ENCRYPT_KEY(or pass explicitly) - For key rotation — set
OPENROUTER_ENCRYPT_KEY_PREVIOUS(comma-separated old keys)
React Components (quickest path)
import { OpenRouterConnect, OpenRouterCallback } from "@moikapy/openrouter-auth/react";
// Any page — renders connect/disconnect UI
<OpenRouterConnect
onConnected={() => console.log("connected!")}
onDisconnected={() => console.log("disconnected")}
/>
// /auth/callback page — handles post-redirect code exchange
<OpenRouterCallback redirectUrl="/" />
// Full control via render prop
<OpenRouterConnect>
{({ connected, connect, disconnect, loading, error }) =>
connected
? <button onClick={disconnect}>Disconnect</button>
: <button onClick={connect} disabled={loading}>Connect</button>
}
</OpenRouterConnect>Server-side (Next.js)
import { getApiKeyFromCookie, exchangeCodeAndSetCookie } from "@moikapy/openrouter-auth/next";
// Callback route — exchange code, encrypt, set cookie
const encrypted = await exchangeCodeAndSetCookie(code, verifier, {
encryptKey: process.env.OPENROUTER_ENCRYPT_KEY,
previousKeys: process.env.OPENROUTER_ENCRYPT_KEY_PREVIOUS?.split(","),
});
// Any route — get decrypted API key (auto re-encrypts if key was rotated)
const apiKey = await getApiKeyFromCookie({
encryptKey: process.env.OPENROUTER_ENCRYPT_KEY,
previousKeys: process.env.OPENROUTER_ENCRYPT_KEY_PREVIOUS?.split(","),
});Server-side (Express, Hono, etc.)
import { exchangeAndEncrypt, decryptFromCookie, buildCookieOptions } from "@moikapy/openrouter-auth/server";
// Callback
const encrypted = await exchangeAndEncrypt(code, verifier, {
encryptKey: process.env.OPENROUTER_ENCRYPT_KEY,
});
res.cookie("or_session", encrypted, buildCookieOptions());
// Any route
const result = await decryptFromCookie(req.cookies.or_session, {
encryptKey: process.env.OPENROUTER_ENCRYPT_KEY,
});
if (!result) return res.status(401).json({ error: "Not connected or session expired" });
// If key was rotated, re-encrypt and update the cookie
if (result.needsReEncrypt) {
const newCookie = await encryptForCookie(result.apiKey, { encryptKey: process.env.OPENROUTER_ENCRYPT_KEY });
res.cookie("or_session", newCookie, buildCookieOptions());
}Key Rotation
When you need to rotate the encryption key:
- Generate a new key:
openssl rand -hex 32 - Set
OPENROUTER_ENCRYPT_KEYto the new key - Set
OPENROUTER_ENCRYPT_KEY_PREVIOUSto the old key - Existing cookies decrypt with the old key and auto re-encrypt with the new key on read
- After 30 days (or your
sessionMaxAge), removeOPENROUTER_ENCRYPT_KEY_PREVIOUS - Users with old cookies will get
nullfromgetApiKeyFromCookieand need to re-authenticate
Session Expiry
Sessions expire in 30 days by default. This is enforced two ways:
- Cookie maxAge — browser deletes the cookie after 30 days
- Embedded timestamp — encrypted inside the ciphertext, can't be tampered with. Server rejects expired sessions even if the cookie is still present.
Configure with sessionMaxAge (seconds):
// 7-day sessions
await getApiKeyFromCookie({
encryptKey: process.env.OPENROUTER_ENCRYPT_KEY,
sessionMaxAge: 7 * 24 * 60 * 60,
});Exports
| Path | Runtime | Description |
|---|---|---|
| @moikapy/openrouter-auth | Any | Types + re-exports |
| @moikapy/openrouter-auth/react | React | <OpenRouterConnect> + <OpenRouterCallback> |
| @moikapy/openrouter-auth/pkce | Browser | PKCE flow: start, check, disconnect |
| @moikapy/openrouter-auth/server | Any server | Framework-agnostic helpers |
| @moikapy/openrouter-auth/next | Next.js | next/headers cookie helpers |
| @moikapy/openrouter-auth/crypto | Anywhere | Low-level encrypt/decrypt with rotation |
Encrypted Format
[0x01] [keyFingerprint:4] [timestamp:4] [iv:12] [ciphertext+authTag]0x01— format versionkeyFingerprint— first 4 bytes of SHA-256(encryption key), identifies which key to use for decryptiontimestamp— uint32 seconds since epoch (valid until 2106), enforced on decryptiv— 12-byte random IV (unique per encryption)ciphertext+authTag— AES-256-GCM output with 16-byte authentication tag
License
MIT
