# Use JWT claims to control access to a surface

> Add Policy elements to a surface to control which inbound requests reach your managed agent, based on JWT claims, request attributes, or caller context.
This guide uses Policy elements to filter which requests are forwarded to your managed agent, based on JWT claims or other available context.

The Caller Context element establishes who the caller is. A Policy element decides what they are allowed to do, based on JWT claims, request attributes, or other available context. Without one, every authenticated caller has the same level of access to your managed agent.

Use this guide when your surface already uses authentication and you need to restrict specific callers or request types. You create a policy definition in Policies, drop the Policy element onto a canvas arrow, and confirm with three targeted requests.

## Prerequisites

- A surface already created in the dashboard with at least an Access Point and a Managed Agent configured.

- The surface must have a Caller Context element using JWT bearer auth. The sample policy checks JWT claims. If your surface uses API key auth instead, adapt the policy to check input.source_auth.subject only.

- Administrator or Power User role in the dashboard.

- Basic familiarity with Rego. See [OPA policies](/products/affinidi-trust-fabric/agent-gateway/concepts/opa-policies.md) for the input schema and examples.

## Where a policy can be attached

The surface canvas connects nodes with directional arrows. Each arrow represents a step in the request or response pipeline. A Policy element dropped on an arrow evaluates your Rego rule at that exact step, before the pipeline continues in that direction.

| Drop target | What it gates | Config field |
| Arrow from Access Point outward | All inbound requests before any target processing begins. | access_point.inbound_policy |
| Arrow into Managed Agent | Inbound requests just before forwarding to the managed agent. | target.policy |
| Response arrow from Managed Agent back to caller | Responses from the managed agent before they reach the caller. | target.response_policy |
| Arrow into a Transit Point | Outbound calls from the managed agent before they leave the gateway. | transit.points[n].policy |
| Response arrow from a Transit Point | Responses from the external Transit Point destination before they return to the managed agent. | transit.points[n].response_policy |

You can attach policies to multiple positions on the same surface. Each runs independently: a deny at any position blocks that request or response at that step.

## Steps

Create a policy definition

This guide uses the following sample policy throughout. It allows requests only when the caller presents a valid JWT that contains role = "admin" in its claims. Requests without a token, or with a token that carries a different role, are denied.

```rego
package surface.policy

default allow = false

# Allow callers whose JWT contains role = "admin".
allow if {
    input.source_auth.subject
    input.source_auth.claims.role == "admin"
}
```

Create this policy definition before attaching it to the surface:

- In the sidebar, select Policies.

- Select the Agent Surfaces tab.

- Select Define Agent Surface Policy.

- Fill in the form:

- Name: require-admin-role.

- Type: pre-set to Agent Surfaces.

- Enabled: on.

- Policy Content (Rego): paste the Rego above.

- Select Create.

The policy appears in the list with Enabled status.

Open the surface canvas

- Open Surfaces in the dashboard.

- Select the surface to configure.

- The canvas opens showing the current element layout.

Drag a Policy element onto an arrow

- In the left palette, find Policy under the Security & Policy category.

- Drag Policy onto the arrow where you want to evaluate it.

- Drop it on the arrow leaving the Access Point to gate all inbound requests early.

- Drop it on the arrow entering the Managed Agent to gate requests just before forwarding.

- Drop it on the response arrow (pointing back toward the caller) to gate what the caller receives.

- Drop it on a Transit Point or its outbound arrow to gate outbound calls from the managed agent.

- Drop it on a Transit Point response arrow to gate what returns from the external destination.

The element appears on the arrow. A second Policy element can be dropped on the same arrow if two independent rules must both pass before the pipeline continues.

Configure the Policy element

- Select the Policy element on the canvas to open its config panel.

- In the Policy Definition dropdown, select require-admin-role (the definition you created in Step 1).

- Leave Enrich policy input with agent context disabled. The sample policy does not reference input.agent.* fields.

Caution

Enabling Enrich policy input with agent context adds a trust registry query to every request that hits this policy position. Only enable it when the policy rule explicitly references input.agent.* fields.

Save the surface

Select the Save changes button (the blue disk icon in the toolbar) or press Cmd+S (macOS) / Ctrl+S (Windows/Linux).

The gateway applies the new policy attachment immediately through hot reload.

## Confirm

Replace <YOUR_GATEWAY_HOST> and <CHANNEL_ROUTE> with values from the Access Point panel.

The three tests below map directly to the three outcomes the sample policy produces. Each test shows the request condition, the policy condition it triggers, and the expected response.

| Test | What the request sends | Policy condition | Expected response |
| 1 | No Authorization header | input.source_auth.subject is absent. Caller Context rejects before the policy runs. | 401 Unauthorized |
| 2 | JWT with "role": "developer" | input.source_auth.subject is present, but claims.role != "admin". Policy denies. | 403 Forbidden |
| 3 | JWT with "role": "admin" | Both conditions met. Policy allows. | 200 OK |

### Test 1: no token (Caller Context rejects, 401)

The Caller Context element rejects unauthenticated requests before they reach the policy. This is a 401, not a 403. The policy never runs.

```bash
curl -k -X POST "https://" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"message/send","id":"test-1","params":{"message":{"role":"user","messageId":"msg-001","parts":[{"kind":"text","text":"Hello"}]}}}'
```

The -k flag disables TLS certificate verification. Use this for local testing only. Remove it in production.

Expected response: 401 Unauthorized.

### Test 2: wrong role token (policy denies, 403)

The Caller Context accepts the request because the token is valid. The policy then evaluates and finds claims.role == "developer", which does not satisfy claims.role == "admin". The policy returns allow = false.

Generate a JWT whose payload contains "role": "developer" using your identity provider or [jwt.io](https://jwt.io). The token must be accepted by the JWT bearer strategy configured on your surface.

```json
// JWT payload required for this test
{
  "sub": "user@example.com",
  "role": "developer"
}
```

```bash
curl -k -X POST "https://" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer " \
  -d '{"jsonrpc":"2.0","method":"message/send","id":"test-2","params":{"message":{"role":"user","messageId":"msg-002","parts":[{"kind":"text","text":"Hello"}]}}}'
```

The -k flag disables TLS certificate verification. Use this for local testing only. Remove it in production.

Expected response: 403 Forbidden. The Caller Context passed. The policy denied because input.source_auth.claims.role was "developer", not "admin".

### Test 3: correct role token (policy allows, 200)

The Caller Context accepts the request. The policy evaluates input.source_auth.subject (present) and input.source_auth.claims.role == "admin" (true). Both conditions are met, so allow = true.

Generate a JWT whose payload contains "role": "admin":

```json
// JWT payload required for this test
{
  "sub": "admin@example.com",
  "role": "admin"
}
```

```bash
curl -k -X POST "https://" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer " \
  -d '{"jsonrpc":"2.0","method":"message/send","id":"test-3","params":{"message":{"role":"user","messageId":"msg-003","parts":[{"kind":"text","text":"Hello"}]}}}'
```

The -k flag disables TLS certificate verification. Use this for local testing only. Remove it in production.

Expected response: 200 OK. The gateway forwarded the request to the managed agent.

Open the surface Monitoring tab to see all three requests. Test 1 appears as a 401, test 2 as a 403 (policy denial), and test 3 as a 200.

## Policy input reference

All Policy element positions use package surface.policy and receive the same PolicyInput structure. The fields available depend on the pipeline position and request direction.

| Field | Inbound positions | Response positions |
| input.source_auth.* | Present if authenticated | Present |
| input.http.method / input.http.path | Present | Present |
| input.gateway.direction | "inbound" | "outbound" |
| input.mcp.* | Present for MCP protocol | Not present |
| input.agent.* | Present when Enrich policy input with agent context is enabled | Present when enabled |
| input.extension_identity.* | Present if VP provided | Not present |
| input.payment.* | Present if x402 verified | Not present |

For the full input schema and example policies, see [OPA policies](/products/affinidi-trust-fabric/agent-gateway/concepts/opa-policies.md).

## Troubleshooting

| Symptom | Likely cause | Fix |
| 403 Forbidden on every request, including expected-allow cases. | The policy evaluates allow = false for all input. | Open the policy in Policies and test it in the [OPA Playground](https://play.openpolicyagent.org/) with a representative input document. |
| Policy element is visible on the canvas but has no effect. | The surface was not saved after attaching the element. | Select Save and retry. |
| Policy definition not visible in the element dropdown. | The definition was created with type Gateway instead of Agent Surfaces. | Create a new definition with type Agent Surfaces and select it. |
| High latency on every request after attaching the policy. | Enrich policy input with agent context is enabled but the policy does not use input.agent. | Open the Policy element config and disable Enrich policy input with agent context. |
| 403 only on some requests for the same caller. | The policy references a field that is present on some requests but absent on others (for example, input.mcp.* on non-MCP methods). | Add a guard in the policy to check the field exists before testing its value, or split the rule by method. |
| 403 on responses, not requests. | A response-position policy is attached and is evaluating allow = false on the response. | Check whether a Policy element is attached to a response arrow. Response policies gate what returns to the caller, not what enters the surface. |

## Next steps

- [OPA policies](/products/affinidi-trust-fabric/agent-gateway/concepts/opa-policies.md): the full input schema for each policy scope and example Rego rules.

- [Control MCP tool access with per-tool policies](/products/affinidi-trust-fabric/agent-gateway/how-to-guides/policies/control-mcp-tool-access-with-per-tool-policies.md): gate individual MCP tools behind separate Rego policies.

- [Apply OPA policies to your gateway and surfaces](/products/affinidi-trust-fabric/agent-gateway/how-to-guides/policies/apply-opa-policies.md): create and manage policy definitions, and apply gateway-wide policies.
