Give an agent single-use tokens
A single-use token is good for one check. The first time a resource server sends it to /introspect, the answer is "active": true and the server revokes the token. Every later check answers "active": false. A token that leaks after its first use is worth nothing.
The jit_single_use control decides which tokens are single-use. Its own row applies only to SPIFFE workloads. To use it for anything else, bind it to a segment. This page binds it to every token for the resource type urn:example:files.
We ran every block on this page, in order, against a new 0.65.0 environment.
Before you start
You need curl and jq, POLICYARC_URL and POLICYARC_ADMIN_KEY, as on Register a client.
Set up an agent and a resource server
The agent gets tokens for urn:example:files. The resource server is the client that checks them, so it is entitled to introspection.
curl -fsS -o /dev/null -X PATCH "$POLICYARC_URL/admin/api/policy-data/by-key?dataKey=pbac.operator.controls.resource_registry&path=resource.types.urn:example:files" \
-H "X-Admin-API-Key: $POLICYARC_ADMIN_KEY" -H 'Content-Type: application/json' -d '{"scopes": ["read"]}'
client() {
local resp; resp=$(curl -fsS -X POST "$POLICYARC_URL/register" -H 'Content-Type: application/json' \
-d "{\"client_name\": \"$1\", \"redirect_uris\": [\"https://$1.example/cb\"]}")
curl -fsS -o /dev/null -X PATCH "$POLICYARC_URL/admin/api/policy-data/by-key?dataKey=pbac.operator.controls.client_entitlements&path=client_id.$(jq -r .client_id <<<"$resp")" \
-H "X-Admin-API-Key: $POLICYARC_ADMIN_KEY" -H 'Content-Type: application/json' \
-d "{\"grant_types\": [\"client_credentials\"], \"granted_resources\": [{\"type\": \"$2\", \"scopes\": [\"$3\"]}]}"
jq -r '"\(.client_id):\(.client_secret)"' <<<"$resp"
}
export AGENT=$(client files-agent urn:example:files read)
export FILES_API=$(client files-api urn:as:introspect introspection)
Bind the control to the resource type
Segments and bindings live in one row, pbac.operator.segments. A new environment already has two bindings in it, so read the row, add yours and write it back. The segment matches any request for urn:example:files. The binding turns jit_single_use on where the segment holds.
curl -fsS "$POLICYARC_URL/admin/api/policy-data" -H "X-Admin-API-Key: $POLICYARC_ADMIN_KEY" |
jq -e '.[] | select(.dataKey == "pbac.operator.segments") | .payload | fromjson' |
jq '.segments.files_tokens = {match: {all: [{fact: "resource.resource_type", op: "eq", value: "urn:example:files"}]}}
| .bindings = [(.bindings[] | select(.segments != ["files_tokens"])), {control: "jit_single_use", segments: ["files_tokens"], settings: {jit_single_use: true}, source: "operator"}]
| {dataKey: "pbac.operator.segments", payload: .}' |
curl -fsS -o /dev/null -X PUT "$POLICYARC_URL/admin/api/policy-data/by-key?dataKey=pbac.operator.segments" \
-H "X-Admin-API-Key: $POLICYARC_ADMIN_KEY" -H 'Content-Type: application/json' -d @-
sleep 5
Running the block again replaces the binding rather than adding a second one. The policy engine loads the row a few seconds after the write, so the block waits.
Use a token twice
The agent gets a token, and the resource server checks it twice.
token=$(curl -fsS -u "$AGENT" "$POLICYARC_URL/token" \
-d grant_type=client_credentials -d scope=read -d resource_types=urn:example:files | jq -er .access_token)
first=$(curl -fsS -u "$FILES_API" "$POLICYARC_URL/introspect" -d token="$token")
second=$(curl -fsS -u "$FILES_API" "$POLICYARC_URL/introspect" -d token="$token")
echo "$first"; echo "$second"
jq -e '.active == true and .scope == "read"' <<<"$first" >/dev/null &&
jq -e '.active == false' <<<"$second" >/dev/null ||
{ echo 'Expected active on the first check and inactive on the second' >&2; false; }
What we saw.
{"active": true, "scope": "read", "client_id": "8ee75175-189b-4d43-acbb-c451d92d9fae", "resource_types": ["urn:example:files"], "...": "..."}
{"active": false, "inactive_reason": "invalid_token"}
The token response looks the same as any other. Nothing in it tells the agent the token is single-use. A token for any other resource type stays reusable. The resource server's own token, for urn:as:introspect, answered "active": true on both checks in our run.
The second check writes an audit row for /introspect with outcome deny.
Who checks the token
A token is used up when something sends it to /introspect. A resource server that checks each request at /introspect therefore accepts the token for one request. A resource server that does not introspect never uses it up. Token introspection covers the call.
The managed gateway introspects every call it forwards, so a single-use token is good for one call through it. We registered a REST resource behind the gateway, bound the control to its resource type, and sent one token through /gateway/<name>/... twice. The first call reached the upstream with HTTP 200. The second got HTTP 401 {"error":"Invalid or expired token"} and never reached the upstream. An agent that makes several calls through the gateway needs a new token for each one. We did not run an MCP tool call this way.
Turn it off
Remove the binding and the segment.
curl -fsS "$POLICYARC_URL/admin/api/policy-data" -H "X-Admin-API-Key: $POLICYARC_ADMIN_KEY" |
jq -e '.[] | select(.dataKey == "pbac.operator.segments") | .payload | fromjson' |
jq 'del(.segments.files_tokens) | .bindings |= map(select(.segments != ["files_tokens"]))
| {dataKey: "pbac.operator.segments", payload: .}' |
curl -fsS -o /dev/null -X PUT "$POLICYARC_URL/admin/api/policy-data/by-key?dataKey=pbac.operator.segments" \
-H "X-Admin-API-Key: $POLICYARC_ADMIN_KEY" -H 'Content-Type: application/json' -d @-
Shorter lifetimes
A single-use token still lives until it is used. To shorten how long any token lives, a control can set expires_in_override. Write a policy shows one.
Send feedback about this page