Skip to the content.

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.

← Back to index

Watch the demo

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):

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.

The UI presents the consent screen to the user:

The user sees:

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:

The key innovation of AAuth is dual identity:

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 →

← Back to index