Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,13 @@ All notable changes to this project are tracked in this file.
- `tools/call` through the `/apps/mcp/{appId}` MCP App proxy: tool error result. Listing and reading App tools and resources stay available to any authenticated role.
- MCP `openclaw.send_message`, `openclaw.run_workflow`, and `openclaw.respond_workflow`: tool error result. Read-only MCP tools stay available to viewers.
- Each denial is logged under `OpenClaw.Gateway.Authorization` with the surface, account, and role, so admins can find accounts to promote.
- Migration aid: `OpenClaw:Security:AllowViewerAgentExecution=true` restores the previous behavior for authenticated identities below `operator`, logs each such request, and adds the `viewer_agent_execution_allowed` risk flag to `admin posture`. It is temporary and will be removed in the next release.
- Removed the temporary `OpenClaw:Security:AllowViewerAgentExecution` migration switch and its posture risk flag. Viewer credentials remain read-only on every agent-execution surface. Grant `operator` to accounts that need to chat or run the agent, and remove the retired key from configuration (including environment variables), even if it is `false`; the gateway now refuses to start while the key is present.
- Stopped trusting a loopback client IP on `/apps/health`, `/apps/chat`, and `/apps/mcp/{appId}`. Behind a same-host reverse proxy without `TrustForwardedHeaders`, every caller has a loopback IP, so these routes answered unauthenticated requests, including agent runs and MCP App tool calls, and ignored `AlwaysRequireAuth`. They now follow the gateway's bind-based rule: open only on a loopback-bound gateway without `AlwaysRequireAuth`.
- Turns now run as the signed-in account instead of a caller-supplied sender id. `Session.AuthenticatedUserId` scopes per-user capability bindings and is passed to MCP servers as `_meta.userId`. Previously only `/ws` set it, so REST messages, MCP `send_message`, A2A, `/v1/*`, and `/apps/chat` turns ran as whatever `senderId` or `contextId` the caller supplied. MCP servers that read `_meta.userId` now receive account ids for those turns.
- A turn from an external sender without an account no longer inherits the previous writer's account. Previously, after an operator posted into a Telegram session, the Telegram user's next turns ran as that operator. System, scheduled, automation, and background-continuation turns keep the session's identity.
- Sessions record the account that created them as `ownerAccountId`, and only the owner or an admin can post to an owned session. Every surface refuses other accounts: REST (403), MCP `send_message` (tool error), `/apps/chat` (403), `/v1/*` stable sessions (403, `session_forbidden`), A2A (error event), and pipeline turns including `/ws` (reply). Session management follows the same rule: delete, metadata, abort, branch restore, and guided recovery return 403 for other non-admin accounts. `/ws` now resolves its caller like the other surfaces, so loopback-bound gateways with `AlwaysRequireAuth` or OIDC record and enforce owners. Previously any operator could post into any session whose id it knew. Unowned sessions (channels, cron, sessions created before this change) stay open and are never claimed by writing to them. Reading is unchanged. `GET /api/integration/sessions?owner=me` lists the caller's own sessions.
- `POST /apps/chat` now holds the session lock for the whole turn and saves the session afterwards, like the other turn surfaces. Previously two requests for the same `sessionId` ran concurrently against one history, and a turn was saved only if its session later expired or was evicted, so a gateway restart could lose it.
- Added `OpenClawWebSocketClient.OnClosed`, raised with the gateway's close status and reason. Companion now marks itself disconnected and shows the reason instead of appearing connected after the gateway closes the socket. It also asks the gateway before connecting: `GET /auth/session` now reports `canExecuteAgent`, which follows `AllowViewerAgentExecution`, and when it is `false` Companion explains the missing `operator` role instead of opening chat. The read-only status views stay available. Against a gateway that does not report the field, Companion connects and the gateway decides.
- Added `OpenClawWebSocketClient.OnClosed`, raised with the gateway's close status and reason. Companion now marks itself disconnected and shows the reason instead of appearing connected after the gateway closes the socket. It also asks the gateway before connecting: `GET /auth/session` now reports `canExecuteAgent` from the authorization and role check, and when it is `false` Companion explains the missing `operator` role instead of opening chat. The read-only status views stay available. Against a gateway that does not report the field, Companion connects and the gateway decides.
- Bound tool-approval decisions to the original requester (`channelId` + `senderId`) for non-loopback/public binds.
- Kept `POST /tools/approve` as an explicit admin override path.
- Added WhatsApp official webhook signature validation support (`ValidateSignature`, `WebhookAppSecret`/`WebhookAppSecretRef`).
Expand Down
9 changes: 5 additions & 4 deletions docs/AUTHENTICATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ Authentication configuration lives under the `OpenClaw.Security` node in `appset
|-------|------|---------|-------------|
| `AuthToken` | `string?` | `null` | Static bootstrap token. When `null`, bootstrap auth is disabled |
| `AlwaysRequireAuth` | `bool` | `false` | When `true`, even loopback-bound requests must carry valid credentials |
| `AllowViewerAgentExecution` | `bool` | `false` | **Temporary, to be removed in the next release.** When `true`, identities below `operator` can still run the agent (see [3.3](#33-role-required-for-agent-execution)). Each such request is logged and `admin posture` reports the risk |
| `AuthMode` | `string` | `"token"` | Authentication mode: `"token"` or `"oidc"` |
| `AllowQueryStringToken` | `bool` | `false` | Whether to accept tokens from the `?token=` query string parameter |
| `BrowserSessionIdleMinutes` | `int` | `60` | Idle timeout for browser admin sessions (minutes) |
Expand Down Expand Up @@ -194,9 +193,11 @@ WebSocket connected

Bootstrap tokens and open loopback resolve to `admin` and are unaffected. New operator accounts default to `viewer`, so accounts used for Companion, CLI/TUI chat, or API clients need the `operator` role.

Each denial is logged as a warning under the `OpenClaw.Gateway.Authorization` category, naming the surface, auth mode, account, and role (never the credential), so admins can find the accounts to promote. To migrate without an outage, set `OpenClaw:Security:AllowViewerAgentExecution=true`, watch the log for the admitted accounts, grant them `operator`, then turn the setting off. The setting is temporary and will be removed in the next release.
Each denial is logged as a warning under the `OpenClaw.Gateway.Authorization` category, naming the surface, auth mode, account, and role (never the credential), so admins can find the accounts to promote.

`GET /auth/session` reports the result of this check as `canExecuteAgent`, including the effect of `AllowViewerAgentExecution`, so clients can explain a refusal before connecting. Companion uses it; a gateway that predates the field omits it.
The temporary `OpenClaw:Security:AllowViewerAgentExecution` migration switch has been removed. Before upgrading, grant `operator` to accounts that need to chat or run the agent. Remove the retired key from JSON configuration, command-line arguments, and environment variables (`OpenClaw__Security__AllowViewerAgentExecution`), even if its value is `false`; the gateway refuses to start while the key is present. Viewer accounts retain read-only access.

`GET /auth/session` reports the result of the authorization and role check as `canExecuteAgent`, so clients can explain a refusal before connecting. Companion uses it; a gateway that predates the field omits it.

### 3.4 `IsAuthorizedRequest` — Detailed Logic

Expand Down Expand Up @@ -476,4 +477,4 @@ In this mode, `AuthMode` is `"token"` but `Oidc.Authority` is set, so the JWT au
3. **CSRF protection**: Browser sessions require CSRF token validation on API endpoints; WebSocket endpoints are exempt because browsers do send cookies during WebSocket handshakes, so WebSocket security relies on strict `Origin` header validation (see item 6) rather than cookie-based CSRF tokens
4. **JWT validation**: Full signature, issuer, audience, and expiration validation is provided by the ASP.NET Core JWT Bearer middleware
5. **Rate limiting**: All authenticated endpoints are subject to rate limiting, keyed by IP, operator account, and browser session
6. **Origin checking**: WebSocket endpoints validate the `Origin` header to prevent cross-site WebSocket hijacking
6. **Origin checking**: WebSocket endpoints validate the `Origin` header to prevent cross-site WebSocket hijacking
9 changes: 5 additions & 4 deletions docs/zh-CN/AUTHENTICATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ OpenClaw.NET Gateway 支持多层认证体系,涵盖静态令牌、OIDC/JWT Be
|------|------|--------|------|
| `AuthToken` | `string?` | `null` | 静态 Bootstrap 令牌。`null` 时禁用 Bootstrap 认证 |
| `AlwaysRequireAuth` | `bool` | `false` | `true` 时,即使是 loopback 绑定也需要认证 |
| `AllowViewerAgentExecution` | `bool` | `false` | **临时设置,将在下一个版本移除。** `true` 时,低于 `operator` 的身份仍可执行智能体(见 3.3 节)。每个此类请求都会记录日志,`admin posture` 也会报告该风险 |
| `AuthMode` | `string` | `"token"` | 认证模式:`"token"` 或 `"oidc"` |
| `AllowQueryStringToken` | `bool` | `false` | 是否允许从查询字符串 `?token=` 读取令牌 |
| `BrowserSessionIdleMinutes` | `int` | `60` | 浏览器会话空闲超时(分钟) |
Expand Down Expand Up @@ -194,9 +193,11 @@ WebSocket 已连接

引导令牌和开放回环会解析为 `admin`,不受影响。新建的操作员账户默认为 `viewer`,因此用于 Companion、CLI/TUI 聊天或 API 客户端的账户需要 `operator` 角色。

每次拒绝都会在 `OpenClaw.Gateway.Authorization` 类别下记录一条警告日志,包含入口、认证方式、账户和角色(绝不包含凭据),便于管理员找出需要提升角色的账户。如需无中断迁移,可设置 `OpenClaw:Security:AllowViewerAgentExecution=true`,从日志中找出被放行的账户,为其授予 `operator` 角色,然后关闭该设置。该设置是临时的,将在下一个版本移除。
每次拒绝都会在 `OpenClaw.Gateway.Authorization` 类别下记录一条警告日志,包含入口、认证方式、账户和角色(绝不包含凭据),便于管理员找出需要提升角色的账户。

`GET /auth/session` 会以 `canExecuteAgent` 字段报告这项检查的结果(包括 `AllowViewerAgentExecution` 的影响),客户端可以在连接前说明拒绝原因。Companion 会使用该字段;早于此字段的网关不会返回它。
临时迁移开关 `OpenClaw:Security:AllowViewerAgentExecution` 已移除。升级前,请为需要聊天或运行智能体的账户授予 `operator` 角色,并从 JSON 配置、命令行参数和环境变量(`OpenClaw__Security__AllowViewerAgentExecution`)中删除该键,即使其值为 `false`;只要该键仍存在,网关就会拒绝启动。Viewer 账户仍可使用只读功能。

`GET /auth/session` 会以 `canExecuteAgent` 字段报告认证和角色检查的结果,客户端可以在连接前说明拒绝原因。Companion 会使用该字段;早于此字段的网关不会返回它。

### 3.4 `IsAuthorizedRequest` 详细逻辑

Expand Down Expand Up @@ -476,4 +477,4 @@ if (!resp.ok) {
3. **CSRF 保护**:浏览器会话在 API 端点上要求 CSRF 令牌验证;WebSocket 端点在握手阶段浏览器会携带 Cookie,因此依赖严格的 `Origin` 头校验(参见第 6 条)来防止跨域攻击,而非基于 Cookie 的 CSRF 令牌
4. **JWT 验证**:由 ASP.NET Core JWT Bearer 中间件提供完整的签名、签发者、受众和过期时间验证
5. **速率限制**:所有认证端点均受速率限制保护,以 IP、操作员账户和浏览器会话为维度
6. **Origin 检查**:WebSocket 端点验证 `Origin` 头,防止跨域 WebSocket 攻击
6. **Origin 检查**:WebSocket 端点验证 `Origin` 头,防止跨域 WebSocket 攻击
4 changes: 2 additions & 2 deletions src/OpenClaw.Companion/ViewModels/MainWindowViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -530,8 +530,8 @@ private async Task ConnectAsync()
Status = "Connecting…";

// Ask the gateway first: an account it won't let run the agent would be admitted and then closed, so say why
// up front and keep the read-only status views. Only its answer counts, not the role: a viewer can still chat
// under Security.AllowViewerAgentExecution, and a gateway that doesn't report it decides at connect time.
// up front and keep the read-only status views. Only its answer counts: a gateway that doesn't report
// canExecuteAgent decides at connect time.
await LoadAdminStatusAsyncInternal();
if (_agentExecutionAllowedByGateway == false)
{
Expand Down
8 changes: 0 additions & 8 deletions src/OpenClaw.Core/Models/GatewayConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -384,14 +384,6 @@ public sealed class SecurityConfig
public string[] KnownProxies { get; set; } = [];
public bool RequireRequesterMatchForHttpToolApproval { get; set; } = false;

/// <summary>
/// Temporary compatibility switch, to be removed in the next release. When true, authenticated identities
/// below the operator role (such as viewer accounts) can still run the agent through /ws, /v1/*, A2A,
/// /apps/chat, MCP App tool calls, mutating MCP tools, and /ws/live. Each such request is logged so the accounts
/// can be promoted.
/// </summary>
public bool AllowViewerAgentExecution { get; set; } = false;

/// <summary>
/// When binding to a non-loopback address, the gateway refuses to start if the local tooling
/// is configured in an unsafe way (e.g. shell enabled or wildcard roots). Set this to true
Expand Down
8 changes: 8 additions & 0 deletions src/OpenClaw.Gateway/Bootstrap/GatewayBootstrapExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,14 @@ private static void WriteConfigSourceDiagnostics(ConfigSourceDiagnostics diagnos
internal static GatewayConfig LoadGatewayConfig(IConfiguration configuration, bool loadPersistedSettings = true)
{
var openClawSection = configuration.GetSection("OpenClaw");
if (openClawSection.GetSection("Security").GetChildren()
.Any(section => string.Equals(section.Key, "AllowViewerAgentExecution", StringComparison.OrdinalIgnoreCase)))
{
throw new InvalidOperationException(
"OpenClaw:Security:AllowViewerAgentExecution has been removed. " +
"Remove this key from configuration and grant the operator role to accounts that need to chat or run the agent.");
}

var config = openClawSection.Get<GatewayConfig>() ?? new GatewayConfig();
ApplyConfiguredLlmOverrides(openClawSection, config);
ApplyConfiguredToolingOverrides(openClawSection, config);
Expand Down
2 changes: 1 addition & 1 deletion src/OpenClaw.Gateway/Endpoints/AdminEndpoints.Support.cs
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ private static AuthSessionResponse MapAuthSessionResponse(
Username = auth.Username,
DisplayName = auth.DisplayName,
IsBootstrapAdmin = auth.IsBootstrapAdmin,
CanExecuteAgent = EndpointHelpers.AllowsAgentExecution(auth, startup),
CanExecuteAgent = EndpointHelpers.AllowsAgentExecution(auth),
PublicBind = startup.IsNonLoopbackBind,
AllowedAuthModes = [.. policy.AllowedAuthModes],
EffectiveToolSurface = preset.Surface,
Expand Down
17 changes: 4 additions & 13 deletions src/OpenClaw.Gateway/Endpoints/EndpointHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -360,13 +360,12 @@ public static (OperatorAuthorizationResult? Authorization, IResult? Failure) Aut
/// A2A, MCP Apps chat and tool calls, mutating MCP tools, the live model bridge) require the same role as
/// POST /api/integration/messages.
/// Authentication alone is not enough: viewer credentials must stay read-only.
/// Denials, and admissions under Security.AllowViewerAgentExecution, are logged with the account so
/// admins can find identities that need the operator role.
/// Denials are logged with the account so admins can find identities that need the operator role.
/// </summary>
// The rule CanExecuteAgent enforces, without its logging, so /auth/session can report it before a client tries.
internal static bool AllowsAgentExecution(OperatorAuthorizationResult auth, GatewayStartupContext startup)
internal static bool AllowsAgentExecution(OperatorAuthorizationResult auth)
=> auth.IsAuthorized
&& (IsRoleAllowed(auth.Role, "integration.mutate.agent", out _) || startup.Config.Security.AllowViewerAgentExecution);
&& IsRoleAllowed(auth.Role, "integration.mutate.agent", out _);

public static bool CanExecuteAgent(
HttpContext ctx,
Expand All @@ -376,7 +375,7 @@ public static bool CanExecuteAgent(
{
var browserSessions = ctx.RequestServices.GetRequiredService<BrowserSessionAuthService>();
var auth = AuthorizeOperatorRequest(ctx, startup, browserSessions, requireCsrf);
if (auth.IsAuthorized && IsRoleAllowed(auth.Role, "integration.mutate.agent", out _))
if (AllowsAgentExecution(auth))
return true;

var logger = ctx.RequestServices.GetRequiredService<ILoggerFactory>().CreateLogger("OpenClaw.Gateway.Authorization");
Expand All @@ -390,14 +389,6 @@ public static bool CanExecuteAgent(
return false;
}

if (startup.Config.Security.AllowViewerAgentExecution)
{
logger.LogWarning(
"Allowed {Action} for {AuthMode} account {AccountId} ({Username}) with role {Role} only because Security.AllowViewerAgentExecution is on. Grant the operator role before that setting is removed.",
action, auth.AuthMode, auth.AccountId, auth.Username, auth.Role);
return true;
}

logger.LogWarning(
"Denied {Action} for {AuthMode} account {AccountId} ({Username}) with role {Role}: this action requires the operator role.",
action, auth.AuthMode, auth.AccountId, auth.Username, auth.Role);
Expand Down
6 changes: 0 additions & 6 deletions src/OpenClaw.Gateway/SecurityPostureBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,12 +99,6 @@ public static SecurityPostureResponse Build(GatewayStartupContext startup, Gatew
recommendations.Add("Enable Discord interaction signature validation before exposing a public bind.");
}

if (config.Security.AllowViewerAgentExecution)
{
riskFlags.Add("viewer_agent_execution_allowed");
recommendations.Add("Grant the operator role to accounts that chat or run the agent, then turn off OpenClaw:Security:AllowViewerAgentExecution. The setting is temporary and will be removed.");
}

if (browserAvailability.ConfiguredEnabled && !browserAvailability.Registered)
{
riskFlags.Add("browser_tool_unavailable");
Expand Down
Loading
Loading