Client Initiated Backchannel Authentication (aka CIBA) and Keycloak how-to and tool.

What is the goal ? People will think that method is close to device code authentication, a way to authenticate a user without a UI. Device code without a UI : no, device code needs a UI ! Remember, we need a way to display a QRCode or at least a code, then the user uses another UI for login (IE : smartphone).

CIBA uses another mechanism, a Client calls another backend application for authentication.

https://www.keycloak.org/docs/latest/serveradmin/index.html#clientinitiatedbackchannelauthenticationgrant

https://openid.net/specs/openid-client-initiated-backchannel-authentication-core-1_0.html

So, the application does not need any user interface, the « relying party » is in charge of authentication and it opens new ways of authentication, for new user experience. Maybe send a push notification directly to the smartphone.

Are you starting to see the possibilities ? Let’s take a look at an example with bash.

How it works ?

The application (IE, the online shop) asks the identity provider for an external authentication.

This is done by using « backchannel endpoint » on Keycloak, a POST request with :

  • client_id
  • client_secret
  • username : the user you want to authenticate
  • scopes
  • a binding message to transmit

Keycloak will ask an external backend, called the « relying party » with an POST request containing :

  • scopes
  • the message in « binding_message » field
  • the user in « login_hint »
  • An authorization bearer token, needed for the answer

This « relying party » is in charge of authentication, for example sending a push notification to the user device. When the user is authenticated, the « relying party » sends a POST request to Keycloak with :

  • the authorization bearer token got previously
  • the status of the authentication

Keycloak tells the application (with a request if in « ping » mode, or on an answer if in « poll » mode) and sends back a token.

How to use this implementation

Keycloak

Launch a Keycloak with :

./kc.sh start-dev --spi-ciba-auth-channel-ciba-http-auth-channel-http-authentication-channel-uri=http://127.0.0.1:8081

Then, you need a private client with the CIBA protocol enabled

And of course, a single user.

relying-party

Launch « relying-party.sh » script, it uses netcat and jq.

./relying-party.sh --ciba-callback-endpoint http://127.0.0.1:8080/realms/master/protocol/openid-connect/ext/ciba/auth/callback

First, it launches a local server on port 8081, waiting for a POST request.

echo -e 'HTTP/1.1 201 OK\r\n'  | nc -l 8081

After, this app will be in charge of authentication, keep it open in a terminal.

note : if this script does not answer 201/OK, Keycloak will return a 503 error at the next step

ciba authentication application

By launching « ciba-auth.sh » script, you will :

  • make the CIBA auth request
  • get the response, with interval, expiration and the authentication request id
  • launch a poll job for the token response.
./ciba-auth.sh --backchannel-authentication-endpoint http://127.0.0.1:8080/realms/master/protocol/openid-connect/ext/ciba/auth --token-endpoint http://127.0.0.1:8080/realms/master/protocol/openid-connect/token --client-id private --client-secret iq0wvuhkASCPeKJNunCx3wJO6qTGRiSF --username please-open.it --scope openid --hint please_auth

note : you can use –openid-endpoint with the URL http://127.0.0.1:8080/realms/master/.well-known/openid-configuration instead of –backchannel-authentication-endpoint and –token-endpoint

CIBA authentication request

curl --location --request POST "$BACKCHANNEL_ENDPOINT" \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode "client_id=$CLIENT_ID" \
--data-urlencode "client_secret=$CLIENT_SECRET" \
--data-urlencode "login_hint=$USERNAME" \
--data-urlencode "scope=$SCOPE" \
--data-urlencode "binding_message=$HINT" \
--data-urlencode 'is_consent_required=true'

This returns :

{
  "auth_req_id": "eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0..Tu1sCuk2JlPC4O3mTn8nrQ.OSSM58GCFn9oUWh4WPDFEHMhj9zUDQR3zlJimp_Vu1b6zQuCA20xuZfgZHjUs2v2YezmeZzqkhA8c1g8BgmVTIiO3dM0JcYANqY09Tnx2qVVLegBXFYS0ngtkn4KFG_bJUyApMookzFUo4Hk_PkLV6IpnuKyxNTaxA2AbsOEKXVrsVYWv7HmRGtknCi9PVg-Pxs5jDIPRKSCH4CsdGRCpzETgcLXsB0eJ7_x38Z9vo7R5nBPU-0EGXU9frCisfpjIL6jM5u9upANUITuWAr-6QH37-LiPbXp0zKa69ZrxgnhzQuaoo1ES7Pk3iXixV20K8AtcLpSdU9Qh9Cqy3uICspqPyI45tNn0DSUN6FvjXKVRT_VXqi5xJQVjBrdpK12VSA7kdvy4LQN5o1K-R3ZB_kKtQ2x8qsgJSS_8d2G-llm_XLMU7XaIE8tV22H98ee_xb0O6eEosrrjvQQ8rxRAowFupp3uNgGmx6Am_pPGPOJAnuf2yyzxZdIm6H7eriKBoBdb9EM5x6LNX-pRRZxYbJoVAYvBNaaR-K062L0gjv3h6wFbmCFIbdfAV1Vb50TdMH2k_YkMVRJqINY5FC0a__zaN2ma49cmJOtGeArrLiaaR7nFZ8efKu9opE-gHd3oUhsOIuUMJ2ALseApqfGB4j3z8lRqpyRa_u1tFhpZmw8N6PU935KDpdLKILjNQ4400j7C_L65ORKGgrA-ElNixgDhkv2kQFrbpIeKi1ZirB6SGE2ZRTE_-snrAAmiqZg6od81D-nG16W2LpEmxWrpnQoRsWji0ZJu8CZ2Kt770ygUc9QxtxSqbMPCJ2XezHx7NCfE0fvskZ7AS27G4Llxg3xML1Q4r2nxRL8Xx8IRj8bqXsIn6fYfcGys165rQX2.u36LQBjtX80qbj3FK234bQ",
  "expires_in": 120,
  "interval": 5
}

Note : the auth_req_id is a JWT token… empty of content !

Poll for a token

Start a loop, with the information ontained previously. This loop polls on the « /token » endpoint

while ((LOOP<EXPIRES_IN))
do
    OUTPUT=$(curl --location --request POST "$TOKEN_ENDPOINT" \
    --header 'Content-Type: application/x-www-form-urlencoded' \
    --data-urlencode 'grant_type=urn:openid:params:grant-type:ciba' \
    --data-urlencode "auth_req_id=$AUTH_REQ_ID" \
    --data-urlencode "client_id=$CLIENT_ID" \
    --data-urlencode "client_secret=$CLIENT_SECRET")
    echo $OUTPUT | jq
    if [[ $OUTPUT != *"pending"* ]]; then
        break
    fi
    sleep $INTERVAL
    LOOP=$LOOP+$INTERVAL
done

relying-party (again)

backchannel script has received an authentication request with :

  • the username
  • an authorization token (in header Authorization)
  • scopes
  • a binding message
{
  "scope": "openid roles profile email",
  "binding_message": "please_auth",
  "login_hint": "user",
  "is_consent_required": "true"
}
For user please-open.it : please_auth (Succeed/Unauthorized/Cancelled)

In the « real life », the application will send, for example, a push notification on your smartphone to ask you if you authorize the application or not.

After the answer app send a POST request to Keycloak with :

  • Authorization bearer
  • the answer

ciba authentication application (again)

The poll job receive a token during the poll job, that’s it, the user is authenticated.

  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJ4N0dPSW9OZUhIYkFvMjM0UWlrRTk1TW5VVnh6bjVQb1Bsb2llSnVTMlBNIn0.eyJleHAiOjE2NjY3MzkyODAsImlhdCI6MTY2NjczOTIyMCwiYXV0aF90aW1lIjoxNjY2NzM5MjIwLCJqdGkiOiJhNjE0MWE5MC1mM2YwLTQwMWYtYTQzMy0wN2Q5OThkNjE5Y2UiLCJpc3MiOiJodHRwOi8vMTI3LjAuMC4xOjgwODAvcmVhbG1zL21hc3RlciIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiI5Nzk2YmE3OC04NGM1LTRiZWItYTNjMy04NGFmMzczY2Q1NDEiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJwcml2YXRlIiwic2Vzc2lvbl9zdGF0ZSI6IjUwNGJmYzIwLTcwZmItNGZhZi1hYTk0LTQ1NTI3YzM2YTFjOSIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiaHR0cHM6Ly9wbGF5Z3JvdW5kLnBsZWFzZS1vcGVuLml0Il0sInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJkZWZhdWx0LXJvbGVzLW1hc3RlciIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6Im9wZW5pZCBlbWFpbCBwcm9maWxlIiwic2lkIjoiNTA0YmZjMjAtNzBmYi00ZmFmLWFhOTQtNDU1MjdjMzZhMWM5IiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJ1c2VyIn0.XKGRAFEcYOqC-oALMuXicOk8XiM3IGedfGMwyoOUayohIsjrQo_ABtuljsemPBNUPS7OQrLiKZIUuZYyy8VogkbeKfiwKtcZBVa5bV_Id8H2P7fR2IEZLqv8h-G1q_Pkc5RDyciicHKRV8R25y8_txOCCtpZxP6aMGv1O5lBTFeUmAshbCDLV-bMQZN6u7R9-5GPNGSSxTlDA1o49mlTl21YoFxiJLl69-C84QMXvxtu-h4xy7bKuk2BadNSN8rxLpwj3MXwDL29zhq-DdTavo3A7pE8wbKEHoVfkwrB67_vvTK7HoMaP0k2UfDa2bDOVnNaBkVsExq0j3eMBMT5DA",
  "expires_in": 60,
  "refresh_expires_in": 1800,
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI0NGM5MDk2Ny0yMDQyLTQwNTMtYmExZi1iNjU0MWUyM2YwYjIifQ.eyJleHAiOjE2NjY3NDEwMjAsImlhdCI6MTY2NjczOTIyMCwianRpIjoiNzc0ZTMzZjQtNGQ3Mi00ZGFhLWE3OGUtMjAzNTY0YzIyYjRhIiwiaXNzIjoiaHR0cDovLzEyNy4wLjAuMTo4MDgwL3JlYWxtcy9tYXN0ZXIiLCJhdWQiOiJodHRwOi8vMTI3LjAuMC4xOjgwODAvcmVhbG1zL21hc3RlciIsInN1YiI6Ijk3OTZiYTc4LTg0YzUtNGJlYi1hM2MzLTg0YWYzNzNjZDU0MSIsInR5cCI6IlJlZnJlc2giLCJhenAiOiJwcml2YXRlIiwic2Vzc2lvbl9zdGF0ZSI6IjUwNGJmYzIwLTcwZmItNGZhZi1hYTk0LTQ1NTI3YzM2YTFjOSIsInNjb3BlIjoib3BlbmlkIGVtYWlsIHByb2ZpbGUiLCJzaWQiOiI1MDRiZmMyMC03MGZiLTRmYWYtYWE5NC00NTUyN2MzNmExYzkifQ.SVb51lxEwinAbMlhxQhSmmdhq9QOL60XTrK4x00G5QU",
  "token_type": "Bearer",
  "id_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJ4N0dPSW9OZUhIYkFvMjM0UWlrRTk1TW5VVnh6bjVQb1Bsb2llSnVTMlBNIn0.eyJleHAiOjE2NjY3MzkyODAsImlhdCI6MTY2NjczOTIyMCwiYXV0aF90aW1lIjoxNjY2NzM5MjIwLCJqdGkiOiI5YTg2MWFjNC04OWFjLTRkZjAtODFiZC1mYmJkMjU3YWYwYzMiLCJpc3MiOiJodHRwOi8vMTI3LjAuMC4xOjgwODAvcmVhbG1zL21hc3RlciIsImF1ZCI6InByaXZhdGUiLCJzdWIiOiI5Nzk2YmE3OC04NGM1LTRiZWItYTNjMy04NGFmMzczY2Q1NDEiLCJ0eXAiOiJJRCIsImF6cCI6InByaXZhdGUiLCJzZXNzaW9uX3N0YXRlIjoiNTA0YmZjMjAtNzBmYi00ZmFmLWFhOTQtNDU1MjdjMzZhMWM5IiwiYXRfaGFzaCI6InRrZTlUQ1lfZWFYdFVKUDk5Q0VnM1EiLCJhY3IiOiIxIiwic2lkIjoiNTA0YmZjMjAtNzBmYi00ZmFmLWFhOTQtNDU1MjdjMzZhMWM5IiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJ1c2VyIn0.TAx3lAQYRrUcZDUVUaajPhNh5vjJoEq-1TaIXxRSuFBHWGTVQk74midfup9F7W6c4facghl-yZaY9urxPDrvEbvDf_Ti1N_HnDhuYMVwpkJN3gRefriSHdX-sdw_1cGa8zaMqZi29ovHwtRvdqUZQXDf1NXJckhWrui0s2wByS8-yI0G0OISU16EjlIM1L4UZdmu4HneK4NoOnmf-IqI9h3yxjVmW8Q-k3TxOGk_STZsvyY6be8cr7c1nDvtg4dLKdFFUryB0gTJjGAgcL04a1pQOTBBDOYQiHV4kk3WlRd28IcSD_J2-IOnQra8_2OrBS-BALjjd6Mfw9YVi_0r2w",
  "not-before-policy": 0,
  "session_state": "504bfc20-70fb-4faf-aa94-45527c36a1c9",
  "scope": "openid email profile"
}

if the user said « unauthorize » or cancel the job, the answer is :

{
  "error": "access_denied",
  "error_description": "not authorized"
}

Is it a decentralized authentication or… impersonation ?

During this example, you noticed that you never put any user password. After answering « s » for Succeed, the application got a token for the user directly.

It means that the « relying-party » is in charge of authentication, by any method it has. If the user is already authenticated, verify the token, refresh it, or ask for a new authentication but also send a push notification for agreement validation. That’s why CIBA is used for many validation processes.

Is it dangerous ?

The protocol itself could be if the relying party is not secured by design. It is in charge of the whole authentication process, a bad implementation (like this example) could authenticate any user without asking for credentials.

So, this protocol with a great implementation is a real big improvement for SSO.

You may find this original article on our partner’s blog : https://blog.please-open.it/ciba-bash/

TL;DR : Their ciba-bash implementation is available here : https://github.com/please-openit/ciba-bash

Mathieu PASSENAUD