Files
claude-code/packages/remote-control-server/src/auth/middleware.ts
claude-code-best c7bc8c8636 feat: remote control 支持 auto bind 功能 (#300)
* feat: acp-link 支持 --group 参数指定 channel group

- 添加 --group CLI flag,校验格式 [a-zA-Z0-9_-]+
- 支持 ACP_RCS_GROUP 环境变量 fallback
- 传递 channelGroupId 到 RcsUpstreamClient
- 更新 README 文档说明 --group 和相关环境变量

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: RCS 后端 session 复用与 group 绑定

- storeFindEnvironmentByMachineName 匹配 offline 状态,防止重连创建重复 session
- registerEnvironment 复用已有 session 而非每次新建
- EnvironmentResponse 返回 channel_group_id 字段
- 注册时将 session 绑定到 group ID,支持 web UI 按 group 查询
- apiKeyAuth 不再设置 uuid,由 uuidAuth 统一处理

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: Web UI Token Manager — 多 token 切换与 session 隔离

- 新增 useTokens hook 管理 localStorage token CRUD
- 新增 TokenManagerDialog 弹窗组件(添加/编辑/删除/切换 token)
- api client 支持Bearer token 认证,UUID 跟随 token 变化
- Navbar 添加 token 切换按钮
- 切换 token 时自动 reload,实现 session 数据隔离

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: 修复 useTokens useState 初始化函数签名错误

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-19 13:04:09 +08:00

114 lines
3.6 KiB
TypeScript

import type { Context, Next } from "hono";
import { validateApiKey } from "./api-key";
import { verifyWorkerJwt } from "./jwt";
import { resolveToken } from "./token";
/** Extract Bearer token from Authorization header or ?token= query param */
function extractBearerToken(c: Context): string | undefined {
const authHeader = c.req.header("Authorization");
const queryToken = c.req.query("token");
return authHeader?.replace("Bearer ", "") || queryToken;
}
/**
* Unified authentication middleware — supports two modes:
*
* 1. **Token mode** (Web UI): Bearer token resolved via server-side lookup → username injected
* 2. **API Key mode** (CLI bridge): Valid API key + X-Username header → username injected
*/
export async function apiKeyAuth(c: Context, next: Next) {
const token = extractBearerToken(c);
// Try token authentication (Web UI)
const tokenUsername = resolveToken(token);
if (tokenUsername) {
c.set("username", tokenUsername);
await next();
return;
}
// Try API Key authentication (CLI bridge)
if (validateApiKey(token)) {
// Extract username from X-Username header or ?username= query param
const username = c.req.header("X-Username") || c.req.query("username");
if (username) {
c.set("username", username);
}
await next();
return;
}
return c.json({ error: { type: "unauthorized", message: "Invalid or missing auth token" } }, 401);
}
/**
* Session ingress authentication — accepts both API key and worker JWT.
*
* Used for SSE stream, CCR worker events, and WebSocket ingress endpoints.
* On JWT validation, stores the decoded payload in c.set("jwtPayload") for
* downstream handlers to inspect session_id if needed.
*/
export async function sessionIngressAuth(c: Context, next: Next) {
const token = extractBearerToken(c);
if (!token) {
return c.json({ error: { type: "unauthorized", message: "Missing auth token" } }, 401);
}
// Try API key first (backward compatible)
if (validateApiKey(token)) {
await next();
return;
}
// Try JWT verification — validate session_id matches route param
const payload = verifyWorkerJwt(token);
if (payload) {
const routeSessionId = c.req.param("id") || c.req.param("sessionId");
if (routeSessionId && payload.session_id !== routeSessionId) {
return c.json({ error: { type: "forbidden", message: "JWT session_id does not match target session" } }, 403);
}
c.set("jwtPayload", payload);
await next();
return;
}
return c.json({ error: { type: "unauthorized", message: "Invalid API key or JWT" } }, 401);
}
/** Accept CLI headers but don't validate them */
export async function acceptCliHeaders(c: Context, next: Next) {
await next();
}
/**
* Extract UUID from request — query param ?uuid= or header X-UUID
*/
export function getUuidFromRequest(c: Context): string | undefined {
return c.req.query("uuid") || c.req.header("X-UUID");
}
/**
* UUID-based auth for Web UI routes (no-login mode).
* Accepts UUID in query param/header, OR a valid API key via Authorization header.
*/
export async function uuidAuth(c: Context, next: Next) {
// Try API key auth via Authorization header
const bearer = extractBearerToken(c);
if (bearer && validateApiKey(bearer)) {
// Valid API key — generate a stable UUID from the key for downstream use
const uuid = getUuidFromRequest(c);
c.set("uuid", uuid || bearer);
await next();
return;
}
// Fall back to UUID auth
const uuid = getUuidFromRequest(c);
if (!uuid) {
return c.json({ error: { type: "unauthorized", message: "Missing UUID" } }, 401);
}
c.set("uuid", uuid);
await next();
}