Files
TJWaterAgent/src/auth/agentAuth.ts
T
jiang d782b7fa11
Agent CI/CD / docker-image (push) Failing after 15m0s
Agent CI/CD / deploy-fallback-log (push) Successful in 0s
fix(session): propagate network context
2026-07-17 16:33:11 +08:00

151 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
network: string;
projectRole: string;
tokenExpiresAt?: string;
};
type AgentAuthContextResponse = {
user_id?: unknown;
keycloak_sub?: unknown;
username?: unknown;
role?: unknown;
is_superuser?: unknown;
project_id?: unknown;
network?: 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) {
return "";
}
return value.replace(/^Bearer\s+/i, "").trim();
};
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.network !== "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,
network: payload.network,
projectRole: payload.project_role,
tokenExpiresAt:
typeof payload.token_expires_at === "string"
? payload.token_expires_at
: undefined,
};
};
// Agent API 使用 TJWater 后端专用认证上下文:后端负责校验 Keycloak token、
// metadata 用户状态和项目 membershipAgent 只信任后端返回的 user/project。
export const requireAgentAuth = async (
req: Request,
res: Response,
next: NextFunction,
) => {
const token = extractBearerToken(req.header("authorization"));
if (!token) {
res.status(401).json({ message: "authorization token is required" });
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/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;
}
const detail = await response.text();
res.status(response.status === 403 ? 403 : 401).json({
message: response.status === 403 ? "forbidden" : "unauthorized",
detail: detail || undefined,
});
} catch (error) {
const detail = error instanceof Error ? error.message : String(error);
logger.warn({ err: error }, "agent auth validation failed");
res.status(503).json({
message: "authentication service unavailable",
detail,
});
} finally {
clearTimeout(timer);
}
};
export const getAgentAuthContext = (req: Request) => {
if (!req.agentAuth) {
throw new Error("agent auth context is missing");
}
return req.agentAuth;
};