Implementando SSO corporativo con Keycloak y OpenID Connect (paso a paso con un ejemplo práctico)
- Fabrizio Piminchumo
- Application , Technology
- 05 Dec, 2025
El escenario: de auth legacy a SSO corporativo
Partimos de una app interna de ejemplo:
- Nombre:
secrets-admin - URL:
https://secrets-admin.example.com - Tipo: aplicación web interna que gestiona secretos/credenciales.
- Estado actual:
- Maneja su propio login (usuario/clave, JWT casero, etc.).
- Tiene roles internos, pero la autenticación está duplicada y es difícil de mantener.
Objetivo:
- Dejar de manejar contraseñas en la app.
- Usar un Identity Provider (IdP) central: Keycloak en
https://auth.example.com. - Que la app confíe en Keycloak vía OpenID Connect (OIDC) usando Authorization Code Flow.
- Controlar el acceso por roles del realm (RBAC) para cada app.
- Preparar la app para que, en un futuro portal corporativo, funcione con SSO: si el usuario ya está logueado en el portal, entra a la app sin volver a autenticarse.
Arquitectura en una frase:
Keycloak autentica, emite tokens y roles;
la app (y el futuro portal) deciden qué hacer con esa identidad (qué apps y qué pantallas mostrar).
1. Conceptos clave (sin humo)
Antes de tocar código, aclaremos las piezas principales.
Keycloak como Identity Provider (IdP)
Keycloak se encarga de:
- Pantallas de login (usuario/clave, Google, AD, etc.).
- Gestión de usuarios, grupos y roles.
- Emisión de tokens:
id_token: quién eres.access_token: qué puedes hacer (para APIs).refresh_token: para renovar tokens sin volver a pedir credenciales.
Realm
Un realm es como un “universo” aislado dentro de Keycloak:
- Tiene sus propios usuarios, roles y clientes (apps).
- En este ejemplo usaremos el realm
mycompany.
Client (cliente OIDC)
Cada app que usa Keycloak es un client:
- Tiene un
client_id(por ejemplosecrets-admin-web). - Define qué URLs se pueden usar como callback.
- Define qué tipo de flujo OIDC usará (Standard/Implicit/etc.).
Roles del realm (RBAC)
En lugar de que cada app invente sus roles aislados, podemos centralizarlos en el realm:
secrets-admin-usersecrets-admin-admin
Estos roles:
- Viajan dentro del token (
realm_access.roles). - Son usados por:
- El futuro portal para decidir qué apps mostrar.
- La app para decidir qué pantallas/funciones habilitar.
2. Infraestructura mínima: Keycloak accesible por HTTPS
No voy a entrar al detalle de instalación de Keycloak + reverse proxy Nginx, pero el requisito mínimo es:
-
Keycloak accesible en algo como:
https://auth.example.com -
Con un realm
mycompanycreado. -
Detrás de HTTPS (idealmente con un reverse proxy tipo Nginx o Traefik).
A partir de aquí asumimos que puedes entrar a:
https://auth.example.com/adminy ver la consola de administración.- Realm
mycompanyseleccionado.
3. Configuración de Keycloak para la app
3.1. Crear roles del realm
En el realm mycompany:
- Ir a Roles → Add role:
secrets-admin-user
- (Opcional) Crear también:
secrets-admin-admin
Estos serán los permisos globales para esta app.
¿Por qué realm roles y no client roles?
- Realm roles: > - Son globales y fáciles de usar en muchas apps o en un portal.
- Aparecen en
realm_access.rolesde los tokens.- Client roles: > - Están ligados a un cliente específico.
- Son útiles si cada app tiene un modelo de permisos muy propio.
Para un escenario de portal de aplicaciones donde quieres usar los mismos roles para decidir qué apps mostrar, los realm roles simplifican mucho el diseño.
3.2. Crear un usuario de prueba
En mycompany:
- Users → Add user:
username: `usuarioemail: `user@example.com
- Pestaña Credentials:
- Asignar contraseña y quitar “temporary”.
- Pestaña Role mappings:
- Asignar el rol
secrets-admin-user.
- Asignar el rol
3.3. Registrar la app como Client OIDC
Nuestra app vive en:
https://secrets-admin.example.com
En Clients → Create:
- Client ID:
secrets-admin-web - Client type: OpenID Connect
En pestaña de capacidades (Capability config):
Client authentication: Off- Porque esta app es un cliente “público” (no queremos un
client_secretexpuesto en el front).
- Porque esta app es un cliente “público” (no queremos un
Standard flow: On (Authorization Code Flow).Direct access grants (Resource Owner Password): Off.Implicit flow: Off.- Resto de opciones: Off.
¿Por qué Standard Flow y no otros?
- Standard flow (Authorization Code) ✅
- Es el flujo moderno recomendado para apps web.
- No expone tokens en la URL.
- Permite usar PKCE (ideal para SPAs).
- Implicit flow ❌
- Enviar tokens en el fragmento de la URL.
- Hoy se considera legacy y menos seguro.
- Direct Access Grants (Password) ❌
- La app pide usuario/clave y los envía al IdP.
- Va contra el objetivo de NO manejar credenciales en nuestras apps.
Conclusión: para una app web moderna que quiere SSO con un IdP, Standard Flow es la opción correcta.
3.4. Configurar URLs del cliente
En el client secrets-admin-web:
- Root URL
https://secrets-admin.example.com - Home URL
https://secrets-admin.example.com - Valid redirect URIs
(a dónde Keycloak puede mandar al usuario después del login):https://secrets-admin.example.com/* - Valid post logout redirect URIs
(a dónde puede mandar al usuario después del logout):https://secrets-admin.example.com/* - Web origins (para CORS):
https://secrets-admin.example.com
Más adelante podrás añadir también tus URLs de desarrollo (http://localhost:3000/*, por ejemplo).
4. Variables de entorno en la app
En secrets-admin define algo como:
KEYCLOAK_REALM=mycompany KEYCLOAK_CLIENT_ID=secrets-admin-web KEYCLOAK_ISSUER=https://auth.example.com/realms/mycompany KEYCLOAK_AUTH_URL=https://auth.example.com/realms/mycompany/protocol/openid-connect/auth KEYCLOAK_TOKEN_URL=https://auth.example.com/realms/mycompany/protocol/openid-connect/token KEYCLOAK_LOGOUT_URL=https://auth.example.com/realms/mycompany/protocol/openid-connect/logout KEYCLOAK_REDIRECT_URI=https://secrets-admin.example.com/auth/callback KEYCLOAK_SCOPES=openid profile email
Y, para evitar strings mágicos:
// auth/roles.ts export const ROLE_SECRETS_ADMIN_USER = 'secrets-admin-user'; export const ROLE_SECRETS_ADMIN_ADMIN = 'secrets-admin-admin';

5. Flujo OIDC: teoría + implementación práctica
La teoría del flujo Authorization Code es:
- Usuario entra a la app.
- La app detecta “no tengo sesión” → manda al IdP (Keycloak).
- Keycloak autentica al usuario.
- Redirige a la app con
?code=.... - La app canjea ese
codepor tokens (id_token,access_token,refresh_token). - La app crea su sesión interna basada en esos tokens.
- El usuario navega la app sin volver a loguearse… hasta que expira la sesión o hace logout.
Vamos a implementarlo en rutas de ejemplo:
GET /auth/loginGET /auth/callbackGET /auth/logout
El backend puede ser Node/Express, Next.js API routes, NestJS, etc. La idea es la misma.
5.1. /auth/login – construir la URL de Keycloak y redirigir
// auth/login.ts
import { randomBytes } from 'crypto';
export function handleLogin(req, res) {
const state = randomBytes(16).toString('hex');
const nonce = randomBytes(16).toString('hex');
// Guardamos state/nonce temporalmente (cookie o store de sesión)
res.cookie('kc_auth', { state, nonce }, {
httpOnly: true,
sameSite: 'lax',
});
const params = new URLSearchParams({
client_id: process.env.KEYCLOAK_CLIENT_ID!,
response_type: 'code',
scope: process.env.KEYCLOAK_SCOPES!,
redirect_uri: process.env.KEYCLOAK_REDIRECT_URI!,
state,
nonce,
});
const authUrl = `${process.env.KEYCLOAK_AUTH_URL}?${params.toString()}`;
return res.redirect(authUrl);
}
- Entrada: petición sin sesión.
- Salida: redirección al login de Keycloak.
5.2. /auth/callback – recibir el code y crear sesión
// auth/callback.ts
import axios from 'axios';
import jwt_decode from 'jwt-decode';
interface KcTokenResponse {
access_token: string;
refresh_token: string;
id_token: string;
expires_in: number;
}
interface DecodedToken {
sub: string;
preferred_username: string;
email?: string;
realm_access?: { roles?: string[] };
}
export async function handleCallback(req, res) {
const { code, state } = req.query;
if (!code || !state) {
return res.status(400).send('Missing code or state');
}
// Validar state contra lo que guardamos antes
const kcAuth = req.cookies['kc_auth'];
if (!kcAuth || kcAuth.state !== state) {
return res.status(400).send('Invalid state');
}
// Intercambiar el code por tokens en el token endpoint
const params = new URLSearchParams({
grant_type: 'authorization_code',
client_id: process.env.KEYCLOAK_CLIENT_ID!,
code: String(code),
redirect_uri: process.env.KEYCLOAK_REDIRECT_URI!,
});
const tokenRes = await axios.post<KcTokenResponse>(
process.env.KEYCLOAK_TOKEN_URL!,
params,
{ headers: { 'Content-Type': 'application/x-www-form-urlencoded' } },
);
const { access_token, refresh_token, expires_in, id_token } = tokenRes.data;
const decoded = jwt_decode<DecodedToken>(id_token);
const roles = decoded.realm_access?.roles ?? [];
const now = Date.now();
const expiresAt = now + (expires_in * 1000) - 10_000; // margen de 10s
const session = {
kcSub: decoded.sub,
username: decoded.preferred_username,
email: decoded.email,
roles,
refreshToken: refresh_token,
expiresAt,
};
// Guardar sesión en cookie firmada o en store de sesiones
res.cookie('app_session', session, {
httpOnly: true,
secure: true,
sameSite: 'lax',
});
// Limpiar cookie temporal
res.clearCookie('kc_auth');
return res.redirect('/'); // o /dashboard
}
- Entrada:
codeystatedesde Keycloak. - Proceso:
- Llamar a
/token. - Decodificar
id_token. - Extraer roles del realm.
- Crear sesión interna de la app.
- Llamar a
- Salida: cookie de sesión + redirección al home.
6. Proteger rutas y aplicar RBAC
Ahora que la app tiene una sesión propia, toca proteger rutas y validar roles.
6.1. requireAuth – forzar autenticación
// middleware/auth.ts
export function requireAuth(req, res, next) {
const session = req.cookies['app_session'];
if (!session) {
return res.redirect('/auth/login');
}
const now = Date.now();
if (session.expiresAt <= now) {
// Podrías intentar un refresh; aquí simplificamos
res.clearCookie('app_session');
return res.redirect('/auth/login');
}
req.user = session;
return next();
}
Uso típico:
// middleware/roles.ts
import {
ROLE_SECRETS_ADMIN_USER,
ROLE_SECRETS_ADMIN_ADMIN,
} from '../auth/roles';
export function userHasRole(user, role: string): boolean {
return Array.isArray(user.roles) && user.roles.includes(role);
}
export function requireRole(requiredRole: string) {
return (req, res, next) => {
const user = req.user;
if (!userHasRole(user, requiredRole)) {
return res.status(403).send('Forbidden');
}
next();
};
}
Con esto:
- Aunque alguien conozca la URL de
https://secrets-admin.example.com/admin,
no ve nada si no tiene el rol correcto en Keycloak. `
7. Logout SSO: app + Keycloak
Para cerrar sesión tanto en la app como en el IdP:
// auth/logout.ts
export function handleLogout(req, res) {
const redirect = encodeURIComponent('https://secrets-admin.example.com/');
// Borrar sesión local
res.clearCookie('app_session');
const logoutUrl =
`${process.env.KEYCLOAK_LOGOUT_URL}` +
`?post_logout_redirect_uri=${redirect}` +
`&client_id=${process.env.KEYCLOAK_CLIENT_ID}`;
return res.redirect(logoutUrl);
}
- La app borra su cookie de sesión.
- Keycloak cierra la sesión SSO.
- El usuario vuelve al home de la app (o al portal, si así lo configuras).
8. ¿Y si desde el admin de Keycloak cierro la sesión?
En la consola de Keycloak puedes ir a:
Users → (usuario) → Sessions → LogoutEso:- Mata la SSO session en Keycloak.
- Invalida tokens/refresh tokens asociados.
Pero:
- Tu cookie local no se borra sola (Keycloak no controla tu dominio).
¿Cómo se entera la app?
- Cuando intente:
- refrescar el token (
refresh_token), o - usar el
access_tokencontra una API que lo valide,
- refrescar el token (
- Keycloak responderá con error → en ese momento tú puedes:
- borrar la sesión local,
- redirigir a
/auth/login.
Para la mayoría de escenarios internos, esto es suficiente:
el usuario deja de tener acceso efectivo en cuanto intenta hacer algo relevante.
9. Encajando esto con un futuro portal de aplicaciones
La idea típica de un portal corporativo es:
- El usuario entra a
https://portal.example.com. - El portal se integra con Keycloak como otro client OIDC (
portal-web). - Después del login, el token del portal contiene roles como:
"realm_access": {
"roles": [
"secrets-admin-user",
"billing-app-user"
]
}
- El portal muestra solo los “tiles” de las apps para las que el usuario tiene rol.
- Al hacer clic en Secrets Admin:
- Abre
https://secrets-admin.example.com. - Si la app no tiene sesión local, redirige a Keycloak.
- Keycloak ve que ya hay SSO activo por el portal → no pide credenciales, solo devuelve
code. - La app crea su sesión y listo, sin doble login.
- Abre
Puntos importantes:
- El portal no es el único guardia:
- La app sigue validando roles.
- Ambos confían en el mismo IdP (Keycloak) y el mismo realm
mycompany. - El portal controla la UX; cada app controla su propia seguridad interna.
10. Resumen y checklist final
En este tutorial vimos, con un caso práctico:
- Teoría:
- Qué es un realm, un client, un role.
- Qué flujos OIDC existen y por qué usamos Standard Flow.
- Cómo encaja Keycloak como IdP con un portal y varias apps.
- Práctica:
- Crear roles del realm para una app (
secrets-admin-user,secrets-admin-admin). - Registrar la app como cliente OIDC (
secrets-admin-web). - Configurar URLs de redirect, logout y web origins.
- Implementar:
/auth/login→ redirige a Keycloak./auth/callback→ code → tokens → sesión./auth/logout→ cierra sesión local + SSO.
- Crear middlewares
requireAuthyrequireRolepara aplicar RBAC.
- Crear roles del realm para una app (
Checklist rápido para aplicar esto a tu propia app:
- Instalar y exponer Keycloak con HTTPS (
https://auth.tu-dominio.com). - Crear un realm (por ejemplo,
mycompany). - Definir roles del realm para cada app (ej.
APP_X_USER,APP_X_ADMIN). - Crear un client OIDC por app (
app-x-web) usando Standard Flow. - Configurar:
Valid redirect URIsValid post logout redirect URIsWeb origins
- En tu app:
- Variables de entorno con URLs de Keycloak.
- Rutas
/auth/login,/auth/callback,/auth/logout. - Sesión interna basada en los tokens.
- Middlewares
requireAuthyrequireRole.
- Probar:
- Login completo.
- Acceso con y sin roles.
- Logout y comportamiento cuando se revoca la sesión desde Keycloak.
Con eso ya tienes una base sólida para ir migrando aplicaciones internas a un esquema de SSO centralizado con Keycloak, y preparar el terreno para tu propio portal de aplicaciones corporativas.







