Skip to main content

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 a CIBA-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 canActOnBehalfOfUsers enabled: in the portal, open AI Agents → Agents, edit the agent and turn on May act on behalf of users.

    The agent edit page with the May act on behalf of users switch

  • Organization policy knob: authSettings.agent_ciba_enabled (default true).

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

ParameterRequiredNotes
login_hint / id_token_hintexactly oneidentifies the target user (email/username, or a verified ID token)
authorization_detailsyesone rich authorization request object (or a JSON array containing exactly one); type is required
binding_messagerecommendedsanitised (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_expirynorequest lifetime in seconds (default 300, max 600)
requested_ttlnorequested token TTL in seconds, clamped to 60–900
agent_idclient-credential form onlythe 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).

The Approvals page where reviewers approve or deny permission requests

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​

EventWhen
ciba.agent_initiatedbc-authorize accepted (includes agent, target user, authorization_details, risk level)
ciba.agent_approvedrequest approved (either channel; includes the deciding actor + delegation context)
ciba.agent_deniedrequest denied
jit.token_issuedthe token poll minted the JIT token

See also​

Standards used on this page​

Plain nameStandard
Client-initiated backchannel authentication (CIBA)OpenID CIBA Core 1.0
Rich authorization requestsRFC 9396
Token exchange (delegation)RFC 8693

See the standards reference for what each one covers and how LumoAuth uses it.