sacg-cronogramas/tutorial-keycloak-completo.md
2025-05-13 17:06:16 -04:00

40 KiB

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
  2. Instalación y configuración de OpenLDAP
  3. Crear estructura LDAP para usuarios y grupos
  4. Instalación y configuración de Keycloak
  5. Configuración de Keycloak en la interfaz web
  6. Integración con Angular: Enfoque básico
  7. Integración con Angular 19: Enfoque moderno
  8. Servicios de autenticación avanzados
  9. Guardias de ruta e interceptores HTTP
  10. Componentes de UI para login/logout
  11. Arquitectura del sistema
  12. Resolución de problemas comunes
  13. Verificación y prueba del sistema
  14. Recursos adicionales
  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:

sudo apt update
sudo apt upgrade -y
sudo apt install openjdk-17-jdk -y

Verifica la instalación de Java:

java -version

Instalación de Node.js y Angular CLI

Para el desarrollo de aplicaciones Angular, instala Node.js y Angular CLI:

# 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

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

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

sudo systemctl status slapd

Comprobar la conexión LDAP básica

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

sudo apt install phpldapadmin -y

Configurar phpLDAPadmin

Edita el archivo de configuración:

sudo nano /etc/phpldapadmin/config.php

Busca y modifica las siguientes líneas:

$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:

$servers->setValue('login','anon_bind',true);

por:

$servers->setValue('login','anon_bind',false);

Reinicia el servidor web:

sudo systemctl restart apache2

3. Crear estructura LDAP para usuarios y grupos

Crear unidades organizativas

Crea un archivo para las unidades organizativas:

nano ~/ou.ldif

Con el siguiente contenido:

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:

ldapadd -x -D cn=admin,dc=correos,dc=com -W -f ~/ou.ldif

Crear grupos LDAP

Crea un archivo para los grupos:

nano ~/grupos.ldif

Con el siguiente contenido:

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:

ldapadd -x -D cn=admin,dc=correos,dc=com -W -f ~/grupos.ldif

Crear usuarios LDAP

Primero, genera contraseñas encriptadas para los usuarios:

slappasswd -s "password123"

Anota el hash resultante para usarlo en el siguiente archivo.

Crea un archivo para los usuarios:

nano ~/usuarios.ldif

Con el siguiente contenido (reemplazando {HASH} con el hash que generaste):

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:

ldapadd -x -D cn=admin,dc=correos,dc=com -W -f ~/usuarios.ldif

Asociar usuarios a grupos

Crea un archivo para las membresías:

nano ~/miembros.ldif

Con el siguiente contenido:

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:

ldapmodify -x -D cn=admin,dc=correos,dc=com -W -f ~/miembros.ldif

Verificar la estructura LDAP

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

# 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:

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

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:

sudo nano /etc/systemd/system/keycloak.service

Con el siguiente contenido:

[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:

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
    • 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"

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

# 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:

{
  "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:

{
  "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>
<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:

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

ng generate service auth

Edita src/app/auth.service.ts:

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)

ng generate guard auth

Edita src/app/auth.guard.ts:

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:

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:

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.

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)

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

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)

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

<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:

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:

<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:

sudo dpkg-reconfigure slapd

O también:

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

# 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

# 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:
    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:
    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

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

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

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.