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
16 changes: 2 additions & 14 deletions backend/app/api/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,28 +42,16 @@ def _hash_key(key: str) -> str:

async def _get_agent_by_key(api_key: str, db: AsyncSession) -> Agent:
"""Authenticate an OpenClaw agent by its API key."""
# First try plaintext (new behavior)
key_hash = _hash_key(api_key)
result = await db.execute(
select(Agent).where(
Agent.api_key_hash == api_key,
Agent.api_key_hash == key_hash,
Agent.agent_type == "openclaw",
Agent.deleted_at.is_(None),
)
)
agent = result.scalar_one_or_none()

# Fallback to hashed (legacy behavior)
if not agent:
key_hash = _hash_key(api_key)
result = await db.execute(
select(Agent).where(
Agent.api_key_hash == key_hash,
Agent.agent_type == "openclaw",
Agent.deleted_at.is_(None),
)
)
agent = result.scalar_one_or_none()

if not agent:
raise HTTPException(status_code=401, detail="Invalid API key")
return agent
Expand Down
1 change: 0 additions & 1 deletion backend/app/schemas/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,6 @@ class AgentOut(BaseModel):
openclaw_last_seen: datetime | None = None
unread_count: int = 0
has_api_key: bool = False
api_key_hash: str | None = None
# True when the current viewer already has an onboarding row for this
# agent. Computed per-request by the API layer from the junction table;
# not an ORM attribute, so callers must set it explicitly. Defaults to
Expand Down
34 changes: 33 additions & 1 deletion backend/tests/test_gateway_runtime_a2a.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from app.models.agent import Agent
from app.models.audit import ChatMessage
from app.models.gateway_message import GatewayMessage
from app.schemas.schemas import GatewayReportRequest, GatewaySendMessageRequest
from app.schemas.schemas import AgentOut, GatewayReportRequest, GatewaySendMessageRequest
from app.services.agent_runtime.a2a_runtime import (
GatewayA2ARuntimeCompletion,
GatewayA2ARuntimeIntake,
Expand Down Expand Up @@ -45,10 +45,12 @@ def scalar_one_or_none(self):
class _Session:
def __init__(self, *results: object) -> None:
self.results = deque(results)
self.statements: list[object] = []
self.commits = 0
self.rollbacks = 0

async def execute(self, _statement) -> _Result:
self.statements.append(_statement)
value = self.results.popleft()
return _Result([] if value is None else [value])

Expand All @@ -59,6 +61,36 @@ async def rollback(self) -> None:
self.rollbacks += 1


@pytest.mark.asyncio
async def test_gateway_authentication_hashes_presented_key_before_lookup() -> None:
agent = object()
db = _Session(agent)

authenticated = await gateway._get_agent_by_key("oc-plaintext-key", db)

assert authenticated is agent
assert len(db.statements) == 1
params = db.statements[0].compile().params
assert gateway._hash_key("oc-plaintext-key") in params.values()
assert "oc-plaintext-key" not in params.values()


@pytest.mark.asyncio
async def test_gateway_rejects_stored_hash_as_presented_key() -> None:
stored_hash = gateway._hash_key("oc-plaintext-key")
db = _Session(None)

with pytest.raises(gateway.HTTPException, match="Invalid API key") as exc_info:
await gateway._get_agent_by_key(stored_hash, db)

assert exc_info.value.status_code == 401
assert len(db.statements) == 1


def test_agent_output_never_serializes_api_key_hash() -> None:
assert "api_key_hash" not in AgentOut.model_json_schema()["properties"]


class _ReportSession:
def __init__(self, *results: object) -> None:
self.results = deque(results)
Expand Down
10 changes: 5 additions & 5 deletions frontend/src/pages/OpenClawSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,8 @@ export default function OpenClawSettings({ agent, agentId, canManage }: OpenClaw

{/* API Key Display Logic */}
{(() => {
const activeKey = apiKey || (agent?.api_key_hash?.startsWith('oc-') ? agent.api_key_hash : null);
const isLegacyHash = hasKey && !activeKey;
const activeKey = apiKey;
const hasConfiguredKey = hasKey && !activeKey;

if (activeKey) {
return (
Expand Down Expand Up @@ -171,16 +171,16 @@ export default function OpenClawSettings({ agent, agentId, canManage }: OpenClaw
fontFamily: 'monospace', fontSize: '13px', color: 'var(--text-secondary)',
letterSpacing: '0.5px',
}}>
{isLegacyHash
? (isChinese ? '旧版密钥(已加密隐藏),请重新生成以查看明文' : 'Legacy key (encrypted), please regenerate to view')
{hasConfiguredKey
? (isChinese ? '密钥已配置。为安全起见,无法再次显示明文。' : 'A key is configured. Its plaintext cannot be shown again for security.')
: (isChinese ? '未生成' : 'Not generated')}
</div>
{canManage && <button
className="btn btn-secondary"
onClick={() => setShowConfirm(true)}
style={{ padding: '6px 16px', fontSize: '12px', whiteSpace: 'nowrap' }}
>
{isLegacyHash
{hasConfiguredKey
? (isChinese ? '重新生成' : 'Regenerate')
: (isChinese ? '生成' : 'Generate')}
</button>}
Expand Down