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://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
- How to enrich native metrics in KeyCloak - 21 août 2024
- Keycloak Authenticator explained - 7 mars 2024
- Keycloak OIDC authentication with N8N workflow - 1 décembre 2023