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.
| Field | Value |
|---|---|
login_hint | The approver from the approval_required response. It must match the approver policy names. id_token_hint and login_hint_token are refused. |
binding_message | The 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.
| Answer | When |
|---|---|
HTTP 400 invalid_request | No 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_message | The message differs from the one policy shows the approver |
HTTP 400 unauthorized_client | The client is public |
HTTP 403 access_denied | Policy refuses the request outright |
HTTP 503 temporarily_unavailable | The approval IdP did not accept the notification |
HTTP 500 server_error | Approval 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 token | Meaning |
|---|---|
authorization_pending | No decision yet. Wait interval seconds and poll again. |
slow_down | You polled inside the interval |
access_denied | The approver said no |
expired_token | No decision within expires_in seconds |
invalid_grant | The auth_req_id is unknown, belongs to another client, or was already used |
| HTTP 200 with a token | Approved. 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.
| Claim | Value |
|---|---|
iss | The PolicyArc issuer |
aud | The approval IdP's issuer |
exp, iat, jti | When the request expires, when it was sent, and a unique id |
auth_req_id | The request to decide |
login_hint | The approver |
binding_message | What to show the approver |
client_id, client_name | Who is asking |
scope, resource, authorization_details | What the client asked for, when present |
callback_url | Where to send the decision, <issuer>/bc-authorize/callback |
callback_token | The 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.
issis the IdP'sissuer, andaudis theclientIdregistered for the IdP.expis present, andiatis no earlier than 60 seconds before the request was opened.nonceis theauth_req_id.subis the approver, oremailis the approver andemail_verifiedistrue.
| Callback answer | Meaning |
|---|---|
| HTTP 204 | Decision recorded |
HTTP 400 invalid_request | status is not approved or denied, or an approval has no id_token |
| HTTP 401 | The bearer token or auth_req_id does not match |
HTTP 403 invalid_id_token | The ID token failed a check above |
| HTTP 404 | The request expired |
HTTP 409 invalid_request | The request was already decided |
Each decision writes an audit row for /bc-authorize/callback.
Send feedback about this page