Integrare Keycloak: 5 Decisioni che devi prendere subito

Hai Keycloak avviato con Docker, la console admin davanti, e sai cosa sono realm, client e utenti. Adesso devi collegarlo a un’applicazione vera, in questo esempio consideriamo una web application composta da un frontend React e un backend Node.js.
L’integrazione in sé non è complessa, ma ci sono diverse scelte da prendere in base al contesto in cui andiamo a operare. Lo scopo di questo articolo è proprio quello di vedere in modo concreto quali sono alcune di queste scelte e i ragionamenti dietro ognuna di esse. Alla fine avremo un login funzionante end-to-end e, soprattutto, capiremo perché lo abbiamo implementato in un certo modo.
Per seguire passo passo questo articolo, crea un realm
mia-appdalla console admin, un clientfrontend(public, Standard flow, redirect URIhttp://localhost:3000/*, web originhttp://localhost:3000) e un utente con password non temporanea. Se non sai come fare, la documentazione ufficiale ti guida passo passo.
1. Public o Confidential?
Quando crei un client in Keycloak, la prima scelta è: Client authentication ON o OFF?
- ON (confidential): il client ha un secret che serve per ottenere i token. Adatto a backend e servizi server-side che possono custodirlo in modo sicuro.
- OFF (public): nessun secret, il client si identifica solo tramite il client ID. Adatto a frontend SPA e app mobile, dove il codice è esposto all’utente.
Ma se non c’è un secret, come fa Keycloak a fidarsi? Due meccanismi. Il primo è il redirect URI: dopo il login, Keycloak reindirizza l’utente solo verso gli URL che hai configurato nel client. Il secondo è PKCE (acronimo di Proof Key for Code Exchange): un codice monouso generato dal frontend che impedisce a un attaccante di intercettare l’authorization code e scambiarlo al posto tuo. Da keycloak-js versione 24 in poi, PKCE è abilitato di default.
2. check-sso o login-required?
Siamo nel frontend. Per integrare Keycloak esistono diverse librerie, ma keycloak-js è l’adapter ufficiale mantenuto dal team Keycloak, la scelta più sicura in termini di compatibilità e aggiornamenti.
Quando inizializzi keycloak-js, devi decidere come gestire l’autenticazione:
login-required: l’utente viene subito reindirizzato a Keycloak. Se non è autenticato, vede il form di login. Nessuna pagina della tua app è visibile senza autenticazione. Ideale se l’applicazione nella sua interezza deve essere protetta.check-sso: la libreria verifica silenziosamente (via iframe nascosto) se esiste una sessione attiva su Keycloak. Se sì, l’utente entra. Altrimenti, vede la tua app in stato “non autenticato”: un bottone Login, una landing page, un messaggio di benvenuto. Adatto quando hai una parte pubblica e una protetta.
Nel nostro caso scegliamo check-sso: vogliamo mostrare una pagina pubblica con un bottone di login, non forzare il redirect immediato.
⚠️
check-ssopuò essere combinato con un silent check via iframe per evitare il redirect completo. Questa modalità dipende dai cookie di terze parti e può fallire browser con protezioni anti-tracking attive. In quel caso,keycloak-jsricade automaticamente sul redirect standard.
Ecco come appare l’inizializzazione in React:
import Keycloak from 'keycloak-js';
import { useEffect, useRef, useState } from 'react';
const keycloak = new Keycloak({
url: 'http://localhost:8080',
realm: 'mia-app',
clientId: 'frontend',
});
function App() {
const [authenticated, setAuthenticated] = useState(false);
const initialized = useRef(false);
useEffect(() => {
if (initialized.current) return;
initialized.current = true;
keycloak.init({ onLoad: 'check-sso' }).then(setAuthenticated);
}, []);
if (!authenticated) {
return <button onClick={() => keycloak.login()}>Login</button>;
}
return (
<div>
<h1>Ciao, {keycloak.tokenParsed.name}</h1>
<p>{keycloak.tokenParsed.email}</p>
<button onClick={() => keycloak.logout()}>Logout</button>
</div>
);
}
3. Realm roles o client roles?
Con le prime due scelte abbiamo sistemato l’autenticazione: sappiamo chi abbiamo davanti. Ora serve capire cosa può fare, ovvero gestire l’autorizzazione. In Keycloak l’autorizzazione si modella con i ruoli, che si possono definire a due livelli diversi. La scelta cambia la struttura del JWT.
- Realm roles: globali, condivisi tra tutti i client del realm. Li trovi in
realm_access.rolesnel token. Semplici da gestire quando hai un solo servizio, ma diventano rumorosi quando aggiungi altri servizi, dato che ogni client vede tutti i ruoli. - Client roles: associati a un client specifico (es. il tuo backend). Li trovi in
resource_access.{client-id}.roles. Isolati per servizio, ma richiedono un client dedicato per il backend. Inoltre devi configurare un role mapping dalla console admin per associare ogni utente (o gruppo) ai ruoli del client.
Per un progetto con un solo backend, i realm roles sono sufficienti e più rapidi da configurare. Se prevedi più servizi con ruoli diversi (un backoffice admin, un’API pubblica, un worker), parti con i client roles.
La scelta si riflette direttamente nella struttura del JWT. Con i realm roles, i ruoli finiscono in un campo globale visibile a tutti i client:
{
"realm_access": {
"roles": ["admin", "user"]
}
}
Con i client roles, ogni client ha il proprio blocco isolato:
{
"resource_access": {
"backend": {
"roles": ["admin"]
},
"backoffice": {
"roles": ["editor"]
}
}
}
Nel backend, la differenza si riduce al path da cui leggi i ruoli. Un middleware Express che verifica un realm role:
// Estrae i ruoli dal campo globale del token
function requireRole(role) {
return (req, res, next) => {
const roles = req.user.realm_access?.roles || [];
if (!roles.includes(role)) return res.status(403).json({ error: 'Accesso negato' });
next();
};
}
// Solo gli utenti con il ruolo "admin" possono accedere
app.get('/admin/stats', authMiddleware, requireRole('admin'), (req, res) => {
res.json({ users: 142, orders: 89 });
});
Con i client roles, cambia solo il path: req.user.resource_access?.['backend']?.roles al posto di req.user.realm_access?.roles.
4. Frontchannel o backchannel logout?
Quando un utente fa login, si creano due sessioni distinte: la sessione SSO su Keycloak, condivisa tra tutte le app del realm, e la sessione applicativa di ogni singola app (i token in memoria nel frontend, un’eventuale sessione server-side nel backend). Sono indipendenti: distruggere una non distrugge automaticamente l’altra.
keycloak.logout() redirige l’utente a Keycloak, che distrugge la sessione SSO. Fin qui semplice. Ma se il realm ha più applicazioni collegate, le altre come fanno a sapere che devono invalidare la propria sessione applicativa?
Keycloak offre due meccanismi, configurabili nel client dalla console admin:
- Frontchannel logout: Keycloak notifica le altre app tramite il browser dell’utente, usando redirect o iframe nascosti. Semplice da attivare, non serve codice backend. Ma dipende dal browser, se l’utente ha chiuso il tab, o se il browser blocca i cookie di terze parti, la notifica non arriva.
- Backchannel logout: Keycloak invia una richiesta HTTP POST direttamente al backend di ogni app registrata. Non dipende dal browser, funziona anche se l’utente ha chiuso tutto. Ma richiede che ogni backend esponga un endpoint di logout e mantenga uno stato per sapere quali sessioni invalidare.
Per una singola app, la distinzione è irrilevante dato che keycloak.logout() distrugge comunque la sessione, ma se stai costruendo un ecosistema con più servizi (e il motivo per cui hai scelto Keycloak è probabilmente quello), il backchannel è la scelta più robusta.
// keycloak-js usa il logout verso Keycloak di default
keycloak.logout();
// Con redirect esplicito dopo il logout
keycloak.logout({ redirectUri: 'http://localhost:3000' });
⚠️ Ricorda di aggiungere
http://localhost:3000nei Valid post logout redirect URIs del client, altrimenti Keycloak mostrerà un errore dopo il logout.
5. Cosa salvare dell’utente: sub, email o username?
Quando il backend riceve il JWT, il payload contiene diversi identificativi dell’utente:
{
"sub": "a1b2c3d4-...",
"name": "Mario Rossi",
"email": "mario@example.com",
"preferred_username": "mario",
"realm_access": { "roles": ["default-roles-mia-app"] },
"iss": "http://localhost:8080/realms/mia-app",
"exp": 1739280000
}
Se devi associare dati a un utente nel tuo database, quale campo usi come chiave? La tentazione è email o preferred_username: sono leggibili, li riconosci.
Ma entrambi possono cambiare. Un utente può aggiornare la propria email. Un admin può rinominare uno username. Se il tuo database usa l’email come chiave esterna e l’utente la cambia, perdi l’associazione.
Il campo sub è l’unico stabile e immutabile. È un UUID assegnato da Keycloak alla creazione dell’utente e non cambia mai, indipendentemente da cosa l’utente o l’admin modificano nel profilo. Usa sub come foreign key nel tuo database.
Esempio completo con Docker Compose
Un Docker Compose per avviare Keycloak, frontend e backend:
services:
keycloak:
image: quay.io/keycloak/keycloak:26.2
command: start-dev
environment:
KC_BOOTSTRAP_ADMIN_USERNAME: admin
KC_BOOTSTRAP_ADMIN_PASSWORD: admin
ports:
- '8080:8080'
frontend:
build: ./frontend
ports:
- '3000:3000'
depends_on:
- keycloak
backend:
build: ./backend
ports:
- '4000:4000'
depends_on:
- keycloak
Una nota: depends_on avvia i container nell’ordine giusto ma non aspetta che Keycloak sia pronto. Il server impiega 10-15 secondi ad avviarsi, in sviluppo basta ricaricare la pagina. Per questo, servirebbe un meccanismo di health check o un wait-for-it script che blocca frontend e backend finché Keycloak non risponde.
⚠️
start-devè solo per lo sviluppo locale. In produzione servestartcon HTTPS e un database esterno, come spiegato nell’articolo precedente.
Errori comuni al primo setup
“Invalid redirect URI”: il redirect URI nel client non corrisponde all’URL della tua app. Controlla protocollo, porta e path:
http://localhost:3000ehttp://localhost:3000/sono diversi per Keycloak.Errore CORS: verifica che Web origins nel client Keycloak includa
http://localhost:3000e che il backend abbiacors()con la stessa origine.Password rifiutata: hai creato l’utente nel realm
masterinvece che inmia-app, oppure il flag Temporary è ancora attivo.
Conclusione
Cinque decisioni, cinque ragionamenti. Il codice è poco — un componente React, un middleware Express, un Docker Compose. Ma sapere perché il client è public, perché i realm roles bastano all’inizio e quando il logout deve propagarsi a Keycloak ti evita sorprese quando il progetto cresce.
Questo è il flusso base: un frontend e un backend. Ma cosa succede quando i servizi diventano più di uno e devono parlarsi tra loro? E quando Keycloak ha un URL diverso tra il browser e i container Docker? Nel prossimo articolo vediamo le trappole che funzionano solo su localhost.
Risorse utili:
- keycloak-js Documentation - adapter ufficiale per frontend JavaScript
- Libreria jose - validazione JWT in Node.js, leggera e ben mantenuta
- Keycloak Admin Console Guide - guida completa alla console di amministrazione
- Authorization Code Flow (OAuth 2.0) - il flusso usato in questo articolo
🔗 Leggi anche:










