Summary
While deploying Agent Auth with an external resource server, we hit a verifier gap that seems worth discussing before proposing an implementation.
verifyAgentRequest() resolves the agent session, but a resource server still needs to answer a more specific authorization question:
Does this agent JWT authorize this exact method/path/args under an active capability grant?
Today that logic has to live outside Agent Auth: route-to-capability mapping, active grant lookup, constraint matching, audit-only projection, replay handling, request binding, and unmatched-route behavior.
We would like maintainer guidance on whether this belongs in core @better-auth/agent-auth, a resource-server helper, an extension to verifyAgentRequest(), or documentation/examples.
Motivation
In our deployment, Agent Auth runs in a TypeScript sidecar while protected routes are owned by a separate backend service. The backend needs a typed verifier decision, not only an agent session:
{
decision: "allowed" | "denied" | "audit_only";
code: string;
capability: string | null;
agentId: string | null;
grantId: string | null;
constraints: Record<string, unknown>;
}
Example codes might include allowed, not_gated, missing_jwt, invalid_jwt, expired_jwt, jti_replay, request_binding_mismatch, no_grant, grant_expired, grant_revoked, and constraint_violation. The exact code set is a design question; the main need is a stable result that external services can enforce and audit.
This is not meant to replace /capability/execute. It is for services that already own their execution routes and want Agent Auth as the policy decision point. Ideally, it would reuse the same grant lookup and constraint semantics used by capability execution, so resource-server verification does not drift from core behavior.
Candidate API Shape
export async function verifyAgentRequestWithRoute(
req: Request,
auth: Auth,
options: {
method: string;
path: string;
args?: Record<string, unknown>;
routes: Array<{
method: string;
pathTemplate: string;
capability: string;
}>;
auditOnly?: boolean;
unmatchedRoute?: "allowed" | "denied";
}
): Promise<{
decision: "allowed" | "denied" | "audit_only";
code: string;
capability: string | null;
agentId: string | null;
grantId: string | null;
constraints: Record<string, unknown>;
}>;
Route templates would match path segments, for example:
[
{ method: "POST", pathTemplate: "/repos/{repoId}/deploy", capability: "repo.deploy" },
{ method: "POST", pathTemplate: "/documents/{documentId}/publish", capability: "document.publish" },
]
A concrete request could then be verified with:
await verifyAgentRequestWithRoute(req, auth, {
method: "POST",
path: "/repos/abc123/deploy",
args: { repoId: "abc123", environment: "production" },
routes,
auditOnly: false,
unmatchedRoute: "denied",
});
If the active grant for repo.deploy is constrained to repoId: "abc123" and environment: { in: ["staging", "production"] }, the request should pass. If the route, grant, audience, or constraints do not match, the helper should return a typed denial.
Design Questions
The main design questions we hit:
-
Where should this live, and what should it return?
Should this be a core helper, a resource-server helper package, an extension to verifyAgentRequest(), or a documented pattern? For resource-server use, should it return typed decisions like allowed | denied | audit_only, or follow existing throw/error conventions?
-
How should routes map to capabilities?
Should verification use an explicit route map, capability location, or both? If route templates are supported, should matching be method-aware and segment-based, e.g. POST /repos/{repoId}/deploy -> repo.deploy?
-
How should audience and request binding work?
A route-aware verifier may need to validate aud, htm, htu, and body hash against the original protected resource request, not an internal /agent/session request. Should it support provider/base audiences, configured capability locations, route-template audiences, concrete resource URLs, or some combination?
-
How should replay protection and audit-only mode interact?
If verification consumes jti, middleware retries or repeated policy checks can fail as replay. If it does not, replay protection is weakened or pushed to the resource server. In auditOnly mode, should failed checks project to audit_only while still consuming jti and enforcing request binding?
-
Can this reuse existing grant and constraint semantics?
Ideally this should reuse the same active/revoked/expired grant handling and constraint matching used by capability execution. How should unmatched routes behave: allowed/not_gated, denied/not_gated, throw, or configurable?
Question
Would maintainers prefer this as:
- a core
verifyAgentRequestWithRoute() helper,
- a lower-level resource-server verification helper,
- an extension to
verifyAgentRequest(),
- a documented pattern/example,
- or something else?
If there is interest, we can prepare a small PR with tests around route matching, audience handling, active grant checks, constraint mismatch, replay semantics, request binding, audit-only projection, and unmatched-route behavior.
Summary
While deploying Agent Auth with an external resource server, we hit a verifier gap that seems worth discussing before proposing an implementation.
verifyAgentRequest()resolves the agent session, but a resource server still needs to answer a more specific authorization question:Today that logic has to live outside Agent Auth: route-to-capability mapping, active grant lookup, constraint matching, audit-only projection, replay handling, request binding, and unmatched-route behavior.
We would like maintainer guidance on whether this belongs in core
@better-auth/agent-auth, a resource-server helper, an extension toverifyAgentRequest(), or documentation/examples.Motivation
In our deployment, Agent Auth runs in a TypeScript sidecar while protected routes are owned by a separate backend service. The backend needs a typed verifier decision, not only an agent session:
Example codes might include
allowed,not_gated,missing_jwt,invalid_jwt,expired_jwt,jti_replay,request_binding_mismatch,no_grant,grant_expired,grant_revoked, andconstraint_violation. The exact code set is a design question; the main need is a stable result that external services can enforce and audit.This is not meant to replace
/capability/execute. It is for services that already own their execution routes and want Agent Auth as the policy decision point. Ideally, it would reuse the same grant lookup and constraint semantics used by capability execution, so resource-server verification does not drift from core behavior.Candidate API Shape
Route templates would match path segments, for example:
A concrete request could then be verified with:
If the active grant for
repo.deployis constrained torepoId: "abc123"andenvironment: { in: ["staging", "production"] }, the request should pass. If the route, grant, audience, or constraints do not match, the helper should return a typed denial.Design Questions
The main design questions we hit:
Where should this live, and what should it return?
Should this be a core helper, a resource-server helper package, an extension to
verifyAgentRequest(), or a documented pattern? For resource-server use, should it return typed decisions likeallowed | denied | audit_only, or follow existing throw/error conventions?How should routes map to capabilities?
Should verification use an explicit route map, capability
location, or both? If route templates are supported, should matching be method-aware and segment-based, e.g.POST /repos/{repoId}/deploy -> repo.deploy?How should audience and request binding work?
A route-aware verifier may need to validate
aud,htm,htu, and body hash against the original protected resource request, not an internal/agent/sessionrequest. Should it support provider/base audiences, configured capability locations, route-template audiences, concrete resource URLs, or some combination?How should replay protection and audit-only mode interact?
If verification consumes
jti, middleware retries or repeated policy checks can fail as replay. If it does not, replay protection is weakened or pushed to the resource server. InauditOnlymode, should failed checks project toaudit_onlywhile still consumingjtiand enforcing request binding?Can this reuse existing grant and constraint semantics?
Ideally this should reuse the same active/revoked/expired grant handling and constraint matching used by capability execution. How should unmatched routes behave:
allowed/not_gated,denied/not_gated, throw, or configurable?Question
Would maintainers prefer this as:
verifyAgentRequestWithRoute()helper,verifyAgentRequest(),If there is interest, we can prepare a small PR with tests around route matching, audience handling, active grant checks, constraint mismatch, replay semantics, request binding, audit-only projection, and unmatched-route behavior.