Skip to main content

Get a person's approval with CIBA

Policy can require a person to approve a request before a client gets its token. The client then uses Client Initiated Backchannel Authentication (CIBA). It asks at POST /bc-authorize, an approval IdP asks the person, and the client polls /token until the decision is in. This page covers the client side and the contract the approval IdP implements.

We have not run a full approval by hand for this page. The integration test test_ciba.py runs it against a test approval IdP, and all 31 of its tests passed on our instance. The one block below we did run.

When a client needs approval​

A token request that policy wants approved gets HTTP 400 with approval_required.

{
"error": "approval_required",
"error_description": "<the message the approver will see>",
"bc_authorize_endpoint": "https://your-environment.example/bc-authorize",
"login_hint": "<the approver policy names>"
}

The approval rules live in the row pbac.operator.controls.ciba_approval. Each entry in its obligations list matches a requested resource by its type, intent_type, usually the type of a rich authorization request. It also tests one condition on that request's parameters, such as an amount >= a limit. It names the approver in target, an optional binding_message_template, and the approval IdP in approval_idp. When an entry names no IdP, approval_idp_selection.default_idp picks one.

Ask at /bc-authorize​

Only a confidential client can call /bc-authorize. It authenticates as it does at /token and sends the same scope, resource or authorization_details as the refused request, plus these two fields.

FieldValue
login_hintThe approver from the approval_required response. It must match the approver policy names. id_token_hint and login_hint_token are refused.
binding_messageThe error_description from that response, word for word. It must equal the message policy shows the approver.
curl -fsS -u "$CLIENT_ID:$CLIENT_SECRET" "$POLICYARC_URL/bc-authorize" \
--data-urlencode "authorization_details=$AUTHORIZATION_DETAILS" \
--data-urlencode "login_hint=$APPROVER" \
--data-urlencode "binding_message=$BINDING_MESSAGE"

The answer is HTTP 200.

{"auth_req_id": "<id>", "expires_in": 300, "interval": 5}

/bc-authorize asks policy first. A request that policy would grant without approval is refused, so a client cannot open approvals for nothing. This block shows that answer with the client from Register a client.

resp=$(curl -sS -w '\n%{http_code}' -u "${CLIENT_ID:?register a client first}:$CLIENT_SECRET" "$POLICYARC_URL/bc-authorize" \
-d scope=introspection -d login_hint=approver@example.com -d binding_message=Approve)
echo "$resp"
[ "$(tail -n1 <<<"$resp")" = 400 ] && grep -qF 'Policy does not require approval for this request' <<<"$resp" ||
{ echo 'Expected HTTP 400 invalid_request, no approval required' >&2; false; }

Other answers from /bc-authorize.

AnswerWhen
HTTP 400 invalid_requestNo hint, more than one hint, a hint that does not name the approver, no binding_message, or a request that needs more than one distinct approval
HTTP 400 invalid_binding_messageThe message differs from the one policy shows the approver
HTTP 400 unauthorized_clientThe client is public
HTTP 403 access_deniedPolicy refuses the request outright
HTTP 503 temporarily_unavailableThe approval IdP did not accept the notification
HTTP 500 server_errorApproval is misconfigured, such as an approval IdP that is missing or lacks an endpoint, keys or client ID

Poll /token​

Poll with the CIBA grant no faster than interval seconds.

curl -sS -u "$CLIENT_ID:$CLIENT_SECRET" "$POLICYARC_URL/token" \
-d grant_type=urn:openid:params:grant-type:ciba -d auth_req_id="$AUTH_REQ_ID"
Answer, all HTTP 400 except the tokenMeaning
authorization_pendingNo decision yet. Wait interval seconds and poll again.
slow_downYou polled inside the interval
access_deniedThe approver said no
expired_tokenNo decision within expires_in seconds
invalid_grantThe auth_req_id is unknown, belongs to another client, or was already used
HTTP 200 with a tokenApproved. The token's subject is the approver. An openid scope also returns an ID token.

An approved request is redeemed once. Policy runs again at redemption with the approval noted, so the token reflects policy at that moment.

Build the approval IdP​

The approval IdP is the service that reaches the person, for example by a push notification. Register it as an identity provider through the Admin API with these fields set: idpId, issuer, clientId, jwksUri and backchannelNotificationEndpoint.

1. Receive the notification​

The server sends POST to backchannelNotificationEndpoint with Content-Type: application/jwt. The body is one JWT signed RS256 with the server's ID token key, with header typ ciba-notification+jwt. Verify it with the keys at /.well-known/jwks.json. Answer with a 2xx status. A 4xx or 5xx answer, or no answer, makes /bc-authorize return 503 and drops the request.

ClaimValue
issThe PolicyArc issuer
audThe approval IdP's issuer
exp, iat, jtiWhen the request expires, when it was sent, and a unique id
auth_req_idThe request to decide
login_hintThe approver
binding_messageWhat to show the approver
client_id, client_nameWho is asking
scope, resource, authorization_detailsWhat the client asked for, when present
callback_urlWhere to send the decision, <issuer>/bc-authorize/callback
callback_tokenThe bearer token for that callback

2. Report the decision​

Send POST to callback_url with Authorization: Bearer <callback_token> and a JSON body.

{"auth_req_id": "<id>", "status": "approved", "id_token": "<an ID token for the approver>"}

For a refusal, send "status": "denied" with no id_token. An approval needs an ID token that the IdP signed with a key from its jwksUri. These checks apply to it.

  • iss is the IdP's issuer, and aud is the clientId registered for the IdP.
  • exp is present, and iat is no earlier than 60 seconds before the request was opened.
  • nonce is the auth_req_id.
  • sub is the approver, or email is the approver and email_verified is true.
Callback answerMeaning
HTTP 204Decision recorded
HTTP 400 invalid_requeststatus is not approved or denied, or an approval has no id_token
HTTP 401The bearer token or auth_req_id does not match
HTTP 403 invalid_id_tokenThe ID token failed a check above
HTTP 404The request expired
HTTP 409 invalid_requestThe request was already decided

Each decision writes an audit row for /bc-authorize/callback.