User approval (CIBA)#
Some calls should be confirmed by the person the agent acts for, not by an operations team: a payment from their account, an email in their name. MAQPNA does this with user approval on their own device (CIBA, OpenID Client-Initiated Backchannel Authentication Core 1.0). The gateway asks the customer's identity provider (for example Keycloak) to authenticate the delegating user out of band; the user confirms on their phone or authenticator, and the identity provider returns an ID token that proves who confirmed. Code: cmd/maqpna-gateway/ciba.go, pkg/ciba, pkg/approval.
MAQPNA implements poll mode only. Ping and push modes are not implemented.
Sequence#
sequenceDiagram
autonumber
participant Ag as Agent
participant GW as Gateway
participant AQ as Approval store
participant IdP as Identity provider
actor U as Delegating user
Ag->>GW: tools/call payments.create_transfer
GW->>GW: rule approval.userConfirmation ciba
GW->>AQ: Create approval (userConfirm ciba)
GW->>IdP: discovery (backchannel endpoint, poll mode)
GW->>IdP: backchannel auth request<br/>login_hint, binding_message, requested_expiry
IdP-->>GW: auth_req_id, interval
IdP->>U: prompt with binding message MAQPNA-1A2B3C4D/payments/create_transfer
loop every interval (slow_down adds 5 s)
GW->>IdP: token request, grant urn:openid:params:grant-type:ciba
IdP-->>GW: authorization_pending
end
U->>IdP: approve
GW->>IdP: token request
IdP-->>GW: ID token (acr, amr, auth_time)
GW->>GW: verify ID token, user claim equals the delegating user
GW->>AQ: ConfirmByUser (verdict approve, evidence)
AQ-->>GW: approved
GW->>GW: forward the call, audit with userConfirm ext keys
GW-->>Ag: result
Step by step#
- Trigger. A
require_approvaldecision whose rule hasapproval.userConfirmation: ciba, or whoseapprovalQuorumentry setsuserConfirmation: ciba, creates an approval withuserConfirm: ciba. A four-eyes quorum from the same rule still applies, merged strictest wins. - Check that the user can be asked. If the
cibablock is not configured or the call has no delegating user, the fallback applies at once (reasonsciba_not_configured,no_delegating_user). - Discover endpoints. The backchannel authentication and token endpoints come from the identity provider's discovery document (poll mode must be supported) unless configured. Every request goes through the residency-checking egress transport.
- Start. The gateway sends
login_hint=<user>,scope=openid …, optionalacr_values,requested_expiry(the smaller ofrequestedExpirySecondsand the approval's remaining TTL) and a binding message<prefix>-<ref>/<server>/<tool>: prefixMAQPNAby default,<ref>the first 8 hex digits of the approval ID in capitals, only[A-Za-z0-9-._+/!?#](spaces become-), at most 50 characters (Keycloak's limit). The client authenticates withclient_secret_basicby default. - Poll. The gateway polls the token endpoint with
grant_type=urn:openid:params:grant-type:cibaat the returned interval (default 5 seconds), adding 5 seconds onslow_down. - Approve. On an ID token, the gateway verifies it (issuer, audience = the CIBA client ID, signature) and requires the user claim (
userClaim, defaultpreferred_username, thenemail, thensub) to equal the delegating user. The approval is approved withuserConfirmation {method, verdict, subject, issuer, acr, amr, authTime, bindingMessage, requestRef}. - Decline.
access_deniedfrom the identity provider, an ID token for another user (identity_mismatch) or an invalid ID token (id_token_invalid) deny the approval. - No answer.
expired_tokenleaves the approval to expire on its own (approval_expired). - Fallback. When the request cannot be made (
ciba_unavailable) or the identity provider rejects it (ciba_rejected),ciba.fallbackdecides: -approvers(default): the approval stays pending for ordinary approvers,userConfirmFallback=<reason>is recorded and notifiers are told again; -deny: a system denial; the ledger's approver isurn:maqpna:user-confirm-unavailable, never the user, and the reason isuser_confirmation_unavailable:<reason>. - Restart. Pending CIBA approvals restored from the journal are requested again at start-up.
- Audit. The tool-call record's
approveris the user, andextcarriesuserConfirm,userConfirmVerdict,userConfirmSub,userConfirmIss,userConfirmAcr,userConfirmAmr,userConfirmAuthTime,userConfirmRef,userConfirmReasonanduserConfirmFallback(when set).
Configuration#
"ciba": {"enabled": true, "issuer": "https://idp.acme.eu/realms/dev", "clientID": "maqpna-ciba",
"clientSecretFile": "/etc/maqpna/delegation-secrets/ciba", "userClaim": "email",
"acrValues": ["phr"], "requestedExpirySeconds": 120, "fallback": "approvers"}
In Helm it sits under delegation.ciba (disabled by default).
What you see#
An approver who tries to approve a CIBA approval in the console, Desk or CLI gets 409 user_confirmation_required; they can still deny. The user sees the binding message on their device, for example MAQPNA-1A2B3C4D/payments/create_transfer, and the same reference appears in ext.userConfirmRef and the approval's requestRef so support can match the prompt to the record. Values are illustrative.
Failure modes#
| Situation | Behaviour |
|---|---|
| Identity provider unreachable | ciba_unavailable, then the fallback |
| User never answers | The approval expires (approval_expired) |
| Another person answers | Denied, identity_mismatch |
| Gateway restarts during polling | The request is sent again at start-up |