You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
153 lines
4.5 KiB
153 lines
4.5 KiB
/**
|
|
* 文件作用:
|
|
* 为 SuperAgent 请求签发 Agent 可验证的短时 JWT。
|
|
*
|
|
* 职责:
|
|
* 1. 从 ai-center 登录用户信息提取之前 X-User-Id 对应的 userid,写入 JWT sub。
|
|
* 2. 用 query api 配置的密钥/私钥签名,算法与 Agent AUTH_* 对齐。
|
|
*
|
|
* 不负责:
|
|
* 1. 验证 ai-center 登录 token。
|
|
* 2. 把 X-User-Id 当作 Agent 身份。
|
|
*/
|
|
'use strict';
|
|
|
|
const crypto = require('crypto');
|
|
|
|
const DEFAULT_EXPIRES_IN_SECONDS = 3600;
|
|
const SUPPORTED_ALGORITHMS = new Set(['HS256', 'RS256']);
|
|
|
|
const toBase64Url = (value) => {
|
|
const buffer = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
return buffer.toString('base64url');
|
|
};
|
|
|
|
const encodeJson = (value) => toBase64Url(JSON.stringify(value));
|
|
|
|
const unwrapUserInfo = (userInfo) => {
|
|
if (!userInfo || typeof userInfo !== 'object') return null;
|
|
if (userInfo.AIUserInfo && typeof userInfo.AIUserInfo === 'object') {
|
|
return userInfo.AIUserInfo;
|
|
}
|
|
if (userInfo.userInfo && typeof userInfo.userInfo === 'object') {
|
|
return userInfo.userInfo;
|
|
}
|
|
if (userInfo.user && typeof userInfo.user === 'object') {
|
|
return userInfo.user;
|
|
}
|
|
return userInfo;
|
|
};
|
|
|
|
const firstNonEmpty = (...values) => {
|
|
for (const value of values) {
|
|
if (value === undefined || value === null) continue;
|
|
const text = String(value).trim();
|
|
if (text) return text;
|
|
}
|
|
return '';
|
|
};
|
|
|
|
const createConfigError = (message) => {
|
|
const error = new Error(message);
|
|
error.status = 503;
|
|
return error;
|
|
};
|
|
|
|
const createRequestError = (message, status = 401) => {
|
|
const error = new Error(message);
|
|
error.status = status;
|
|
return error;
|
|
};
|
|
|
|
const normalizePem = (value) => String(value || '').replace(/\\n/g, '\n').trim();
|
|
|
|
const resolveJwtConfig = (rawConfig = {}) => {
|
|
const algorithm = String(rawConfig.algorithm || 'HS256').trim().toUpperCase();
|
|
const expiresInSeconds = Number(rawConfig.expiresInSeconds || DEFAULT_EXPIRES_IN_SECONDS);
|
|
return {
|
|
algorithm,
|
|
secret: String(rawConfig.secret || '').trim(),
|
|
privateKey: normalizePem(rawConfig.privateKey),
|
|
issuer: String(rawConfig.issuer || '').trim(),
|
|
audience: String(rawConfig.audience || '').trim(),
|
|
expiresInSeconds: Number.isFinite(expiresInSeconds) && expiresInSeconds > 0
|
|
? Math.floor(expiresInSeconds)
|
|
: DEFAULT_EXPIRES_IN_SECONDS,
|
|
};
|
|
};
|
|
|
|
const signJwt = (payload, config) => {
|
|
if (!SUPPORTED_ALGORITHMS.has(config.algorithm)) {
|
|
throw createConfigError(`不支持的 Agent JWT 算法:${config.algorithm}`);
|
|
}
|
|
|
|
const header = { alg: config.algorithm, typ: 'JWT' };
|
|
const signingInput = `${encodeJson(header)}.${encodeJson(payload)}`;
|
|
|
|
if (config.algorithm === 'HS256') {
|
|
if (!config.secret) {
|
|
throw createConfigError('未配置 Agent JWT 签名密钥');
|
|
}
|
|
const signature = crypto
|
|
.createHmac('sha256', config.secret)
|
|
.update(signingInput)
|
|
.digest();
|
|
return `${signingInput}.${toBase64Url(signature)}`;
|
|
}
|
|
|
|
if (!config.privateKey) {
|
|
throw createConfigError('未配置 Agent JWT 签名私钥');
|
|
}
|
|
const signature = crypto.createSign('RSA-SHA256').update(signingInput).sign(config.privateKey);
|
|
return `${signingInput}.${toBase64Url(signature)}`;
|
|
};
|
|
|
|
const resolveAgentUserId = (userInfo) => {
|
|
const identity = unwrapUserInfo(userInfo);
|
|
if (!identity) return '';
|
|
return firstNonEmpty(
|
|
identity.localUserId,
|
|
identity.id,
|
|
identity.userId,
|
|
identity.userid,
|
|
identity.user_id,
|
|
identity.uid,
|
|
identity.pepUserId,
|
|
identity.pep_user_id,
|
|
identity.pepId,
|
|
identity.pep_id,
|
|
);
|
|
};
|
|
|
|
const issueAgentAccessToken = ({ userInfo, config, now = Date.now() } = {}) => {
|
|
const jwtConfig = resolveJwtConfig(config);
|
|
const sub = resolveAgentUserId(userInfo);
|
|
if (!sub) {
|
|
throw createRequestError('认证用户信息无效');
|
|
}
|
|
|
|
const issuedAt = Math.floor(now / 1000);
|
|
const expiresAt = issuedAt + jwtConfig.expiresInSeconds;
|
|
const payload = {
|
|
sub,
|
|
iat: issuedAt,
|
|
exp: expiresAt,
|
|
};
|
|
if (jwtConfig.issuer) payload.iss = jwtConfig.issuer;
|
|
if (jwtConfig.audience) payload.aud = jwtConfig.audience;
|
|
|
|
return {
|
|
token: signJwt(payload, jwtConfig),
|
|
token_type: 'Bearer',
|
|
expires_in: jwtConfig.expiresInSeconds,
|
|
expires_at: expiresAt,
|
|
sub,
|
|
};
|
|
};
|
|
|
|
module.exports = {
|
|
DEFAULT_EXPIRES_IN_SECONDS,
|
|
resolveAgentUserId,
|
|
resolveJwtConfig,
|
|
issueAgentAccessToken,
|
|
};
|
|
|