Skip to content

🚧 Roadmap — The capability described here is on the roadmap. The concepts and design are settled; interfaces and steps are subject to the final release.

Protect your APIs: resource-side integration

Everything up to here has been about handing authority to an agent safely. This page is about the last hop, and the one most often overlooked: the token arrives at your API — now what do you check?

Get this hop wrong and every bit of attenuation, approval and audit upstream stops meaning anything, because the party that ultimately decides "allow or deny" is your resource service, not the issuer.

By the end of this guide you will know:

  • The five-step checklist, and what slips through if you skip a step
  • Why you must check act.type explicitly instead of parsing the RFC 8693 standard shape
  • How to make your business logs the final link of the audit chain

Prerequisites

  • You understand the access token's field structure (see Token and claim reference)
  • Your API already validates JWTs (signature, expiry)

The five-step checklist

StepWhat to checkWhat slips through if you skip it
1Signature and expiryForged or expired tokens are accepted
2aud is your resourceA token minted for another resource works against your API (audience confusion)
3Read sub for business authorizationThe agent becomes a superuser, bypassing your existing data permissions
4Read act to identify the actor, and log itThe audit trail cannot separate "the user in person" from "an agent acting for them" — no accountability
5Verify the scope covers this operationA read-only grant is used to write — attenuation is dead

Step 3 is the mistake teams make most

Many integrations treat "the token is valid" as "this operation is allowed", so an agent holding a read-only token sails straight through a write endpoint — because the business authorization layer got skipped.

Do it this way: feed sub into your existing authorization logic as the acting principal. The upper bound on an agent's authority is the delegator's own authority; not one of your existing data permission rules can be dropped. Scope is an additional narrowing, not a replacement.

Pseudocode

typescript
async function authorizeAgentRequest(req: Request, requiredScope: string) {
  const token = readBearer(req);

  // (1) Signature and expiry (use your existing JWT library)
  const claims = await verifyJwt(token);

  // (2) The audience must be your resource
  if (!audienceMatches(claims.aud, MY_RESOURCE_ID)) {
    throw forbidden("audience_mismatch");
  }

  // (3) sub is the person being represented — feed it into your existing business authorization
  const actingFor = claims.sub;
  await assertBusinessPermission(actingFor, req.action, req.resource);

  // (4) act identifies the actual actor
  //     Note: GenAuth's act is an extension object with a type discriminator,
  //     not the RFC 8693 nested-sub shape — you must check type explicitly
  let agentId: string | null = null;
  if (claims.act && claims.act.type === "eak_delegation") {
    agentId = claims.act.agent_id;
  }
  // When act is missing or type does not match: decide by policy whether to reject
  // or treat it as a non-agent call. For agent-only endpoints, fail closed (reject)
  // so a structural change cannot silently skip this check.

  // (5) The scope must cover this operation
  const scopes = normalizeScopes(claims.scope);
  if (!scopes.includes(requiredScope)) {
    throw forbidden("insufficient_scope");
  }

  // The audit seam: these three fields extend the chain from GenAuth into your business logs
  logger.info("agent_request", {
    sub: actingFor,
    agent_id: agentId,
    audit_id: claims.audit_id,
    grant_id: claims.grant_id,
    action: req.action,
  });

  return { actingFor, agentId };
}

Why act needs special handling

RFC 8693 defines act as a nested sub ({"act": {"sub": "..."}}); GenAuth's act is {"type": "eak_delegation", "agent_id", "grant_id", "access_key_id"}. If you point an off-the-shelf RFC 8693 parser at it, you get nothing back and no error — which is the most dangerous outcome: validation appears to pass while checking nothing. Explicitly checking type is the only safe approach.

Keep an emergency cut-off

A delegate token already issued cannot be invalidated individually within its lifetime (see Revocation and incident response). So build a deny list capability on the resource side:

  • Reject by act.agent_id: when one agent misbehaves, cut all of its access to your API in one move
  • Reject by grant_id: when one grant is judged mistaken, cut exactly that one

The implementation can be trivial — a hot-reloadable config or a Redis set, checked right after step (4). You will not use it most days; during an incident it is your only way to cut off a token inside its lifetime.

The gateway option

If your architecture has a unified API gateway, putting these five checks at the gateway beats putting them in every service:

  • Validation logic maintained in one place; new services onboard for free
  • The deny list takes effect at the gateway, so containment is faster
  • The gateway passes sub / agent_id / audit_id down to services (for example as internal headers) and services stay focused on business logic

A gateway plugin is on the roadmap; until then, your gateway's generic JWT or scripting capability implements the logic above.

FAQ

My API already supports OAuth — do I need to change anything? What changes is the meaning of steps (3), (4) and (5): sub used to be the caller; now sub is the person being represented and act is the caller. Authorize on sub, log on act.

Do scope names have to map one-to-one onto my endpoints? Not one-to-one, but they must map. Design them as "resource + action" (for example orders:read) and declare the required scope at the endpoint layer instead of scattering checks through your code.

Can I trust GenAuth's validation and skip mine? No. The issuer can only guarantee "I minted this token and it has not been tampered with". Only you know which record this request touches. Resource-side validation cannot be outsourced.

Next steps