From 52c9d3174b4b35e6cff3f6235fdda3dd64fdb668 Mon Sep 17 00:00:00 2001 From: ZiuChen Date: Fri, 7 Nov 2025 16:14:54 +0800 Subject: [PATCH 1/4] feat: Support specifying multiple apiKeys --- README.md | 42 +++++++++++++++++++++++++++++ src/lib/api-key-auth.ts | 59 +++++++++++++++++++++++++++++++++++++++++ src/lib/state.ts | 3 +++ src/server.ts | 11 ++++++++ src/start.ts | 20 ++++++++++++++ 5 files changed, 135 insertions(+) create mode 100644 src/lib/api-key-auth.ts diff --git a/README.md b/README.md index 0d36c13c..d145f4e6 100644 --- a/README.md +++ b/README.md @@ -163,6 +163,7 @@ The following command line options are available for the `start` command: | --claude-code | Generate a command to launch Claude Code with Copilot API config | false | -c | | --show-token | Show GitHub and Copilot tokens on fetch and refresh | false | none | | --proxy-env | Initialize proxy from environment variables | false | none | +| --api-key | API keys for authentication. Can be specified multiple times | none | none | ### Auth Command Options @@ -209,6 +210,41 @@ New endpoints for monitoring your Copilot usage and quotas. | `GET /usage` | `GET` | Get detailed Copilot usage statistics and quota information. | | `GET /token` | `GET` | Get the current Copilot token being used by the API. | +## API Key Authentication + +The proxy supports API key authentication to restrict access to the endpoints. When API keys are configured, all API endpoints require authentication. + +### Authentication Methods + +The proxy supports both OpenAI and Anthropic authentication formats: + +- **OpenAI format**: Include the API key in the `Authorization` header with `Bearer` prefix: + ```bash + curl -H "Authorization: Bearer your_api_key_here" http://localhost:4141/v1/models + ``` + +- **Anthropic format**: Include the API key in the `x-api-key` header: + ```bash + curl -H "x-api-key: your_api_key_here" http://localhost:4141/v1/messages + ``` + +### Configuration + +Use the `--api-key` flag to enable API key authentication. You can specify multiple keys for different clients: + +```bash +# Single API key +npx copilot-api@latest start --api-key your_secret_key + +# Multiple API keys +npx copilot-api@latest start --api-key key1 --api-key key2 --api-key key3 +``` + +When API keys are configured: +- All API endpoints (`/v1/chat/completions`, `/v1/models`, `/v1/embeddings`, `/v1/messages`, `/usage`, `/token`) require authentication +- Requests without valid API keys will receive a 401 Unauthorized response +- The root endpoint `/` remains accessible without authentication + ## Example Usage Using with npx: @@ -238,6 +274,12 @@ npx copilot-api@latest start --rate-limit 30 --wait # Provide GitHub token directly npx copilot-api@latest start --github-token ghp_YOUR_TOKEN_HERE +# Enable API key authentication with a single key +npx copilot-api@latest start --api-key your_secret_key_here + +# Enable API key authentication with multiple keys +npx copilot-api@latest start --api-key key1 --api-key key2 --api-key key3 + # Run only the auth flow npx copilot-api@latest auth diff --git a/src/lib/api-key-auth.ts b/src/lib/api-key-auth.ts new file mode 100644 index 00000000..c22fe015 --- /dev/null +++ b/src/lib/api-key-auth.ts @@ -0,0 +1,59 @@ +import type { Context, MiddlewareHandler } from "hono" + +import { HTTPException } from "hono/http-exception" + +import { state } from "./state" + +/** + * Extract API key from request headers + * Supports both OpenAI format (Authorization: Bearer token) and Anthropic format (x-api-key: token) + */ +function extractApiKey(c: Context): string | undefined { + // OpenAI format: Authorization header with Bearer prefix + const authHeader = c.req.header("authorization") + if (authHeader?.startsWith("Bearer ")) { + return authHeader.slice(7) // Remove 'Bearer ' prefix + } + + // Anthropic format: x-api-key header + const anthropicKey = c.req.header("x-api-key") + if (anthropicKey) { + return anthropicKey + } + + return undefined +} + +/** + * API key authentication middleware + * Validates that the request contains a valid API key if API keys are configured + */ +export const apiKeyAuthMiddleware: MiddlewareHandler = async (c, next) => { + // If no API keys are configured, skip authentication + if (!state.apiKeys || state.apiKeys.length === 0) { + await next() + return + } + + const providedKey = extractApiKey(c) + + // If no API key is provided, return 401 + if (!providedKey) { + throw new HTTPException(401, { + message: + "API key required. Please provide a valid API key in the Authorization header (Bearer token) or x-api-key header.", + }) + } + + // Check if the provided key matches any of the configured keys + const isValidKey = state.apiKeys.includes(providedKey) + + if (!isValidKey) { + throw new HTTPException(401, { + message: "Invalid API key. Please provide a valid API key.", + }) + } + + // Key is valid, continue with the request + await next() +} diff --git a/src/lib/state.ts b/src/lib/state.ts index 5ba4dc1d..b611c0e9 100644 --- a/src/lib/state.ts +++ b/src/lib/state.ts @@ -15,6 +15,9 @@ export interface State { // Rate limiting configuration rateLimitSeconds?: number lastRequestTimestamp?: number + + // API key validation + apiKeys?: Array } export const state: State = { diff --git a/src/server.ts b/src/server.ts index 462a278f..aeb7b857 100644 --- a/src/server.ts +++ b/src/server.ts @@ -2,6 +2,7 @@ import { Hono } from "hono" import { cors } from "hono/cors" import { logger } from "hono/logger" +import { apiKeyAuthMiddleware } from "./lib/api-key-auth" import { completionRoutes } from "./routes/chat-completions/route" import { embeddingRoutes } from "./routes/embeddings/route" import { messageRoutes } from "./routes/messages/route" @@ -14,6 +15,16 @@ export const server = new Hono() server.use(logger()) server.use(cors()) +server.use("/chat/completions", apiKeyAuthMiddleware) +server.use("/models", apiKeyAuthMiddleware) +server.use("/embeddings", apiKeyAuthMiddleware) +server.use("/usage", apiKeyAuthMiddleware) +server.use("/token", apiKeyAuthMiddleware) +server.use("/v1/chat/completions", apiKeyAuthMiddleware) +server.use("/v1/models", apiKeyAuthMiddleware) +server.use("/v1/embeddings", apiKeyAuthMiddleware) +server.use("/v1/messages", apiKeyAuthMiddleware) + server.get("/", (c) => c.text("Server running")) server.route("/chat/completions", completionRoutes) diff --git a/src/start.ts b/src/start.ts index 14abbbdf..476db11e 100644 --- a/src/start.ts +++ b/src/start.ts @@ -25,6 +25,7 @@ interface RunServerOptions { claudeCode: boolean showToken: boolean proxyEnv: boolean + apiKeys?: Array } export async function runServer(options: RunServerOptions): Promise { @@ -46,6 +47,13 @@ export async function runServer(options: RunServerOptions): Promise { state.rateLimitSeconds = options.rateLimit state.rateLimitWait = options.rateLimitWait state.showToken = options.showToken + state.apiKeys = options.apiKeys + + if (state.apiKeys && state.apiKeys.length > 0) { + consola.info( + `API key authentication enabled with ${state.apiKeys.length} key(s)`, + ) + } await ensurePaths() await cacheVSCodeVersion() @@ -184,6 +192,10 @@ export const start = defineCommand({ default: false, description: "Initialize proxy from environment variables", }, + "api-key": { + type: "string", + description: "API keys for authentication", + }, }, run({ args }) { const rateLimitRaw = args["rate-limit"] @@ -191,6 +203,13 @@ export const start = defineCommand({ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition rateLimitRaw === undefined ? undefined : Number.parseInt(rateLimitRaw, 10) + // Handle multiple API keys - citty may pass a string or array + const apiKeyRaw = args["api-key"] + let apiKeys: Array | undefined + if (apiKeyRaw) { + apiKeys = Array.isArray(apiKeyRaw) ? apiKeyRaw : [apiKeyRaw] + } + return runServer({ port: Number.parseInt(args.port, 10), verbose: args.verbose, @@ -202,6 +221,7 @@ export const start = defineCommand({ claudeCode: args["claude-code"], showToken: args["show-token"], proxyEnv: args["proxy-env"], + apiKeys, }) }, }) From 666a0ece6d704637933901bf2756e7557f7d409a Mon Sep 17 00:00:00 2001 From: ZiuChen Date: Mon, 10 Nov 2025 16:34:26 +0800 Subject: [PATCH 2/4] fix: Improve API key extraction fallback for query parameters --- src/lib/api-key-auth.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/lib/api-key-auth.ts b/src/lib/api-key-auth.ts index c22fe015..16016ecd 100644 --- a/src/lib/api-key-auth.ts +++ b/src/lib/api-key-auth.ts @@ -21,6 +21,12 @@ function extractApiKey(c: Context): string | undefined { return anthropicKey } + // Fallback: query parameter, for extra compatibility of `/usage` or `/token` route + const queryKey = c.req.query("apiKey") + if (queryKey) { + return queryKey + } + return undefined } From fb889b9e24760270015bd75824c0bee4858a9bba Mon Sep 17 00:00:00 2001 From: ZiuChen Date: Mon, 10 Nov 2025 19:49:05 +0800 Subject: [PATCH 3/4] feat: Implement constant time comparison for API key validation --- src/lib/api-key-auth.ts | 5 ++++- src/lib/utils.ts | 11 +++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/lib/api-key-auth.ts b/src/lib/api-key-auth.ts index 16016ecd..e5a0ee45 100644 --- a/src/lib/api-key-auth.ts +++ b/src/lib/api-key-auth.ts @@ -3,6 +3,7 @@ import type { Context, MiddlewareHandler } from "hono" import { HTTPException } from "hono/http-exception" import { state } from "./state" +import { constantTimeEqual } from "./utils" /** * Extract API key from request headers @@ -52,7 +53,9 @@ export const apiKeyAuthMiddleware: MiddlewareHandler = async (c, next) => { } // Check if the provided key matches any of the configured keys - const isValidKey = state.apiKeys.includes(providedKey) + const isValidKey = state.apiKeys.some((key) => + constantTimeEqual(key, providedKey), + ) if (!isValidKey) { throw new HTTPException(401, { diff --git a/src/lib/utils.ts b/src/lib/utils.ts index cc80be66..dff6bf46 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -24,3 +24,14 @@ export const cacheVSCodeVersion = async () => { consola.info(`Using VSCode version: ${response}`) } + +export const constantTimeEqual = (a: string, b: string): boolean => { + if (a.length !== b.length) { + return false + } + let result = 0 + for (let i = 0; i < a.length; i++) { + result |= (a.codePointAt(i) ?? 0) ^ (b.codePointAt(i) ?? 0) + } + return result === 0 +} From c1d01390d5f9fb7a89cb3a0a1699633ca83d6c8e Mon Sep 17 00:00:00 2001 From: ZiuChen Date: Wed, 12 Nov 2025 15:11:51 +0800 Subject: [PATCH 4/4] fix: Remove query parameter fallback for API key extraction and simplify error messages --- src/lib/api-key-auth.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/lib/api-key-auth.ts b/src/lib/api-key-auth.ts index e5a0ee45..49c29904 100644 --- a/src/lib/api-key-auth.ts +++ b/src/lib/api-key-auth.ts @@ -22,12 +22,6 @@ function extractApiKey(c: Context): string | undefined { return anthropicKey } - // Fallback: query parameter, for extra compatibility of `/usage` or `/token` route - const queryKey = c.req.query("apiKey") - if (queryKey) { - return queryKey - } - return undefined } @@ -47,8 +41,7 @@ export const apiKeyAuthMiddleware: MiddlewareHandler = async (c, next) => { // If no API key is provided, return 401 if (!providedKey) { throw new HTTPException(401, { - message: - "API key required. Please provide a valid API key in the Authorization header (Bearer token) or x-api-key header.", + message: "Missing API key", }) } @@ -59,7 +52,7 @@ export const apiKeyAuthMiddleware: MiddlewareHandler = async (c, next) => { if (!isValidKey) { throw new HTTPException(401, { - message: "Invalid API key. Please provide a valid API key.", + message: "Invalid API key", }) }