Skip to main content

Client-Initiated Backchannel Authentication (CIBA)

CIBA is an authentication flow where the client starts a login, the user authenticates out-of-band (typically on their mobile device), and the client polls or is notified when the result is ready. There is no browser redirect and no front-channel interaction between the user and the client. Use CIBA for call centers, point-of-sale terminals, high-value transaction approvals, and IoT scenarios where the user is not sitting at the same device as the one driving the login.

In short: the client starts the login, the user approves it out-of-band (for example with a push notification to the LumoAuth app on their phone), and the client polls the token endpoint for the result. The parameters are the same as any other OAuth 2.0 grant; unfamiliar terms are in the glossary.

Decoupled Authentication

CIBA separates the device initiating authentication from the device where the user authenticates.

How CIBA Works​

  1. Client sends an authentication request with a user hint (email, phone, etc.).
  2. Server returns an auth_req_id immediately.
  3. User receives a notification on their device and approves or denies.
  4. Client polls the token endpoint for the result.
  5. On approval, tokens are issued.
11 / 11
  1. User to Consumption Device: Input User Identifier
  2. Consumption Device to Client: Start Login
  3. Client to Auth Server: POST /bc-authorize (login_hint)
  4. Auth Server to Client: Returns auth_req_id
  5. Auth Server to Auth Device: Push Notification / SMS
  6. Auth Device to User: Prompt for Approval
  7. User to Auth Device: Approves Request
  8. Auth Device to Auth Server: Auth Confirmation
  9. Client to Auth Server: POST /token (auth_req_id)
  10. Auth Server to Client: Tokens (access_token, id_token)
  11. Client to Consumption Device: Login Success

Token Delivery Mode​

LumoAuth delivers tokens in poll mode: the client polls the token endpoint with the auth_req_id until the user has answered. It is the only mode advertised in discovery (backchannel_token_delivery_modes_supported: ["poll"]), so no callback endpoint is needed on your side.

Prerequisites​

  • The OAuth client must have CIBA enabled in its configuration.
  • You need a way to identify the user via a hint (login_hint, login_hint_token, or id_token_hint).
  • The user needs a way to approve: the LumoAuth app for push approval, or another factor the hosted approval page can challenge. See Push Authentication.

Backchannel Authentication Endpoint​

POST /orgs/{orgId}/api/v1/oauth/bc-authorize

Initiates a backchannel authentication request.

Request Parameters​

ParameterRequiredDescription
scopeYesSpace-separated scopes. openid is always included.
login_hintOne hint requiredUser's email address or username
id_token_hintOne hint requiredPreviously issued ID token for this user
login_hint_tokenOne hint requiredJWT containing user identification
binding_messageNoShort message shown to the user during authentication (e.g., "Transaction: $50.00")
requested_expiryNoPreferred auth request lifetime in seconds (the server may use a shorter value)
acr_valuesNoAuthentication level the approval must meet, as on the authorization endpoint
user_codeNot supportedDiscovery advertises backchannel_user_code_parameter_supported: false; sending one returns 400 invalid_user_code
Exactly One Hint Required

Provide exactly one of login_hint, login_hint_token, or id_token_hint. Including more than one results in an error.

Example Request​

curl -X POST https://app.lumoauth.dev/orgs/acme-corp/api/v1/oauth/bc-authorize \
-u "CLIENT_ID:CLIENT_SECRET" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "scope=openid%20profile%20email" \
-d "login_hint=user@example.com" \
-d "binding_message=Approve%20login%20from%20Call%20Center"

Success Response​

{
"auth_req_id": "1c266114-a1be-4252-8ad1-04986c5b9ac1",
"expires_in": 120,
"interval": 5
}
FieldDescription
auth_req_idUnique identifier for this authentication request
expires_inSeconds until this request expires (configured per client; requested_expiry can shorten it)
intervalMinimum seconds between poll requests

LumoAuth never reveals whether the hint matched a user: an unknown user still receives an auth_req_id of the same shape, and polling it never returns tokens.

An AI agent can also start a CIBA request for one specific action by sending agent_id together with authorization_details (a structured description of what it wants to do, in the rich authorization request format). The approved details come back in the token response. See Approvals for the approval side.

Token Endpoint with CIBA Grant​

POST /orgs/{orgId}/api/v1/oauth/token

In poll mode, the client polls this endpoint using the auth_req_id received from the backchannel authentication endpoint. Wait at least interval seconds between polls.

Request Parameters​

ParameterRequiredDescription
grant_typeYesurn:openid:params:grant-type:ciba
auth_req_idYesThe auth_req_id from the backchannel authentication response

Example Request​

curl -X POST https://app.lumoauth.dev/orgs/acme-corp/api/v1/oauth/token \
-u "CLIENT_ID:CLIENT_SECRET" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=urn%3Aopenid%3Aparams%3Agrant-type%3Aciba" \
-d "auth_req_id=1c266114-a1be-4252-8ad1-04986c5b9ac1"

Pending Response (Keep Polling)​

{
"error": "authorization_pending",
"error_description": "The authorization request is still pending"
}

Success Response (User Approved)​

{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4...",
"scope": "openid profile email",
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Error Responses​

CIBA defines specific error codes for different scenarios:

ErrorHTTP StatusDescription
authorization_pending400User hasn't approved or denied yet. Keep polling.
slow_down400Polling too fast. Increase your interval.
expired_token400The auth_req_id has expired. Start a new request.
access_denied400User denied the authentication request.
invalid_grant400The auth_req_id is invalid or was issued to a different client.

The backchannel authentication endpoint itself answers:

ErrorHTTP StatusDescription
invalid_request400scope missing, no hint or more than one hint, or a malformed parameter
invalid_scope400A requested scope is not allowed for this client
unauthorized_client400CIBA is not enabled for this client (or not in the organization's plan)
invalid_user_code400A user_code was sent; the parameter is not supported
invalid_client401Client authentication failed, or the client is disabled
too_many_requests429More than 60 requests per minute

Example: Poll Mode Integration​

A complete poll-mode implementation in Python:

import requests
import time

CLIENT_ID = "your_client_id"
CLIENT_SECRET = "your_client_secret"
BASE_URL = "https://app.lumoauth.dev/orgs/acme-corp/api/v1"

def authenticate_user(email):
# Step 1: Initiate backchannel authentication
auth_response = requests.post(
f"{BASE_URL}/oauth/bc-authorize",
auth=(CLIENT_ID, CLIENT_SECRET),
data={
"scope": "openid profile email",
"login_hint": email,
"binding_message": "Login requested from Support Portal"
}
)

if not auth_response.ok:
raise Exception(f"Auth failed: {auth_response.json()}")

auth_data = auth_response.json()
auth_req_id = auth_data["auth_req_id"]
interval = auth_data.get("interval", 5)
expires_in = auth_data["expires_in"]

print(f"Waiting for user to approve... (expires in {expires_in}s)")

# Step 2: Poll for tokens
start_time = time.time()
while time.time() - start_time < expires_in:
time.sleep(interval)

token_response = requests.post(
f"{BASE_URL}/oauth/token",
auth=(CLIENT_ID, CLIENT_SECRET),
data={
"grant_type": "urn:openid:params:grant-type:ciba",
"auth_req_id": auth_req_id
}
)

if token_response.ok:
print("User approved! Tokens received.")
return token_response.json()

error = token_response.json().get("error")

if error == "authorization_pending":
continue
elif error == "slow_down":
interval += 5
continue
elif error == "access_denied":
raise Exception("User denied the request")
elif error == "expired_token":
raise Exception("Request expired")
else:
raise Exception(f"Unexpected error: {token_response.json()}")

raise Exception("Timeout waiting for user approval")

# Usage
tokens = authenticate_user("john.doe@example.com")

Use Cases​

  • Call center authentication. A customer service agent initiates login verification on their terminal; the customer approves on their mobile device.
  • Point-of-sale. A retail terminal initiates auth, and the customer approves on their phone.
  • Transaction authorization. High-value transactions require out-of-band approval on a separate device.

Discovery Metadata​

CIBA endpoints and capabilities are advertised in the OpenID Connect Discovery document:

curl https://app.lumoauth.dev/orgs/acme-corp/api/v1/.well-known/openid-configuration | jq '{
backchannel_authentication_endpoint,
backchannel_token_delivery_modes_supported,
backchannel_user_code_parameter_supported
}'
{
"backchannel_authentication_endpoint": "https://app.lumoauth.dev/orgs/acme-corp/api/v1/oauth/bc-authorize",
"backchannel_token_delivery_modes_supported": ["poll"],
"backchannel_user_code_parameter_supported": false
}

Standards used on this page​

Plain nameStandard
Client-initiated backchannel authentication (CIBA)OpenID CIBA Core 1.0
OAuth 2.0 (the core sign-in and token protocol)RFC 6749
Rich authorization requestsRFC 9396
Discovery documentRFC 8414 and OpenID Connect Discovery

See the standards reference for what each one covers and how LumoAuth uses it.