# 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` - 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 ```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 ``` ### 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 { return this.keycloak.loadUserProfile(); } public login(): void { this.keycloak.login(); } public logout(): void { this.keycloak.logout(); } public isLoggedIn(): Promise { 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 { // 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({ urlPattern: /^(http:\/\/localhost)(\/.*)?$/i, // URLs que comienzan con http://localhost bearerPrefix: 'Bearer' }); // Condición para APIs const apiCondition = createInterceptorCondition({ 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(null); public user$ = this.userSubject.asObservable(); // Estado de autenticación como signal public isAuthenticated = signal(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 { 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 { 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 { return this.keycloak.login({ redirectUri: redirectUri || window.location.origin }); } logout(): Promise { return this.keycloak.logout({ redirectUri: window.location.origin }); } isLoggedIn(): Observable { try { return from(Promise.resolve(this.keycloak.authenticated || false)); } catch (error) { console.error('Error checking authentication:', error); return from(Promise.resolve(false)); } } getToken(): Promise { try { return Promise.resolve(this.keycloak.token || ''); } catch (error) { console.error('Error getting token:', error); return Promise.resolve(''); } } async updateToken(minValidity = 30): Promise { 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 => { 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 => { 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, next: HttpHandlerFn ): Observable => { 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 ``` ### 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

Demo Keycloak con LDAP - Correos.com

Bienvenido, {{ userProfile?.firstName }} {{ userProfile?.lastName }}

Email: {{ userProfile?.email }}

Nombre de Usuario: {{ userProfile?.username }}

Tus Roles:

  • {{ role }}

Por favor inicia sesión para acceder al sistema

``` ## 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 ## 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 - [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.