There are scenarios where the user cannot interact directly with an application to use its functionality securely. In these scenarios, CIBA can guarantee security and usability at the same time. This article will walk you through CIBA authentication and how to implement it in a .NET application.
What Is CIBA?
CIBA stands for Client-Initiated Backchannel Authentication, and it's a standard flow for decoupled authentication defined by the OpenID Foundation. Decoupled authentication occurs when the user is authenticated outside the context of an application. For example, consider a call center agent that is asked to modify a customer's order on the phone. Before executing that request, the agent needs to be sure that the user on the phone is actually the customer who placed that order. In this case, the call center agent sends an authentication request to the customer's phone. Once the customer confirms their identity, the operator can perform the requested action. Notice that the user authentication does not occur in the order management application, i.e., the application where the requested action needs to be performed, but on the user's device. This is why this type of authentication is called decoupled authentication.
Read this article to learn more about CIBA use cases.
The CIBA Flow
Let's dig into the following diagram to understand how CIBA works:
From this diagram, you see that the actors involved in the flow are:
- The user. This is the user who interacts with the application starting the authentication flow. In the use case described earlier, this is the call center agent.
- The client application. This is the application that initiates the CIBA authentication flow. In our previous example, it was the order management application. Notice that this application must be a confidential client, i.e., an application running in a controlled environment, such as a server environment. You cannot implement the CIBA flow directly in a SPA or desktop application; these applications must rely on a backend server.
- The authorization server. It's the server that authenticates the user and supports the CIBA flow, such as Auth0.
- The end user. This is the customer who needs to be authenticated to allow the user to perform actions on the client application. In the context of our example, this is the customer asking for a change to their order. The end user must have a mobile device with an application enrolled with the authorization server to receive push notifications.
While in this diagram you have two distinct users, in some scenarios, the user and the end user can be the same person. Consider the case of a user accessing a kiosk in a retail shop or a device without a browser. Read here for more use cases.
Following the numbered arrows in the diagram, we have:
- The user interacts with the client application and requests the end user's authentication.
- The client application submits an HTTP request to the authorization server's
/bc-authorize
endpoint and then start polling the/token
endpoint to receive the response. - The authorization server sends a push notification to the end user's device.
- The end user interacts with the push notification. The mobile application retrieves the data associated with the authentication request (consent details) and presents it to the end user. At this point, the end user sends their response, i.e., they can accept or deny the request.
- The client application receives the end user's response. If the response is successful, it also receives the tokens to perform the requested action.
This is a high-level overview of the CIBA flow. For more details on the messages exchanged by the actors involved in the flow, check out this document.
The Sample Application
Once you have a high-level understanding of CIBA flow, let's see how to implement it in a .NET application using Auth0.
Assume a call center scenario where an agent with a user on the phone needs to access that user's sensitive data managed by an application. This could be a hospital call center where users ask for information about their medical records. It could also be a tax center where users ask for information about their current tax situation. The agent needs to be sure that the user is who they say they are before giving this information.
You can imagine a screen like the following in front of the call center agent:
By clicking the "Load sensitive data" button, the agent initiates the CIBA flow to authenticate the user requesting access to their data.
From a technical perspective, the application is a .NET application running on a server. The screenshot above shows a Blazor Web App, but it could be an ASP.NET Core MVC or a Razor Page application. This application has to call a protected API to get the end user's sensitive data, and for this reason, it needs an access token with the appropriate scopes.
The backend application has a mobile counterpart capable of receiving push notifications, and the user must have this application installed on his device, typically a smartphone.
In the following sections, you will learn how to enable CIBA for this application using Auth0. You will start by enabling CIBA support on the backend application and then enable CIBA on the mobile device.
Add CIBA Support to the Backend App
To add CIBA support to your backend application, you need first to configure your Auth0 tenant and then configure and modify your application's code.
Enable CIBA support on your Auth0 tenant
To configure your Auth0 tenant to support CIBA, follow these steps in your Auth0 dashboard:
- Make sure that your application is registered with Auth0 and take note of the domain, client ID, and client secret.
- Scroll down in the application settings and enable the CIBA flow.
- Make sure that the API the application is going to call is registered with Auth0 and take note of its identifier, also known as audience.
- Enable Auth0 Guardian push notifications for your tenant and configure push notifications for MFA.
- The user must have enrolled in MFA push notifications to receive them.
Once you have configured your Auth0 tenant, go to your application's source code and update the appsettings.json
configuration file with the following settings:
// appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Auth0": {
"Domain": "YOUR_AUTH0_DOMAIN",
"ClientId": "YOUR_CLIENT_ID",
"ClientSecret": "YOUR_CLIENT_SECRET",
"Audience": "YOUR_API_AUDIENCE"
}
}
Replace the YOUR_AUTH0_DOMAIN
, YOUR_CLIENT_ID
, and YOUR_CLIENT_SECRET
placeholders with the respective application setting values from the Auth0 dashboard. Replace the YOUR_API_AUDIENCE
placeholder with the audience of the API you want to call.
Initiate the CIBA flow
Now you are ready to implement the CIBA flow in your backend application.
As the first step, install the Auth0 Authentication API SDK for .NET by running the following command in the root folder of your project:
dotnet add package Auth0.AuthenticationApi
The Auth0 Authentication API SDK is a generic .NET client for the Auth0 Authentication API. It's a low-level library that targets .NET Standard 2.0 and can be used in the legacy .NET Framework as well as the most recent .NET versions. At the time of writing, only the Auth0 Authentication API SDK supports CIBA for .NET.
Check out this article to learn more about the various Auth0 SDKs for the .NET platform.
To initiate the CIBA flow, we assume that when the call center agent clicks the "Load sensitive data" button, the following C# method is called with the user ID as its parameter's value:
private async void GetUserData(string userId)
{
var authenticationApiClient = new AuthenticationApiClient(new Uri($"https://{Configuration["Auth0:Domain"]}"));
var response = await authenticationApiClient.ClientInitiatedBackchannelAuthorization(
new ClientInitiatedBackchannelAuthorizationRequest()
{
ClientId = Configuration["Auth0:ClientId"],
ClientSecret = Configuration["Auth0:ClientSecret"],
Audience = Configuration["Auth0:Audience"],
Scope = "openid profile read:sensitivedata",
BindingMessage = "Request to access your private data",
LoginHint = new LoginHint()
{
Format = "iss_sub",
Issuer = $"https://{Configuration["Auth0:Domain"]}/",
Subject = userId
}
});
var isAuthorized = false;
var isPendingAuthorization = true;
var cibaTokenResponse = new ClientInitiatedBackchannelAuthorizationTokenResponse();
while (!isAuthorized && isPendingAuthorization)
{
try
{
cibaTokenResponse = await authenticationApiClient.GetTokenAsync(
new ClientInitiatedBackchannelAuthorizationTokenRequest()
{
AuthRequestId = response.AuthRequestId,
ClientId = Configuration["Auth0:ClientId"],
ClientSecret = Configuration["Auth0:ClientSecret"]
});
isAuthorized = true;
isPendingAuthorization = false;
} catch (ErrorApiException ex)
{
if (ex.ApiError.Error == "authorization_pending")
Thread.Sleep(5000);
else
isPendingAuthorization = false;
}
}
if (isAuthorized)
{
await LoadSensitiveUserData(cibaTokenResponse..AccessToken);
}
}
The userId
parameter contains the ID value of the user in the context of Auth0. Typically, it's a string in the form of auth0|3813a4b5e...
. Let's break down the code of the GetUserData()
method into its main parts.
Be aware that the code shown here is a simplified version of the code you should have in production. Its goal is to explain how to implement the CIBA flow, ignoring performance details and other optimizations.
Send the CIBA request
Consider the first section of the method:
private async void GetUserData(string userId)
{
var authenticationApiClient = new AuthenticationApiClient(new Uri($"https://{Configuration["Auth0:Domain"]}"));
var response = await authenticationApiClient.ClientInitiatedBackchannelAuthorization(
new ClientInitiatedBackchannelAuthorizationRequest()
{
ClientId = Configuration["Auth0:ClientId"],
ClientSecret = Configuration["Auth0:ClientSecret"],
Audience = Configuration["Auth0:Audience"],
Scope = "openid profile read:sensitive-data",
BindingMessage = "Request to access your private data",
LoginHint = new LoginHint()
{
Format = "iss_sub",
Issuer = $"https://{Configuration["Auth0:Domain"]}/",
Subject = userId
}
});
//...other code...
}
In this part, you create an instance of the AuthenticationApiClient
class by passing the URL of your Auth0 tenant. Then, you start the CIBA flow by calling the ClientInitiatedBackchannelAuthorization()
method. This method is responsible for submitting the HTTP request to the /bc-authorize
endpoint of Auth0. It needs the details of the authorization request, which is passed through an instance of the ClientInitiatedBackchannelAuthorizationRequest
class.
Apart from the client ID, client secret, and audience, a few other values are passed in the request:
- Scope. This is the string containing the scopes requested by the application. In addition to
openid
andprofile
, which are OpenID Connect standard scopes to get the user profile data in an ID token, notice theread:sensitive-data
scope, which is a specific permission request to access the user's sensitive data. - BindingMessage. This is a message displayed on the user's device to let them know about the request. This ensures that the action taken on the authentication device (the user's phone) is related to the request sent by the application.
- LoginHint. It's an object containing information to identify the user who should authorize the requester to access their sensitive data.
Wait for a CIBA response
After the CIBA request is submitted, the backend application waits for a response from Auth0. This response depends on the action taken by the user on their device, so you need to poll the Auth0's /token
endpoint to get the needed tokens:
private async void GetUserData(string userId)
{
//...other code...
var isAuthorized = false;
var isPendingAuthorization = true;
var cibaTokenResponse = new ClientInitiatedBackchannelAuthorizationTokenResponse();
while (!isAuthorized && isPendingAuthorization)
{
try
{
cibaTokenResponse = await authenticationApiClient.GetTokenAsync(
new ClientInitiatedBackchannelAuthorizationTokenRequest()
{
AuthRequestId = response.AuthRequestId,
ClientId = Configuration["Auth0:ClientId"],
ClientSecret = Configuration["Auth0:ClientSecret"]
});
isAuthorized = true;
isPendingAuthorization = false;
} catch (ErrorApiException ex)
{
if (ex.ApiError.Error == "authorization_pending")
Thread.Sleep(5000);
else
isPendingAuthorization = false;
}
}
//...other code...
}
You can see a few variables that help control the loop you are about to start.
Within this loop, you call the GetTokenAsync()
method to get a response from Auth0. This method needs an object as its parameter, which includes the request ID and the application credentials.
Until a positive response is received from the user, you will get an authorization_pending
exception. In this case, you should suspend the current thread and retry after a few seconds. In the example above, the thread waits 5 seconds. If the user denies authorization, you get an access_denied
exception.
If the user authorizes the request, you get a response in the cibaTokenResponse
object containing the ID and access tokens.
You should catch opportunely the exceptions you get and give the user an appropriate message about why the CIBA flow was not completed.
Use the tokens
Finally, you have the access token that allows you to call the API to get the user's sensitive data:
private async void GetUserData(string userId)
{
//...other code...
if (isAuthorized)
{
await LoadSensitiveUserData(cibaTokenResponse..AccessToken);
}
}
We don't go into the details of calling the protected API using the access token. If you need guidance on this, check out this article.
We have omitted the step of validating the tokens received for simplicity, but it is fundamental for the application's security. Read this article to learn more about token validation in .NET. Specifically, read this section to learn how to validate them without using a middleware, which applies in our case since we are using the Auth0 Authentication API SDK.
Add CIBA Support to the Mobile App
While this article focuses on the implementation of the backend application, we can't help but provide some information on its mobile counterpart.
First, you should use the Auth0 Guardian SDK to handle the communication with Auth0 on your mobile app.
While developing your mobile application, you should take care of the following steps in the CIBA flow:
- Your mobile app receives the push notification from Auth0.
- Then it retrieves the details of the CIBA request, in particular, the binding message.
- The app presents the binding message to the user, asking them to accept or deny the request.
- Finally, it manages the user's response by sending it back to Auth0.
Summary
This article has given you a high-level idea of what CIBA is and in what scenarios you can use it. Then, you learned how to configure your Auth0 tenant and application to support CIBA. So, you have seen how to integrate CIBA into a .NET application that runs on a server and what steps to consider when developing the mobile counterpart.
Stay tuned for future evolutions of CIBA support in Auth0.