hequ commited on
Commit
afb7d7b
·
verified ·
1 Parent(s): e1a1ed4

Upload 15 files

Browse files
Files changed (2) hide show
  1. auth.js +113 -25
  2. server.js +13 -10
auth.js CHANGED
@@ -1,19 +1,22 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import os from 'os';
4
- import fetch from 'node-fetch';
5
- import { logDebug, logError, logInfo } from './logger.js';
6
 
7
  // State management for API key and refresh
8
- let currentApiKey = null;
9
- let currentRefreshToken = null;
10
- let lastRefreshTime = null;
11
- let clientId = null;
12
- let authSource = null; // 'env' or 'file' or 'factory_key' or 'client'
13
- let authFilePath = null;
14
  let factoryApiKey = null; // 单密钥(兼容旧行为)
15
  let factoryApiKeys = []; // 多密钥轮询列表
16
  let factoryKeyIndex = 0; // 轮询指针
 
 
 
17
 
18
  const REFRESH_URL = 'https://api.workos.com/user_management/authenticate';
19
  const REFRESH_INTERVAL_HOURS = 6; // Refresh every 6 hours
@@ -63,7 +66,7 @@ function generateClientId() {
63
  * Load auth configuration with priority system
64
  * Priority: FACTORY_API_KEY > refresh token mechanism > client authorization
65
  */
66
- function loadAuthConfig() {
67
  // 1. Check FACTORY_API_KEY environment variable (highest priority)
68
  const factoryKey = process.env.FACTORY_API_KEY;
69
  if (factoryKey && factoryKey.trim() !== '') {
@@ -124,7 +127,7 @@ function loadAuthConfig() {
124
  logInfo('No auth configuration found, will use client authorization headers');
125
  authSource = 'client';
126
  return { type: 'client', value: null };
127
- }
128
 
129
  /**
130
  * Refresh API key using refresh token
@@ -242,9 +245,9 @@ function shouldRefresh() {
242
  /**
243
  * Initialize auth system - load auth config and setup initial API key if needed
244
  */
245
- export async function initializeAuth() {
246
- try {
247
- const authConfig = loadAuthConfig();
248
 
249
  if (authConfig.type === 'factory_key') {
250
  // FACTORY_API_KEY 模式:固定或轮询
@@ -265,18 +268,26 @@ export async function initializeAuth() {
265
  logInfo('Auth system initialized for client authorization mode');
266
  }
267
 
268
- logInfo('Auth system initialized successfully');
269
- } catch (error) {
270
- logError('Failed to initialize auth system', error);
271
- throw error;
272
- }
273
- }
 
 
 
 
 
 
 
 
274
 
275
  /**
276
  * Get API key based on configured authorization method
277
  * @param {string} clientAuthorization - Authorization header from client request (optional)
278
  */
279
- export async function getApiKey(clientAuthorization = null) {
280
  // Priority 1: FACTORY_API_KEY environment variable
281
  if (authSource === 'factory_key' && (factoryApiKey || factoryApiKeys.length > 0)) {
282
  // 轮询选择密钥(若仅1个则等价于固定密钥)
@@ -310,5 +321,82 @@ export async function getApiKey(clientAuthorization = null) {
310
  }
311
 
312
  // No authorization available
313
- throw new Error('No authorization available. Please configure FACTORY_API_KEY, refresh token, or provide client authorization.');
314
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import fetch from 'node-fetch';
5
+ import { logDebug, logError, logInfo } from './logger.js';
6
 
7
  // State management for API key and refresh
8
+ let currentApiKey = null;
9
+ let currentRefreshToken = null;
10
+ let lastRefreshTime = null;
11
+ let clientId = null;
12
+ let authSource = null; // 'env' or 'file' or 'factory_key' or 'client'
13
+ let authFilePath = null;
14
  let factoryApiKey = null; // 单密钥(兼容旧行为)
15
  let factoryApiKeys = []; // 多密钥轮询列表
16
  let factoryKeyIndex = 0; // 轮询指针
17
+
18
+ // 本服务对外提供的 API Key 访问控制(用于保护 /v1/* 入口)
19
+ let accessKeys = null; // Set<string> 或 null(未启用鉴权)
20
 
21
  const REFRESH_URL = 'https://api.workos.com/user_management/authenticate';
22
  const REFRESH_INTERVAL_HOURS = 6; // Refresh every 6 hours
 
66
  * Load auth configuration with priority system
67
  * Priority: FACTORY_API_KEY > refresh token mechanism > client authorization
68
  */
69
+ function loadAuthConfig() {
70
  // 1. Check FACTORY_API_KEY environment variable (highest priority)
71
  const factoryKey = process.env.FACTORY_API_KEY;
72
  if (factoryKey && factoryKey.trim() !== '') {
 
127
  logInfo('No auth configuration found, will use client authorization headers');
128
  authSource = 'client';
129
  return { type: 'client', value: null };
130
+ }
131
 
132
  /**
133
  * Refresh API key using refresh token
 
245
  /**
246
  * Initialize auth system - load auth config and setup initial API key if needed
247
  */
248
+ export async function initializeAuth() {
249
+ try {
250
+ const authConfig = loadAuthConfig();
251
 
252
  if (authConfig.type === 'factory_key') {
253
  // FACTORY_API_KEY 模式:固定或轮询
 
268
  logInfo('Auth system initialized for client authorization mode');
269
  }
270
 
271
+ logInfo('Auth system initialized successfully');
272
+
273
+ // 载入对外访问的 API Key 列表(用于入站鉴权)
274
+ loadAccessKeysFromEnv();
275
+ if (accessKeys && accessKeys.size > 0) {
276
+ logInfo(`Inbound API Key enforcement enabled (${accessKeys.size} key(s))`);
277
+ } else {
278
+ logInfo('Inbound API Key not configured; API is publicly accessible');
279
+ }
280
+ } catch (error) {
281
+ logError('Failed to initialize auth system', error);
282
+ throw error;
283
+ }
284
+ }
285
 
286
  /**
287
  * Get API key based on configured authorization method
288
  * @param {string} clientAuthorization - Authorization header from client request (optional)
289
  */
290
+ export async function getApiKey(clientAuthorization = null) {
291
  // Priority 1: FACTORY_API_KEY environment variable
292
  if (authSource === 'factory_key' && (factoryApiKey || factoryApiKeys.length > 0)) {
293
  // 轮询选择密钥(若仅1个则等价于固定密钥)
 
321
  }
322
 
323
  // No authorization available
324
+ throw new Error('No authorization available. Please configure FACTORY_API_KEY, refresh token, or provide client authorization.');
325
+ }
326
+
327
+ /**
328
+ * 从环境变量加载对外访问的 API Key 列表
329
+ * 支持:
330
+ * - ACCESS_KEYS: 多个密钥,逗号/分号/空白分隔
331
+ * - ACCESS_KEY: 单个密钥
332
+ * 默认:未配置则不启用鉴权(保持向后兼容);在公开部署时建议务必配置。
333
+ */
334
+ function loadAccessKeysFromEnv() {
335
+ const multi = process.env.ACCESS_KEYS;
336
+ const single = process.env.ACCESS_KEY;
337
+
338
+ let keys = [];
339
+ if (multi && multi.trim() !== '') {
340
+ keys = multi.split(/[\s,;]+/).map(k => k.trim()).filter(Boolean);
341
+ } else if (single && single.trim() !== '') {
342
+ keys = [single.trim()];
343
+ }
344
+
345
+ accessKeys = keys.length > 0 ? new Set(keys) : null;
346
+ }
347
+
348
+ /**
349
+ * 提取客户端传入的访问密钥
350
+ * 支持:
351
+ * - 请求头 X-API-Key: <key>
352
+ * - Authorization: Bearer <key> | Api-Key <key> | Key <key>
353
+ * - 查询参数 api_key=<key>(可选,尽量用请求头)
354
+ */
355
+ function extractClientAccessKey(req) {
356
+ const hdrKey = req.headers['x-api-key'] || req.headers['x-api_key'];
357
+ if (typeof hdrKey === 'string' && hdrKey.trim() !== '') {
358
+ return hdrKey.trim();
359
+ }
360
+
361
+ const auth = req.headers['authorization'];
362
+ if (typeof auth === 'string' && auth.trim() !== '') {
363
+ const m = auth.match(/^(Bearer|Api-Key|Key)\s+(.+)$/i);
364
+ if (m && m[2]) return m[2].trim();
365
+ }
366
+
367
+ if (req.query && typeof req.query.api_key === 'string' && req.query.api_key.trim() !== '') {
368
+ return req.query.api_key.trim();
369
+ }
370
+ return null;
371
+ }
372
+
373
+ /**
374
+ * 入站 API Key 鉴权中间件(保护 /v1/*)
375
+ * - 如未配置 ACCESS_KEYS/ACCESS_KEY,则放行且记录提醒日志
376
+ * - 如已配置,则要求客户端提供有效 Key,否则 401
377
+ */
378
+ export function accessKeyMiddleware(req, res, next) {
379
+ // 仅在配置了密钥时启用
380
+ if (!accessKeys || accessKeys.size === 0) {
381
+ return next();
382
+ }
383
+
384
+ const key = extractClientAccessKey(req);
385
+ if (!key) {
386
+ res.setHeader('WWW-Authenticate', 'Bearer realm="droid2api"');
387
+ return res.status(401).json({
388
+ error: 'unauthorized',
389
+ message: 'Missing API key. Provide X-API-Key or Authorization: Bearer.'
390
+ });
391
+ }
392
+
393
+ if (!accessKeys.has(key)) {
394
+ return res.status(401).json({
395
+ error: 'unauthorized',
396
+ message: 'Invalid API key'
397
+ });
398
+ }
399
+
400
+ // 通过校验
401
+ return next();
402
+ }
server.js CHANGED
@@ -1,26 +1,29 @@
1
  import express from 'express';
2
  import { loadConfig, isDevMode, getPort } from './config.js';
3
  import { logInfo, logError } from './logger.js';
4
- import router from './routes.js';
5
- import { initializeAuth } from './auth.js';
6
 
7
  const app = express();
8
 
9
  app.use(express.json({ limit: '50mb' }));
10
  app.use(express.urlencoded({ extended: true, limit: '50mb' }));
11
 
12
- app.use((req, res, next) => {
13
- res.header('Access-Control-Allow-Origin', '*');
14
- res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
15
- res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key, anthropic-version');
16
 
17
  if (req.method === 'OPTIONS') {
18
  return res.sendStatus(200);
19
  }
20
- next();
21
- });
22
-
23
- app.use(router);
 
 
 
24
 
25
  app.get('/', (req, res) => {
26
  res.json({
 
1
  import express from 'express';
2
  import { loadConfig, isDevMode, getPort } from './config.js';
3
  import { logInfo, logError } from './logger.js';
4
+ import router from './routes.js';
5
+ import { initializeAuth, accessKeyMiddleware } from './auth.js';
6
 
7
  const app = express();
8
 
9
  app.use(express.json({ limit: '50mb' }));
10
  app.use(express.urlencoded({ extended: true, limit: '50mb' }));
11
 
12
+ app.use((req, res, next) => {
13
+ res.header('Access-Control-Allow-Origin', '*');
14
+ res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
15
+ res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key, anthropic-version');
16
 
17
  if (req.method === 'OPTIONS') {
18
  return res.sendStatus(200);
19
  }
20
+ next();
21
+ });
22
+
23
+ // 入站 API Key 鉴权:仅保护 /v1/* 路由
24
+ app.use('/v1', accessKeyMiddleware);
25
+
26
+ app.use(router);
27
 
28
  app.get('/', (req, res) => {
29
  res.json({