User Authorization with CIBA
Client-Initiated Backchannel Authentication (CIBA) is an OAuth 2.0 specification that allows a client application to initiate an authentication and/or authorization flow without requiring direct user interaction on the initiating application. Rich Authorization Requests (RAR) is an OAuth 2.0 extension that allows client applications to request for more complex permissions beyond standard OAuth 2.0 scopes in an authorization request.
You can use CIBA with RAR to pass fine-grained authorization data to the authorization server in a backchannel request. The authorization_details
parameter contains details about the request that you can customize in a consent prompt to show the user.
Common use cases
Use RAR with the CIBA flow for use cases that require more fine-grained control over resource access. Common use cases include:
A payments app prompts the user to confirm a money transfer. The
authorization_details
can be customized to show the transaction details.An AI agent prompts the user with details about a rescheduled doctor’s appointment. The
authorization_details
can be customized to show the new time and date.
How it works
The User Authorization with CIBA flow is similar to the User Authentication with CIBA flow, where RAR support enables clients to pass the authorization_details
to the authorization server via the /bc-authorize
endpoint.
The following sequence diagram explains the end-to-end User Authorization with CIBA flow:

The sequence diagram defines two actors: an authorizing user and an initiating user. The authorizing and initiating user can be two different entities, such as an AI agent performing a task on a user's behalf. In other use cases, they can be the same entity, such as a user authorizing a transaction on a retail kiosk or another connected device.
Step 6: Mobile application presents the consent details to the user
Step 7: Mobile application sends the user response back to Auth0
Step 8: Auth0 receives user response after the flow completes
Step 9: Auth0 returns access token to the client application
Prerequisites
To initiate a CIBA push request using Auth0, you must complete the following prerequisites:
Integrate your custom mobile application with the Guardian SDK. To learn more, read Configure Client-Initiated Backchannel Authentication.
Enroll the authorizing user in MFA using push notifications via the Guardian SDK. To verify in the Auth0 Dashboard, navigate to User Management > Users and click on the user:

If you have set Multi-factor Authentication as always required for your tenant, users are prompted to enroll for MFA at their next login. You can also use Actions to prompt for MFA enrollment.
Configure Client-Initiated Backchannel Authentication for your tenant and application.
Configure Rich Authorization Requests for your resource server, which includes registering your
authorization_details
types.
Step 1: Client application initiates a CIBA request
Use the User Search APIs to find the authorizing user for whom you’d like to initiate a CIBA request and obtain their user ID.
Once you have a user ID for the authorizing user, use the Authentication API to send a CIBA request with the authorization_details
to the /bc-authorize
endpoint:
curl --location 'https://$tenant/bc-authorize' \
--request POST \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=$client_id' \
--data-urlencode 'client_secret=$client_secret' \
--data-urlencode 'login_hint={ "format": "iss_sub", "iss": "https://$tenant/", "sub":"$user_id"}' \
--data-urlencode 'audience=https://api.example.com' \
--data-urlencode 'binding_message=Confirm payment of 2500' \
--data-urlencode 'authorization_details=[{
"type": "money_transfer",
"instructedAmount": {
"amount": 2500,
"currency": "USD"
},
"sourceAccount": "xxxxxxxxxxx1234",
"destinationAccount": "xxxxxxxxxxx9876",
"beneficiary": "Hanna Herwitz",
"subject": "A Lannister Always Pays His Debts"
}]'
Was this helpful?
Parameters | Description |
---|---|
tenant |
Tenant name. It can also be a custom domain. |
client_id |
Client application identifier. |
client_secret |
Client authentication method used for user authentication with CIBA, such as Client Secret, Private Key JWT, or mTLS Authentication. If you're using Private Key JWT or mTLS, you don't need to include the client secret. |
scope |
If the client needs to identify the user (i.e., obtain an id_token ), openid must be included. If the client only needs an access token for authorization, openid is optional, but commonly included.The scope can optionally include offline_access to request a refresh token. However, for one-time authorization of a transaction with the CIBA Flow, a refresh token is not needed and does not have any meaning in this context. |
user_id |
User ID for the authorizing user that is passed within the login_hint structure. If iss_sub format is used, then the user ID is passed within the sub claim.The user ID for a federated connection may have a different format. |
request_expiry |
The CIBA flow's requested expiry is between 1 and 300 seconds, and it defaults to 300 seconds. Include the request_expiry parameter to set a custom expiry for the CIBA flow. |
binding_message |
Human-readable message used to bind the CIBA flow across the authentication and consumption devices. The binding message is required and up to 64 characters. Use only alphanumeric and +-_.,:# characters |
audience |
Unique identifier of the audience for the issued token. |
authorization_details |
An optional JSON array of objects that describes the permissions to be authorized. You should register each object’s type value on the resource server using the resource server’s authorization_details parameter. To learn more, read Configure Rich Authorization Requests. |
Step 2: Auth0 tenant acknowledges the CIBA request
If the Auth0 tenant successfully receives the POST
request, you should receive a response containing an auth-req-id
that references the request:
{
"auth_req_id": "eyJh...",
"expires_in": 300,
"interval": 5
}
Was this helpful?
The auth_req_id
value is passed to the /token
endpoint to poll for the completion of the CIBA flow.
Step 3: Client application polls for a response
Use the Authentication API to call the /token
endpoint using the urn:openid:params:grant-type:ciba
grant type and the auth_req_id
you received from the /bc-authorize
endpoint:
curl --location 'https://$tenant.auth0.com/oauth/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=$client_id' \
--data-urlencode 'client_secret=$client_secret' \
--data-urlencode 'auth_req_id=$auth_req_id' \
--data-urlencode 'grant_type=urn:openid:params:grant-type:ciba'
Was this helpful?
var token = await authenticationApiClient.GetTokenAsync(
new ClientInitiatedBackchannelAuthorizationTokenRequest()
{
AuthRequestId = response.AuthRequestId,
ClientId = "your-client-id",
ClientSecret = "your-client-secret"
}
);
Was this helpful?
token, err := authAPI.OAuth.LoginWithGrant(context.Background(),
"urn:openid:params:grant-type:ciba",
url.Values{
"auth_req_id": []string{resp.AuthReqID},
"client_id": []string{clientID},
"client_secret": []string{clientSecret},
},
oauth.IDTokenValidationOptions{})
Was this helpful?
Request<BackChannelTokenResponse> tokenRequest = auth.getBackChannelLoginStatus(authReqId, "grant-type");
BackChannelTokenResponse tokenResponse = tokenRequest.execute().getBody();
Was this helpful?
Until the authorizing user approves the transaction, you should receive the following response:
{
"error": "authorization_pending",
"error_description": "The end-user authorization is pending"
}
Was this helpful?
There is approximately a five-second wait interval for polling. If you poll too frequently, you will receive the following response, where the description varies depending on the backoff interval:
{
"error": "slow_down",
"error_description": "You are polling faster than allowed. Try again in 10 seconds."
"interval": 10
}
Was this helpful?
To resolve the error, wait until the next interval (in seconds) to poll the /token
endpoint.
Step 4: Mobile application receives the push notification
Auth0 sends a push notification to the user's registered mobile app or device. The Guardian SDK provides methods to parse the data received from the push notification and return a ready-to-use Notification
instance. The Notification
instance includes a transaction linking ID, or txlinkid
, that the mobile application uses to retrieve the consent details from Auth0.
The following code samples are example iOS and Android mobile push notification implementations using the Guardian SDK:
//implementing UNUserNotificationCenterDelegate
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: (UNNotificationPresentationOptions) -> Void) {
let userInfo = notification.request.content.userInfo
if let notification = Guardian.notification(from: userInfo) {
// Implement this function to display the prompt and handle user's consent/rejection.
handleGuardianNotification(notification: notification)
}
}
Was this helpful?
// at the FCM listener you receive a RemoteMessage
@Override
public void onMessageReceived(RemoteMessage message) {
Notification notification = Guardian.parseNotification(message.getData());
if (notification != null) {
// you received a Guardian notification, handle it
handleGuardianNotification(notification);
return;
}
/* handle other push notifications you might be using ... */
}
Was this helpful?
Step 5: Mobile application retrieves the consent details
Call the Guardian SDK from your mobile application to retrieve the consent details i.e. the contents of the binding_message
from the Auth0 Consent API.
The following code samples are example iOS and Android implementations that retrieve data from the Auth0 Consent API:
let device: AuthenticationDevice = // the object you obtained during the initial Guardian SDK enrollment process and stored locally
if let consentId = notification.transactionLinkingId {
Guardian
.consent(forDomain: {yourTenantDomain}, device: device)
.fetch(consentId: consentId, notificationToken: notification.transactionToken)
.start{result in
switch result {
case .success(let payload):
// present consent details to the user
case .failure(let cause):
// something went wrong
}
}
}
Was this helpful?
Enrollment enrollment = // the object you obtained during the initial Guardian SDK enrollment process and stored locally
if (notification.getTransactionLinkingId() != null) {
guardian
.fetchConsent(notification, enrollment)
.start(new Callback<Enrollment> {
@Override
void onSuccess(RichConsent consentDetails) {
// present consent details to the user
}
@Override
void onFailure(Throwable exception) {
// something went wrong
}
});
}
Was this helpful?
Step 6: Mobile application presents the consent details to the user
The Auth0 Consent API responds to the mobile application with the consent details, including the binding_message
, scope
, audience
, and authorization_details
if configured. The scopes returned to the mobile application are filtered according to your RBAC policy. To learn more, read Role-Based Access Control.
The following code sample is an example response from the Auth0 Consent API, where the authorization_details_id
uniquely identifies the authorization_details
array for the request:
{
"id": "cns_2309dsfsd098",
"requested_details": {
"audience": "https://api.example.com",
"scope": ["read:profile", "write:profile"],
"binding_message": "abc123",
"authorization_details_id": "xyz098"
},
"created_at": 1632739200,
"expires_at": 1632739200
}
Was this helpful?
The mobile application presents the authorization request and/or the consent details with the authorization_details
to the user in a push notification via the Guardian SDK. The user can accept or decline the authorization request at this point.
Step 7: Mobile application sends the user response back to Auth0
Depending on whether the user accepts or rejects the authorization request, the mobile application sends the user response back to Auth0.
The following code samples are example iOS and Android implementations that handle the user response:
User accepts the authorization request
Guardian
.authentication(forDomain: "{yourTenantDomain}", device: device)
.allow(notification: notification)
// or reject(notification: notification, withReason: "hacked")
.start { result in
switch result {
case .success:
// the auth request was successfully rejected
case .failure(let cause):
// something failed, check cause to see what went wrong
}
}
Was this helpful?
guardian
.allow(notification, enrollment)
.execute(); // or start(new Callback<> ...)
Was this helpful?
User rejects the authorization request
Guardian
.authentication(forDomain: "{yourTenantDomain}", device: device)
.reject(notification: notification)
// or reject(notification: notification, withReason: "hacked")
.start { result in
switch result {
case .success:
// the auth request was successfully rejected
case .failure(let cause):
// something failed, check cause to see what went wrong
}
}
Was this helpful?
guardian
.reject(notification, enrollment) // or reject(notification, enrollment, reason)
.execute(); // or start(new Callback<> ...)
Was this helpful?
Step 8: Auth0 receives user response after the flow completes
The client application completes the polling upon receiving a response from the /token
endpoint. A CIBA flow always requires a response, either an approval or decline, from the authorizing user, and existing grants are not checked. This means Auth0 treats every CIBA request as a fresh authorization for the authorizing user.
Step 9: Auth0 returns access token to client application
If the user rejects the push request, Auth0 returns an error response like the following to the client application:
{
"error": "access_denied",
"error_description": "The end-user denied the authorization request or it has been expired"
}
Was this helpful?
If the user approves the push request, Auth0 returns an access token with the authorization_details
like the following to the client application:
{
"id_token": "...",
"access_token": "...",
"expires_in": "...",
"scope": "$granted_scopes",
"authorization_details": [{
"type": "money_transfer",
"instructedAmount": {
"amount": 2500,
"currency": "USD"
},
"sourceAccount": "xxxxxxxxxxx1234",
"destinationAccount": "xxxxxxxxxxx9876",
"beneficiary": "Hanna Herwitz",
"subject": "A Lannister Always Pays His Debts"
}]
}
Was this helpful?
Note: The id_token
will only be present if the openid
scope was included in the initial /bc-authorize
request.
Query authorization_details
At compile time, you can query the type and objects of authorization_details
from the consent details in a strongly typed manner as you would dynamically query JSON:
let requestedDetails: ConsentRequestedDetails = payload.requestedDetails
let myAuthorizationDetailsTypes = requestedDetails.authorizationDetails[0].objectValue!;
let type = myAuthorizationDetailsTypes["type"]?.stringValue // Your pre-registered type value
let stringProperty = myAuthorizationDetailsTypes["string_property"]?.stringValue
let boolProperty = myAuthorizationDetailsTypes["bool_property"]?.boolValue
let numericProperty = myAuthorizationDetailsTypes["numeric_property"]?.doubleValue
let nestedObjectProperty = myAuthorizationDetailsTypes["nested_property"]?.objectValue
let nestedArrayProperty = myAuthorizationDetailsTypes["nested_array_property"]?.arrayValue
Was this helpful?
RichConsentRequestedDetails requestedDetails = consentDetails.getRequestedDetails();
Map<String, Object> authorizationDetails = requestedDetails.getAuthorizationDetails().get(0);
String type = (String) myAuthorizationDetailsTypes.get("type");
String stringProperty = (String) myAuthorizationDetailsTypes.get("string_property");
boolean booleanProperty = (boolean) myAuthorizationDetailsTypes.get("boolean_property");
int numericProperty = (int) myAuthorizationDetailsTypes.get("numeric_property");
Object nestedObjectProperty = myAuthorizationDetailsTypes.get("nested_property");
List<Object> nestedArrayProperty = (List<Object>) myAuthorizationDetailsTypes.get("nested_array_property");
Was this helpful?
If you define a custom type to represent your object, you can use the filterAuthorizationDetailsByType()
function to return all authorization_details
objects that match the desired type.
The following code sample queries authorization_details
with the payment
type:
// Must implement AuthorizationDetailsType
struct Payment : AuthorizationDetailsType {
static let type = "payment";
let amount: Double;
let currency: String;
}
...
let requestedDetails: ConsentRequestedDetails = payload.requestedDetails
let payments = requestedDetails.filterAuthorizationDetailsByType(Payment.self)
let firstPayment = payments.first!
let type: String = firstPayment.type // "payment"
let amount: Double = firstPayment.amount
let currency: String = firstPayment.currency
Was this helpful?
@AuthorizatioDetailsType("payment")
class Payment {
private String type;
private int amount;
private String currency;
public Payment(String type, int amount, String currency) {
this.type = type;
this.amount = amount;
this.currency = currency;
}
public String getType() {
return type;
}
public int getAmount() {
return amount;
}
public String getCurrency() {
return currency;
}
}
...
RichConsentRequestedDetails requestedDetails = consentDetails.getRequestedDetails();
List<Payment> payments = requestedDetails.filterAuthorizationDetailsByType(Payment.class);
Payment firstPayment = payments.get(0);
String type = firstPayment.getType();
int amount = firstPayment.getAmount();
String currency = firstPayment.getCurrency();
Was this helpful?
filterAuthorizationDetailsByType()
only returns objects matching the specified authorization_details
type. As a result, your mobile application should present all relevant authorization_details
to the user for consent, regardless of their type, to ensure a complete understanding of the request
You can also query the authorization_details
when the AI agent or application polls the /oauth/token
endpoint for a response:
POST https://$tenant/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=urn:openid:params:grant-type:ciba&
client_id=$client_id&
client_secret=$client_secret&
auth_req_id=$auth_req_id
Was this helpful?
Parameters | Description |
---|---|
grant_type |
Set to the CIBA grant type: urn:openid:params:grant-type:ciba |
client_id |
Set to the application’s client ID. |
client_secret |
Set to the application’s client secret. |
auth_req_id |
Returned from the Auth0 tenant when it acknowledges the CIBA request. References the CIBA request. |
When the authorizing user approves the request, Auth0 receives the user response, and the CIBA flow completes, returning an access token and authorization_details
array:
{
"access_token": "ey...ZQ",
"expires_in": 86400,
"authorization_details": [{
"type": "money_transfer",
"instructedAmount": {
"amount": 2500,
"currency": "USD"
},
"sourceAccount": "xxxxxxxxxxx1234",
"destinationAccount": "xxxxxxxxxxx9876",
"beneficiary": "Hanna Herwitz",
"subject": "A Lannister Always Pays His Debts"
}],
"token_type": "Bearer"
}
Was this helpful?
Limitations
Auth0 doesn’t support:
Accessing or modifying RAR in Actions for CIBA flows.
Advertising RAR types for clients to discover, which means you need to pre-register clients with the
authorization_details
types they can send.Validating RAR objects beyond checking that they have a
type
property that matches allowed types for the API. Your resource server is responsible for the granular validation of the content withinauthorization_details
. For more information, see Configure RAR.