sacg-cronogramas/tutorial-keycloak-completo.md
luis cespedes db0815f3ed Soluciona el problema de sincronización de grupos LDAP en el tutorial de Keycloak
Soluciona un problema por el que los miembros del grupo no se mostraban en Keycloak
después de la sincronización LDAP.

Explica la importancia de configurar «Membership Attribute Type» a «UID»
en lugar de «DN» en la configuración del mapeador de grupos LDAP. Esto asegura
correcta recuperación de miembros basada en el simple ID de usuario almacenado en la estructura LDAP.
de LDAP.

Explains the importance of setting "Membership Attribute Type" to "UID"
instead of "DN" in the LDAP group mapper configuration. This ensures
correct member retrieval based on the simple user ID stored in the LDAP
structure.
2025-05-13 18:21:33 -04:00

1492 lines
42 KiB
Markdown

# Tutorial Completo: Implementación de Keycloak con LDAP e Integración con Angular
Este tutorial comprensivo explica cómo configurar un sistema de autenticación completo utilizando Keycloak como proveedor de identidad, OpenLDAP como directorio de usuarios, y cómo integrar este sistema con una aplicación Angular moderna. La guía cubre desde la configuración del entorno básico hasta las técnicas más avanzadas de integración con Angular 19.
## Índice
1. [Preparación del entorno](#1-preparación-del-entorno)
2. [Instalación y configuración de OpenLDAP](#2-instalación-y-configuración-de-openldap)
3. [Crear estructura LDAP para usuarios y grupos](#3-crear-estructura-ldap-para-usuarios-y-grupos)
4. [Instalación y configuración de Keycloak](#4-instalación-y-configuración-de-keycloak)
5. [Configuración de Keycloak en la interfaz web](#5-configuración-de-keycloak-en-la-interfaz-web)
6. [Integración con Angular: Enfoque básico](#6-integración-con-angular-enfoque-básico)
7. [Integración con Angular 19: Enfoque moderno](#7-integración-con-angular-19-enfoque-moderno)
8. [Servicios de autenticación avanzados](#8-servicios-de-autenticación-avanzados)
9. [Guardias de ruta e interceptores HTTP](#9-guardias-de-ruta-e-interceptores-http)
10. [Componentes de UI para login/logout](#10-componentes-de-ui-para-loginlogout)
11. [Arquitectura del sistema](#11-arquitectura-del-sistema)
12. [Resolución de problemas comunes](#12-resolución-de-problemas-comunes)
13. [Verificación y prueba del sistema](#13-verificación-y-prueba-del-sistema)
14. [Recursos adicionales](#14-recursos-adicionales)
15. [Resumen](#15-resumen)
## 1. Preparación del entorno
### Requisitos del sistema
Para seguir este tutorial, necesitarás:
- Un sistema Ubuntu Server o una distribución similar de Linux
- Acceso de root o privilegios sudo
- Node.js (versión 18.x o superior)
- Angular CLI (versión 19.x o compatible)
- Java JDK (OpenJDK 17 o superior)
### Actualización del sistema e instalación de Java
Primero, actualiza el sistema e instala Java:
```bash
sudo apt update
sudo apt upgrade -y
sudo apt install openjdk-17-jdk -y
```
Verifica la instalación de Java:
```bash
java -version
```
### Instalación de Node.js y Angular CLI
Para el desarrollo de aplicaciones Angular, instala Node.js y Angular CLI:
```bash
# Instalar Node.js
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt-get install -y nodejs
# Verificar la instalación
node -v
npm -v
# Instalar Angular CLI
npm install -g @angular/cli
# Verificar la instalación
ng version
```
## 2. Instalación y configuración de OpenLDAP
### Instalar OpenLDAP y utilidades
```bash
sudo apt install slapd ldap-utils -y
```
Durante la instalación, se te pedirá configurar una contraseña de administrador para LDAP.
### Reconfigurar LDAP con el dominio correcto
```bash
sudo dpkg-reconfigure slapd
```
En la configuración:
1. "¿Omitir configuración del servidor LDAP?" → No
2. "Nombre de dominio DNS:" → **correos.com**
3. "Nombre de la organización:" → Correos Org
4. "Contraseña de administrador:" → [tu contraseña segura]
5. "Confirmar contraseña:" → [repetir la contraseña]
6. "Motor de base de datos:" → MDB
7. "¿Quiere que se elimine la base de datos cuando se purgue slapd?" → No
8. "¿Mover la base de datos antigua?" → Sí
### Verificar que LDAP se esté ejecutando correctamente
```bash
sudo systemctl status slapd
```
### Comprobar la conexión LDAP básica
```bash
ldapsearch -x -H ldap://localhost -b dc=correos,dc=com -D "cn=admin,dc=correos,dc=com" -W
```
### Instalar phpLDAPadmin para la gestión gráfica
```bash
sudo apt install phpldapadmin -y
```
### Configurar phpLDAPadmin
Edita el archivo de configuración:
```bash
sudo nano /etc/phpldapadmin/config.php
```
Busca y modifica las siguientes líneas:
```php
$servers->setValue('server','host','127.0.0.1');
$servers->setValue('server','base',array('dc=correos,dc=com'));
$servers->setValue('login','bind_id','cn=admin,dc=correos,dc=com');
```
Y cambia esta línea:
```php
$servers->setValue('login','anon_bind',true);
```
por:
```php
$servers->setValue('login','anon_bind',false);
```
Reinicia el servidor web:
```bash
sudo systemctl restart apache2
```
## 3. Crear estructura LDAP para usuarios y grupos
### Crear unidades organizativas
Crea un archivo para las unidades organizativas:
```bash
nano ~/ou.ldif
```
Con el siguiente contenido:
```ldif
dn: ou=grupos,dc=correos,dc=com
objectClass: organizationalUnit
ou: grupos
dn: ou=usuarios,dc=correos,dc=com
objectClass: organizationalUnit
ou: usuarios
```
Aplica los cambios:
```bash
ldapadd -x -D cn=admin,dc=correos,dc=com -W -f ~/ou.ldif
```
### Crear grupos LDAP
Crea un archivo para los grupos:
```bash
nano ~/grupos.ldif
```
Con el siguiente contenido:
```ldif
dn: cn=administradores,ou=grupos,dc=correos,dc=com
objectClass: posixGroup
cn: administradores
gidNumber: 1000
dn: cn=desarrolladores,ou=grupos,dc=correos,dc=com
objectClass: posixGroup
cn: desarrolladores
gidNumber: 1001
dn: cn=usuarios,ou=grupos,dc=correos,dc=com
objectClass: posixGroup
cn: usuarios
gidNumber: 1002
```
Aplica los cambios:
```bash
ldapadd -x -D cn=admin,dc=correos,dc=com -W -f ~/grupos.ldif
```
### Crear usuarios LDAP
Primero, genera contraseñas encriptadas para los usuarios:
```bash
slappasswd -s "password123"
```
Anota el hash resultante para usarlo en el siguiente archivo.
Crea un archivo para los usuarios:
```bash
nano ~/usuarios.ldif
```
Con el siguiente contenido (reemplazando {HASH} con el hash que generaste):
```ldif
dn: uid=admin,ou=usuarios,dc=correos,dc=com
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
uid: admin
sn: Admin
givenName: Admin
cn: Admin User
displayName: Admin User
uidNumber: 1000
gidNumber: 1000
userPassword: {HASH}
loginShell: /bin/bash
homeDirectory: /home/admin
mail: admin@correos.com
dn: uid=developer,ou=usuarios,dc=correos,dc=com
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
uid: developer
sn: Developer
givenName: Dev
cn: Dev User
displayName: Developer User
uidNumber: 1001
gidNumber: 1001
userPassword: {HASH}
loginShell: /bin/bash
homeDirectory: /home/developer
mail: developer@correos.com
dn: uid=user,ou=usuarios,dc=correos,dc=com
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
uid: user
sn: User
givenName: Normal
cn: Normal User
displayName: Normal User
uidNumber: 1002
gidNumber: 1002
userPassword: {HASH}
loginShell: /bin/bash
homeDirectory: /home/user
mail: user@correos.com
```
Aplica los cambios:
```bash
ldapadd -x -D cn=admin,dc=correos,dc=com -W -f ~/usuarios.ldif
```
### Asociar usuarios a grupos
Crea un archivo para las membresías:
```bash
nano ~/miembros.ldif
```
Con el siguiente contenido:
```ldif
dn: cn=administradores,ou=grupos,dc=correos,dc=com
changetype: modify
add: memberUid
memberUid: admin
dn: cn=desarrolladores,ou=grupos,dc=correos,dc=com
changetype: modify
add: memberUid
memberUid: developer
dn: cn=usuarios,ou=grupos,dc=correos,dc=com
changetype: modify
add: memberUid
memberUid: user
```
Aplica los cambios:
```bash
ldapmodify -x -D cn=admin,dc=correos,dc=com -W -f ~/miembros.ldif
```
### Verificar la estructura LDAP
```bash
ldapsearch -x -H ldap://localhost -b dc=correos,dc=com -D "cn=admin,dc=correos,dc=com" -W
```
## 4. Instalación y configuración de Keycloak
### Descargar e instalar Keycloak
```bash
# Crear directorio para Keycloak
mkdir -p ~/keycloak
cd ~/keycloak
# Descargar la última versión de Keycloak
wget https://github.com/keycloak/keycloak/releases/download/26.2.4/keycloak-26.2.4.tar.gz
# Extraer el archivo
tar -xvzf keycloak-26.2.4.tar.gz
# Mover a una ubicación más adecuada
sudo mv keycloak-26.2.4 /opt/keycloak
# Crear un usuario para Keycloak
sudo useradd -r -s /sbin/nologin keycloak
# Asignar permisos
sudo chown -R keycloak:keycloak /opt/keycloak
```
### Configurar usuario administrador inicial para Keycloak
Crea un archivo de propiedades:
```bash
sudo nano /opt/keycloak/conf/keycloak.conf
```
Agrega estas líneas:
```
# Configuración básica
http-port=8080
https-port=8443
hostname=localhost
# Configuración de administrador inicial
http-enabled=true
```
### Iniciar Keycloak en modo desarrollo
```bash
cd /opt/keycloak
export KEYCLOAK_ADMIN=admin
export KEYCLOAK_ADMIN_PASSWORD=admin
sudo -u keycloak bin/kc.sh start-dev
```
Esto iniciará Keycloak con un usuario administrador "admin" y contraseña "admin".
### Configurar Keycloak como servicio
En otra terminal, crea un archivo de servicio systemd:
```bash
sudo nano /etc/systemd/system/keycloak.service
```
Con el siguiente contenido:
```ini
[Unit]
Description=Keycloak Application Server
After=network.target
[Service]
Type=idle
User=keycloak
Group=keycloak
Environment="KEYCLOAK_ADMIN=admin"
Environment="KEYCLOAK_ADMIN_PASSWORD=admin"
ExecStart=/opt/keycloak/bin/kc.sh start-dev
TimeoutStartSec=600
TimeoutStopSec=600
[Install]
WantedBy=multi-user.target
```
Habilita e inicia el servicio:
```bash
sudo systemctl daemon-reload
sudo systemctl enable keycloak
sudo systemctl start keycloak
```
## 5. Configuración de Keycloak en la interfaz web
Ahora puedes acceder a la consola de administración de Keycloak en http://localhost:8080/admin/ e iniciar sesión con:
- Usuario: `admin`
- Contraseña: `admin`
### Crear un nuevo Reino (Realm)
1. Haz clic en el menú desplegable superior izquierdo que dice "master"
2. Selecciona "Create Realm"
3. Ingresa el nombre: `angular-app`
4. Haz clic en "Create"
### Configurar la Federación de Usuarios LDAP
1. En el menú lateral izquierdo, selecciona "User Federation"
2. Haz clic en "Add provider" → "ldap"
3. Completa los siguientes campos:
- Console Display Name: `LDAP`
- Vendor: `Other`
- Connection URL: `ldap://localhost:389`
- Enable StartTLS: `OFF`
- Bind Type: `simple`
- Bind DN: `cn=admin,dc=correos,dc=com`
- Bind Credential: (la contraseña de admin LDAP)
- Edit Mode: `WRITABLE`
- Users DN: `ou=usuarios,dc=correos,dc=com`
- Username LDAP attribute: `uid`
- RDN LDAP attribute: `uid`
- UUID LDAP attribute: `entryUUID`
- User Object Classes: `inetOrgPerson, posixAccount`
- Custom User LDAP Filter: (dejar en blanco)
- Search Scope: `One Level`
4. Haz clic en "Test connection" y "Test authentication" para verificar
5. Guarda la configuración
6. En la pantalla del proveedor LDAP, ve a la pestaña "Synchronization"
7. Haz clic en "Sync all users"
### Configurar el Mapeo de Grupos LDAP
1. En la pantalla del proveedor LDAP, ve a la pestaña "Mappers"
2. Haz clic en "Create"
3. Completa:
- Name: `group-mapper`
- Mapper Type: `group-ldap-mapper`
- LDAP Groups DN: `ou=grupos,dc=correos,dc=com`
- Group Object Classes: `posixGroup`
- Membership LDAP Attribute: `memberUid`
- Group Name LDAP Attribute: `cn`
- **Membership Attribute Type: `UID`** (¡IMPORTANTE! Debe ser UID, no DN)
- Membership User LDAP Attribute: `uid`
- User Roles Retrieve Strategy: `LOAD_GROUPS_BY_MEMBER_ATTRIBUTE`
- Member-Of LDAP Attribute: `memberOf`
- Mapped Group Attributes: (dejar en blanco)
- Drop non-existing groups during sync: `ON`
4. Haz clic en "Save"
5. En la pantalla del mapper, haz clic en "Sync LDAP Groups to Keycloak"
> **Nota importante sobre el Membership Attribute Type:**
> Es crucial seleccionar `UID` y no `DN` porque en nuestra estructura LDAP, los valores de `memberUid` contienen solo el uid simple del usuario (admin, developer, user) y no el DN completo. Si configuras este campo incorrectamente como `DN`, aparecerá un error al intentar ver los miembros de cualquier grupo.
### Crear un Cliente para Angular
1. En el menú lateral izquierdo, selecciona "Clients"
2. Haz clic en "Create client"
3. Completa:
- Client type: `OpenID Connect`
- Client ID: `angular-app`
- Name: `Angular Application`
- Haz clic en "Next"
4. En la siguiente pantalla (para aplicaciones SPA modernas):
- Client authentication: `OFF` (para aplicaciones SPA)
- Authorization: `OFF`
- Haz clic en "Next"
5. En la siguiente pantalla:
- Root URL: `http://localhost:4200`
- Home URL: `/`
- Valid redirect URIs: `http://localhost:4200/*`
- Web origins: `http://localhost:4200` (o usar `+` para permitir todos los orígenes durante desarrollo)
- Haz clic en "Save"
Para aplicaciones que no son SPA, puedes habilitar "Client authentication" y obtener un client secret que deberás usar en la configuración.
## 6. Integración con Angular: Enfoque básico
Para las versiones más antiguas de Angular, vamos a utilizar el enfoque tradicional de integración con Keycloak.
### Crear una aplicación Angular
```bash
# Crear una nueva aplicación
ng new angular-keycloak-app
cd angular-keycloak-app
# Instalar la biblioteca para integrar Keycloak
npm install keycloak-angular keycloak-js
```
### Configurar Keycloak en Angular
Crea un archivo de configuración en `src/assets/keycloak.json`:
```json
{
"realm": "angular-app",
"auth-server-url": "http://localhost:8080/",
"resource": "angular-app",
"public-client": true
}
```
Si tu cliente en Keycloak tiene autenticación habilitada, deberás agregar el campo credentials con el client secret:
```json
{
"realm": "angular-app",
"auth-server-url": "http://localhost:8080/",
"resource": "angular-app",
"credentials": {
"secret": "TU_CLIENT_SECRET_AQUÍ"
}
}
```
Crea un archivo `src/assets/silent-check-sso.html`:
```html
<html>
<body>
<script>
parent.postMessage(location.href, location.origin);
</script>
</body>
</html>
```
### Configurar el módulo principal (Angular tradicional)
Modifica el archivo `src/app/app.module.ts`:
```typescript
import { NgModule, APP_INITIALIZER } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { KeycloakAngularModule, KeycloakService } from 'keycloak-angular';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
function initializeKeycloak(keycloak: KeycloakService) {
return () =>
keycloak.init({
config: {
url: 'http://localhost:8080',
realm: 'angular-app',
clientId: 'angular-app'
},
initOptions: {
onLoad: 'check-sso',
silentCheckSsoRedirectUri:
window.location.origin + '/assets/silent-check-sso.html'
}
});
}
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
AppRoutingModule,
KeycloakAngularModule
],
providers: [
{
provide: APP_INITIALIZER,
useFactory: initializeKeycloak,
multi: true,
deps: [KeycloakService]
}
],
bootstrap: [AppComponent]
})
export class AppModule { }
```
### Crear un servicio de autenticación básico
```bash
ng generate service auth
```
Edita `src/app/auth.service.ts`:
```typescript
import { Injectable } from '@angular/core';
import { KeycloakService } from 'keycloak-angular';
import { KeycloakProfile } from 'keycloak-js';
@Injectable({
providedIn: 'root'
})
export class AuthService {
constructor(private keycloak: KeycloakService) { }
public getLoggedUser(): Promise<KeycloakProfile | null> {
return this.keycloak.loadUserProfile();
}
public login(): void {
this.keycloak.login();
}
public logout(): void {
this.keycloak.logout();
}
public isLoggedIn(): Promise<boolean> {
return this.keycloak.isLoggedIn();
}
public getRoles(): string[] {
return this.keycloak.getUserRoles();
}
}
```
### Crear un guardia para rutas protegidas (Angular tradicional)
```bash
ng generate guard auth
```
Edita `src/app/auth.guard.ts`:
```typescript
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, RouterStateSnapshot, Router, UrlTree } from '@angular/router';
import { KeycloakAuthGuard, KeycloakService } from 'keycloak-angular';
@Injectable({
providedIn: 'root'
})
export class AuthGuard extends KeycloakAuthGuard {
constructor(
protected override readonly router: Router,
protected readonly keycloak: KeycloakService
) {
super(router, keycloak);
}
public async isAccessAllowed(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Promise<boolean | UrlTree> {
// Verifica si el usuario está autenticado
if (!this.authenticated) {
await this.keycloak.login({
redirectUri: window.location.origin + state.url,
});
return false;
}
// Obtiene los roles requeridos desde la ruta
const requiredRoles = route.data['roles'];
// Permite el acceso si no hay roles requeridos
if (!requiredRoles || requiredRoles.length === 0) {
return true;
}
// Verifica si el usuario tiene al menos uno de los roles requeridos
return requiredRoles.some((role: string) => this.roles.includes(role));
}
}
```
### Configurar las rutas con protección
Modifica `src/app/app-routing.module.ts`:
```typescript
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from './auth.guard';
import { AppComponent } from './app.component';
const routes: Routes = [
{
path: 'admin',
component: AppComponent,
canActivate: [AuthGuard],
data: { roles: ['administradores'] }
},
{
path: 'developer',
component: AppComponent,
canActivate: [AuthGuard],
data: { roles: ['desarrolladores'] }
},
{
path: 'user',
component: AppComponent,
canActivate: [AuthGuard],
data: { roles: ['usuarios'] }
},
{
path: '',
component: AppComponent
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
```
## 7. Integración con Angular 19: Enfoque moderno
Para Angular 19 y versiones más recientes, utilizaremos el enfoque moderno que aprovecha características como componentes independientes y el sistema de inyección de dependencias mejorado.
### Configuración en app.config.ts (Angular 19)
Configura Keycloak en el archivo `src/app/app.config.ts`:
```typescript
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter, withPreloading, PreloadAllModules } from '@angular/router';
import { provideHttpClient, withFetch, withInterceptors } from '@angular/common/http';
import { routes } from './app.routes';
import { provideAnimations } from '@angular/platform-browser/animations';
import {
provideKeycloak,
createInterceptorCondition,
IncludeBearerTokenCondition,
INCLUDE_BEARER_TOKEN_INTERCEPTOR_CONFIG,
includeBearerTokenInterceptor
} from 'keycloak-angular';
// Define condiciones para incluir el token en las peticiones
const localhostCondition = createInterceptorCondition<IncludeBearerTokenCondition>({
urlPattern: /^(http:\/\/localhost)(\/.*)?$/i, // URLs que comienzan con http://localhost
bearerPrefix: 'Bearer'
});
// Condición para APIs
const apiCondition = createInterceptorCondition<IncludeBearerTokenCondition>({
urlPattern: /^(\/api)(\/.*)?$/i, // URLs que comienzan con /api
bearerPrefix: 'Bearer'
});
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(
routes,
withPreloading(PreloadAllModules)
),
provideAnimations(),
// Usar el interceptor de Keycloak para adjuntar tokens de autenticación
provideHttpClient(
withFetch(),
withInterceptors([includeBearerTokenInterceptor])
),
// Configuración para Keycloak
provideKeycloak({
config: {
url: 'http://localhost:8080', // URL del servidor Keycloak
realm: 'angular-app', // Nombre del realm
clientId: 'angular-app' // ID del cliente
},
initOptions: {
onLoad: 'check-sso', // Opciones: 'login-required' o 'check-sso'
silentCheckSsoRedirectUri: `${window.location.origin}/silent-check-sso.html`,
checkLoginIframe: false,
pkceMethod: 'S256' // Mejora la seguridad
},
// Configurar el interceptor especificando las URLs donde incluir el token
providers: [
{
provide: INCLUDE_BEARER_TOKEN_INTERCEPTOR_CONFIG,
useValue: [localhostCondition, apiCondition]
}
]
})
]
};
```
## 8. Servicios de autenticación avanzados
Con Angular 19, podemos crear un servicio de autenticación más avanzado utilizando signals y effects.
```typescript
import { Injectable, inject, effect, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable, from } from 'rxjs';
import { KEYCLOAK_EVENT_SIGNAL, KeycloakEventType } from 'keycloak-angular';
import { Router } from '@angular/router';
import Keycloak from 'keycloak-js';
@Injectable({
providedIn: 'root'
})
export class AuthService {
// Inyectar Keycloak directamente
private keycloak = inject(Keycloak);
private keycloakEvents = inject(KEYCLOAK_EVENT_SIGNAL);
private router = inject(Router);
// Estado del usuario
private userSubject = new BehaviorSubject<any>(null);
public user$ = this.userSubject.asObservable();
// Estado de autenticación como signal
public isAuthenticated = signal<boolean>(false);
constructor(private http: HttpClient) {
// Verificar estado inicial
this.checkInitialAuthState();
// Configurar manejadores de eventos usando Angular effects
effect(() => {
const event = this.keycloakEvents();
if (!event) return;
console.log('Keycloak event:', event.type);
// Autenticación exitosa
if (event.type === KeycloakEventType.AuthSuccess) {
this.isAuthenticated.set(true);
this.loadUserInfo();
}
// Cierre de sesión
if (event.type === KeycloakEventType.AuthLogout) {
this.isAuthenticated.set(false);
this.userSubject.next(null);
this.router.navigate(['/login']);
}
// Error de autenticación
if (event.type === KeycloakEventType.AuthError) {
console.error('Authentication error:', event);
this.isAuthenticated.set(false);
this.userSubject.next(null);
}
// Expiración del token
if (event.type === KeycloakEventType.TokenExpired) {
console.log('Token expired, refreshing...');
this.updateToken();
}
});
}
private async checkInitialAuthState(): Promise<void> {
try {
const isLoggedIn = await this.keycloak.authenticated;
this.isAuthenticated.set(isLoggedIn);
if (isLoggedIn) {
await this.loadUserInfo();
}
} catch (error) {
console.error('Error checking initial auth state:', error);
this.isAuthenticated.set(false);
}
}
private async loadUserInfo(): Promise<void> {
try {
const isLoggedIn = await this.keycloak.authenticated;
if (!isLoggedIn) {
this.userSubject.next(null);
return;
}
const userProfile = await this.keycloak.loadUserProfile();
const isAdmin = this.keycloak.hasRealmRole('admin');
// Obtener roles del usuario
const realmRoles = this.keycloak.realmAccess?.roles || [];
const resourceRoles = this.keycloak.resourceAccess || {};
const user = {
id: userProfile.id,
username: userProfile.username,
name: `${userProfile.firstName || ''} ${userProfile.lastName || ''}`.trim(),
email: userProfile.email,
role: isAdmin ? 'admin' : 'user',
roles: {
realm: realmRoles,
resource: resourceRoles
},
isAdmin: isAdmin
};
this.userSubject.next(user);
} catch (error) {
console.error('Error loading user profile:', error);
this.userSubject.next(null);
}
}
login(redirectUri?: string): Promise<void> {
return this.keycloak.login({
redirectUri: redirectUri || window.location.origin
});
}
logout(): Promise<void> {
return this.keycloak.logout({
redirectUri: window.location.origin
});
}
isLoggedIn(): Observable<boolean> {
try {
return from(Promise.resolve(this.keycloak.authenticated || false));
} catch (error) {
console.error('Error checking authentication:', error);
return from(Promise.resolve(false));
}
}
getToken(): Promise<string> {
try {
return Promise.resolve(this.keycloak.token || '');
} catch (error) {
console.error('Error getting token:', error);
return Promise.resolve('');
}
}
async updateToken(minValidity = 30): Promise<boolean> {
try {
return await this.keycloak.updateToken(minValidity);
} catch (error) {
console.error('Error refreshing token:', error);
await this.login();
return false;
}
}
getCurrentUser(): any {
return this.userSubject.value;
}
// Verificar si el usuario tiene un rol específico
hasRole(role: string): boolean {
return this.keycloak.hasRealmRole(role);
}
// Verificar si el usuario tiene alguno de los roles especificados
hasAnyRole(roles: string[]): boolean {
for (const role of roles) {
if (this.keycloak.hasRealmRole(role)) {
return true;
}
}
return false;
}
}
```
## 9. Guardias de ruta e interceptores HTTP
### Guardia de ruta funcional (Angular 19)
```typescript
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import Keycloak from 'keycloak-js';
import { ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree } from '@angular/router';
import { createAuthGuard, AuthGuardData } from 'keycloak-angular';
// Implementación directa
export const authGuard: CanActivateFn = async (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Promise<boolean | UrlTree> => {
const keycloak = inject(Keycloak);
const router = inject(Router);
try {
// Verificar si el usuario está autenticado
const authenticated = await keycloak.authenticated;
if (authenticated) {
// Verificar roles si están especificados en la ruta
const requiredRoles = route.data['roles'] as string[];
if (requiredRoles && requiredRoles.length > 0) {
// Comprobar si el usuario tiene los roles requeridos
const hasRequiredRole = requiredRoles.some(role =>
keycloak.hasRealmRole(role)
);
if (!hasRequiredRole) {
console.log('Usuario no tiene los roles requeridos');
return router.createUrlTree(['/unauthorized']);
}
}
return true;
}
// Si no está autenticado, redirigir a la página de login
console.log('Usuario no autenticado, redirigiendo a login');
return router.createUrlTree(['/login'], {
queryParams: { returnUrl: state.url !== '/' ? state.url : '/inicio' }
});
} catch (error) {
console.error('Error al verificar autenticación:', error);
return router.createUrlTree(['/login']);
}
};
// Alternativa usando el helper de keycloak-angular
const isAccessAllowed = async (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
authData: AuthGuardData
): Promise<boolean | UrlTree> => {
const { authenticated, grantedRoles } = authData;
const router = inject(Router);
if (authenticated) {
const requiredRoles = route.data['roles'] as string[];
if (!requiredRoles || requiredRoles.length === 0) {
return true;
}
const hasRequiredRole = requiredRoles.some(role =>
grantedRoles.realmRoles.includes(role)
);
if (hasRequiredRole) {
return true;
}
return router.createUrlTree(['/unauthorized']);
}
return router.createUrlTree(['/login'], { queryParams: { returnUrl: state.url } });
};
// Helper para crear el guardia (opcional)
export const authGuardWithHelper = createAuthGuard(isAccessAllowed);
```
### Interceptor HTTP para manejo de errores
```typescript
import { inject } from '@angular/core';
import {
HttpRequest,
HttpHandlerFn,
HttpInterceptorFn,
HttpErrorResponse
} from '@angular/common/http';
import { Observable, throwError, from, switchMap } from 'rxjs';
import { catchError } from 'rxjs/operators';
import Keycloak from 'keycloak-js';
import { Router } from '@angular/router';
/**
* Este interceptor complementa al incluido en app.config.ts
* Proporciona funcionalidad adicional para manejo de errores
*/
export const authInterceptor: HttpInterceptorFn = (
request: HttpRequest<unknown>,
next: HttpHandlerFn
): Observable<any> => {
const keycloak = inject(Keycloak);
const router = inject(Router);
// Manejar la petición con gestión de errores de autenticación
return next(request).pipe(
catchError((error: HttpErrorResponse) => {
// Manejar errores 401 Unauthorized
if (error.status === 401) {
console.log('Error 401, refrescando token o redirigiendo a login');
// Intentar refrescar el token primero
return from(keycloak.updateToken(30)).pipe(
switchMap(refreshed => {
if (refreshed) {
// Token refrescado, reintentar la petición
return next(request);
} else {
// No se pudo refrescar el token, redirigir a login
keycloak.login();
return throwError(() => error);
}
}),
catchError(refreshError => {
console.error('Error al refrescar token:', refreshError);
// Redirigir a login en caso de error
router.navigate(['/login']);
return throwError(() => error);
})
);
}
// Para otros errores, simplemente pasarlos
return throwError(() => error);
})
);
};
```
## 10. Componentes de UI para login/logout
### Componente de login (Angular 19)
```typescript
import { Component, OnInit, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Router, ActivatedRoute } from '@angular/router';
import { AuthService } from '../../services/auth.service';
import { take } from 'rxjs/operators';
@Component({
selector: 'app-login',
standalone: true,
imports: [CommonModule],
templateUrl: './login.component.html',
styleUrl: './login.component.scss'
})
export class LoginComponent implements OnInit {
private authService = inject(AuthService);
private route = inject(ActivatedRoute);
private router = inject(Router);
loading: boolean = false;
returnUrl: string = '';
ngOnInit() {
// Obtener el returnUrl de los query params
this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/inicio';
// Verificar si ya está autenticado y redirigir
this.authService.isLoggedIn().subscribe({
next: (isLoggedIn) => {
if (isLoggedIn) {
console.log('Usuario ya autenticado, redirigiendo a:', this.returnUrl);
// Comprobar si la URL de retorno es válida
const effectiveReturnUrl = this.returnUrl === '/' ? '/inicio' : this.returnUrl;
// Redirigir
this.router.navigate([effectiveReturnUrl], { replaceUrl: true });
}
},
error: (error) => console.error('Error al verificar autenticación:', error)
});
}
async onLogin() {
this.loading = true;
try {
// Comprobar si la URL de retorno es válida
const effectiveReturnUrl = this.returnUrl === '/' ? '/inicio' : this.returnUrl;
// Construir la redirectUri
const redirectUri = window.location.origin + effectiveReturnUrl;
console.log('Iniciando login con redirectUri:', redirectUri);
// Iniciar el flujo de autenticación
await this.authService.login(redirectUri);
// Keycloak se encargará de la redirección
} catch (error) {
console.error('Error al iniciar sesión:', error);
this.loading = false;
}
}
}
```
### Plantilla HTML para el componente de login
```html
<div class="login-container">
<div class="login-card">
<h2>Iniciar Sesión</h2>
<p>Por favor, inicia sesión para acceder al sistema.</p>
<button
[disabled]="loading"
(click)="onLogin()"
class="login-button">
{{ loading ? 'Cargando...' : 'Iniciar Sesión con Keycloak' }}
</button>
</div>
</div>
```
### Componente principal con información de usuario (común para ambos enfoques)
Edita `src/app/app.component.ts`:
```typescript
import { Component, OnInit } from '@angular/core';
import { AuthService } from './auth.service';
import { KeycloakProfile } from 'keycloak-js';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
title = 'angular-keycloak-app';
isLoggedIn = false;
userProfile: KeycloakProfile | null = null;
userRoles: string[] = [];
constructor(private authService: AuthService) {}
async ngOnInit() {
this.isLoggedIn = await this.authService.isLoggedIn();
if (this.isLoggedIn) {
this.userProfile = await this.authService.getLoggedUser();
this.userRoles = this.authService.getRoles();
}
}
login() {
this.authService.login();
}
logout() {
this.authService.logout();
}
}
```
Edita `src/app/app.component.html`:
```html
<div class="container mt-5">
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h2>Demo Keycloak con LDAP - Correos.com</h2>
</div>
<div class="card-body">
<div *ngIf="isLoggedIn; else loginButton">
<h3>Bienvenido, {{ userProfile?.firstName }} {{ userProfile?.lastName }}</h3>
<p>Email: {{ userProfile?.email }}</p>
<p>Nombre de Usuario: {{ userProfile?.username }}</p>
<h4>Tus Roles:</h4>
<ul>
<li *ngFor="let role of userRoles">{{ role }}</li>
</ul>
<button class="btn btn-danger" (click)="logout()">Cerrar Sesión</button>
</div>
<ng-template #loginButton>
<p>Por favor inicia sesión para acceder al sistema</p>
<button class="btn btn-primary" (click)="login()">Iniciar Sesión</button>
</ng-template>
</div>
</div>
</div>
</div>
</div>
```
## 11. Arquitectura del sistema
La arquitectura del sistema está compuesta por los siguientes componentes:
- **OpenLDAP**: Directorio de usuarios y grupos (puerto 389)
- **phpLDAPadmin**: Interfaz gráfica para administrar LDAP (puerto 80)
- **Keycloak**: Servidor de autenticación y autorización (puerto 8080)
- **Aplicación Angular**: Cliente que utiliza el SSO de Keycloak (puerto 4200)
La estructura del directorio LDAP para correos.com es:
- dc=correos,dc=com (raíz del directorio)
- ou=usuarios (unidad organizativa para usuarios)
- uid=admin (usuario administrador)
- uid=developer (usuario desarrollador)
- uid=user (usuario normal)
- ou=grupos (unidad organizativa para grupos)
- cn=administradores (grupo de administradores)
- cn=desarrolladores (grupo de desarrolladores)
- cn=usuarios (grupo de usuarios)
### Flujo de autenticación
El flujo de autenticación funciona de la siguiente manera:
1. El usuario accede a la aplicación Angular
2. Si el usuario no ha iniciado sesión, es redirigido a Keycloak
3. Keycloak autentica al usuario contra LDAP
4. Si la autenticación es exitosa, Keycloak redirige al usuario de vuelta a la aplicación con un token JWT
5. La aplicación Angular verifica el token y permite el acceso
6. Los interceptores HTTP añaden automáticamente el token a las peticiones API
7. Las rutas protegidas verifican los roles del usuario antes de permitir el acceso
## 12. Resolución de problemas comunes
### Problema: "ldap_bind: Invalid credentials (49)"
Este error ocurre cuando intentas conectarte a LDAP con credenciales incorrectas. Para resolverlo:
1. Verifica que estás usando el dominio correcto: `dc=correos,dc=com`
2. Asegúrate de usar el DN correcto: `cn=admin,dc=correos,dc=com`
3. Comprueba que la contraseña de administrador es correcta
Si olvidaste la contraseña, puedes restablecerla:
```bash
sudo dpkg-reconfigure slapd
```
O también:
```bash
sudo slappasswd
# Copia el hash generado
sudo nano /etc/ldap/slapd.d/cn=config/olcDatabase={1}mdb.ldif
# Busca olcRootPW y reemplaza el valor con el nuevo hash
sudo systemctl restart slapd
```
### Problema: No puedes conectarte a LDAP en absoluto
```bash
# Verifica que el servicio esté ejecutándose
sudo systemctl status slapd
# Reinicia el servicio si es necesario
sudo systemctl restart slapd
# Verifica que el puerto esté abierto
sudo netstat -tulpn | grep 389
```
### Problema: Keycloak no se inicia
```bash
# Verifica los logs
sudo journalctl -u keycloak
# Asegúrate de que Java esté instalado correctamente
java -version
# Verifica los permisos
sudo ls -la /opt/keycloak
```
### Problema: Error 401 Unauthorized al autenticar con Keycloak
Este error suele ocurrir cuando hay problemas con la configuración del cliente en Keycloak:
1. Verifica que el realm y el clientId sean correctos en la configuración de Angular
2. Asegúrate de que el cliente en Keycloak tenga la configuración correcta:
- Web Origins: debe incluir `http://localhost:4200` o `+` para desarrollo
- Valid redirect URIs: debe incluir `http://localhost:4200/*`
- Client authentication debe estar OFF para aplicaciones SPA
- Access Type debe ser "public"
### Problema: Redireccionamiento circular
Si ves redirecciones constantes entre tu aplicación y Keycloak:
1. Verifica la lógica en el componente app.component.ts y login.component.ts
2. Asegúrate de que no haya múltiples redirecciones activándose a la vez
3. Usa el parámetro `replaceUrl: true` en las navegaciones para evitar acumular entradas en el historial
4. Maneja correctamente las rutas especiales como '/' redirigiendo a una ruta válida como '/inicio'
### Problema: "Can't resolve 'keycloak-angular'"
Si encuentras este error al compilar:
1. Asegúrate de haber instalado correctamente las dependencias:
```bash
npm install keycloak-angular keycloak-js
```
2. Verifica que las versiones sean compatibles con tu versión de Angular
3. Limpia la caché de npm y reinstala:
```bash
npm cache clean --force
rm -rf node_modules
npm install
```
### Problema: El token no se adjunta a las peticiones HTTP
1. Verifica que estés utilizando el interceptor correcto
2. Asegúrate de que las condiciones de URL para el token sean correctas
3. Revisa si estás usando `provideHttpClient` con `withInterceptors([includeBearerTokenInterceptor])`
4. Comprueba los patrones en `createInterceptorCondition` para asegurarte de que coincidan con tus URLs
### Problema: No puedes ver los miembros de un grupo en Keycloak
Este error ocurre cuando hay una configuración incorrecta en el mapeo de grupos LDAP:
1. **Causa principal**: El tipo de atributo de membresía (Membership Attribute Type) está configurado como `DN` cuando debería ser `UID`.
2. **Solución**: En User Federation → LDAP → group-mapper → Edit, cambia "Membership Attribute Type" de `DN` a `UID`.
3. **Explicación**: En la estructura LDAP creada, los valores de `memberUid` contienen sólo el uid simple (admin, developer, user) y no el DN completo.
4. Después de hacer el cambio, vuelve a ejecutar "Sync LDAP Groups to Keycloak".
## 13. Verificación y prueba del sistema
### Verificar que Keycloak esté sincronizando correctamente con LDAP
1. Inicia sesión en la consola de administración de Keycloak (http://localhost:8080/admin/)
2. Navega a "Users" en el reino "angular-app"
3. Deberías ver los usuarios de LDAP: admin, developer y user
4. Navega a "Groups" y verifica que los grupos de LDAP estén presentes
5. Haz clic en cada grupo y verifica que los miembros aparezcan correctamente
- Si no aparecen, revisa la sección "Resolución de problemas" sobre el tipo de atributo de membresía
### Probar la aplicación Angular
1. Asegúrate de que Keycloak esté ejecutándose
2. Inicia la aplicación Angular con `ng serve`
3. Navega a http://localhost:4200
4. Haz clic en "Iniciar Sesión"
5. Deberías ser redirigido a la pantalla de inicio de sesión de Keycloak
6. Inicia sesión con uno de los usuarios LDAP:
- Usuario: admin, Contraseña: password123
- Usuario: developer, Contraseña: password123
- Usuario: user, Contraseña: password123
7. Después de iniciar sesión, serás redirigido de vuelta a la aplicación
8. La aplicación mostrará tu perfil y roles
## 14. Recursos adicionales
- [Documentación oficial de Keycloak](https://www.keycloak.org/documentation)
- [Documentación oficial de OpenLDAP](https://www.openldap.org/doc/)
- [Documentación oficial de keycloak-angular](https://github.com/mauriciovigolo/keycloak-angular)
- [Guía de migración para Keycloak-Angular v19](https://www.keycloak.org/securing-apps/v19.0.2/angular)
- [Ejemplos de implementación en GitHub](https://github.com/mauriciovigolo/keycloak-angular/tree/main/examples/standalone-app)
## 15. Resumen
Esta configuración proporciona un sistema completo de gestión de identidades y acceso con:
- **Autenticación centralizada**: Los usuarios solo necesitan recordar un conjunto de credenciales
- **Gestión de usuarios unificada**: Todos los usuarios se administran en LDAP
- **Control de acceso basado en roles**: Las rutas y funcionalidades pueden ser protegidas según los roles del usuario
- **Experiencia de inicio de sesión único (SSO)**: Una vez autenticado, el usuario puede acceder a todas las aplicaciones integradas
- **Soporte para versiones modernas de Angular**: Incluye configuraciones para Angular tradicional y Angular 19 con standalone components y signals
Este sistema es adecuado para entornos empresariales donde se requiere un control de acceso detallado y una gestión centralizada de usuarios.
La implementación es nativa en un sistema Ubuntu, lo que la hace ideal para despliegues en servidores VPS o entornos similares sin necesidad de contenedores. También funciona perfectamente en entornos de desarrollo local.