Skip to content

No-browser flows: CIBA vs Device Code

Device Code (RFC 8628) and CIBA (OpenID Connect Client-Initiated Backchannel Authentication 1.0) are the two grants the spec ecosystem provides for the same broad situation: the device that wants the access token cannot host a usable browser. Smart TVs, gaming consoles, CLI tools, IoT devices, voice assistants, POS terminals, call-center panels, server-side processes that act on behalf of a person.

From a distance the two flows look like the same shape — "two surfaces meet at the OP, the user approves on a phone". They are not. They differ in who initiates the request and how the user is identified to the OP, and that single distinction drives almost every other difference: the wire endpoints, the polling subject, the anti-phishing primitive, the regulatory profile.

This page is the selection guide. The mechanics live on the dedicated pages for Device Code and CIBA.

The two flows in one paragraph each

Device Code (RFC 8628). The device with no browser asks the OP for a one-time code, displays it on its own screen, and tells the user "open this URL on your phone and enter this code". The user authenticates on whatever browser they happen to have at hand and approves. Meanwhile the device polls /token until approval lands. The user's identity is discovered during the flow — the OP did not know who would walk up to the TV.

CIBA (Core 1.0). The RP already knows who the user is — login_hint ([email protected], an account number), id_token_hint (a previously issued ID token), or login_hint_token (a signed JWT from an upstream system). The RP asks the OP "please authenticate this user out-of-band". The embedder's notification and authentication-device services notify the user; the OP stores the request and the RP polls until approval lands. This library implements poll mode only — ping and push delivery modes are not provided. The user's identity is supplied upfront by the RP so the deployment can route the approval to the right authentication device.

Comparison

AspectDevice Code (RFC 8628)CIBA (Core 1.0)
Trigger originThe device with no browser (input device → OP)The RP / API client (RP → OP)
User identificationThe user types user_code on a separate browserThe RP supplies login_hint / id_token_hint / login_hint_token upfront
User deviceAny browser the user happens to haveA device pre-registered by the deployment for out-of-band approval
Anti-phishing primitiveuser_code displayed by the device + verification URI host visible to the userbinding_message shown on the user's authentication device
Browser involvementYes (on the user's phone)Optional / none (the embedder's authentication device confirms)
Polling subjectThe device with no browserThe RP
Spec endpoint/device_authorization/bc-authorize
Token grant_typeurn:ietf:params:oauth:grant-type:device_codeurn:openid:params:grant-type:ciba
Typical use casesTV apps, CLI tools, kiosks, voice assistants, low-input IoTStrong customer authentication (PSD2-style), finance / health out-of-band approval, customer-support flows that reset access without sharing a screen
Library statusRFC 8628 — full support, gated by op.WithGrants(grant.DeviceCode) plus op.WithDeviceCodeGrant()OIDC CIBA Core 1.0 — poll mode only; discovery advertises poll only; ping and push are not implemented
Brute-force defenseop/devicecodekit opaque-attempt-key limiter plus record lockout (MaxUserCodeStrikes)Poll-abuse lockout — rate-limited /token retries per auth_req_id, AuditCIBAPollAbuseLockout on crossing the cap
FAPI profileNot in FAPI 2.0 (RFC 8628 itself is sufficient for Baseline-like deployments)FAPI-CIBA — a separate FAPI profile from FAPI 2.0 Baseline / Message Signing, pinning JAR + DPoP | mTLS + 10-minute access TTL

Two grants, one symptom

Both grants exist because the canonical authorization_code + PKCE flow assumes a usable browser on the device that wants the token. That assumption breaks for TVs, CLIs, IoT, voice assistants, and for backend services acting on a user's behalf. RFC 8628 and CIBA solve two different shapes of "the browser is somewhere else".

Picking between them — decision tree

Run through these four questions in order. The first one usually settles it.

CIBA or Device Code — four questions
Four sequential questions that decide between CIBA and the Device Authorization Grant: whether the RP already knows the user, whether the device has a screen, whether the user has a pre-registered authentication device, and whether out-of-band approval is a regulatory requirement.Does the RP already know which user this isit can supply a login_hint before the flow startsyesCIBAthe OP can address the user without a redirectnoDoes the device that wants the token have a screenenough to print a short code and a URL onyesDevice Codethe user carries the code to a second screennoHas the user pre-registered an authentication devicea phone the OP already knows how to reachnoDevice Codenothing to notify — the user has to come to ityesRegulated finance or health, out-of-band approvalapproval must happen away from the transacting deviceyesCIBAbinding_message ties the two devices togethernoDefault: Device Codeit asks less of your infrastructure — no hint resolver, no notification channel to operate
The dividing line is who starts the conversation. Device Code puts the user in charge of finding the OP; CIBA has the OP reach out to a device it was told about in advance, which only works if you already run that channel.

1. Does the RP know who the user is before the flow starts?

  • Yes → CIBA. The RP already has login_hint (or an ID token, or a hint token) and can send it with /bc-authorize. The user does not need to type anything to identify themselves.
  • No → Device Code. The user identifies themselves during the flow by signing in on the verification page; the OP discovers who they are when the user authenticates on their phone.

2. Is there a screen on the device that wants the token?

  • Yes → Device Code can display the user_code and verification_uri directly. This is the canonical TV / console / CLI case.
  • No (voice assistant, headless IoT) → Both can work. Device Code can emit the user_code via TTS or print it as a QR (verification_uri_complete). CIBA uses a separate authentication device; the embedder's notification service delivers the approval prompt, so the consumption device needs no display.

3. Does the user have a registered authentication device?

  • CIBA assumes yes. Without a registered device the embedder's approval service has no destination. Provisioning that device — the banking app, the staff phone, the regulator-issued authenticator — is part of the deployment.
  • Device Code does not assume. Any browser session the user can sign into works. The user's phone, a colleague's laptop, a kiosk in the store.

4. Is this a regulated finance / health context with out-of-band approval requirements?

  • CIBA is designed for it. The FAPI-CIBA profile pins JAR, sender constraint (DPoP or mTLS), and a 10-minute access TTL on top of CIBA Core. binding_message is the audit primitive regulators look for ("the user saw exactly what they were approving").
  • Device Code is general-purpose. It can be deployed to good effect, but it is not the shape regulators normally point to for SCA.

If you are still unsure after running the questions: default to Device Code for consumer-facing "the device has a screen but no browser" cases, and CIBA for "the RP knows the user and just needs them to approve out-of-band".

Sequence diagrams

Device Code (RFC 8628)

Device Code (RFC 8628)
The Device Authorization Grant. A device with no browser asks for a device code and a short user code, prints the code and a URL, and polls the token endpoint while the user approves from any other browser.Device — no browserTV · CLI · IoTOPgo-oidc-providerUser's browserany phone or laptop1POST /device_authorization2200 · { device_code, user_code, verification_uri }3the device prints the user_code and the URL to visitparloop · every interval s4POST /token · grant_type=…:device_code5400 · authorization_pending6open verification_uri, enter the user_code,log in and approve7POST /token — the next poll8200 · { access_token, id_token? }
The device and the browser are never connected to each other. The only thing that travels between them is the user_code, carried by a person reading one screen and typing into another.

CIBA (Core 1.0, poll mode)

CIBA (OIDC Core 1.0, poll mode)
CIBA in poll mode. The RP supplies a login hint at the backchannel authentication endpoint, the OP resolves it to a subject, the embedder's notification service reaches the user's pre-registered device, and the RP polls until the approval lands.RP / API clientPOS · call-centre panelOPgo-oidc-providerUser's auth devicepre-registered1POST /bc-authorizelogin_hint=alice · binding_message2HintResolver → subject3the embedder's notification service"Approve $80 at Acme Coffee?"4200 · { auth_req_id, expires_in, interval }parloop · every interval s5POST /token · grant_type=…:ciba6400 · authorization_pending7the user approves on the deviceApprove(auth_req_id)8POST /token — the next poll9200 · { access_token, id_token, refresh_token? }
Compared with the figure above, the difference is step 3: here the OP addresses the user, so it needs both a hint it can resolve and a channel it can send on. Steps 5 to 9 are the same polling loop either way.

The shape is similar enough to map between the two mentally. The decisive difference is the diagonal arrow: in Device Code the user walks to the OP with a code in hand; in CIBA the OP reaches out to a device the user already trusts.

Threat model side-by-side

Phishing — attacker tricks the user into approving the attacker's request.

  • Device Code: the user verifies the URL host they are typing the code into. The user_code itself has no per-session secret value (entropy is intentional but moderate). If the user mistypes the host or clicks a link in a phishing email, the same user_code works on the attacker's site.
  • CIBA: binding_message is attached to /bc-authorize and shown on the user's authentication device. The user sees "Acme POS terminal #14: Approve $80 at Acme Coffee?" before approving. An out-of-band prompt without context ("we noticed unusual activity, approve?") is the failure mode.

Replay / brute-force on user_code.

  • Device Code: the user_code is short (BDWP-HQPK) on purpose so users can type it. That makes it brute-forceable in principle. The library ships op/devicecodekit — the first manual entry must call VerifyUserCodeByAttemptKey with an opaque server-side ceremony key, then ApproveUserCode or DenyUserCode; record-bound helpers still enforce MaxUserCodeStrikes (default 5). Embedder verification pages MUST route through the helper and must not use the raw user_code as the attempt key.
  • CIBA: there is no user-typed code. The auth_req_id is opaque and OP-issued.

Replay / abuse on /token polling.

  • Both flows return authorization_pending while the user is deciding and slow_down if the device polls faster than the negotiated interval. RFC 8628 §3.5 makes honoring the new interval a MUST; the OP persists the value atomically with LastPolledAt so a multi-replica deployment cannot reset it.
  • CIBA additionally has a poll-abuse lockout: when the per-auth_req_id violation counter crosses the threshold the request is denied with reason="poll_abuse" and the audit catalogue records AuditCIBAPollAbuseLockout.

What this library implements today

Device Code (RFC 8628). Full support, gated by op.WithGrants(grant.DeviceCode) plus op.WithDeviceCodeGrant(). The verification ceremony page (where the user types the user_code) is embedder-hosted; the first manual entry should call devicecodekit.VerifyUserCodeByAttemptKey with an opaque server-side ceremony key, followed by ApproveUserCode / DenyUserCode. Retain devicecodekit.Deps and pass it by pointer; its default limiter is process-local, so multi-instance deployments need a shared atomic AttemptLimiter. Audit records from these helpers go through Deps.AuditLogger. Audit catalogue:

  • AuditDeviceAuthorizationIssued, AuditDeviceAuthorizationRejected, AuditDeviceAuthorizationUnboundRejected
  • AuditDeviceCodeVerificationApproved, AuditDeviceCodeVerificationDenied, AuditDeviceCodeUserCodeBruteForce
  • AuditDeviceCodeTokenIssued, AuditDeviceCodeTokenRejected, AuditDeviceCodeTokenSlowDown
  • AuditDeviceCodeRevoked (from the public Revoke helper)

CIBA (Core 1.0). Poll mode only; discovery advertises poll only. Wire it with op.WithGrants(grant.CIBA) plus op.WithCIBA(op.WithCIBAHintResolver(...)). The HintResolver is the embedder hook that maps the inbound hint (login_hint, id_token_hint, login_hint_token) to a subject. For id_token_hint, the OP verifies signature, issuer, and authenticated-client audience before calling the resolver and passes only the verified sub; it intentionally does not check exp, and rejects pairwise clients before resolution. Ping and push delivery modes are not provided. Audit catalogue:

  • AuditCIBAAuthorizationIssued, AuditCIBAAuthorizationRejected, AuditCIBAAuthorizationUnboundRejected
  • AuditCIBAPollAbuseLockout
  • AuditCIBATokenIssued, AuditCIBATokenRejected, AuditCIBATokenSlowDown
  • AuditCIBAPollObservationFailed (the token endpoint observed a state transition it could not act on cleanly)

The authentication-device callback is an embedder path that calls the CIBA substore directly. The library does not emit AuditCIBAAuthDeviceApproved or AuditCIBAAuthDeviceDenied for those calls; those catalog names are available for deployment-owned telemetry.