import type { IncomingMessage } from "node:http"; import type { GatewayAuthConfig, GatewayTrustedProxyConfig } from "../config/config.js"; import { readTailscaleWhoisIdentity, type TailscaleWhoisIdentity } from "../infra/tailscale.js"; import { safeEqualSecret } from "../security/secret-equal.js"; import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
} from "../shared/string-coerce.js"; import {
AUTH_RATE_LIMIT_SCOPE_SHARED_SECRET,
type AuthRateLimiter,
type RateLimitCheckResult,
} from "./auth-rate-limit.js"; import { type ResolvedGatewayAuth } from "./auth-resolve.js"; import {
isLoopbackAddress,
resolveRequestClientIp,
isTrustedProxyAddress,
resolveClientIp,
} from "./net.js"; import { checkBrowserOrigin } from "./origin-check.js"; import { withSerializedRateLimitAttempt } from "./rate-limit-attempt-serialization.js";
export {
resolveEffectiveSharedGatewayAuth,
resolveGatewayAuth,
type EffectiveSharedGatewayAuth,
type ResolvedGatewayAuth,
type ResolvedGatewayAuthMode,
type ResolvedGatewayAuthModeSource,
} from "./auth-resolve.js";
export type GatewayAuthResult = {
ok: boolean;
method?:
| "none"
| "token"
| "password"
| "tailscale"
| "device-token"
| "bootstrap-token"
| "trusted-proxy";
user?: string;
reason?: string; /** Present when the request was blocked by the rate limiter. */
rateLimited?: boolean; /** Milliseconds the client should wait before retrying (when rate-limited). */
retryAfterMs?: number;
};
type ConnectAuth = {
token?: string;
password?: string;
};
export type GatewayAuthSurface = "http" | "ws-control-ui";
export type AuthorizeGatewayConnectParams = {
auth: ResolvedGatewayAuth;
connectAuth?: ConnectAuth | null;
req?: IncomingMessage;
trustedProxies?: string[];
tailscaleWhois?: TailscaleWhoisLookup; /** *Explicitauthsurface.HTTPkeepsTailscaleforwarded-headerauthdisabled. *WSControlUIenablesitintentionallyfortokenlesstrusted-hostlogin.
*/
authSurface?: GatewayAuthSurface; /** Optional rate limiter instance; when provided, failed attempts are tracked per IP. */
rateLimiter?: AuthRateLimiter; /** Client IP used for rate-limit tracking. Falls back to proxy-aware request IP resolution. */
clientIp?: string; /** Optional limiter scope; defaults to shared-secret auth scope. */
rateLimitScope?: string; /** Trust X-Real-IP only when explicitly enabled. */
allowRealIpFallback?: boolean; /** Optional browser-origin policy for trusted-proxy HTTP requests. */
browserOriginPolicy?: {
requestHost?: string;
origin?: string;
allowedOrigins?: string[];
allowHostHeaderOriginFallback?: boolean;
};
};
export function assertGatewayAuthConfigured(
auth: ResolvedGatewayAuth,
rawAuthConfig?: GatewayAuthConfig | null,
): void { if (auth.mode === "token" && !auth.token) { if (auth.allowTailscale) { return;
} thrownew Error( "gateway auth mode is token, but no token was configured (set gateway.auth.token or OPENCLAW_GATEWAY_TOKEN)",
);
} if (auth.mode === "password" && !auth.password) { if (
rawAuthConfig?.password != null && // pragma: allowlist secret typeof rawAuthConfig.password !== "string"// pragma: allowlist secret
) { thrownew Error( "gateway auth mode is password, but gateway.auth.password contains a provider reference object instead of a resolved string — bootstrap secrets (gateway.auth.password) must be plaintext strings or set via the OPENCLAW_GATEWAY_PASSWORD environment variable because the secrets provider system has not initialised yet at gateway startup", // pragma: allowlist secret
);
} thrownew Error("gateway auth mode is password, but no password was configured");
} if (auth.mode === "trusted-proxy") { if (!auth.trustedProxy) { thrownew Error( "gateway auth mode is trusted-proxy, but no trustedProxy config was provided (set gateway.auth.trustedProxy)",
);
} if (!auth.trustedProxy.userHeader || auth.trustedProxy.userHeader.trim() === "") { thrownew Error( "gateway auth mode is trusted-proxy, but trustedProxy.userHeader is empty (set gateway.auth.trustedProxy.userHeader)",
);
} if (auth.token) { thrownew Error( "gateway auth mode is trusted-proxy, but a shared token is also configured; remove gateway.auth.token / OPENCLAW_GATEWAY_TOKEN because trusted-proxy and token auth are mutually exclusive",
);
}
}
}
// Keep the limiter strict on the async Tailscale branch by serializing // attempts for the same {scope, ip} key across the pre-check and failure write. if (
limiter &&
shouldAllowTailscaleHeaderAuth(authSurface) &&
auth.allowTailscale &&
!localDirect
) { return await withSerializedRateLimitAttempt({
ip,
scope: rateLimitScope,
run: async () => await authorizeGatewayConnectCore(params),
});
}
if (auth.mode === "trusted-proxy") { // Same-host reverse proxies may forward identity headers without a full // forwarded chain; keep those on the trusted-proxy path so allowUsers and // requiredHeaders still apply. if (!auth.trustedProxy) { return { ok: false, reason: "trusted_proxy_config_missing" };
} if (!trustedProxies || trustedProxies.length === 0) { return { ok: false, reason: "trusted_proxy_no_proxies_configured" };
}
const result = authorizeTrustedProxy({
req,
trustedProxies,
trustedProxyConfig: auth.trustedProxy,
});
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.