feat(auth): validate agent context upstream
This commit is contained in:
+89
-3
@@ -3,6 +3,37 @@ import type { NextFunction, Request, Response } from "express";
|
||||
import { config } from "../config.js";
|
||||
import { logger } from "../logger.js";
|
||||
|
||||
export type AgentAuthContext = {
|
||||
accessToken: string;
|
||||
userId: string;
|
||||
keycloakSub: string;
|
||||
username: string;
|
||||
role: string;
|
||||
isSuperuser: boolean;
|
||||
projectId: string;
|
||||
projectRole: string;
|
||||
tokenExpiresAt?: string;
|
||||
};
|
||||
|
||||
type AgentAuthContextResponse = {
|
||||
user_id?: unknown;
|
||||
keycloak_sub?: unknown;
|
||||
username?: unknown;
|
||||
role?: unknown;
|
||||
is_superuser?: unknown;
|
||||
project_id?: unknown;
|
||||
project_role?: unknown;
|
||||
token_expires_at?: unknown;
|
||||
};
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface Request {
|
||||
agentAuth?: AgentAuthContext;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const extractBearerToken = (authorization?: string) => {
|
||||
const value = authorization?.trim();
|
||||
if (!value) {
|
||||
@@ -11,8 +42,39 @@ export const extractBearerToken = (authorization?: string) => {
|
||||
return value.replace(/^Bearer\s+/i, "").trim();
|
||||
};
|
||||
|
||||
// Agent API 复用 TJWater 后端的登录态:每个请求都向 /auth/me 校验 Bearer token,
|
||||
// 成功后才允许进入会话路由,避免 Agent 服务维护第二套用户体系。
|
||||
const normalizeAgentAuthContext = (
|
||||
payload: AgentAuthContextResponse,
|
||||
accessToken: string,
|
||||
): AgentAuthContext | null => {
|
||||
if (
|
||||
typeof payload.user_id !== "string" ||
|
||||
typeof payload.keycloak_sub !== "string" ||
|
||||
typeof payload.username !== "string" ||
|
||||
typeof payload.role !== "string" ||
|
||||
typeof payload.is_superuser !== "boolean" ||
|
||||
typeof payload.project_id !== "string" ||
|
||||
typeof payload.project_role !== "string"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
accessToken,
|
||||
userId: payload.user_id,
|
||||
keycloakSub: payload.keycloak_sub,
|
||||
username: payload.username,
|
||||
role: payload.role,
|
||||
isSuperuser: payload.is_superuser,
|
||||
projectId: payload.project_id,
|
||||
projectRole: payload.project_role,
|
||||
tokenExpiresAt:
|
||||
typeof payload.token_expires_at === "string"
|
||||
? payload.token_expires_at
|
||||
: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
// Agent API 使用 TJWater 后端专用认证上下文:后端负责校验 Keycloak token、
|
||||
// metadata 用户状态和项目 membership,Agent 只信任后端返回的 user/project。
|
||||
export const requireAgentAuth = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
@@ -24,20 +86,37 @@ export const requireAgentAuth = async (
|
||||
return;
|
||||
}
|
||||
|
||||
const projectId = req.header("x-project-id")?.trim();
|
||||
if (!projectId) {
|
||||
res.status(400).json({ message: "x-project-id is required" });
|
||||
return;
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), config.AGENT_AUTH_TIMEOUT_MS);
|
||||
|
||||
try {
|
||||
const response = await fetch(new URL("/api/v1/auth/me", config.TJWATER_API_BASE_URL), {
|
||||
const response = await fetch(new URL("/api/v1/agent/auth/context", config.TJWATER_API_BASE_URL), {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
"X-Project-Id": projectId,
|
||||
...(req.header("x-trace-id") ? { "X-Trace-Id": req.header("x-trace-id") as string } : {}),
|
||||
},
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const context = normalizeAgentAuthContext(
|
||||
(await response.json()) as AgentAuthContextResponse,
|
||||
token,
|
||||
);
|
||||
if (!context) {
|
||||
res.status(502).json({ message: "invalid authentication context" });
|
||||
return;
|
||||
}
|
||||
req.agentAuth = context;
|
||||
next();
|
||||
return;
|
||||
}
|
||||
@@ -58,3 +137,10 @@ export const requireAgentAuth = async (
|
||||
clearTimeout(timer);
|
||||
}
|
||||
};
|
||||
|
||||
export const getAgentAuthContext = (req: Request) => {
|
||||
if (!req.agentAuth) {
|
||||
throw new Error("agent auth context is missing");
|
||||
}
|
||||
return req.agentAuth;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user