Setting up Keycloak SSO authentication flow and connecting to Centrifugo WebSocket
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:
- Create a new realm named
myrealm
. - Create a new client named
myclient
. Set valid redirect URIs tohttp://localhost:5173/*
, and web origins ashttp://localhost:5173
. - 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.
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.