199 lines
6.1 KiB
TypeScript
199 lines
6.1 KiB
TypeScript
import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
import {
|
|
createDefaultGlobalConfig,
|
|
getGlobalConfigPath,
|
|
loadGlobalConfig,
|
|
saveGlobalConfig,
|
|
} from "@aoagents/ao-core";
|
|
|
|
export type RemoteAuthCredentials = {
|
|
username: string;
|
|
password?: string;
|
|
};
|
|
|
|
const TOKEN_VERSION = "v1";
|
|
const TOKEN_TTL_MS = 5 * 60 * 1000;
|
|
|
|
function base64UrlEncode(input: string | Buffer): string {
|
|
return Buffer.from(input).toString("base64url");
|
|
}
|
|
|
|
function base64UrlDecode(input: string): string | null {
|
|
try {
|
|
return Buffer.from(input, "base64url").toString("utf-8");
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function getTokenSecret(): string | undefined {
|
|
return process.env.AO_REMOTE_WS_TOKEN_SECRET?.trim() || undefined;
|
|
}
|
|
|
|
export function ensureRemoteWsTokenSecret(): string {
|
|
const existing = process.env.AO_REMOTE_WS_TOKEN_SECRET?.trim() || undefined;
|
|
if (existing) return existing;
|
|
const secret = randomBytes(32).toString("base64url");
|
|
process.env.AO_REMOTE_WS_TOKEN_SECRET = secret;
|
|
return secret;
|
|
}
|
|
|
|
export function ensureRemoteAuthCredentials(): RemoteAuthCredentials {
|
|
const configPath = getGlobalConfigPath();
|
|
const config = loadGlobalConfig(configPath) ?? createDefaultGlobalConfig();
|
|
const remoteAccess =
|
|
config.remoteAccess && typeof config.remoteAccess === "object"
|
|
? (config.remoteAccess as { username?: string; password?: string })
|
|
: {};
|
|
const username = remoteAccess.username?.trim() || process.env.AO_REMOTE_AUTH_USER?.trim() || "ao";
|
|
const password =
|
|
remoteAccess.password?.trim() ||
|
|
process.env.AO_REMOTE_AUTH_PASSWORD?.trim() ||
|
|
randomBytes(18).toString("base64url");
|
|
|
|
if (remoteAccess.username?.trim() !== username || remoteAccess.password?.trim() !== password) {
|
|
saveGlobalConfig(
|
|
{
|
|
...config,
|
|
remoteAccess: {
|
|
...remoteAccess,
|
|
username,
|
|
password,
|
|
},
|
|
},
|
|
configPath,
|
|
);
|
|
}
|
|
|
|
process.env.AO_REMOTE_AUTH_USER = username;
|
|
process.env.AO_REMOTE_AUTH_PASSWORD = password;
|
|
return { username, password };
|
|
}
|
|
|
|
export function rotateRemoteWsTokenSecret(): string {
|
|
const secret = randomBytes(32).toString("base64url");
|
|
process.env.AO_REMOTE_WS_TOKEN_SECRET = secret;
|
|
return secret;
|
|
}
|
|
|
|
function credentialDigest(credentials: RemoteAuthCredentials): string {
|
|
return createHash("sha256")
|
|
.update(credentials.username)
|
|
.update("\0")
|
|
.update(credentials.password ?? "")
|
|
.digest("base64url");
|
|
}
|
|
|
|
export function decodeBasicToken(
|
|
input: string | undefined | null,
|
|
): { username: string; password: string } | null {
|
|
if (!input) return null;
|
|
try {
|
|
const decoded = Buffer.from(input, "base64").toString("utf-8");
|
|
const separator = decoded.indexOf(":");
|
|
if (separator === -1) return null;
|
|
return { username: decoded.slice(0, separator), password: decoded.slice(separator + 1) };
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function readConfiguredRemoteAuth(): RemoteAuthCredentials {
|
|
const config = loadGlobalConfig(getGlobalConfigPath()) ?? createDefaultGlobalConfig();
|
|
const remoteAccess =
|
|
config.remoteAccess && typeof config.remoteAccess === "object"
|
|
? (config.remoteAccess as { username?: string; password?: string })
|
|
: undefined;
|
|
|
|
return {
|
|
username: remoteAccess?.username?.trim() || "ao",
|
|
password: remoteAccess?.password?.trim() || undefined,
|
|
};
|
|
}
|
|
|
|
export function activeRemoteAuth(
|
|
initialConfiguredAuth?: RemoteAuthCredentials,
|
|
): RemoteAuthCredentials {
|
|
const configured = readConfiguredRemoteAuth();
|
|
if (
|
|
initialConfiguredAuth &&
|
|
(configured.username !== initialConfiguredAuth.username ||
|
|
configured.password !== initialConfiguredAuth.password)
|
|
) {
|
|
return configured;
|
|
}
|
|
|
|
const active = {
|
|
username:
|
|
process.env.AO_REMOTE_AUTH_USER || initialConfiguredAuth?.username || configured.username,
|
|
password:
|
|
process.env.AO_REMOTE_AUTH_PASSWORD || initialConfiguredAuth?.password || configured.password,
|
|
};
|
|
return active;
|
|
}
|
|
|
|
export function createRemoteWsToken(credentials: RemoteAuthCredentials): string | undefined {
|
|
const secret = getTokenSecret();
|
|
if (!secret) return undefined;
|
|
|
|
const payload = base64UrlEncode(
|
|
JSON.stringify({
|
|
u: credentials.username,
|
|
c: credentialDigest(credentials),
|
|
exp: Date.now() + TOKEN_TTL_MS,
|
|
n: randomBytes(12).toString("base64url"),
|
|
}),
|
|
);
|
|
const signature = createHmac("sha256", secret).update(payload).digest("base64url");
|
|
return `${TOKEN_VERSION}.${payload}.${signature}`;
|
|
}
|
|
|
|
export function verifyRemoteWsToken(
|
|
token: string | undefined | null,
|
|
expectedCredentials: RemoteAuthCredentials,
|
|
): boolean {
|
|
const secret = getTokenSecret();
|
|
if (!secret || !token) return false;
|
|
|
|
const parts = token.split(".");
|
|
if (parts.length !== 3 || parts[0] !== TOKEN_VERSION) return false;
|
|
const [, payload, signature] = parts;
|
|
if (!payload || !signature) return false;
|
|
|
|
const expectedSignature = createHmac("sha256", secret).update(payload).digest("base64url");
|
|
const provided = Buffer.from(signature);
|
|
const expectedSignatureBuffer = Buffer.from(expectedSignature);
|
|
if (
|
|
provided.length !== expectedSignatureBuffer.length ||
|
|
!timingSafeEqual(provided, expectedSignatureBuffer)
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
const rawPayload = base64UrlDecode(payload);
|
|
if (!rawPayload) return false;
|
|
|
|
try {
|
|
const parsed = JSON.parse(rawPayload) as { u?: unknown; c?: unknown; exp?: unknown };
|
|
return (
|
|
parsed.u === expectedCredentials.username &&
|
|
parsed.c === credentialDigest(expectedCredentials) &&
|
|
typeof parsed.exp === "number" &&
|
|
parsed.exp >= Date.now()
|
|
);
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export function isBasicAuthHeaderAllowed(
|
|
authorization: string | string[] | undefined,
|
|
expected: RemoteAuthCredentials,
|
|
): boolean {
|
|
if (!expected.password) return true;
|
|
const header = Array.isArray(authorization) ? authorization[0] : (authorization ?? "");
|
|
const headerMatch = /^Basic\s+(.+)$/i.exec(header);
|
|
const credentials = decodeBasicToken(headerMatch?.[1]);
|
|
return credentials?.username === expected.username && credentials.password === expected.password;
|
|
}
|