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
- Preparación del entorno
- Instalación y configuración de OpenLDAP
- Crear estructura LDAP para usuarios y grupos
- Instalación y configuración de Keycloak
- Configuración de Keycloak en la interfaz web
- Integración con Angular: Enfoque básico
- Integración con Angular 19: Enfoque moderno
- Servicios de autenticación avanzados
- Guardias de ruta e interceptores HTTP
- Componentes de UI para login/logout
- Arquitectura del sistema
- Resolución de problemas comunes
- Verificación y prueba del sistema
- Recursos adicionales
- 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:
- "¿Omitir configuración del servidor LDAP?" → No
- "Nombre de dominio DNS:" → correos.com
- "Nombre de la organización:" → Correos Org
- "Contraseña de administrador:" → [tu contraseña segura]
- "Confirmar contraseña:" → [repetir la contraseña]
- "Motor de base de datos:" → MDB
- "¿Quiere que se elimine la base de datos cuando se purgue slapd?" → No
- "¿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)
- Haz clic en el menú desplegable superior izquierdo que dice "master"
- Selecciona "Create Realm"
- Ingresa el nombre:
angular-app - Haz clic en "Create"
Configurar la Federación de Usuarios LDAP
-
En el menú lateral izquierdo, selecciona "User Federation"
-
Haz clic en "Add provider" → "ldap"
-
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
- Console Display Name:
-
Haz clic en "Test connection" y "Test authentication" para verificar
-
Guarda la configuración
-
En la pantalla del proveedor LDAP, ve a la pestaña "Synchronization"
-
Haz clic en "Sync all users"
Configurar el Mapeo de Grupos LDAP
- En la pantalla del proveedor LDAP, ve a la pestaña "Mappers"
- Haz clic en "Create"
- 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
- Name:
- Haz clic en "Save"
- En la pantalla del mapper, haz clic en "Sync LDAP Groups to Keycloak"
Crear un Cliente para Angular
-
En el menú lateral izquierdo, selecciona "Clients"
-
Haz clic en "Create client"
-
Completa:
- Client type:
OpenID Connect - Client ID:
angular-app - Name:
Angular Application - Haz clic en "Next"
- Client type:
-
En la siguiente pantalla (para aplicaciones SPA modernas):
- Client authentication:
OFF(para aplicaciones SPA) - Authorization:
OFF - Haz clic en "Next"
- Client authentication:
-
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"
- Root URL:
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)
- ou=usuarios (unidad organizativa para usuarios)
Flujo de autenticación
El flujo de autenticación funciona de la siguiente manera:
- El usuario accede a la aplicación Angular
- Si el usuario no ha iniciado sesión, es redirigido a Keycloak
- Keycloak autentica al usuario contra LDAP
- Si la autenticación es exitosa, Keycloak redirige al usuario de vuelta a la aplicación con un token JWT
- La aplicación Angular verifica el token y permite el acceso
- Los interceptores HTTP añaden automáticamente el token a las peticiones API
- 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:
- Verifica que estás usando el dominio correcto:
dc=correos,dc=com - Asegúrate de usar el DN correcto:
cn=admin,dc=correos,dc=com - 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:
- Verifica que el realm y el clientId sean correctos en la configuración de Angular
- Asegúrate de que el cliente en Keycloak tenga la configuración correcta:
- Web Origins: debe incluir
http://localhost:4200o+para desarrollo - Valid redirect URIs: debe incluir
http://localhost:4200/* - Client authentication debe estar OFF para aplicaciones SPA
- Access Type debe ser "public"
- Web Origins: debe incluir
Problema: Redireccionamiento circular
Si ves redirecciones constantes entre tu aplicación y Keycloak:
- Verifica la lógica en el componente app.component.ts y login.component.ts
- Asegúrate de que no haya múltiples redirecciones activándose a la vez
- Usa el parámetro
replaceUrl: trueen las navegaciones para evitar acumular entradas en el historial - 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:
- Asegúrate de haber instalado correctamente las dependencias:
npm install keycloak-angular keycloak-js - Verifica que las versiones sean compatibles con tu versión de Angular
- 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
- Verifica que estés utilizando el interceptor correcto
- Asegúrate de que las condiciones de URL para el token sean correctas
- Revisa si estás usando
provideHttpClientconwithInterceptors([includeBearerTokenInterceptor]) - Comprueba los patrones en
createInterceptorConditionpara asegurarte de que coincidan con tus URLs
13. Verificación y prueba del sistema
Verificar que Keycloak esté sincronizando correctamente con LDAP
- Inicia sesión en la consola de administración de Keycloak (http://localhost:8080/admin/)
- Navega a "Users" en el reino "angular-app"
- Deberías ver los usuarios de LDAP: admin, developer y user
- Navega a "Groups" y verifica que los grupos de LDAP estén presentes
Probar la aplicación Angular
- Asegúrate de que Keycloak esté ejecutándose
- Inicia la aplicación Angular con
ng serve - Navega a http://localhost:4200
- Haz clic en "Iniciar Sesión"
- Deberías ser redirigido a la pantalla de inicio de sesión de Keycloak
- Inicia sesión con uno de los usuarios LDAP:
- Usuario: admin, Contraseña: password123
- Usuario: developer, Contraseña: password123
- Usuario: user, Contraseña: password123
- Después de iniciar sesión, serás redirigido de vuelta a la aplicación
- La aplicación mostrará tu perfil y roles
14. Recursos adicionales
- Documentación oficial de Keycloak
- Documentación oficial de OpenLDAP
- Documentación oficial de keycloak-angular
- Guía de migración para Keycloak-Angular v19
- Ejemplos de implementación en GitHub
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.