Files
claude-code/packages/remote-control-server/src/auth/jwt.ts
claude-code-best 2da6514095 feat: 支持自托管的 remote-control-server (#214)
* feat: 支持自托管的 remote-control-server (#214)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-04-09 17:40:50 +08:00

93 lines
2.6 KiB
TypeScript

import { createHmac, timingSafeEqual } from "node:crypto";
/**
* Lightweight JWT implementation using HMAC-SHA256.
* No external dependencies — uses Node.js crypto.
*
* Token format: base64url(header).base64url(payload).base64url(signature)
* Used for V2 worker authentication (session ingress / SSE / CCR).
*/
interface JwtPayload {
session_id: string;
role: string;
iat: number;
exp: number;
}
function base64url(data: string | Buffer): string {
return Buffer.from(data as unknown as ArrayLike<number>)
.toString("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
}
function base64urlDecode(str: string): string {
const padded = str.replace(/-/g, "+").replace(/_/g, "/");
return Buffer.from(padded, "base64").toString("utf-8");
}
function getSigningKey(): string {
const key = process.env.RCS_API_KEYS?.split(",").filter(Boolean)[0];
if (!key) throw new Error("No API key configured for JWT signing");
return key;
}
/** Generate a JWT for worker authentication. */
export function generateWorkerJwt(
sessionId: string,
expiresInSeconds: number,
): string {
const header = { alg: "HS256", typ: "JWT" };
const payload: JwtPayload = {
session_id: sessionId,
role: "worker",
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + expiresInSeconds,
};
const headerB64 = base64url(JSON.stringify(header));
const payloadB64 = base64url(JSON.stringify(payload));
const signingInput = `${headerB64}.${payloadB64}`;
const signature = createHmac("sha256", getSigningKey())
.update(signingInput)
.digest();
return `${signingInput}.${base64url(signature)}`;
}
/**
* Verify a JWT and return its payload, or null if invalid/expired.
* Uses timing-safe comparison to prevent timing attacks.
*/
export function verifyWorkerJwt(token: string): JwtPayload | null {
const parts = token.split(".");
if (parts.length !== 3) return null;
const [headerB64, payloadB64, signatureB64] = parts;
// Verify signature
const signingInput = `${headerB64}.${payloadB64}`;
const expectedSig = createHmac("sha256", getSigningKey())
.update(signingInput)
.digest();
const actualSig = Buffer.from(
signatureB64.replace(/-/g, "+").replace(/_/g, "/"),
"base64",
);
if (expectedSig.length !== actualSig.length) return null;
if (!timingSafeEqual(expectedSig, actualSig)) return null;
// Decode payload
try {
const payload: JwtPayload = JSON.parse(base64urlDecode(payloadB64));
if (payload.exp < Math.floor(Date.now() / 1000)) return null;
return payload;
} catch {
return null;
}
}