Dev.to Security πŸ” Cybersecurity πŸ‘ 0 πŸ“– 5 min read

Test OAuth agent delegation without trusting the wrong actor

A token can be valid while the policy consuming it is wrong. One particularly useful regression test for agent delegation is an unapproved current actor with an approved actor buried in its history. If the API walks the

Test OAuth agent delegation without trusting the wrong actor

A token can be valid while the policy consuming it is wrong. One particularly useful regression test for agent delegation is an unapproved current actor with an approved actor buried in its history.

If the API walks the chain until it finds an allowed name, the test should expose that mistake.

This walkthrough uses a fictional invoice-fetching workflow. It includes a small tested policy fixture, not an OAuth server, JWT validator or production authorisation library. The separation is deliberate: parsing a token and deciding whether a business operation is allowed are different jobs.

Specify the exchange before writing the policy

Nadia at Cedar asks a procurement-review agent to explain invoice INV-204. The agent calls invoice-fetcher, which needs a token for the invoice API with invoice-reading access.

In RFC 8693, an exchange request includes subject_token and subject_token_type. actor_token is optional in the general protocol; when supplied, its corresponding type is required. Client authentication is a separate part of the deployment's contract.

For this proposed delegation profile, the server validates the subject and actor credentials and checks the permitted relationship before issuing a downstream token. Valid input tokens do not automatically establish that relationship. The server must also determine which destinations and privileges it will issue.

Sequence shows input validation and delegation policy before token issuance, followed by invoice API validation and authorisation.

The exchange is evaluated under a configured delegation policy. It is not a universal conversion from any valid credential into any requested credential.

The API should consume the profile the issuer actually supports. Do not assume every provider emits an act claim simply because it has a token exchange endpoint, or that an access token must be a JWT.

Read the actor structure correctly

In a JWT representation, Nadia is the top-level subject. The outer act identifies invoice-fetcher as the current actor. A nested act can identify procurement-reviewer as a prior actor.

RFC 8693 section 4.1 explicitly makes prior nested actors informational for access control. Consumers use the current actor and top-level claims. They must not grant access because a permitted actor appears somewhere in the historical chain.

Annotated token excerpt distinguishes Nadia, current actor invoice-fetcher and informational prior actor procurement-reviewer.

The diagram is a claims excerpt. It is not a complete signed token, and its readable contents do not demonstrate successful validation.

Actor identity may require both issuer and subject. The fixture below uses issuer-qualified application identifiers after trusted mapping. The client_id identifying the OAuth client requesting a token should not silently replace the current actor.

Establish the validation boundary first

For JWT access tokens following RFC 9068, use an appropriate maintained implementation and configured issuer trust. Validate the required type, signature, issuer, audience and expiry. Honour the additional requirements of the chosen profile and deployment.

Only then build the policy context. For an opaque token, use the supported server-side validation mechanism instead. The agent client should not depend on decoding access-token internals.

The fixture assumes these checks already succeeded. It also assumes a trusted grant lookup and tenant resolution. A caller constructing the Python objects directly has bypassed those assumptions; the function cannot authenticate them afterwards.

Test a narrow read policy

The relevant context contains a subject, current actor, informational prior actors, accepted audiences, scopes and tenant. The grant contains the allowed subject, current actors, tenant, resources, scopes, expiry and active state. The request contains the target tenant and invoice.

Here is the complete decision function from the local fixture:

def decide(ctx: Context, grant: Grant, request: Read, now: int) -> str:
    if grant.active is not True or now >= grant.expires_at:
        return "deny: inactive grant"
    if ctx.subject != grant.subject:
        return "deny: subject"
    if ctx.current_actor not in grant.actors:
        return "deny: current actor"
    if "https://invoices.example" not in ctx.audiences:
        return "deny: audience"
    if "invoices.read" not in ctx.scopes & grant.scopes:
        return "deny: scope"
    if not (ctx.tenant == grant.tenant == request.tenant):
        return "deny: tenant"
    if request.invoice not in grant.resources:
        return "deny: invoice"
    return "allow: illustrative read policy"

The types are immutable fixture records. audiences, scopes, actors and resources are sets of exact identifiers. The now argument is supplied explicitly so expiry tests are deterministic. A production service needs a trustworthy time source and an appropriate current-grant mechanism.

The function intentionally never reads prior_actors. Changing a prior actor therefore cannot rescue an unapproved current actor. This is not an assertion that the whole workflow is trustworthy; it tests this resource-side authorisation rule.

Keep scope and resource constraints separate

The audience is the invoice API. invoices.read is the required scope in both the token context and the grant. Cedar and INV-204 provide the narrower business boundary in this fictional application.

Resource indicators identify intended protected resources for token issuance. They do not automatically implement the application's invoice-level policy. Custom claims or grant references need a documented consumer and enforcement path.

Matrix shows a permitted Cedar invoice read and refusals for a wrong service, write operation or different tenant invoice.

Passing the audience check does not pass the scope check. Passing both does not authorise another tenant's invoice.

OAuth security best practice recommends restricting privileges and audience. This fixture accepts the expected invoice audience if present; it does not claim that all conforming deployments must reject every multi-audience token. A deployment can impose a stricter issuance policy.

The regression cases that matter

The local suite passes eleven tests. It covers a permitted read, the approved-prior-actor trap, an irrelevant history change, a wrong audience, scope missing from either token or grant, a subject mismatch, another tenant, another invoice, expiry at the boundary, an inactive grant and a string pretending to be the active boolean.

Those results establish behaviour of this small fixture only. They do not validate token issuance, signature verification, tenant extraction, cache freshness, runtime isolation or the live API. The happy-path context and grant are invented test data, not captured production evidence.

An integration test should obtain real test credentials through the intended exchange path, then vary the target and actor under controlled conditions. It should establish which component refused the request and whether any protected data was returned. β€œHTTP error somewhere” leaves too much unexplained.

Finally, token exchange does not generally link output revocation to its input tokens. Decide and test your deployment's freshness behaviour. For work that resumes later, the checkpoint and permission distinction still applies.

A token containing an impressive history is useful evidence. The current actor still has to earn the current permission.

πŸ“° Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.