You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This issue was filed at 2026-04-24 02:47 UTC; PR #311 merged 2026-04-24 17:18 UTC. The PR's commit 2ba2b2ae ("Harden service invoke and binding identity handling", 2026-04-23) had already landed the two main residues described below. After-the-fact audit confirms:
A. Invoke caller identity — FIXED
src/platform/Aevatar.GAgentService.Hosting/Endpoints/ServiceEndpoints.cs:452-475 — new ResolveInvocationCaller. When authenticated, body's callerServiceKey / callerTenantId / callerAppId are ignored and claims are used (ServiceKey set to empty string until a verifiable caller-service contract exists). Unauthenticated path still falls back to body.
B. Bound-service identity mismatch — FIXED (loud, not silent)
src/platform/Aevatar.GAgentService.Governance.Hosting/Endpoints/ServiceBindingEndpoints.cs:193-221 — new TryValidateBoundServiceIdentity. With an authenticated context, request.Service.{TenantId,AppId,Namespace} mismatching claims now returns 400 BOUND_SERVICE_IDENTITY_CONFLICT, not silent rewrite.
Two items from the original Acceptance Criteria are not yet addressed:
Binding owner identity is still claim-silent-overrides-body, asymmetric with bound-service identity which is now loud. ServiceBindingEndpoints.cs resolves owner via ServiceIdentityEndpointAccess.TryResolveContext → claims win silently when present.
DTO field exposure: InvokeServiceHttpRequest, BoundServiceHttpRequest, and friends still expose TenantId / AppId / Namespace as caller-controlled fields. The C# layer no longer trusts them, but the contract advertises them — half-contract per the original acceptance criterion "DTOs should stop advertising cross-tenant identity fields".
Recommendation
Close this issue and (if pursued) split the residue into two narrower issues:
"Make binding owner identity mismatch loud (parallel to bound-service)"
"Prune body-controlled identity fields from /api/services and governance DTOs"
Both are smaller-scope and easier to land than this combined issue.
Original issue below for history.
Background
PR #311 hardens the target service identity for authenticated /api/services/** and governance endpoints by making tenant_id / app_id / namespace claims authoritative.
That closes the target-side spoofing gap, but the latest review still shows two contract-level inconsistencies on the same surface:
HandleInvokeAsync still accepts caller identity from the request body (callerServiceKey / callerTenantId / callerAppId) even when the request is authenticated.
Service binding endpoints silently rewrite request.Service.{TenantId,AppId,Namespace} to the authenticated caller identity while still keeping the body serviceId, which leaves a confusing half-contract.
Both issues point at the same structural problem:
authenticated service identity is not yet modeled as one explicit, body-independent contract across invoke + binding paths.
Problem
1. Caller identity on invoke is still body-controlled
When authenticated, the endpoint currently lets claims win over body TenantId/AppId/Namespace, but keeps the requested serviceId.
So a client can believe it is binding to another tenant's service while the server silently rewrites ownership context and creates an in-tenant reference to the same serviceId string.
That behavior is hard to reason about:
if cross-tenant binding is illegal, explicit mismatch should fail loudly
if those fields are not allowed from clients, they should not stay in the DTO as dead or misleading inputs
3. Silent override hides client bugs
Today several identity helpers let authenticated claims silently override conflicting body/query values.
That is secure enough in the narrow sense, but it creates a poor contract:
misconfigured clients still get 202 / success responses
writes land under the authenticated identity instead of the body identity the caller thought they used
debugging becomes harder because the API accepts requests whose identity payload is semantically wrong
Goal
Make authenticated service identity a single explicit contract across invoke and binding-related endpoints:
authenticated caller identity must come from authenticated context, not body fields
explicit body/query identity mismatches should be rejected instead of silently rewritten
DTOs should stop advertising cross-tenant identity fields if those fields are not actually caller-controlled
Proposed scope
A. Invoke caller identity
For authenticated invoke requests:
derive caller identity from authenticated service claims (or an equivalent authenticated caller-service contract)
stop trusting callerServiceKey / callerTenantId / callerAppId from the request body as authoritative inputs
add integration coverage proving authenticated caller spoofing is rejected or ignored safely
B. Bound service identity semantics
For authenticated service binding requests:
decide one contract and make it explicit:
either reject explicit request.Service.{TenantId,AppId,Namespace} mismatches with 400, or
remove those fields from the caller-facing DTO / request model if cross-tenant binding is not supported
do not keep the current silent rewrite behavior as the long-term API contract
C. Claim/body mismatch policy
Across the affected service/governance endpoints:
authenticated identity mismatches should be loud and diagnosable
avoid claim wins silently when the body/query explicitly says something else
return a clear client error for identity conflicts where appropriate
Acceptance criteria
authenticated invoke no longer authorizes from body-controlled caller identity
integration tests cover authenticated caller spoofing attempts on invoke
binding endpoints no longer silently rewrite explicit bound-service tenant/app/namespace mismatches as the steady-state contract
the API contract clearly states whether cross-tenant binding is unsupported or supported through an explicit mechanism
authenticated identity mismatch behavior is consistent and diagnosable across the affected endpoints
Out of scope
redesigning the whole service governance surface
changing unrelated unauthenticated or internal-only paths
After this issue, authenticated service identity should be one explicit, body-independent contract across invoke and binding flows, instead of a mix of claim precedence plus body fallback/silent rewrite.
Background
PR #311 hardens the target service identity for authenticated
/api/services/**and governance endpoints by makingtenant_id / app_id / namespaceclaims authoritative.That closes the target-side spoofing gap, but the latest review still shows two contract-level inconsistencies on the same surface:
HandleInvokeAsyncstill accepts caller identity from the request body (callerServiceKey / callerTenantId / callerAppId) even when the request is authenticated.request.Service.{TenantId,AppId,Namespace}to the authenticated caller identity while still keeping the bodyserviceId, which leaves a confusing half-contract.Both issues point at the same structural problem:
authenticated service identity is not yet modeled as one explicit, body-independent contract across invoke + binding paths.
Problem
1. Caller identity on invoke is still body-controlled
src/platform/Aevatar.GAgentService.Hosting/Endpoints/ServiceEndpoints.csThe invoke path still builds
ServiceInvocationCallerfrom body fields:CallerServiceKeyCallerTenantIdCallerAppIdThat value flows into
DefaultInvokeAdmissionEvaluator, so an authenticated caller can still spoof an allowed caller key through the body.This is the same spoofing class PR #311 is trying to eliminate on the target side.
2. Bound-service identity mismatch is silently rewritten
src/platform/Aevatar.GAgentService.Governance.Hosting/Endpoints/ServiceBindingEndpoints.csWhen authenticated, the endpoint currently lets claims win over body
TenantId/AppId/Namespace, but keeps the requestedserviceId.So a client can believe it is binding to another tenant's service while the server silently rewrites ownership context and creates an in-tenant reference to the same
serviceIdstring.That behavior is hard to reason about:
3. Silent override hides client bugs
Today several identity helpers let authenticated claims silently override conflicting body/query values.
That is secure enough in the narrow sense, but it creates a poor contract:
202/ success responsesGoal
Make authenticated service identity a single explicit contract across invoke and binding-related endpoints:
Proposed scope
A. Invoke caller identity
For authenticated invoke requests:
callerServiceKey / callerTenantId / callerAppIdfrom the request body as authoritative inputsB. Bound service identity semantics
For authenticated service binding requests:
request.Service.{TenantId,AppId,Namespace}mismatches with400, orC. Claim/body mismatch policy
Across the affected service/governance endpoints:
claim wins silentlywhen the body/query explicitly says something elseAcceptance criteria
Out of scope
Related
src/platform/Aevatar.GAgentService.Hosting/Endpoints/ServiceEndpoints.cssrc/platform/Aevatar.GAgentService.Governance.Hosting/Endpoints/ServiceBindingEndpoints.cssrc/platform/Aevatar.GAgentService.Governance.Infrastructure/Admission/DefaultInvokeAdmissionEvaluator.csOne-line outcome
After this issue, authenticated service identity should be one explicit, body-independent contract across invoke and binding flows, instead of a mix of claim precedence plus body fallback/silent rewrite.