Async authorization (agent-initiated CIBA)
Sometimes an agent needs a user's authorization but the user is nowhere near the agent's execution context — a batch job wants to act on Ada's mailbox at 3 AM, a long-running workflow needs sign-off mid-run. Agent-initiated CIBA (Client-Initiated Backchannel Authentication, OpenID CIBA Core) lets the agent open an authorization request targeting a specific user, keep polling the standard OAuth token endpoint, and receive a scoped token once the user (or an organization reviewer) approves.
Behind the scenes, every agent CIBA request rides the existing JIT (just-in-time permission) human-in-the-loop machinery:
- the request appears in the portal approvals inbox (AI Agents → Approvals,
/orgs/{orgId}/portal/approvals) as aCIBA-tagged pending card that updates live; - a push prompt is sent to the target user's enrolled LumoAuth mobile device (same ECDSA P-256 signed respond flow as push approval);
- the decision — from either channel — resolves the CIBA request, and the token poll succeeds or fails accordingly;
- everything is audited:
ciba.agent_initiated,ciba.agent_approved/ciba.agent_denied, each carrying the delegation context (act.sub = agent_<agentId>).
The issued token is a JIT token: 60–900 s TTL, single-use grant, revocable, carrying the approved authorization_details and the act delegation claim. There is no refresh token — re-run the flow for new access.
Prerequisites
-
The agent must have
canActOnBehalfOfUsersenabled: in the portal, open AI Agents → Agents, edit the agent and turn on May act on behalf of users.
-
Organization policy knob:
authSettings.agent_ciba_enabled(defaulttrue). -
Rate limit: 10 initiations per
(tenant, agent, target user)per 5 minutes.
1. Initiate (bc-authorize)
Two ways to authenticate the initiation:
Agent bearer — the agent presents its own credential (an agent JWT from the client_credentials grant, or its agk_ API key):
POST /orgs/acme-corp/api/v1/oauth/bc-authorize HTTP/1.1
Authorization: Bearer agk_3f0c9a...
Content-Type: application/x-www-form-urlencoded
login_hint=ada%40acme.com
&binding_message=Send%20Q3%20report%20to%20board%20list
&authorization_details=%7B%22type%22%3A%22email_access%22%2C%22actions%22%3A%5B%22send%22%5D%2C%22identifier%22%3A%22board%40acme.com%22%7D
&requested_expiry=300
&requested_ttl=300
Client credentials on behalf of an agent — a CIBA-enabled OAuth client initiates and supplies the acting agent. Naming an agent is not a right to act as it: the agent must authenticate the same request with its own credential in the Authorization header (agent JWT or agk_ key), and it must be the agent named by agent_id; otherwise the server answers 403 unauthorized_client. The client remains the polling principal.
POST /orgs/acme-corp/api/v1/oauth/bc-authorize HTTP/1.1
Authorization: Bearer agk_3f0c9a...
Content-Type: application/x-www-form-urlencoded
client_id=workflow-svc&client_secret=...
&agent_id=agt_report_bot
&login_hint=ada%40acme.com
&binding_message=Send%20Q3%20report
&authorization_details=%7B%22type%22%3A%22email_access%22%2C%22actions%22%3A%5B%22send%22%5D%7D
Parameters:
| Parameter | Required | Notes |
|---|---|---|
login_hint / id_token_hint | exactly one | identifies the target user (email/username, or a verified ID token) |
authorization_details | yes | one rich authorization request object (or a JSON array containing exactly one); type is required |
binding_message | recommended | sanitised (control, bidi-override and zero-width characters removed; max 255 chars) and shown as a clearly labelled quoted field under the server-built summary — never as the prompt headline. The headline the user decides on is always <Agent> wants to <actions> on <type> <identifier> on your behalf, derived from authorization_details. The inbox shows it as the justification (CIBA: …). |
requested_expiry | no | request lifetime in seconds (default 300, max 600) |
requested_ttl | no | requested token TTL in seconds, clamped to 60–900 |
agent_id | client-credential form only | the registered agent acting on the user's behalf; that agent must also authenticate the request (see above) |
Response (CIBA §7.3):
{
"auth_req_id": "9d1c5f2e6b...",
"expires_in": 300,
"interval": 5
}
The authorization_details are validated with the same rules as JIT requests: an explicit PDP (ABAC) deny rejects the initiation with access_denied; risk is scored server-side and shown to the approver. Requests exceeding the agent's registered capabilities are flagged for the reviewer.
2. Approval
The target user gets a push prompt on their enrolled device (approve/deny is device-signed). Organization reviewers see the same request in the portal approvals inbox (AI Agents → Approvals, filter Permission requests) and can approve or deny it there (subject to jit.approve and the agent's user-access mode).

Approval-fatigue note: agent CIBA requests are never auto-approved by approval policies — a human always decides. The end user's approval of the push prompt is the consent step, so a CIBA task is recorded as a consented delegation (unlike an asserted on_behalf_of on the JIT task API).
3. Poll the token endpoint
Standard CIBA poll mode. The polling principal must be the initiator — the agent authenticates with its agt_/agk_ credentials, or the initiating client with its own:
POST /orgs/acme-corp/api/v1/oauth/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded
grant_type=urn%3Aopenid%3Aparams%3Agrant-type%3Aciba
&auth_req_id=9d1c5f2e6b...
&client_id=agk_3f0c9a...
While pending: {"error": "authorization_pending"}. Poll faster than interval and you get slow_down. Denied: {"error": "access_denied"}. Expired: {"error": "expired_token"}.
On approval:
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 300,
"authorization_details": [{
"type": "email_access",
"actions": ["send"],
"identifier": "board@acme.com"
}],
"jit_request_id": "jit_5f2e...",
"task_id": "task_ab12..."
}
Decoded access-token claims (JIT token):
{
"iss": "https://app.lumoauth.dev/orgs/acme-corp",
"sub": "0195f3c2-...-user-uuid",
"jit": true,
"exp": 1767222900,
"permissions": {
"authorization_details": [{ "type": "email_access", "actions": ["send"] }],
"act": { "sub": "agent_agt_report_bot" },
"task_id": "task_ab12...",
"jit_request_id": "jit_5f2e..."
}
}
sub is the approving user; act.sub records the agent acting on their behalf (token-exchange delegation semantics). The auth_req_id is single-use — a second poll after success returns invalid_grant.
Audit events
| Event | When |
|---|---|
ciba.agent_initiated | bc-authorize accepted (includes agent, target user, authorization_details, risk level) |
ciba.agent_approved | request approved (either channel; includes the deciding actor + delegation context) |
ciba.agent_denied | request denied |
jit.token_issued | the token poll minted the JIT token |
See also
- JIT permissions — the underlying HITL + token model
- Push approval for agent actions — yes/no approval without a token
- Mobile approvals device API — the push payload and device-signed endpoints the phone uses (CIBA prompts carry
origin: "ciba")
Standards used on this page
| Plain name | Standard |
|---|---|
| Client-initiated backchannel authentication (CIBA) | OpenID CIBA Core 1.0 |
| Rich authorization requests | RFC 9396 |
| Token exchange (delegation) | RFC 8693 |
See the standards reference for what each one covers and how LumoAuth uses it.