funcionando v1

This commit is contained in:
luis cespedes 2025-05-13 15:21:24 -04:00
parent ea91f1c8f0
commit 01b93eca37
15 changed files with 2298 additions and 1104 deletions

View File

@ -3,7 +3,7 @@
"version": 1,
"newProjectRoot": "projects",
"projects": {
"cronogramas": {
"correo": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
@ -17,7 +17,7 @@
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": "dist/cronogramas",
"outputPath": "dist/correo",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": [
@ -82,10 +82,10 @@
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "cronogramas:build:production"
"buildTarget": "correo:build:production"
},
"development": {
"buildTarget": "cronogramas:build:development"
"buildTarget": "correo:build:development"
}
},
"defaultConfiguration": "development"

1658
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
{
"name": "cronogramas",
"name": "correo",
"version": "0.1.0",
"engines": {
"node": ">=18.x"
@ -27,6 +27,8 @@
"animate.css": "^4.1.1",
"exceljs": "^4.4.0",
"file-saver": "^2.0.5",
"keycloak-angular": "^19.0.2",
"keycloak-js": "^26.2.0",
"pdfmake": "^0.2.19",
"primeflex": "^4.0.0",
"primeicons": "^7.0.0",
@ -51,5 +53,5 @@
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.7.2"
},
"description": "cronogramas - Proyecto generado desde template"
"description": "correo - Proyecto generado desde template"
}

8
public/keycloak.json Normal file
View File

@ -0,0 +1,8 @@
{
"realm": "angular-app",
"auth-server-url": "http://localhost:8080/",
"resource": "angular-app",
"credentials": {
"secret": "zYbODELDmLjK9c9gHNbTUe8mSZlcLFZm"
}
}

View File

@ -0,0 +1,7 @@
<html>
<body>
<script>
parent.postMessage(location.href, location.origin);
</script>
</body>
</html>

View File

@ -1,15 +1,105 @@
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { Component, inject, OnInit, OnDestroy, effect, Signal } from '@angular/core';
import { Router, RouterOutlet, NavigationEnd } from '@angular/router';
import { ConfirmDialogModule } from 'primeng/confirmdialog';
import { ConfirmationService } from 'primeng/api';
import { filter, Subscription } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import Keycloak from 'keycloak-js';
import { AuthService } from './services/auth.service';
import { KEYCLOAK_EVENT_SIGNAL, KeycloakEventType, KeycloakEvent } from 'keycloak-angular';
@Component({
selector: 'app-root',
imports: [RouterOutlet, ConfirmDialogModule],
templateUrl: './app.component.html',
styleUrl: './app.component.scss',
providers: [ConfirmationService]
providers: [ConfirmationService],
standalone: true,
})
export class AppComponent {
export class AppComponent implements OnInit, OnDestroy {
title = 'SACG - Sistema Administrador de Cronogramas';
private keycloak = inject(Keycloak);
private keycloakEvents = inject(KEYCLOAK_EVENT_SIGNAL);
private authService = inject(AuthService);
private router = inject(Router);
private subscriptions: Subscription[] = [];
constructor() {
// Using effect to handle Keycloak events through Angular's Signal API
effect(() => {
const event = this.keycloakEvents();
if (!event) return;
console.log('Keycloak event received:', event.type);
// Authentication success handling
if (event.type === KeycloakEventType.AuthSuccess) {
console.log('Authentication successful, redirecting to home');
this.router.navigate(['/inicio']);
}
// Authentication error handling
if (event.type === KeycloakEventType.AuthError) {
console.error('Authentication error, redirecting to login');
this.router.navigate(['/login']);
}
// Logout handling
if (event.type === KeycloakEventType.AuthLogout) {
console.log('Logged out, redirecting to login');
this.router.navigate(['/login']);
}
});
}
ngOnInit() {
// Subscribe to navigation events
this.router.events
.pipe(
filter(event => event instanceof NavigationEnd),
takeUntilDestroyed()
)
.subscribe(() => {
console.log('Navigation completed to:', this.router.url);
});
// Check authentication status on load
this.checkAuthenticationStatus();
}
ngOnDestroy(): void {
// Unsubscribe from any remaining subscriptions
this.subscriptions.forEach(sub => sub.unsubscribe());
}
private async checkAuthenticationStatus(): Promise<void> {
try {
const isLoggedIn = await this.keycloak.authenticated;
console.log('Initial authentication status:', isLoggedIn ? 'Authenticated' : 'Not authenticated');
if (isLoggedIn) {
// Already authenticated, check if on protected route
if (this.router.url === '/login') {
this.router.navigate(['/inicio']);
}
} else {
// Not authenticated but trying to access a protected route
const isPublicRoute = this.router.url === '/login';
if (!isPublicRoute) {
console.log('User not authenticated, redirecting to Keycloak login');
// Store current URL for redirect after login
const currentUrl = this.router.url;
// Start Keycloak login flow
this.keycloak.login({
redirectUri: window.location.origin + currentUrl
});
}
}
} catch (error) {
console.error('Error checking authentication status:', error);
}
}
}

View File

@ -1,13 +1,30 @@
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 { providePrimeNG } from 'primeng/config';
import Aura from '@primeng/themes/aura';
import { authInterceptor } from './interceptors/auth.interceptor';
import { MessageService } from 'primeng/api';
import {
provideKeycloak,
createInterceptorCondition,
IncludeBearerTokenCondition,
INCLUDE_BEARER_TOKEN_INTERCEPTOR_CONFIG,
includeBearerTokenInterceptor
} from 'keycloak-angular';
// Define condition for including the token in requests
const localhostCondition = createInterceptorCondition<IncludeBearerTokenCondition>({
urlPattern: /^(http:\/\/localhost)(\/.*)?$/i, // Match URLs starting with http://localhost
bearerPrefix: 'Bearer'
});
// Define another condition for API URLs
const apiCondition = createInterceptorCondition<IncludeBearerTokenCondition>({
urlPattern: /^(\/api)(\/.*)?$/i, // Match URLs starting with /api
bearerPrefix: 'Bearer'
});
export const appConfig: ApplicationConfig = {
providers: [
@ -17,7 +34,11 @@ export const appConfig: ApplicationConfig = {
withPreloading(PreloadAllModules)
),
provideAnimations(),
provideHttpClient(withFetch(), withInterceptors([authInterceptor])),
// Use the Keycloak interceptor to attach authentication tokens
provideHttpClient(
withFetch(),
withInterceptors([includeBearerTokenInterceptor])
),
providePrimeNG({
theme: {
preset: Aura,
@ -26,6 +47,27 @@ export const appConfig: ApplicationConfig = {
}
}
}),
MessageService
MessageService,
// Provide Keycloak with initOptions - this automatically creates an APP_INITIALIZER
provideKeycloak({
config: {
url: 'http://localhost:8080',
realm: 'angular-app',
clientId: 'angular-app'
},
initOptions: {
onLoad: 'check-sso',
silentCheckSsoRedirectUri: `${window.location.origin}/silent-check-sso.html`,
checkLoginIframe: false,
pkceMethod: 'S256' // Added for better security
},
// Configure the interceptor by providing the URLs where to include the token
providers: [
{
provide: INCLUDE_BEARER_TOKEN_INTERCEPTOR_CONFIG,
useValue: [localhostCondition, apiCondition]
}
]
})
]
};

View File

@ -5,7 +5,13 @@ import { authGuard } from './guards/auth.guard';
import { NotFoundComponent } from './pages/not-found/not-found.component';
export const routes: Routes = [
{ path: 'login', component: LoginComponent },
// Public routes that don't require authentication
{
path: 'login',
component: LoginComponent,
data: { title: 'Login' }
},
// Protected routes that require authentication
{
path: '',
component: LayoutComponent,
@ -50,5 +56,4 @@ export const routes: Routes = [
]
},
{ path: '**', redirectTo: '404' }
];

View File

@ -1,16 +1,61 @@
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from '../services/auth.service';
import Keycloak from 'keycloak-js';
import { ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree } from '@angular/router';
import { createAuthGuard, AuthGuardData } from 'keycloak-angular';
export const authGuard: CanActivateFn = () => {
const authService = inject(AuthService);
// This is a simplified direct implementation
export const authGuard: CanActivateFn = async (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Promise<boolean | UrlTree> => {
const keycloak = inject(Keycloak);
const router = inject(Router);
if (authService.isLoggedIn()) {
// Check if user is authenticated
const authenticated = await keycloak.authenticated;
if (authenticated) {
return true;
}
// Redirect to login if not authenticated
router.navigate(['/login']);
return false;
// If not authenticated, redirect to Keycloak login
console.log('User not authenticated, redirecting to Keycloak login');
// Store the URL the user was trying to access
const returnUrl = state.url;
try {
await keycloak.login({
redirectUri: window.location.origin + returnUrl
});
return false;
} catch (error) {
console.error('Error during login redirect:', error);
// Fallback to local login component if Keycloak login fails
return router.createUrlTree(['/login'], { queryParams: { returnUrl } });
}
};
// Alternative implementation using the helper function from keycloak-angular
const isAccessAllowed = async (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
authData: AuthGuardData
): Promise<boolean | UrlTree> => {
const { authenticated } = authData;
if (authenticated) {
return true;
}
// Store the URL the user was trying to access
const returnUrl = state.url;
const router = inject(Router);
// Redirect to login page with return URL
return router.createUrlTree(['/login'], { queryParams: { returnUrl } });
};
// Optional: Use the createAuthGuard helper if needed
export const authGuardWithHelper = createAuthGuard(isAccessAllowed);

View File

@ -5,42 +5,53 @@ import {
HttpInterceptorFn,
HttpErrorResponse
} from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { Observable, throwError, from, switchMap } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { AuthService } from '../services/auth.service';
import Keycloak from 'keycloak-js';
import { Router } from '@angular/router';
/**
* Note: This interceptor is not strictly necessary when using keycloak-angular's
* built-in includeBearerTokenInterceptor, which is configured in app.config.ts.
* It's included here to provide additional error handling functionality.
*/
export const authInterceptor: HttpInterceptorFn = (
request: HttpRequest<unknown>,
next: HttpHandlerFn
): Observable<any> => {
const authService = inject(AuthService);
const keycloak = inject(Keycloak);
const router = inject(Router);
// Get the auth token
const token = authService.getToken();
// Handle the request with error handling for auth issues
return next(request).pipe(
catchError((error: HttpErrorResponse) => {
// Handle 401 Unauthorized errors
if (error.status === 401) {
console.log('401 Unauthorized error, refreshing token or redirecting to login');
// Clone the request and add the token if it exists
if (token) {
const authRequest = request.clone({
setHeaders: {
Authorization: `Bearer ${token}`
// Try to refresh the token first
return from(keycloak.updateToken(30)).pipe(
switchMap(refreshed => {
if (refreshed) {
// Token was refreshed, retry the request
return next(request);
} else {
// Token couldn't be refreshed, redirect to login
keycloak.login();
return throwError(() => error);
}
}),
catchError(refreshError => {
console.error('Error refreshing token:', refreshError);
// Redirect to login in case of refresh error
router.navigate(['/login']);
return throwError(() => error);
})
);
}
});
// Handle the authenticated request
return next(authRequest).pipe(
catchError((error: HttpErrorResponse) => {
// Handle 401 Unauthorized errors by logging out and redirecting to login
if (error.status === 401) {
authService.logout();
router.navigate(['/login']);
}
return throwError(() => error);
})
);
}
// If no token, just pass the request through
return next(request);
// For other errors, just pass them through
return throwError(() => error);
})
);
};

View File

@ -21,94 +21,36 @@
<!-- Contenedor principal con posición relativa -->
<div class="position-relative overflow-hidden">
<!-- PANEL DE LOGIN -->
<div class="panel-container w-full"
[ngClass]="{'animate__animated animate__fadeOut d-none': showRecovery,
'animate__animated animate__fadeIn': !showRecovery && !isInitialLoad}">
<!-- PANEL DE LOGIN CON KEYCLOAK -->
<div class="panel-container w-full animate__animated animate__fadeIn">
<div class="login-card shadow-2 border-round">
<div class="login-header">
<h2>Iniciar Sesión</h2>
</div>
<form (ngSubmit)="onLogin()" class="p-3">
<!-- Email -->
<div class="field mb-3">
<input type="email" pInputText [(ngModel)]="email" name="email" placeholder="Email"
class="input-with-icon w-full" required />
<div class="p-3">
<!-- Mensaje de información -->
<div class="info-message mb-4 text-center">
<p>Haz clic en el botón para iniciar sesión con la cuenta de usuario registrada en el sistema.</p>
</div>
<!-- Password -->
<div class="field mb-3">
<input type="password" pInputText [(ngModel)]="password" name="password" placeholder="Password"
class="input-with-lock w-full" required />
</div>
<!-- Mensaje de error -->
<div *ngIf="errorMessage" class="error-message my-2">
<p-message severity="error" [text]="errorMessage"></p-message>
</div>
<!-- Botón -->
<!-- Botón para iniciar sesión con Keycloak -->
<div class="login-actions">
<button pButton type="submit" [label]="loading ? 'Autenticando...' : 'Autenticar'"
class="p-button-primary w-full" [disabled]="loading || !email || !password">
<button pButton type="button" (click)="onLogin()"
[label]="loading ? 'Redirigiendo...' : 'Iniciar Sesión'"
icon="pi pi-sign-in"
class="p-button-primary w-full"
[disabled]="loading">
<i *ngIf="loading" class="pi pi-spin pi-spinner mr-2"></i>
</button>
</div>
</form>
<!-- Recuperar contraseña -->
<div class="password-recovery px-3 pb-3">
<a href="#" (click)="toggleRecovery($event)">Recuperar Contraseña</a>
</div>
<!-- Credenciales de prueba -->
<!-- Información adicional (opcional) -->
<div class="test-credentials mx-3 mb-3 p-2 border-round bg-gray-100">
<p class="mb-1 font-bold">Credenciales de prueba:</p>
<p class="mb-1">Email: admin&#64;example.com</p>
<p>Password: admin123</p>
</div>
</div>
</div>
<!-- PANEL DE RECUPERACIÓN -->
<div class="panel-container w-full"
[ngClass]="{'animate__animated animate__fadeIn': showRecovery,
'animate__animated animate__fadeOut d-none': !showRecovery && !isInitialLoad}">
<div class="login-card shadow-2 border-round">
<div class="login-header">
<h2>Recuperar Contraseña</h2>
</div>
<form (ngSubmit)="onRecoverPassword()" class="p-3">
<!-- Email para recuperación -->
<div class="field mb-3">
<input type="email" pInputText [(ngModel)]="recoveryEmail" name="recoveryEmail"
placeholder="Ingresa tu email" class="input-with-icon w-full" required />
</div>
<!-- Mensaje informativo -->
<div class="info-message mb-3">
<p class="text-sm">Te enviaremos un enlace para restablecer tu contraseña.</p>
</div>
<!-- Mensaje de estado de recuperación -->
<div *ngIf="recoveryMessage" class="recovery-message my-2">
<p-message [severity]="recoveryStatus" [text]="recoveryMessage"></p-message>
</div>
<!-- Botón de enviar -->
<div class="login-actions">
<button pButton type="submit" [label]="recoveryLoading ? 'Enviando...' : 'Enviar Enlace'"
class="p-button-primary w-full" [disabled]="recoveryLoading || !recoveryEmail">
<i *ngIf="recoveryLoading" class="pi pi-spin pi-spinner mr-2"></i>
</button>
</div>
</form>
<!-- Volver al login -->
<div class="password-recovery px-3 pb-3">
<a href="#" (click)="toggleRecovery($event)">Volver al Login</a>
<p class="mb-1 font-bold">Información:</p>
<p class="mb-1">Serás redirigido al sistema de autenticación.</p>
<p>Si tienes problemas, contacta al administrador.</p>
</div>
</div>
</div>

View File

@ -1,7 +1,6 @@
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { Router } from '@angular/router';
import { Router, ActivatedRoute } from '@angular/router';
import { CardModule } from 'primeng/card';
import { InputTextModule } from 'primeng/inputtext';
import { ButtonModule } from 'primeng/button';
@ -16,9 +15,9 @@ import { AuthService } from '../../services/auth.service';
@Component({
selector: 'app-login',
standalone: true,
imports: [
CommonModule,
FormsModule,
CardModule,
InputTextModule,
ButtonModule,
@ -34,103 +33,66 @@ import { AuthService } from '../../services/auth.service';
styleUrl: './login.component.scss'
})
export class LoginComponent implements OnInit {
// Login form
email: string = '';
password: string = '';
// Inyectar el servicio de autenticación
private authService = inject(AuthService);
private route = inject(ActivatedRoute);
private router = inject(Router);
loading: boolean = false;
errorMessage: string = '';
returnUrl: string = '';
// Password recovery form
recoveryEmail: string = '';
recoveryLoading: boolean = false;
recoveryMessage: string = '';
recoveryStatus: string = 'info';
constructor(private messageService: MessageService) {}
// Control de formularios
showRecovery: boolean = false;
isInitialLoad: boolean = false;
constructor(
private router: Router,
private authService: AuthService,
private messageService: MessageService
) { }
ngOnInit() {
async ngOnInit() {
// Obtener el returnUrl de los query params si existe
this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/inicio';
// Verificar si ya está autenticado y redirigir
this.authService.isLoggedIn().subscribe(isLoggedIn => {
if (isLoggedIn) {
console.log('Usuario ya autenticado, redirigiendo...');
this.router.navigateByUrl(this.returnUrl);
}
});
}
/**
* Cambia entre el formulario de login y recuperación
*/
// Método de login con Keycloak usando nuestro servicio AuthService
async onLogin() {
this.loading = true;
try {
// Construir el redirectUri para que vuelva a la URL que estaba intentando acceder
const redirectUri = window.location.origin + this.returnUrl;
console.log('Iniciando login con redirectUri:', redirectUri);
await this.authService.login(redirectUri);
// No es necesario manejar el redirect aquí, Keycloak lo hará automáticamente
} catch (error) {
console.error('Error al iniciar sesión:', error);
this.loading = false;
this.messageService.add({
severity: 'error',
summary: 'Error',
detail: 'Error al intentar iniciar sesión. Por favor, inténtelo de nuevo.'
});
}
}
// Mantenemos los métodos de recuperación de contraseña para compatibilidad
toggleRecovery(event: Event) {
event.preventDefault();
this.showRecovery = !this.showRecovery;
this.errorMessage = '';
this.recoveryMessage = '';
// Si estamos cambiando al formulario de recuperación, copiar el email actual
if (this.showRecovery && this.email) {
this.recoveryEmail = this.email;
}
// Forzar actualización del DOM con un pequeño retraso
setTimeout(() => {
// Este timeout ayuda a que Angular aplique los cambios de clase completamente
}, 50); // Aumentado para asegurar la transición suave
this.messageService.add({
severity: 'info',
summary: 'Información',
detail: 'Para recuperar tu contraseña, utiliza la opción en la pantalla de login de Keycloak'
});
}
/**
* Proceso de login
*/
onLogin() {
this.loading = true;
this.errorMessage = '';
// Llamar al servicio auth
this.authService.login({ email: this.email, password: this.password })
.subscribe({
next: () => {
console.log('Login exitoso');
this.loading = false;
this.router.navigate(['/inicio']);
},
error: (error) => {
console.error('Error en login:', error);
this.loading = false;
this.errorMessage = 'Credenciales incorrectas';
this.messageService.add({
severity: 'error',
summary: 'Error',
detail: 'Credenciales incorrectas'
});
}
});
}
/**
* Proceso de recuperación de contraseña
*/
onRecoverPassword() {
if (!this.recoveryEmail) {
this.recoveryMessage = 'Debes ingresar un email';
this.recoveryStatus = 'error';
return;
}
this.recoveryLoading = true;
this.recoveryMessage = '';
// Simulamos la recuperación (en producción, esto llamaría a un servicio real)
setTimeout(() => {
this.recoveryLoading = false;
this.recoveryMessage = 'Hemos enviado un enlace de recuperación a tu email';
this.recoveryStatus = 'success';
this.messageService.add({
severity: 'success',
summary: 'Email enviado',
detail: 'Se ha enviado un enlace de recuperación a tu correo'
});
}, 1500);
this.messageService.add({
severity: 'info',
summary: 'Información',
detail: 'Esta funcionalidad ahora es manejada por Keycloak'
});
}
}

View File

@ -1,111 +1,162 @@
import { Injectable } from '@angular/core';
import { Injectable, inject, effect, signal, computed } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, BehaviorSubject, of, throwError } from 'rxjs';
import { tap, delay } from 'rxjs/operators';
interface LoginCredentials {
username?: string;
password?: string;
email?: string;
}
interface AuthResponse {
token: string;
user: {
id: number;
username: string;
name: string;
role: string;
email?: string;
};
}
import { BehaviorSubject, Observable, from } from 'rxjs';
import { KEYCLOAK_EVENT_SIGNAL, KeycloakEventType } from 'keycloak-angular';
import { Router } from '@angular/router';
import Keycloak from 'keycloak-js';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Injectable({
providedIn: 'root'
})
export class AuthService {
private apiUrl = 'api/auth';
// Inject Keycloak instance directly
private keycloak = inject(Keycloak);
private keycloakEvents = inject(KEYCLOAK_EVENT_SIGNAL);
private router = inject(Router);
// User state
private userSubject = new BehaviorSubject<any>(null);
public user$ = this.userSubject.asObservable();
// Usuarios de prueba para simular login
private mockUsers = [
{
email: 'admin@example.com',
password: 'admin123',
user: {
id: 1,
username: 'admin',
name: 'Administrador',
role: 'admin',
email: 'admin@example.com'
}
},
{
email: 'user@example.com',
password: 'user123',
user: {
id: 2,
username: 'user',
name: 'Usuario',
role: 'user',
email: 'user@example.com'
}
}
];
// Authentication state as a signal
public isAuthenticated = signal<boolean>(false);
constructor(private http: HttpClient) {
// Check if user is already logged in on service initialization
const user = localStorage.getItem('user');
if (user) {
this.userSubject.next(JSON.parse(user));
// Check initial state
this.checkInitialAuthState();
// Set up event handlers using Angular effects
effect(() => {
const event = this.keycloakEvents();
if (!event) return;
console.log('Keycloak event:', event.type);
// On successful login
if (event.type === KeycloakEventType.AuthSuccess) {
this.isAuthenticated.set(true);
this.loadUserInfo();
}
// On logout
if (event.type === KeycloakEventType.AuthLogout) {
this.isAuthenticated.set(false);
this.userSubject.next(null);
this.router.navigate(['/login']);
}
// On authentication error
if (event.type === KeycloakEventType.AuthError) {
console.error('Authentication error:', event);
this.isAuthenticated.set(false);
this.userSubject.next(null);
}
// On token expiration
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);
}
}
login(credentials: LoginCredentials): Observable<AuthResponse> {
// Simular login con usuarios de prueba
const user = this.mockUsers.find(u =>
u.email === credentials.email && u.password === credentials.password);
private async loadUserInfo(): Promise<void> {
try {
const isLoggedIn = await this.keycloak.authenticated;
if (user) {
// Generar token falso
const mockResponse: AuthResponse = {
token: 'mock-jwt-token-' + Math.random().toString(36).substring(2, 15),
user: user.user
if (!isLoggedIn) {
this.userSubject.next(null);
return;
}
const userProfile = await this.keycloak.loadUserProfile();
const isAdmin = this.keycloak.hasRealmRole('admin');
// Get user roles
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
};
return of(mockResponse).pipe(
// Simular retraso de red
delay(800),
tap(response => {
// Store token and user info
localStorage.setItem('token', response.token);
localStorage.setItem('user', JSON.stringify(response.user));
this.userSubject.next(response.user);
})
);
} else {
// Simular error de credenciales inválidas
return throwError(() => new Error('Credenciales incorrectas'));
this.userSubject.next(user);
} catch (error) {
console.error('Error loading user profile:', error);
this.userSubject.next(null);
}
}
logout(): void {
// Clear storage and update subject
localStorage.removeItem('token');
localStorage.removeItem('user');
this.userSubject.next(null);
login(redirectUri?: string): Promise<void> {
return this.keycloak.login({
redirectUri: redirectUri || window.location.origin
});
}
isLoggedIn(): boolean {
return !!localStorage.getItem('token');
logout(): Promise<void> {
return this.keycloak.logout({
redirectUri: window.location.origin
});
}
getToken(): string | null {
return localStorage.getItem('token');
isLoggedIn(): Observable<boolean> {
return from(Promise.resolve(this.keycloak.authenticated));
}
getToken(): Promise<string> {
return Promise.resolve(this.keycloak.token || '');
}
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;
}
// Check if user has a specific role
hasRole(role: string): boolean {
return this.keycloak.hasRealmRole(role);
}
// Check if user has any of the specified roles
hasAnyRole(roles: string[]): boolean {
for (const role of roles) {
if (this.keycloak.hasRealmRole(role)) {
return true;
}
}
return false;
}
}

View File

@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="utf-8">
<title>cronogramas</title>
<title>correo</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">

963
tutorial keycloack.md Normal file
View File

@ -0,0 +1,963 @@
# Tutorial: Implementación de Keycloak con LDAP para Angular
Este tutorial explica cómo configurar un sistema de autenticación completo utilizando Keycloak como proveedor de identidad, OpenLDAP como directorio de usuarios y una aplicación Angular que consume estos servicios.
## Índice
1. [Preparación del entorno Ubuntu](#1-preparaci%C3%B3n-del-entorno-ubuntu)
2. [Instalación y configuración de OpenLDAP](#2-instalaci%C3%B3n-y-configuraci%C3%B3n-de-openldap)
3. [Crear estructura LDAP para correos.com](#3-crear-estructura-ldap-para-correoscom)
4. [Instalación y configuración de Keycloak](#4-instalaci%C3%B3n-y-configuraci%C3%B3n-de-keycloak)
5. [Configuración de Keycloak en la interfaz web](#5-configuraci%C3%B3n-de-keycloak-en-la-interfaz-web)
6. [Configuración de una aplicación Angular](#6-configuraci%C3%B3n-de-una-aplicaci%C3%B3n-angular)
7. [Arquitectura del sistema](#7-arquitectura-del-sistema)
8. [Resolución de problemas comunes](#8-resoluci%C3%B3n-de-problemas-comunes)
9. [Verificación y prueba del sistema](#9-verificaci%C3%B3n-y-prueba-del-sistema)
10. [Resumen](#10-resumen)
## 1. Preparación del entorno Ubuntu
Primero, actualizamos el sistema e instalamos 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
```
## 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 correos.com
### 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/21.1.1/keycloak-21.1.1.tar.gz
# Extraer el archivo
tar -xvzf keycloak-21.1.1.tar.gz
# Mover a una ubicación más adecuada
sudo mv keycloak-21.1.1 /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
```
En el futuro, podrás habilitar e iniciar el servicio con:
```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:
- Client authentication: `ON`
- 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`
- Haz clic en "Save"
6. En la pestaña "Credentials" del cliente, copia el "Client secret" (lo necesitarás para la aplicación Angular)
## 6. Configuración de una aplicación Angular
Primero, instala Node.js y npm:
```bash
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt-get install -y nodejs
```
### Crear una aplicación Angular
```bash
# Instalar Angular CLI
npm install -g @angular/cli
# 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",
"credentials": {
"secret": "TU_CLIENT_SECRET_AQUÍ"
}
}
```
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 { }
```
Crea un archivo `src/assets/silent-check-sso.html`:
```html
<html>
<body>
<script>
parent.postMessage(location.href, location.origin);
</script>
</body>
</html>
```
### Crear un servicio de autenticación
```bash
ng generate service auth
```
Edita `src/app/auth.service.ts`:
```typescript
import { Injectable } from '@angular/core';
import { KeycloakService } from 'keycloak-angular';
import { KeycloakProfile } from 'keycloak-js';
@Injectable({
providedIn: 'root'
})
export class AuthService {
constructor(private keycloak: KeycloakService) { }
public getLoggedUser(): Promise<KeycloakProfile | null> {
return this.keycloak.loadUserProfile();
}
public login(): void {
this.keycloak.login();
}
public logout(): void {
this.keycloak.logout();
}
public isLoggedIn(): Promise<boolean> {
return this.keycloak.isLoggedIn();
}
public getRoles(): string[] {
return this.keycloak.getUserRoles();
}
}
```
### Crear un guardia para rutas protegidas
```bash
ng generate guard auth
```
Edita `src/app/auth.guard.ts`:
```typescript
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, RouterStateSnapshot, Router, UrlTree } from '@angular/router';
import { KeycloakAuthGuard, KeycloakService } from 'keycloak-angular';
@Injectable({
providedIn: 'root'
})
export class AuthGuard extends KeycloakAuthGuard {
constructor(
protected override readonly router: Router,
protected readonly keycloak: KeycloakService
) {
super(router, keycloak);
}
public async isAccessAllowed(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Promise<boolean | UrlTree> {
// Verifica si el usuario está autenticado
if (!this.authenticated) {
await this.keycloak.login({
redirectUri: window.location.origin + state.url,
});
return false;
}
// Obtiene los roles requeridos desde la ruta
const requiredRoles = route.data['roles'];
// Permite el acceso si no hay roles requeridos
if (!requiredRoles || requiredRoles.length === 0) {
return true;
}
// Verifica si el usuario tiene al menos uno de los roles requeridos
return requiredRoles.some((role: string) => this.roles.includes(role));
}
}
```
### Configurar las rutas con protección
Modifica `src/app/app-routing.module.ts`:
```typescript
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from './auth.guard';
import { AppComponent } from './app.component';
const routes: Routes = [
{
path: 'admin',
component: AppComponent,
canActivate: [AuthGuard],
data: { roles: ['administradores'] }
},
{
path: 'developer',
component: AppComponent,
canActivate: [AuthGuard],
data: { roles: ['desarrolladores'] }
},
{
path: 'user',
component: AppComponent,
canActivate: [AuthGuard],
data: { roles: ['usuarios'] }
},
{
path: '',
component: AppComponent
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
```
### Modificar el componente principal
Edita `src/app/app.component.ts`:
```typescript
import { Component, OnInit } from '@angular/core';
import { AuthService } from './auth.service';
import { KeycloakProfile } from 'keycloak-js';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
title = 'angular-keycloak-app';
isLoggedIn = false;
userProfile: KeycloakProfile | null = null;
userRoles: string[] = [];
constructor(private authService: AuthService) {}
async ngOnInit() {
this.isLoggedIn = await this.authService.isLoggedIn();
if (this.isLoggedIn) {
this.userProfile = await this.authService.getLoggedUser();
this.userRoles = this.authService.getRoles();
}
}
login() {
this.authService.login();
}
logout() {
this.authService.logout();
}
}
```
Edita `src/app/app.component.html`:
```html
<div class="container mt-5">
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h2>Demo Keycloak con LDAP - Correos.com</h2>
</div>
<div class="card-body">
<div *ngIf="isLoggedIn; else loginButton">
<h3>Bienvenido, {{ userProfile?.firstName }} {{ userProfile?.lastName }}</h3>
<p>Email: {{ userProfile?.email }}</p>
<p>Nombre de Usuario: {{ userProfile?.username }}</p>
<h4>Tus Roles:</h4>
<ul>
<li *ngFor="let role of userRoles">{{ role }}</li>
</ul>
<button class="btn btn-danger" (click)="logout()">Cerrar Sesión</button>
</div>
<ng-template #loginButton>
<p>Por favor inicia sesión para acceder al sistema</p>
<button class="btn btn-primary" (click)="login()">Iniciar Sesión</button>
</ng-template>
</div>
</div>
</div>
</div>
</div>
```
Agrega Bootstrap a `src/index.html`:
```html
<head>
<!-- Otras etiquetas -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
```
### Ejecutar la aplicación
```bash
ng serve
```
Abre http://localhost:4200 en tu navegador.
## 7. 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)
## 8. 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: La aplicación Angular no puede conectarse a Keycloak
1. Verifica que Keycloak esté en ejecución
2. Comprueba la URL del servidor Keycloak en la configuración de Angular
3. Asegúrate de que el cliente esté correctamente configurado en Keycloak
4. Verifica el Client Secret
5. Comprueba las URLs de redirección
## 9. 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
## 10. 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
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.