Agent authorization (with User Consent)
In this demo, we’ll explore how Agent Identity Authorization works when user consent is required. This builds on the autonomous authorization flow but adds a critical dimension: user delegation. When an agent needs to act on behalf of a user, the authorization server (Keycloak) ensures the user explicitly grants permission.
When user consent is needed, the auth server does not return an auth_token on the first signed POST to the token endpoint. Instead it responds with 202 Accepted, a Location header pointing at a pending URL, and an AAuth: require=interaction; code="..." header so the user can complete consent out-of-band. The agent (here, the backend) polls that pending URL with signed GET requests (often with Prefer: wait) until the server responds with 200 and an auth_token. There is no separate opaque “request token” the browser exchanges for the auth token, the pending URL and polling carry that role. The same pattern is walked through in Agent authorization with user consent.
Watch the demo
Set up Keycloak to Require Consent
First, we need to tell Keycloak which scopes require user consent. With Keycloak running, execute the consent configuration script:
./keycloak/set_aauth_consent_attributes.sh
==============================================
AAuth Consent Attributes Configuration
==============================================
Keycloak: http://localhost:8080
Realm: aauth-test
Connecting to Keycloak...
What would you like to do?
1) Scopes only - configure exact scope names (e.g. openid, profile, email)
2) Prefixes only - configure scope prefixes (e.g. user., profile., email.)
3) Both scopes and prefixes
4) Use defaults for both
5) View current scopes/prefixes
6) Clear scopes and prefixes (set to empty)
7) Quit (no changes)
Choice [1-7]:
Choose 1 for scopes only, then configure supply-chain:optimize as a scope requiring user consent:
Choice [1-7]: 1
--- Configure scopes ---
Examples: openid, profile, email
Default: ["openid","profile","email"]
Enter scopes (comma-separated, or press Enter for default): supply-chain:optimize
Summary:
Scopes: [ "supply-chain:optimize"]
Prefixes: ["user.","profile.","email."]
Apply these to Keycloak? [Y/n]:
Hit ENTER to continue and you should see keycloak updated:
Applying to Keycloak...
✅ Successfully set AAuth consent attributes for realm 'aauth-test'!
Configured values:
aauth.consent.required.scopes: ["supply-chain:optimize"]
aauth.consent.required.scope.prefixes: ["user.","profile.","email."]
Non-interactive usage:
export AAUTH_CONSENT_SCOPES='["openid","profile","email"]'
export AAUTH_CONSENT_PREFIXES='["user.","profile.","email."]'
./keycloak/set_aauth_consent_attributes.sh http://localhost:8080 aauth-test admin admin
Now restart the supply-chain-agent with user-delegated authorization:
From the supply-chain-agent directory:
> cd supply-chain-agent
> uv run . --signature-scheme jwks_uri --authorization-scheme user-delegated
From the market-analysis-agent directory:
> cd market-analysis-agent
> uv run . --signature-scheme jwks_uri --authorization-scheme autonomous
Navigate to the UI and click “Optimize Laptop Supply Chain”. The flow now includes the deferred consent path: the token endpoint may return 202 instead of issuing an auth_token immediately.
sequenceDiagram
participant UI as UI
participant BE as Backend
participant KC as Keycloak
participant SCA as "Supply-Chain Agent"
UI->>BE: 1. User clicks Optimize Laptop Supply Chain
BE->>SCA: 2. POST signed, no auth_token yet
SCA-->>BE: 3. 401 + AAuth require=auth-token + resource-token
BE->>KC: 4. POST token + resource_token, Prefer wait
KC-->>BE: 5. 202 + Location pending URL + interaction code
BE-->>UI: 6. interaction_endpoint, code, request_id, callback_url
Note over BE: Backend polls pending URL with signed GET
UI->>KC: 7. User opens interaction URL and completes consent
BE->>KC: 8. Poll returns 200 + auth_token after consent
BE->>SCA: 9. Retry with auth_token, scheme=jwt
SCA-->>BE: 10. Success
The UI response is a convenience for this demo so the browser can open Keycloak’s consent page. The authoritative protocol step is polling the pending URL from the 202 response; the backend starts that polling when it returns interaction_required to the frontend (and the optional callback URL is only a UX hint to return to the app).
Step By Step
1. Initial Request Fails (401)
When the backend calls the supply-chain-agent, it receives a 401 with a resource token (same as autonomous flow):
INFO:aauth.tokens:🔐 401 from supply-chain-agent (url=http://supply-chain-agent.localhost:3000): headers={'date': 'Sat, 07 Feb 2026 16:56:35 GMT', 'server': 'uvicorn', 'aauth': 'require=auth-token; resource-token="eyJhbGciOiJFZERTQSIsImtpZCI6InN1cHBseS1jaGFpbi1hZ2VudC1lcGhlbWVyYWwtMSIsInR5cCI6InJlc291cmNlK2p3dCJ9.eyJpc3MiOiJodHRwOi8vc3VwcGx5LWNoYWluLWFnZW50LmxvY2FsaG9zdDozMDAwIiwiYXVkIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL3JlYWxtcy9hYXV0aC10ZXN0IiwiYWdlbnQiOiJodHRwOi8vYmFja2VuZC5sb2NhbGhvc3Q6ODAwMCIsImFnZW50X2prdCI6IlVDaWE5dEpNV3lEMWZPMGlhV1YxV2NzQmRaQzIwb0E5MVZYLS1VY2NXM0UiLCJleHAiOjE3NzA0ODM2OTYsInNjb3BlIjoic3VwcGx5LWNoYWluOm9wdGltaXplIG1hcmtldC1hbmFseXNpczphbmFseXplIn0.jHesCOn3qIXke_aAe3VrIzS7RbLhW9_rMRfLNqVeMDC9YZl16a1RvOEELHiy0wXA-Cy7y3CUzW7t5N_FbxgiCA"; auth-server="http://localhost:8080/realms/aauth-test"', 'content-length': '22', 'content-type': 'text/plain; charset=utf-8'}
2. Keycloak returns 202 (interaction required)
The backend exchanges the resource token at Keycloak’s AAuth token endpoint (signed POST with resource_token, typically with Prefer: wait=…). When policy requires user consent, the auth server does not return auth_token in the first response. It returns a deferred response (see also flow-04-user):
202 AcceptedLocation: pending URL to poll withGETAAuth: require=interaction; code="…"(and a JSON body echoingstatus,location,require,code)
The backend maps that to an API payload for the UI (interaction_required with interaction_endpoint, interaction_code, request_id, callback_url) and begins polling the pending URL with signed GET requests until Keycloak responds with 200 and an auth_token.
Illustrative HTTP shape (same idea as the auth-server response in flow-04-user; the exact Location path depends on your auth server):
HTTP/1.1 202 Accepted
Location: http://localhost:8080/realms/aauth-test/pending/2e44214dc421
AAuth: require=interaction; code="7R6XKPRP"
Content-Type: application/json
{"status":"pending","location":"http://localhost:8080/realms/aauth-test/pending/2e44214dc421","require":"interaction","code":"7R6XKPRP"}
You should see aauth.tokens poll lines in the backend logs (exact wording varies by attempt), for example:
INFO:aauth.tokens:AAuth poll started: pending_url=http://localhost:8080/realms/aauth-test/... max_attempts=120 Prefer_wait=45 min_interval=3.0
INFO:aauth.tokens:AAuth poll GET attempt 1/120 url=http://localhost:8080/realms/aauth-test/...
INFO:aauth.tokens:AAuth poll response: status=202 attempt=1/120
INFO:aauth.tokens:AAuth poll 202: body_status=pending require=interaction retry_after=0 clarification=False is_awaiting=False
After the user completes consent, a later poll returns 200 and AAuth poll complete: auth_token received.
3. User Consent Screen
The UI presents the consent screen to the user:

The user sees:
- Which agent is requesting access (backend)
- What scopes are being requested (supply-chain:optimize)
- The ability to approve or deny
The browser reaches this screen via the interaction endpoint and interaction code returned to the UI (step 2 above). AAuth expects the user to be sent to the auth server’s interaction URL with that code while the agent keeps polling the pending URL.
4. Authorization token with user identity
After the pending poll completes, the backend holds a valid auth_token and retries the call to the supply-chain-agent using scheme=jwt in the Signature-Key header. The supply-chain-agent receives:
INFO:agent_executor:✅ AAuth signature verification successful
INFO:aauth.tokens:🔐 Received auth_token in request (HTTPSig scheme=jwt): eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiYXV0aCtqd3QiLCJraWQiIDogIjF2SGZlTWk5U0E4VTdWZlNKRTN3SnVTQklOZUhVeWpOY0pzZ2tYWWNHQlkifQ.eyJleHAiOjE3NzA0ODM3NjUsImlhdCI6MTc3MDQ4MzQ2NSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL3JlYWxtcy9hYXV0aC10ZXN0IiwiYXVkIjoiaHR0cDovL3N1cHBseS1jaGFpbi1hZ2VudC5sb2NhbGhvc3Q6MzAwMCIsInN1YiI6IjAwYjUxOWU4LWY0MDktNDIwMS04OTExLTFjYjQwOGU4YTA4MiIsImFnZW50IjoiaHR0cDovL2JhY2tlbmQubG9jYWxob3N0OjgwMDAiLCJjbmYiOnsiandrIjp7ImtpZCI6IkdmWHZZS3ZscktGb3V2S1JQdUFnbFJZX3RiTmJJTFpMRzJBaTJNd2l6bVUiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJjcnYiOiJFZDI1NTE5IiwieCI6IklDSEVXYUwyRTBFaGJkU3F4eGZ5ZUY4RjF5WndiNEViQzJKQXZ0dl9ZR3cifX0sInNjb3BlIjoic3VwcGx5LWNoYWluOm9wdGltaXplIG1hcmtldC1hbmFseXNpczphbmFseXplIn0.NAsgPE4DHrS36m98J76QbsZHWj9EIYJzVg1mqa5IJv6xp6lRbsqYO7jnqodh5QV86yhrLpBWpVzyrSYm1LU9DJnh8VegIR9JWUwO-c9j4xdFIQLdDIAzCMalCUTWnC2E2dwZA1raaw3kxxJ1eKkIOWQqWRp-oeubHdtoqI2yJHbVZNs1VQ7YeajGygyEHFG3W7F1eWpt8TChF8sy5gvqvk5DPiHXRykyxpghK-klq4hzQACIAXoFhtBUo8zqYFtF_gtSkQPcs_CdNhjp5ksr-ZkyqpXQjhBmajaARNGoVxUtdAtVOyoz4wFSFwBTTYpFg-f4IrkjA-kwCE-_71UZ5A
INFO:agent_executor:🔐 Auth token detected in request (scheme=jwt)
INFO:httpx:HTTP Request: GET http://localhost:8080/realms/aauth-test/protocol/openid-connect/certs "HTTP/1.1 200 OK"
INFO:agent_executor:✅ Auth token verified successfully
INFO:agent_executor:✅ Authorization successful: auth_token verified for agent: http://backend.localhost:8000
If we decode the token:
{
"exp": 1770483765,
"iat": 1770483465,
"iss": "http://localhost:8080/realms/aauth-test",
"aud": "http://supply-chain-agent.localhost:3000",
"sub": "00b519e8-f409-4201-8911-1cb408e8a082",
"agent": "http://backend.localhost:8000",
"cnf": {
"jwk": {
"kid": "GfXvYKvlrKFouvKRPuAglRY_tbNbILZLG2Ai2MwizmU",
"kty": "OKP",
"use": "sig",
"crv": "Ed25519",
"x": "ICHEWaL2E0EhbdSqxxfyeF8F1yZwb4EbC2JAvtv_YGw"
}
},
"scope": "supply-chain:optimize market-analysis:analyze"
}
Compare to Autonomous Scheme
Compare this to the autonomous auth flow token:
| Claim | Autonomous | User-Delegated | Meaning |
|---|---|---|---|
| sub | ❌ Not present | ✅ "00b519e8..." |
User identity - who authorized this action |
| agent | ✅ "http://backend..." |
✅ "http://backend..." |
Agent identity - which agent is acting |
| cnf | ✅ Bound to agent’s key | ✅ Bound to agent’s key | Proof-of-possession - only this agent can use the token |
How This Relates to OIDC
If you’re familiar with OAuth 2.0 and OpenID Connect, this pattern should feel familiar:
- OIDC login establishes the user’s identity (
subclaim in ID token) - User consent grants the agent permission to act with specific scopes
- Auth token carries both user identity (
sub) and agent identity (agent)
The key innovation of AAuth is dual identity:
- Traditional OAuth: tokens represent either the user OR the application
- AAuth: tokens represent the user AND the agent simultaneously
This enables fine-grained audit trails: “User Alice authorized Backend Agent to optimize the supply chain at 4:56 PM on February 7th, 2026.”
Summary
Use autonomous mode when: Agents are acting on their own authority (background jobs, system tasks, agent-to-agent coordination).
Use user-delegated mode when: Agents must act on behalf of a specific user (accessing user data, making decisions with user accountability, compliance requirements).
In the next post, we’ll explore token exchange and delegation chains—how agents can delegate to other agents while preserving the user’s identity and consent. Read more: Agent Token Exchange →