Skip to content

CIBA — Client-Initiated Backchannel Authentication ​

CIBA solves a different shape of problem than the device flow. With device code, two surfaces meet at the OP through a short code shown on the device. With CIBA, the device that initiates the request is not visible to the user at all — the embedder's notification service reaches a separate authentication device the user already trusts.

The canonical setup:

  • Consumption device — a POS terminal, a call-center agent screen, an in-store kiosk, a bank's wire-transfer reviewer panel. It knows who the user is supposed to be (loyalty card, phone number, account number) but cannot authenticate them.
  • Authentication device — the user's phone, with a banking app already installed and signed in. The embedder's notification service presents (for example) "Approve $80.00 at Acme Coffee?", and the user taps Approve or Deny.

The consumption device never asks the user for credentials — it just asks the OP "please ask Alice to approve this on her phone".

Specs referenced on this page
Vocabulary refresher
  • auth_req_id — the opaque identifier returned from /bc-authorize. The consumption device polls /token with this; the authentication device approves against the same identifier.
  • Hint — how the consumption device tells the OP which user to ask. CIBA Core §7.1 defines three:
    • login_hint — opaque value the embedder maps to a subject ([email protected], account number, …).
    • id_token_hint — a previously issued ID token whose sub identifies the user.
    • login_hint_token — a signed JWT the embedder verifies and maps to a subject (e.g. issued by another upstream system).
  • Delivery mode — how the OP tells the consumption device that approval landed:
    • poll — the device polls /token with auth_req_id. This is the only delivery mode implemented by the library.

How the flow runs (poll mode) ​

The OP reaches the user's phone; the terminal only polls
CIBA in poll mode. The point-of-sale terminal calls the backchannel authentication endpoint, the OP resolves the login hint to a subject, the embedder's notification service reaches the staff phone, and the terminal polls the token endpoint until the user approves.Start the requestWait for approvalPOS terminalconsumption deviceOPgo-oidc-providerStaff phoneauthentication devicePOST /bc-authorizelogin_hint · binding_messagemap the hint to a subjectHintResolver → subjectnotification serviceAcme Coffee: approve $80?200 · auth_req_idexpires_in: 600 · interval: 5POST /token · pollgrant_type=…:ciba400 · authorization_pendinguser approves on the phoneapprove(auth_req_id)POST /token · next poll200 · access_tokenid_token · refresh_token?
The terminal and the phone never talk to each other; the OP is the only thing both touch. That is why binding_message matters: it is the one string the person at the terminal and the person holding the phone can compare.

The consumption device never holds a credential for the user. The user never types into the consumption device. The authentication device — which already authenticated Alice when she signed into her banking app — is the only place where consent is exercised.

CIBA vs Device Code — when do you pick which? ​

Both flows have two devices. The difference is whether the user knows the consumption device is asking.

Device CodeCIBA
Who initiates the trust?The user types user_code on the verification page.The consumption device asks the OP; the embedder's authentication-device service presents the request to the user.
Does the user have to discover the URL?Yes — verification_uri is shown on screen.No — the deployment already knows the authentication device.
Trust model on consumption side?Anonymous device asking the user to bind it.Pre-registered device asking the OP to ask the user to confirm.
Typical surfaceSmart TV, console, CLI, IoT pairing.POS, call center, fraud-confirmation, in-app payments.
Identifier the user typesuser_code (e.g. BDWP-HQPK).None — the OP has the user's identifier already (login_hint).
Risk of misdirectionLow — the URL is on the device's screen.Medium — the user must trust the out-of-band notification text matches the consumption surface. Use binding_message so the prompt on the phone shows what the POS is requesting.

If the user is standing in front of the device but the device cannot show a screen with a code — choose CIBA. If the user is across the room from the device and the device has a screen — choose device code. CIBA's authentication device must already know the user; device code's verification page works for any signed-in browser session.

Hints — telling the OP "which user" ​

The consumption device cannot authenticate the user, so it has to tell the OP which user to identify for the approval service. CIBA Core §7.1 lists three hint kinds; the OP supports all three through a single HintResolver interface:

go
op.WithCIBAGrant(op.CIBAGrant{
    HintResolver: op.HintResolverFunc(
        func(ctx context.Context, kind op.HintKind, value string) (string, error) {
            switch kind {
            case op.HintLoginHint:
                // value = "alice", "[email protected]", account number, etc.
                return resolveLoginHint(ctx, value)
            case op.HintIDTokenHint:
                // value is the OP-verified subject, not the raw JWT.
                // The OP checked its signature, issuer, and the authenticated
                // client's audience. It intentionally does not enforce exp.
                return lookupSubject(ctx, value)
            case op.HintLoginHintToken:
                // value = a signed JWT issued by another system you trust.
                return verifyAndMap(ctx, value)
            }
            return "", op.ErrUnknownCIBAUser
        },
    ),
})

Returning op.ErrUnknownCIBAUser collapses the wire response to unknown_user_id. Any other error becomes login_required. The handler is required — op.WithCIBAGrant without a HintResolver fails at op.New.

For HintIDTokenHint, the OP verifies the JWT against its own keys, requires its issuer and the authenticated client's audience, and passes only the verified sub to Resolve. It intentionally does not check exp, and it refuses a token whose typ header is anything other than absent or JWT, so a logout token or access token cannot pass as a hint. A client registered with pairwise subjects is rejected before Resolve, because the per-sector sub is not reversible; that client must use login_hint or login_hint_token instead. Never parse the raw JWT in the resolver.

binding_message — the anti-confusion field ​

CIBA binding_message is a short string the consumption device sends with /bc-authorize. The OP forwards it to the authentication device so the prompt on the user's phone shows the same text the cashier sees on the POS:

Acme POS terminal #14: Approve $80.00 at Acme Coffee?

[ Approve ] [ Deny ]

Without binding_message the user has only the OP's generic prompt to go on, and a phishing flow ("we noticed unusual activity, please approve this notification") becomes much more plausible. Treat binding_message as mandatory in the embedder's UX even though the spec marks it optional.

The OP accepts at most 50 characters and refuses control characters (400 invalid_binding_message). It forwards the string exactly as received and does not HTML-escape it, so both devices can show identical text; escape it for the rendering context yourself.

What the endpoints answer ​

/bc-authorize rejects at the request:

ConditionResponse
The hint does not resolve (op.ErrUnknownCIBAUser)400 unknown_user_id
The resolver returns any other errorlogin_required
id_token_hint from a pairwise client400 invalid_request
binding_message too long or containing control characters400 invalid_binding_message
user_code or client_notification_token present400 invalid_request (neither is supported)
requested_expiry not a positive integer, or above 10 minutes under profile.FAPICIBA400 invalid_request (outside FAPI-CIBA a value above CIBAGrant.MaxRequestTTL is clamped)
Request object whose client JWKS cannot be fetched503

/token with grant_type=urn:openid:params:grant-type:ciba answers with the same four polling codes as the device flow: authorization_pending, slow_down, access_denied (denied on the authentication device, or five slow_down offences) and expired_token.

The lifetime of an auth_req_id is 600 s by default (requested_expiry can shorten it), and interval is 5 s. Tune them with the DefaultRequestTTL, MaxRequestTTL and PollInterval fields of op.CIBAGrant; negative values and, under profile.FAPICIBA, an expiry above 10 minutes make op.New fail.

When the approved request is redeemed, the OP records a consent grant for that client, as the device flow does. Introspection and /userinfo report the public (pairwise) subject, revoking the subject reaches the tokens, and a later /auth for the same scopes skips the consent screen.

See it run ​

examples/32-ciba-pos ships a complete POS-terminal scenario: the POS posts to /bc-authorize, the staff phone (simulated by a goroutine that calls CIBARequestStore.Approve directly) approves the request, and the POS polls until the OP issues a token. End-to-end runtime is around five seconds.

sh
(cd examples/32-ciba-pos && GOWORK=off go run -tags example .)

The example is split into role-tagged files (op.go for the OP wiring + HintResolver, rp.go for the POS-side polling, device.go for the simulated phone approval).

  • Use case: CIBA wiring — op.WithCIBAGrant, the HintResolver contract, FAPI-CIBA profile constraints, and how the embedder's authentication-device callback talks back to CIBARequestStore.Approve.
  • Authorization code + PKCE — the flow to use when the user is at the client.
  • Device Code primer — the conceptual sibling for "user is on a different surface" but with a code-on-screen ceremony.