Escribe algo para buscar...
Implementando SSO corporativo con Keycloak y OpenID Connect (paso a paso con un ejemplo práctico)

Implementando SSO corporativo con Keycloak y OpenID Connect (paso a paso con un ejemplo práctico)

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:

  1. Dejar de manejar contraseñas en la app.
  2. Usar un Identity Provider (IdP) central: Keycloak en https://auth.example.com.
  3. Que la app confíe en Keycloak vía OpenID Connect (OIDC) usando Authorization Code Flow.
  4. Controlar el acceso por roles del realm (RBAC) para cada app.
  5. 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 ejemplo secrets-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-user
  • secrets-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 mycompany creado.

  • 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/admin y ver la consola de administración.
  • Realm mycompany seleccionado.

3. Configuración de Keycloak para la app

3.1. Crear roles del realm

En el realm mycompany:

  1. Ir a Roles → Add role:
    • secrets-admin-user
  2. (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.roles de 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:

  1. Users → Add user:
  2. Pestaña Credentials:
    • Asignar contraseña y quitar “temporary”.
  3. Pestaña Role mappings:
    • Asignar el rol secrets-admin-user.

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_secret expuesto en el front).
  • 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:

  1. Usuario entra a la app.
  2. La app detecta “no tengo sesión” → manda al IdP (Keycloak).
  3. Keycloak autentica al usuario.
  4. Redirige a la app con ?code=....
  5. La app canjea ese code por tokens (id_token, access_token, refresh_token).
  6. La app crea su sesión interna basada en esos tokens.
  7. 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/login
  • GET /auth/callback
  • GET /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: code y state desde Keycloak.
  • Proceso:
    • Llamar a /token.
    • Decodificar id_token.
    • Extraer roles del realm.
    • Crear sesión interna de la app.
  • 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 → Logout Eso:
  • 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_token contra una API que lo valide,
  • 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:

  1. El usuario entra a https://portal.example.com.
  2. El portal se integra con Keycloak como otro client OIDC (portal-web).
  3. Después del login, el token del portal contiene roles como:
"realm_access": {
	"roles": [
		"secrets-admin-user",
		"billing-app-user"
	]
}
  1. El portal muestra solo los “tiles” de las apps para las que el usuario tiene rol.
  2. 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.

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 requireAuth y requireRole para aplicar RBAC.

Checklist rápido para aplicar esto a tu propia app:

  1. Instalar y exponer Keycloak con HTTPS (https://auth.tu-dominio.com).
  2. Crear un realm (por ejemplo, mycompany).
  3. Definir roles del realm para cada app (ej. APP_X_USER, APP_X_ADMIN).
  4. Crear un client OIDC por app (app-x-web) usando Standard Flow.
  5. Configurar:
    • Valid redirect URIs
    • Valid post logout redirect URIs
    • Web origins
  6. 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 requireAuth y requireRole.
  7. 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.

Related Posts

Introducción al flujo de trabajo en GitHub

Introducción al flujo de trabajo en GitHub

El flujo de GitHub Además de ser una plataforma de desarrollo de software colaborativo, GitHub ofrece también un flujo de trabajo diseñado para optimizar el uso de sus diversas características. Au

leer más
Comandos de Git

Comandos de Git

Git es un sistema de control de versiones que permite a los desarrolladores colaborar en proyectos de software y mantener un historial de cambios en el código fuente. A continuación, se describen algu

leer más
Instalación de n8n con Docker: Guía Completa

Instalación de n8n con Docker: Guía Completa

En esta guía explicaremos cómo instalar n8n utilizando Docker, la forma recomendada para la mayoría de casos de uso. Docker proporciona un entorno aislado y limpio, evita incompatibilidades en

leer más
Descargue y configurar el servidor OpenVPN en Ubuntu

Descargue y configurar el servidor OpenVPN en Ubuntu

InstalaciónDescargar openvpn-install.sh💻 $ wget https://git.io/vpn -O openvpn-install.sh... Saving to: ‘openvpn-install.sh’ openvpn-install.sh 100%[====

leer más
Desplegando n8n en una VM de GCP con Cloudflare Tunnel

Desplegando n8n en una VM de GCP con Cloudflare Tunnel

En esta guía explico cómo instalar y exponer n8n en una máquina virtual de Google Cloud Platform (GCP) utilizando Cloudflare Tunnel. La idea es mantener la instancia lo más ligera posible,

leer más
Descargar Nginx y configurar el Reverse Proxy en Ubuntu

Descargar Nginx y configurar el Reverse Proxy en Ubuntu

Paso 1: Actualizar repositorios Antes de comenzar, debemos asegurarnos de que los repositorios estén actualizados. Para ello, ejecutamos el siguiente comando en la terminal: sudo apt

leer más
Guía Completa del Servidor Hytale: Configuración y Administración

Guía Completa del Servidor Hytale: Configuración y Administración

Guía Completa del Servidor Hytale: Configuración y Administración ¿Quieres crear tu propio servidor de Hytale pero no sabes por dónde empezar? Esta guía te llevará paso a paso por todo lo que neces

leer más
Guía Completa de Markdown: Domina el Lenguaje de Marcado Ligero

Guía Completa de Markdown: Domina el Lenguaje de Marcado Ligero

¿Qué es Markdown? Markdown es un lenguaje de marcado ligero creado por John Gruber en 2004. Está diseñado para ser fácil de leer y escribir, utilizando una sintaxis de texto plano que se puede

leer más