Skip to content

Commit b0e6ff4

Browse files
feat: support specifying multiple API keys for authentication
Add API key authentication middleware with: - Support for OpenAI format (Authorization: Bearer token) - Support for Anthropic format (x-api-key: token) - Constant time comparison for security - Multiple API keys via --api-key flag When API keys are configured, all API endpoints require authentication. The root endpoint / remains accessible without authentication. PR: ericc-ch#144
1 parent 24deca5 commit b0e6ff4

6 files changed

Lines changed: 136 additions & 0 deletions

File tree

README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ The following command line options are available for the `start` command:
166166
| --claude-code | Generate a command to launch Claude Code with Copilot API config | false | -c |
167167
| --show-token | Show GitHub and Copilot tokens on fetch and refresh | false | none |
168168
| --proxy-env | Initialize proxy from environment variables | false | none |
169+
| --api-key | API keys for authentication. Can be specified multiple times | none | none |
169170

170171
#### Usage examples with --host
171172

@@ -233,6 +234,44 @@ New endpoints for monitoring your Copilot usage and quotas.
233234
| `GET /usage` | `GET` | Get detailed Copilot usage statistics and quota information. |
234235
| `GET /token` | `GET` | Get the current Copilot token being used by the API. |
235236

237+
## API Key Authentication
238+
239+
The proxy supports API key authentication to restrict access to the endpoints. When API keys are configured, all API endpoints require authentication.
240+
241+
### Authentication Methods
242+
243+
The proxy supports both OpenAI and Anthropic authentication formats:
244+
245+
- **OpenAI format**: Include the API key in the `Authorization` header with `Bearer` prefix:
246+
247+
```bash
248+
curl -H "Authorization: Bearer your_api_key_here" http://localhost:4141/v1/models
249+
```
250+
251+
- **Anthropic format**: Include the API key in the `x-api-key` header:
252+
253+
```bash
254+
curl -H "x-api-key: your_api_key_here" http://localhost:4141/v1/messages
255+
```
256+
257+
### Configuration
258+
259+
Use the `--api-key` flag to enable API key authentication. You can specify multiple keys for different clients:
260+
261+
```bash
262+
# Single API key
263+
npx copilot-api@latest start --api-key your_secret_key
264+
265+
# Multiple API keys
266+
npx copilot-api@latest start --api-key key1 --api-key key2 --api-key key3
267+
```
268+
269+
When API keys are configured:
270+
271+
- All API endpoints (`/v1/chat/completions`, `/v1/models`, `/v1/embeddings`, `/v1/messages`, `/usage`, `/token`) require authentication
272+
- Requests without valid API keys will receive a 401 Unauthorized response
273+
- The root endpoint `/` remains accessible without authentication
274+
236275
## Example Usage
237276

238277
Using with npx:
@@ -262,6 +301,12 @@ npx copilot-api@latest start --rate-limit 30 --wait
262301
# Provide GitHub token directly
263302
npx copilot-api@latest start --github-token ghp_YOUR_TOKEN_HERE
264303

304+
# Enable API key authentication with a single key
305+
npx copilot-api@latest start --api-key your_secret_key_here
306+
307+
# Enable API key authentication with multiple keys
308+
npx copilot-api@latest start --api-key key1 --api-key key2 --api-key key3
309+
265310
# Run only the auth flow
266311
npx copilot-api@latest auth
267312

src/lib/api-key-auth.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import type { Context, MiddlewareHandler } from "hono"
2+
3+
import { HTTPException } from "hono/http-exception"
4+
5+
import { state } from "./state"
6+
import { constantTimeEqual } from "./utils"
7+
8+
function extractApiKey(c: Context): string | undefined {
9+
const authHeader = c.req.header("authorization")
10+
if (authHeader?.startsWith("Bearer ")) {
11+
return authHeader.slice(7)
12+
}
13+
14+
const anthropicKey = c.req.header("x-api-key")
15+
if (anthropicKey) {
16+
return anthropicKey
17+
}
18+
19+
return undefined
20+
}
21+
22+
export const apiKeyAuthMiddleware: MiddlewareHandler = async (c, next) => {
23+
if (!state.apiKeys || state.apiKeys.length === 0) {
24+
await next()
25+
return
26+
}
27+
28+
const providedKey = extractApiKey(c)
29+
30+
if (!providedKey) {
31+
throw new HTTPException(401, {
32+
message: "Missing API key",
33+
})
34+
}
35+
36+
const isValidKey = state.apiKeys.some((key) =>
37+
constantTimeEqual(key, providedKey),
38+
)
39+
40+
if (!isValidKey) {
41+
throw new HTTPException(401, {
42+
message: "Invalid API key",
43+
})
44+
}
45+
46+
await next()
47+
}

src/lib/state.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ export interface State {
1515
// Rate limiting configuration
1616
rateLimitSeconds?: number
1717
lastRequestTimestamp?: number
18+
19+
// API key validation
20+
apiKeys?: Array<string>
1821
}
1922

2023
export const state: State = {

src/lib/utils.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,14 @@ export const cacheVSCodeVersion = async () => {
2424

2525
consola.info(`Using VSCode version: ${response}`)
2626
}
27+
28+
export const constantTimeEqual = (a: string, b: string): boolean => {
29+
if (a.length !== b.length) {
30+
return false
31+
}
32+
let result = 0
33+
for (let i = 0; i < a.length; i++) {
34+
result |= (a.codePointAt(i) ?? 0) ^ (b.codePointAt(i) ?? 0)
35+
}
36+
return result === 0
37+
}

src/server.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Hono } from "hono"
22
import { cors } from "hono/cors"
33
import { logger } from "hono/logger"
44

5+
import { apiKeyAuthMiddleware } from "./lib/api-key-auth"
56
import { completionRoutes } from "./routes/chat-completions/route"
67
import { embeddingRoutes } from "./routes/embeddings/route"
78
import { eventLoggingRoutes } from "./routes/event-logging/route"
@@ -15,6 +16,16 @@ export const server = new Hono()
1516
server.use(logger())
1617
server.use(cors())
1718

19+
server.use("/chat/completions", apiKeyAuthMiddleware)
20+
server.use("/models", apiKeyAuthMiddleware)
21+
server.use("/embeddings", apiKeyAuthMiddleware)
22+
server.use("/usage", apiKeyAuthMiddleware)
23+
server.use("/token", apiKeyAuthMiddleware)
24+
server.use("/v1/chat/completions", apiKeyAuthMiddleware)
25+
server.use("/v1/models", apiKeyAuthMiddleware)
26+
server.use("/v1/embeddings", apiKeyAuthMiddleware)
27+
server.use("/v1/messages", apiKeyAuthMiddleware)
28+
1829
server.get("/", (c) => c.text("Server running"))
1930

2031
server.route("/chat/completions", completionRoutes)

src/start.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ interface RunServerOptions {
2626
claudeCode: boolean
2727
showToken: boolean
2828
proxyEnv: boolean
29+
apiKeys?: Array<string>
2930
}
3031

3132
export async function runServer(options: RunServerOptions): Promise<void> {
@@ -47,6 +48,13 @@ export async function runServer(options: RunServerOptions): Promise<void> {
4748
state.rateLimitSeconds = options.rateLimit
4849
state.rateLimitWait = options.rateLimitWait
4950
state.showToken = options.showToken
51+
state.apiKeys = options.apiKeys
52+
53+
if (state.apiKeys && state.apiKeys.length > 0) {
54+
consola.info(
55+
`API key authentication enabled with ${state.apiKeys.length} key(s)`,
56+
)
57+
}
5058

5159
await ensurePaths()
5260
await cacheVSCodeVersion()
@@ -192,13 +200,23 @@ export const start = defineCommand({
192200
default: false,
193201
description: "Initialize proxy from environment variables",
194202
},
203+
"api-key": {
204+
type: "string",
205+
description: "API keys for authentication",
206+
},
195207
},
196208
run({ args }) {
197209
const rateLimitRaw = args["rate-limit"]
198210
const rateLimit =
199211
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
200212
rateLimitRaw === undefined ? undefined : Number.parseInt(rateLimitRaw, 10)
201213

214+
const apiKeyRaw = args["api-key"]
215+
let apiKeys: Array<string> | undefined
216+
if (apiKeyRaw) {
217+
apiKeys = Array.isArray(apiKeyRaw) ? apiKeyRaw : [apiKeyRaw]
218+
}
219+
202220
return runServer({
203221
port: Number.parseInt(args.port, 10),
204222
host: args.host,
@@ -211,6 +229,7 @@ export const start = defineCommand({
211229
claudeCode: args["claude-code"],
212230
showToken: args["show-token"],
213231
proxyEnv: args["proxy-env"],
232+
apiKeys,
214233
})
215234
},
216235
})

0 commit comments

Comments
 (0)