Skip to main content

Setting up Keycloak SSO authentication flow and connecting to Centrifugo WebSocket

· 5 min read
Alexander Emelin

Securing user authentication and management can often be a challenging task when developing a modern application. As a result, many developers choose to delegate this responsibility to third-party identity providers, such as Okta, Auth0, or Keycloak.

In this blog post, we'll go through the process of setting up Single Sign-On (SSO) authentication using Keycloak - popular and powerful identity provider. After setting up SSO we will create React application and connect to Centrifugo using access token generated by Keycloak for our test user:

TLDR

The integraion is possible since Centrifugo works with standard JWT for authentication and additionally supports JSON Web Key specification.

Here is a final source code.

Keycloak

First, run Keycloak using the following Docker command:

docker run --rm -it -p 8080:8080 \
-e KEYCLOAK_ADMIN=admin \
-e KEYCLOAK_ADMIN_PASSWORD=admin \
quay.io/keycloak/keycloak:21.0.1 start-dev

After starting Keycloak, go to http://localhost:8080/admin and login. Then perform the following tasks:

  1. Create a new realm named myrealm.
  2. Create a new client named myclient. Set valid redirect URIs to http://localhost:5173/*, and web origins as http://localhost:5173.
  3. Create a user named myuser and set a password for it (in Credentials tab).

See this guide for additional details and illustrations of the process.

Make sure your created client is public (this is default) since we will request token directly from the web application.

Centrifugo

Next, run Centrifugo using the following Docker command:

docker run --rm -it -p 8000:8000 \
-e CENTRIFUGO_ALLOWED_ORIGINS="http://localhost:5173" \
-e CENTRIFUGO_TOKEN_JWKS_PUBLIC_ENDPOINT="http://host.docker.internal:8080/realms/myrealm/protocol/openid-connect/certs" \
-e CENTRIFUGO_ALLOW_USER_LIMITED_CHANNELS=true \
-e CENTRIFUGO_ADMIN=true \
-e CENTRIFUGO_ADMIN_SECRET=secret \
-e CENTRIFUGO_ADMIN_PASSWORD=admin \
centrifugo/centrifugo:v4.1.2 centrifugo

Some comments about environment variables used here:

  • CENTRIFUGO_TOKEN_JWKS_PUBLIC_ENDPOINT allows tell Centrifugo to use JSON Web Key spec when validating tokens, we point to Keycloak's JWKS endpoint
  • CENTRIFUGO_ALLOWED_ORIGINS is required since we will build Vite + React based app running on http://localhost:5173
  • CENTRIFUGO_ALLOW_USER_LIMITED_CHANNELS - not required to connect, but you will see in the source code that we additionally subscribe to a user personal channel
  • CENTRIFUGO_ADMIN, CENTRIFUGO_ADMIN_SECRET, CENTRIFUGO_ADMIN_PASSWORD - to enable Centrifugo admin web UI

Also note we are using host.docker.internal to access host port from inside the Docker network.

React app with Vite

Now, let's create a new React app using very popular Vite tool:

npm create vite@latest keycloak_sso_auth -- --template react
cd keycloak_sso_auth
npm install

Also, install the necessary additional packages for the React app:

npm install --save @react-keycloak/web centrifuge keycloak-js

And start the development server:

npm run dev

Navigate to http://localhost:5173/. You should see default Vite template working, we are going to modify it a bit.

caution

Use localhost, not 127.0.0.1 - since we used localhost for Keyloak and Centrifugo configurations above.

Add the following into main.jsx:

import React from 'react'
import ReactDOM from 'react-dom/client'
import { ReactKeycloakProvider } from '@react-keycloak/web'
import App from './App'
import './index.css'

import Keycloak from "keycloak-js";

const keycloakClient = new Keycloak({
url: "http://localhost:8080",
realm: "myrealm",
clientId: "myclient"
})

ReactDOM.createRoot(document.getElementById('root')).render(
<ReactKeycloakProvider authClient={keycloakClient}>
<React.StrictMode>
<App />
</React.StrictMode>
</ReactKeycloakProvider>,
)

Note that we configured Keycloak instance pointing it to our Keycloak server. We also use @react-keycloak/web package to wrap React app into ReactKeycloakProvider component. It simplifies working with Keycloak by providing some useful hooks - we are using this hook below.

Our App component inside App.jsx may look like this:

import React, { useState, useEffect } from 'react';
import logo from './assets/centrifugo.svg'
import { Centrifuge } from "centrifuge";
import { useKeycloak } from '@react-keycloak/web'
import './App.css'

function App() {
const { keycloak, initialized } = useKeycloak()

if (!initialized) {
return null;
}

return (
<div>
<header>
<p>
SSO with Keycloak and Centrifugo
</p>
{keycloak.authenticated ? (
<div>
<p>Logged in as {keycloak.tokenParsed?.preferred_username}</p>
<button type="button" onClick={() => keycloak.logout()}>
Logout
</button>
</div>
) : (
<button type="button" onClick={() => keycloak.login()}>
Login
</button>
)}
</header>
</div >
);
}

export default App

This is actually enough for SSO flow to start working! You can click on login button and make sure that it's possible to use myuser credentials to log into the application. And log out after that.

The only missing part is Centrifugo. We can initialize connection inside useEffect hook of App component:

useEffect(() => {
if (!initialized || !keycloak.authenticated) {
return;
}
const centrifuge = new Centrifuge("ws://localhost:8000/connection/websocket", {
token: keycloak.token,
getToken: function () {
return new Promise((resolve, reject) => {
keycloak.updateToken(5).then(function () {
resolve(keycloak.token);
}).catch(function (err) {
reject(err);
keycloak.logout();
});
})
}
});

centrifuge.connect();

return () => {
centrifuge.disconnect();
};
}, [keycloak, initialized]);

The important thing here is how we configure tokens: we are using Keycloak client methods to set initial token and refresh the token when required.

I also added some extra elements to the code to make it look a bit nicer. For example, we can listen to Centriffuge client state changes and show connection indicator on the page:

function App() {
const [connectionState, setConnectionState] = useState("disconnected");
const stateToEmoji = {
"disconnected": "🔴",
"connecting": "🟠",
"connected": "🟢"
}
...

useEffect(() => {
...
centrifuge.on('state', function (ctx) {
setConnectionState(ctx.newState);
})
...

return (
...
<span className={"connectionState " + connectionState}>
{stateToEmoji[connectionState]}
</span>

You can find more details about Centrifugo client SDK API and states in client SDK spec.

If you look at source code on Github - you will also find an example of channel subscription to a user personal channel:

function App() {
...
const [publishedData, setPublishedData] = useState("");
...

useEffect(() => {
...
const userChannel = "#" + keycloak.tokenParsed?.sub;
const sub = centrifuge.newSubscription(userChannel);
sub.on("publication", function (ctx) {
setPublishedData(JSON.stringify(ctx.data));
}).subscribe();
...

You can now:

  • test the SSO setup by logging into application
  • making sure connection is successful
  • try publishing a message into a user channel via the Centrifugo Web UI. The published message will appear on application screen in real-time.

That's it! We have successfully set up Keycloak SSO authentication with Centrifugo and a React application. Again, source code is on Github.