MAQPNADocs

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#

  1. Trigger. A require_approval decision whose rule has approval.userConfirmation: ciba, or whose approvalQuorum entry sets userConfirmation: ciba, creates an approval with userConfirm: ciba. A four-eyes quorum from the same rule still applies, merged strictest wins.
  2. Check that the user can be asked. If the ciba block is not configured or the call has no delegating user, the fallback applies at once (reasons ciba_not_configured, no_delegating_user).
  3. 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.
  4. Start. The gateway sends login_hint=<user>, scope=openid …, optional acr_values, requested_expiry (the smaller of requestedExpirySeconds and the approval's remaining TTL) and a binding message <prefix>-<ref>/<server>/<tool>: prefix MAQPNA by 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 with client_secret_basic by default.
  5. Poll. The gateway polls the token endpoint with grant_type=urn:openid:params:grant-type:ciba at the returned interval (default 5 seconds), adding 5 seconds on slow_down.
  6. Approve. On an ID token, the gateway verifies it (issuer, audience = the CIBA client ID, signature) and requires the user claim (userClaim, default preferred_username, then email, then sub) to equal the delegating user. The approval is approved with userConfirmation {method, verdict, subject, issuer, acr, amr, authTime, bindingMessage, requestRef}.
  7. Decline. access_denied from the identity provider, an ID token for another user (identity_mismatch) or an invalid ID token (id_token_invalid) deny the approval.
  8. No answer. expired_token leaves the approval to expire on its own (approval_expired).
  9. Fallback. When the request cannot be made (ciba_unavailable) or the identity provider rejects it (ciba_rejected), ciba.fallback decides: - 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 is urn:maqpna:user-confirm-unavailable, never the user, and the reason is user_confirmation_unavailable:<reason>.
  10. Restart. Pending CIBA approvals restored from the journal are requested again at start-up.
  11. Audit. The tool-call record's approver is the user, and ext carries userConfirm, userConfirmVerdict, userConfirmSub, userConfirmIss, userConfirmAcr, userConfirmAmr, userConfirmAuthTime, userConfirmRef, userConfirmReason and userConfirmFallback (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