From 196129659336336ad51f327adb6e8e10ef9558fd Mon Sep 17 00:00:00 2001 From: acazares Date: Mon, 20 Oct 2025 19:25:29 -0500 Subject: [PATCH] feat(auth): implement OAuth2 exchange code flow for Microsoft login, update environment configurations, and enhance frontend SSO handling --- .env.example | 8 + azure.crt | 44 ++++ backend/api/v1/modules/a76/auth/dto.py | 16 ++ backend/api/v1/modules/a76/auth/routes.py | 21 +- backend/api/v1/modules/a76/auth/service.py | 75 ++++++ docker-compose.yml | 10 +- docs/MICROSOFT_SSO_SETUP.md | 214 +++++++++++++++++ docs/VERIFICAR_MICROSOFT_CONFIG.md | 219 ++++++++++++++++++ frontend/.env.example | 8 +- .../src/routes/auth/callback/+page.svelte | 140 +++++++++++ frontend/src/routes/login/+page.svelte | 45 ++++ 11 files changed, 793 insertions(+), 7 deletions(-) create mode 100644 azure.crt create mode 100644 docs/MICROSOFT_SSO_SETUP.md create mode 100644 docs/VERIFICAR_MICROSOFT_CONFIG.md create mode 100644 frontend/src/routes/auth/callback/+page.svelte diff --git a/.env.example b/.env.example index d8d9569d..f7a9c0c4 100644 --- a/.env.example +++ b/.env.example @@ -22,7 +22,15 @@ KEYCLOAK_FRONTEND_CLIENT_ID=anexo76-frontend DEBUG=True ENVIRONMENT=development +CORE_DB_HOST=postgres-a76 +CORE_DB_PORT=5432 +CORE_DB_NAME=anexo76_core +CORE_DB_USER=postgres +CORE_DB_PASSWORD=postgres + + # ----- Frontend ----- NODE_ENV=development PUBLIC_API_URL=http://localhost:8000 +PUBLIC_KEYCLOAK_REALM=master PUBLIC_KEYCLOAK_URL=http://localhost:8080 diff --git a/azure.crt b/azure.crt new file mode 100644 index 00000000..f50e5bbd --- /dev/null +++ b/azure.crt @@ -0,0 +1,44 @@ +-----BEGIN CERTIFICATE----- +MIIH1jCCBr6gAwIBAgIQC6Mxbk470/aejYi9ZXtTGjANBgkqhkiG9w0BAQsFADBN +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMScwJQYDVQQDEx5E +aWdpQ2VydCBTSEEyIFNlY3VyZSBTZXJ2ZXIgQ0EwHhcNMjUwOTIyMDAwMDAwWhcN +MjYwMzIyMjM1OTU5WjB/MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3Rv +bjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0 +aW9uMSkwJwYDVQQDEyBzdGFtcDIubG9naW4ubWljcm9zb2Z0b25saW5lLmNvbTCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM3K1uJNEzm2EnB1LY34OThA +fC0P/F5bue+IV4GnTxjmfDTeWBW4VcInt6Row7UyCNh6EyRXIGyr1zJr34HI2NcF +5TgkftNCDua8v3SivzknmVnXNVj51ct70+UBjgN6CMhl9/b61R7nguQIVs+GdyoX +deFqgMn+awDEmLcjUS3ijw1OVbf3O5Oha5LZwxmTx3gDkb+kwH7Tba2gIr7IvVQe +dbfIO0eYQKyBCPvyCiuNS3YHGEMug+I5y1ycPQV+OmrxYbEjoS3m2cic0YV/P7xY +vdx8O2XUAVTzfJs5GAkY+IiNtpG5oaZYXJbbxIM5v11CyYeJthfpdALGA5MkiR0C +AwEAAaOCBH4wggR6MB8GA1UdIwQYMBaAFA+AYRyCMWHVLyjnjUY4tCzhxtniMB0G +A1UdDgQWBBQiLJufwFGrmBabwkp+oOZQ2USotzCCASYGA1UdEQSCAR0wggEZgiBz +dGFtcDIubG9naW4ubWljcm9zb2Z0b25saW5lLmNvbYIdbG9naW4ubWljcm9zb2Z0 +b25saW5lLWludC5jb22CG2xvZ2luLm1pY3Jvc29mdG9ubGluZS1wLmNvbYIZbG9n +aW4ubWljcm9zb2Z0b25saW5lLmNvbYIebG9naW4yLm1pY3Jvc29mdG9ubGluZS1p +bnQuY29tghpsb2dpbjIubWljcm9zb2Z0b25saW5lLmNvbYIfbG9naW5leC5taWNy +b3NvZnRvbmxpbmUtaW50LmNvbYIbbG9naW5leC5taWNyb3NvZnRvbmxpbmUuY29t +giRzdGFtcDIubG9naW4ubWljcm9zb2Z0b25saW5lLWludC5jb20wPgYDVR0gBDcw +NTAzBgZngQwBAgIwKTAnBggrBgEFBQcCARYbaHR0cDovL3d3dy5kaWdpY2VydC5j +b20vQ1BTMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB +BQUHAwIwgY0GA1UdHwSBhTCBgjA/oD2gO4Y5aHR0cDovL2NybDMuZGlnaWNlcnQu +Y29tL0RpZ2ljZXJ0U0hBMlNlY3VyZVNlcnZlckNBLTEuY3JsMD+gPaA7hjlodHRw +Oi8vY3JsNC5kaWdpY2VydC5jb20vRGlnaWNlcnRTSEEyU2VjdXJlU2VydmVyQ0Et +MS5jcmwwfgYIKwYBBQUHAQEEcjBwMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5k +aWdpY2VydC5jb20wSAYIKwYBBQUHMAKGPGh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0 +LmNvbS9EaWdpQ2VydFNIQTJTZWN1cmVTZXJ2ZXJDQS0yLmNydDAMBgNVHRMBAf8E +AjAAMIIBfwYKKwYBBAHWeQIEAgSCAW8EggFrAWkAdgCWl2S/VViXrfdDh2g3CEJ3 +6fA61fak8zZuRqQ/D8qpxgAAAZlxQ3dQAAAEAwBHMEUCICRSWP825/83352Dv6WQ +IcXT2bmT1D6lrJQ1W6U+6PX0AiEA7XUiPn8JJmJWCK0jUS/ZrOUEKf1cML3MWeQg +6WX7MfMAdwBkEcRspBLsp4kcogIuALyrTygH1B41J6vq/tUDyX3N8AAAAZlxQ3dT +AAAEAwBIMEYCIQCL6HjqOV6VliTqVirdscl+wjEPNfq/fkzm26WJ5m26yQIhANJ9 +Gc6Kt5AX15DNNJ5ukpAWgtENmpT3M+oTw/7SB0SdAHYASZybad4dfOz8Nt7Nh2Sm +uFuvCoeAGdFVUvvp6ynd+MMAAAGZcUN3bwAABAMARzBFAiEAssa9lKaXdaV7UbAj +ZwpILJrsHrszTewahn6yIo24XYoCIGl+1U3yR/RN/Ox3mNlPpJO2tDSkxcUBk3mR +BaNcrLcuMA0GCSqGSIb3DQEBCwUAA4IBAQBIru75Gq6GMI/GG+3fLKFT+NYwHqHq +J97CcTPwFRW0iv3EKKEZCyxB5su+gB6JkYFq4B+n7KZgmkuIXyO50uAgJ3toeYNs +S9WWVTqL2ETWSUl+4iNbTXnaj2/eW+27OJcM6z0phQbu/uZh8wsD2pJzv7y6isyF +5FbvC4pbb+z9LfAUH7D3kJEYEjZfD4c/WZH13s0rSlXmBPXweOFGEFCeCO1/BI0Q +2l5PdbgYAtCYUwZl3Vy7/J8uhrs0papWAMauKZWrvTauCtKcPjSI8oRwsVWrf0vL +dcWYPQmaWGxUd1+mEqcB9OiB7lTlwx+ipRJDURninhn1dOef6+qg9oV2 +-----END CERTIFICATE----- diff --git a/backend/api/v1/modules/a76/auth/dto.py b/backend/api/v1/modules/a76/auth/dto.py index 31e6d4ee..9107c03a 100644 --- a/backend/api/v1/modules/a76/auth/dto.py +++ b/backend/api/v1/modules/a76/auth/dto.py @@ -109,3 +109,19 @@ class RegisterResponseDTO(BaseModel): "message": "User registered successfully" } } + + +class ExchangeCodeRequestDTO(BaseModel): + """DTO para intercambiar authorization code por tokens (OAuth2 flow)""" + code: str = Field(..., description="Authorization code de OAuth2") + redirect_uri: str = Field(..., description="Redirect URI usado en la autorización") + tenant_slug: Optional[str] = Field(None, description="Slug del tenant (opcional)") + + class Config: + json_schema_extra = { + "example": { + "code": "eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2Ii...", + "redirect_uri": "http://localhost:5173/auth/callback", + "tenant_slug": "empresa-abc" + } + } diff --git a/backend/api/v1/modules/a76/auth/routes.py b/backend/api/v1/modules/a76/auth/routes.py index 34824a7b..ab604f6e 100644 --- a/backend/api/v1/modules/a76/auth/routes.py +++ b/backend/api/v1/modules/a76/auth/routes.py @@ -14,7 +14,8 @@ from .dto import ( UserInfoResponseDTO, LogoutRequestDTO, RegisterRequestDTO, - RegisterResponseDTO + RegisterResponseDTO, + ExchangeCodeRequestDTO ) from .service import AuthService @@ -101,6 +102,24 @@ async def logout( return service.logout(logout_data) +@router.post("/exchange-code", response_model=TokenResponseDTO) +async def exchange_code( + exchange_data: ExchangeCodeRequestDTO, + db: Session = Depends(get_core_db) +): + """ + Intercambia un authorization code de OAuth2 por tokens + + Este endpoint es útil cuando el frontend usa el flujo de autorización + con proveedores externos (Microsoft, Google, etc.) a través de Keycloak. + + El código se obtiene después de que el usuario se autentica con el proveedor + externo y Keycloak lo redirige al frontend con el código en los query params. + """ + service = AuthService(db) + return service.exchange_code(exchange_data) + + @router.get("/health") async def auth_health(): """ diff --git a/backend/api/v1/modules/a76/auth/service.py b/backend/api/v1/modules/a76/auth/service.py index 95285957..24a35dd0 100644 --- a/backend/api/v1/modules/a76/auth/service.py +++ b/backend/api/v1/modules/a76/auth/service.py @@ -275,3 +275,78 @@ class AuthService: except Exception as e: logger.error(f"Registration error: {str(e)}") raise HTTPException(status_code=500, detail="Registration error") + + def exchange_code(self, exchange_data) -> TokenResponseDTO: + """ + Intercambia un authorization code por tokens + + Este método se usa cuando el frontend recibe un código de autorización + después de un login con proveedor externo (Microsoft, Google, etc.) + a través de Keycloak. + + Args: + exchange_data: Datos del código y redirect_uri + + Returns: + TokenResponseDTO con access_token y refresh_token + + Raises: + HTTPException: Si el código es inválido o expiró + """ + try: + # Importar el DTO aquí para evitar referencias circulares + from .dto import ExchangeCodeRequestDTO + + # Intercambiar código por tokens usando Keycloak + token_response = self.keycloak_openid.token( + grant_type='authorization_code', + code=exchange_data.code, + redirect_uri=exchange_data.redirect_uri + ) + + logger.info(f"Code exchanged successfully") + + # Si se proporciona tenant_slug, podríamos validar que el usuario pertenece a ese tenant + # Por ahora simplemente retornamos los tokens + if exchange_data.tenant_slug: + # Decodificar token para obtener tenant_id del usuario + user_info = self.keycloak_openid.introspect(token_response['access_token']) + user_tenant_id = user_info.get('tenant_id') + + # Validar que el tenant existe y está activo + from api.v1.modules.a76.tenants.service import TenantService + tenant_service = TenantService(self.db) + tenant = tenant_service.get_tenant_by_slug(exchange_data.tenant_slug) + + if not tenant: + raise HTTPException(status_code=404, detail="Tenant not found") + + if not tenant.is_active: + raise HTTPException(status_code=403, detail="Tenant is not active") + + # Opcional: Verificar que el usuario pertenece al tenant + # Esto depende de cómo manejes los tenants en tu aplicación + + return TokenResponseDTO( + access_token=token_response['access_token'], + refresh_token=token_response['refresh_token'], + token_type=token_response.get('token_type', 'bearer'), + expires_in=token_response.get('expires_in', 3600) + ) + + except KeycloakError as e: + error_message = str(e) + logger.warning(f"Code exchange failed: {error_message}") + + if "invalid_grant" in error_message.lower(): + raise HTTPException(status_code=400, detail="Invalid or expired authorization code") + elif "invalid_client" in error_message.lower(): + raise HTTPException(status_code=401, detail="Invalid client credentials") + else: + raise HTTPException(status_code=500, detail="Token exchange error") + + except HTTPException: + raise + except Exception as e: + logger.error(f"Code exchange error: {str(e)}") + raise HTTPException(status_code=500, detail="Code exchange error") diff --git a/docker-compose.yml b/docker-compose.yml index 7861eb4d..4a270727 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -153,12 +153,12 @@ services: - ENVIRONMENT=${ENVIRONMENT:-development} - PYTHONUNBUFFERED=1 - PYTHONDONTWRITEBYTECODE=1 - - CORE_DB_HOST=postgres-a76 - - CORE_DB_PORT=5432 - - CORE_DB_NAME=anexo76_core - - CORE_DB_USER=postgres + - CORE_DB_HOST=${CORE_DB_HOST:-postgres-a76} + - CORE_DB_PORT=${CORE_DB_PORT:-5432} + - CORE_DB_NAME=${CORE_DB_NAME:-anexo76_core} + - CORE_DB_USER=${CORE_DB_USER:-postgres} - CORE_DB_PASSWORD=${POSTGRES_APP_PASSWORD:-postgres} - - KEYCLOAK_SERVER_URL=http://keycloak:8080 + - KEYCLOAK_SERVER_URL=${KEYCLOAK_SERVER_URL:-http://keycloak:8080} - KEYCLOAK_REALM=${KEYCLOAK_REALM:-master} - KEYCLOAK_CLIENT_ID=${KEYCLOAK_CLIENT_ID:-anexo76-backend} - KEYCLOAK_CLIENT_SECRET=${KEYCLOAK_CLIENT_SECRET:-dev-secret} diff --git a/docs/MICROSOFT_SSO_SETUP.md b/docs/MICROSOFT_SSO_SETUP.md new file mode 100644 index 00000000..fad12844 --- /dev/null +++ b/docs/MICROSOFT_SSO_SETUP.md @@ -0,0 +1,214 @@ +# Configuración de Login con Microsoft (Azure AD) + +Esta guía te ayudará a configurar el login con Microsoft junto con el login tradicional. + +## Parte 1: Configurar Aplicación en Azure AD + +### 1.1 Crear App Registration en Azure Portal + +1. Ve a [Azure Portal](https://portal.azure.com) +2. Busca "Azure Active Directory" o "Microsoft Entra ID" +3. En el menú lateral, selecciona **App registrations** +4. Clic en **New registration** +5. Configura: + - **Name**: `Anexo76` + - **Supported account types**: + - "Accounts in any organizational directory (Any Azure AD directory - Multitenant)" + - O "Accounts in any organizational directory and personal Microsoft accounts" si quieres permitir cuentas @outlook.com, @hotmail.com + - **Redirect URI**: + - Platform: `Web` + - URI: `http://localhost:8080/realms/master/broker/microsoft/endpoint` + - Clic en **Register** + +### 1.2 Obtener Client ID y crear Client Secret + +1. En la página de tu aplicación, copia el **Application (client) ID** +2. Ve a **Certificates & secrets** en el menú lateral +3. Clic en **New client secret** +4. Descripción: `keycloak-integration` +5. Expires: Selecciona el tiempo que prefieras (ej: 24 months) +6. Clic en **Add** +7. **IMPORTANTE**: Copia el **Value** del secret inmediatamente (solo se muestra una vez) + +### 1.3 Configurar API Permissions (Opcional pero recomendado) + +1. Ve a **API permissions** +2. Deberías ver `Microsoft Graph` > `User.Read` (Delegated) - esto es suficiente +3. Si quieres más información del usuario, agrega: + - `email` + - `profile` + - `openid` + +## Parte 2: Configurar Identity Provider en Keycloak + +### 2.1 Agregar Microsoft como Identity Provider + +1. Abre Keycloak Admin Console: http://localhost:8080 +2. Login como admin +3. Asegúrate de estar en el realm correcto (probablemente `master`) +4. En el menú lateral, ve a **Identity providers** +5. En el dropdown "Add provider", selecciona **Microsoft** +6. Configura: + - **Alias**: `microsoft` (o cualquier nombre que prefieras) + - **Display name**: `Microsoft` (esto es lo que verá el usuario) + - **Enabled**: ON + - **Store tokens**: ON (opcional, para poder usar tokens de Microsoft después) + - **Stored tokens readable**: OFF + - **Trust email**: ON + - **First login flow**: `first broker login` + - **Client ID**: Pega el Application (client) ID de Azure + - **Client Secret**: Pega el client secret que copiaste + - Clic en **Save** + +### 2.2 Configurar Mappers (Mapeo de atributos) + +Después de guardar, configura los mappers para traer información del usuario de Microsoft: + +1. En la misma página del Identity Provider, ve a la pestaña **Mappers** +2. Clic en **Add mapper** + +**Mapper 1: Email** +- Name: `email` +- Sync mode override: `inherit` +- Mapper type: `Attribute Importer` +- Social profile JSON field path: `email` +- User attribute name: `email` +- Clic en **Save** + +**Mapper 2: First Name** +- Name: `firstName` +- Mapper type: `Attribute Importer` +- Social profile JSON field path: `given_name` +- User attribute name: `firstName` +- Clic en **Save** + +**Mapper 3: Last Name** +- Name: `lastName` +- Mapper type: `Attribute Importer` +- Social profile JSON field path: `family_name` +- User attribute name: `lastName` +- Clic en **Save** + +**Mapper 4: Username** +- Name: `username` +- Mapper type: `Username Template Importer` +- Template: `${CLAIM.email}` +- Target: `BROKER_USERNAME` +- Clic en **Save** + +### 2.3 Configurar Redirect URI en Azure (si es necesario) + +Si usas un realm diferente a `master`, actualiza la Redirect URI en Azure: + +- Formato: `http://localhost:8080/realms/{REALM_NAME}/broker/microsoft/endpoint` +- Para producción: `https://tu-dominio.com/realms/{REALM_NAME}/broker/microsoft/endpoint` + +## Parte 3: Actualizar Frontend + +El frontend necesita detectar y mostrar el botón de Microsoft. Keycloak proporciona esta información automáticamente. + +### 3.1 Obtener Identity Providers disponibles + +Tu frontend puede consultar los Identity Providers disponibles: + +**Endpoint de Keycloak:** +``` +GET http://localhost:8080/realms/master/broker-login/identity-providers +``` + +Esto retorna algo como: +```json +[ + { + "alias": "microsoft", + "displayName": "Microsoft", + "providerId": "microsoft", + "enabled": true + } +] +``` + +### 3.2 URL para iniciar flujo de Microsoft + +Para iniciar el login con Microsoft, redirige al usuario a: +``` +http://localhost:8080/realms/master/broker/microsoft/login?client_id=anexo76-frontend&redirect_uri=http://localhost:5173/auth/callback +``` + +Parámetros: +- `client_id`: Tu client ID de frontend en Keycloak (`anexo76-frontend`) +- `redirect_uri`: URL a la que Keycloak redirigirá después del login exitoso +- `response_type`: `code` (para authorization code flow) +- `scope`: `openid profile email` + +### 3.3 Manejar el Callback + +Después del login con Microsoft, Keycloak redirige a tu `redirect_uri` con un `code`: +``` +http://localhost:5173/auth/callback?code=abc123...&session_state=xyz... +``` + +Tu frontend debe: +1. Extraer el `code` del query string +2. Intercambiar el `code` por tokens llamando a tu backend +3. Tu backend llama a Keycloak para obtener los tokens + +## Parte 4: Testing + +### 4.1 Verificar que Microsoft aparece en la página de login + +Ve a: +``` +http://localhost:8080/realms/master/protocol/openid-connect/auth?client_id=anexo76-frontend&redirect_uri=http://localhost:5173&response_type=code +``` + +Deberías ver: +- Formulario de login tradicional (usuario/contraseña) +- Botón o link de "Microsoft" para login social + +### 4.2 Probar el flujo completo + +1. Haz clic en el botón de Microsoft +2. Serás redirigido a Microsoft login +3. Ingresa credenciales de Microsoft +4. Microsoft redirige a Keycloak +5. Keycloak crea/actualiza el usuario y redirige a tu app +6. Tu app obtiene el token y autentica al usuario + +## Notas Importantes + +### Multi-tenant con Microsoft + +Si tu app es multi-tenant y quieres que cada tenant use su propio Azure AD: +1. Crea múltiples Identity Providers en Keycloak (uno por tenant) +2. Usa aliases diferentes: `microsoft-tenant1`, `microsoft-tenant2` +3. En el frontend, muestra el botón correcto según el tenant + +### Asignación automática de tenant + +Cuando un usuario se loguea por primera vez con Microsoft, puedes: +1. Usar un mapper para asignar atributos basados en el dominio del email +2. Configurar "Default Tenant" en tu backend si el email es de un dominio conocido +3. Solicitar al usuario que seleccione su tenant en el primer login + +### Producción + +Para producción, recuerda: +1. Actualizar las Redirect URIs en Azure con tu dominio real +2. Usar HTTPS +3. Configurar correctamente los Web Origins en Keycloak +4. Usar variables de entorno para las configuraciones + +## Troubleshooting + +### Error: redirect_uri_mismatch +- Verifica que la URI en Azure coincida exactamente con la de Keycloak +- Formato: `https://tu-dominio.com/realms/{realm}/broker/{alias}/endpoint` + +### Usuario se crea pero no tiene tenant_id +- Configura un mapper en Keycloak para asignar tenant_id automáticamente +- O maneja esto en tu backend en el primer login + +### No aparece el botón de Microsoft +- Verifica que el Identity Provider esté habilitado en Keycloak +- Revisa que el Display Name esté configurado diff --git a/docs/VERIFICAR_MICROSOFT_CONFIG.md b/docs/VERIFICAR_MICROSOFT_CONFIG.md new file mode 100644 index 00000000..f4518102 --- /dev/null +++ b/docs/VERIFICAR_MICROSOFT_CONFIG.md @@ -0,0 +1,219 @@ +# ✅ Verificar Configuración de Microsoft en Keycloak + +## Paso 1: Verificar si Microsoft está configurado + +Abre tu navegador y ve a: + +``` +http://localhost:8080/admin/master/console/#/master/identity-providers +``` + +Login: `admin` / `admin` + +### ¿Qué deberías ver? + +Si Microsoft está configurado, verás en la lista de Identity Providers: + +- ✅ **microsoft** (o el alias que hayas usado) +- Con estado: **Enabled** ✅ + +### Si NO ves "microsoft" en la lista: + +**¡Necesitas configurarlo!** Sigue estos pasos: + +--- + +## Paso 2: Configurar Microsoft en Keycloak (SI NO ESTÁ CONFIGURADO) + +### 2.1 Crear App en Azure AD PRIMERO + +Antes de configurar Keycloak, necesitas una aplicación en Azure: + +1. Ve a [Azure Portal](https://portal.azure.com) +2. Busca **Azure Active Directory** o **Microsoft Entra ID** +3. **App registrations** → **New registration** +4. Configura: + + - **Name**: `Anexo76` + - **Supported account types**: `Accounts in any organizational directory (Any Azure AD - Multitenant)` + - **Redirect URI**: + - Platform: `Web` + - URI: `http://localhost:8080/realms/master/broker/microsoft/endpoint` + - Click **Register** +5. **Copia el Application (client) ID** - lo necesitarás +6. Ve a **Certificates & secrets** → **New client secret** + + - Descripción: `keycloak` + - Expira: 24 meses + - Click **Add** + - **¡COPIA EL SECRET VALUE AHORA!** (solo se muestra una vez) + +### 2.2 Agregar Microsoft a Keycloak + +1. En Keycloak Admin Console: http://localhost:8080 +2. Login: `admin` / `admin` +3. Menú lateral: **Identity providers** +4. Dropdown: **Add provider** → Selecciona **Microsoft** +5. Configura: + +``` +Alias: microsoft +Display name: Microsoft +Enabled: ON ✅ +Store tokens: ON ✅ +Trust email: ON ✅ +First login flow: first broker login + +Client ID: [PEGA TU APPLICATION ID DE AZURE] +Client Secret: [PEGA TU SECRET DE AZURE] +``` + +6. Click **Save** +7. Ve a la pestaña **Mappers** y agrega estos 4 mappers: + +**Mapper 1: email** + +``` +Name: email +Mapper type: Attribute Importer +Social profile JSON field path: email +User attribute name: email +``` + +**Mapper 2: firstName** + +``` +Name: firstName +Mapper type: Attribute Importer +Social profile JSON field path: given_name +User attribute name: firstName +``` + +**Mapper 3: lastName** + +``` +Name: lastName +Mapper type: Attribute Importer +Social profile JSON field path: family_name +User attribute name: lastName +``` + +**Mapper 4: username** + +``` +Name: username +Mapper type: Username Template Importer +Template: ${CLAIM.email} +Target: BROKER_USERNAME +``` + +--- + + +## Paso 4: Verificar en tu App + +1. Asegúrate de que el frontend esté corriendo: `http://localhost:5173` +2. Ve a la página de login: `http://localhost:5173/login` +3. Ingresa un tenant (ej: `aduanasoft`) +4. Click en el botón **"Iniciar sesión con Microsoft"** + +### ¿Qué debería pasar? + +✅ **Correcto:** + +- Te redirige a Microsoft login +- Ves la página de Microsoft pidiendo tu email/contraseña +- Después de autenticarte, vuelves a tu app + +❌ **Incorrecto (lo que te está pasando ahora):** + +- Te lleva a la página de login de Keycloak +- Ves el usuario "admin" ya logueado + +--- + +## Troubleshooting Común + +### Error: "Identity provider not found" + +- El alias en Keycloak debe ser exactamente `microsoft` (minúsculas) +- O cambia el código: `loginWithProvider('TU_ALIAS_EXACTO')` + +### Error: redirect_uri_mismatch en Azure + +- La URI en Azure debe ser EXACTAMENTE: + ``` + http://localhost:8080/realms/master/broker/microsoft/endpoint + ``` +- Nota el `/endpoint` al final + +### Error: Unexpected error when authenticating with identity provider + +```bash +docker cp azure.crt anexo76-keycloak:/ +docker exec -it -u root anexo76-keycloak /bin/bash + +keytool -importcert -trustcacerts -file /azure.crt \ +-keystore /etc/java/java-21-openjdk/java-21-openjdk-21.0.8.0.9-1.el9.x86_64/lib/security/cacerts \ +-alias azure-root -storepass changeit -noprompt + +``` + +### Me redirige pero muestra error en Microsoft + +- Verifica que el Client ID y Secret en Keycloak sean correctos +- Verifica que la app en Azure esté habilitada + +### Funciona pero el usuario no tiene tenant_id + +- Esto es normal en el primer login +- Puedes configurar un mapper adicional o manejarlo en tu backend + +--- + +## Comando Rápido de Verificación + +Ejecuta esto en una terminal: + +```bash +# Verificar si el endpoint del broker existe +curl -s -o /dev/null -w "%{http_code}" "http://localhost:8080/realms/master/broker/microsoft/login?client_id=test&redirect_uri=http://localhost" +``` + +**Resultados:** + +- `302` = ✅ Microsoft está configurado (redirige a Microsoft) +- `404` = ❌ Microsoft NO está configurado en Keycloak +- `500` = ⚠️ Hay un error de configuración + +--- + +## Resumen Rápido + +**Para que funcione necesitas:** + +1. ✅ App Registration en Azure AD con Client ID y Secret +2. ✅ Identity Provider "microsoft" configurado en Keycloak +3. ✅ Redirect URI en Azure: `http://localhost:8080/realms/master/broker/microsoft/endpoint` +4. ✅ Variables de entorno en frontend (.env): + ``` + PUBLIC_KEYCLOAK_URL=http://localhost:8080 + PUBLIC_KEYCLOAK_REALM=master + PUBLIC_KEYCLOAK_CLIENT_ID=anexo76-frontend + ``` + +**El flujo correcto es:** + +``` +Tu App → Keycloak Broker → Microsoft Login → Keycloak → Tu App +``` + +**Lo que está pasando ahora:** + +``` +Tu App → Keycloak Login (porque no encuentra el provider) +``` + +--- + +¿Necesitas ayuda con la configuración? Primero verifica en Keycloak Admin Console si existe el Identity Provider "microsoft". diff --git a/frontend/.env.example b/frontend/.env.example index 215c2127..5fa0be78 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -1,5 +1,11 @@ # Environment variables para frontend -PUBLIC_API_URL=http://localhost:8000 +PUBLIC_API_URL=http://localhost:8000/api/ + +# Configuración de Keycloak para SSO PUBLIC_KEYCLOAK_URL=http://localhost:8080 PUBLIC_KEYCLOAK_REALM=master PUBLIC_KEYCLOAK_CLIENT_ID=anexo76-frontend + +# Opcional: Habilitar/deshabilitar proveedores SSO +PUBLIC_ENABLE_MICROSOFT_SSO=true +PUBLIC_ENABLE_GOOGLE_SSO=false diff --git a/frontend/src/routes/auth/callback/+page.svelte b/frontend/src/routes/auth/callback/+page.svelte new file mode 100644 index 00000000..775c674b --- /dev/null +++ b/frontend/src/routes/auth/callback/+page.svelte @@ -0,0 +1,140 @@ + + +
+
+
+

+ {#if processing} + Procesando autenticación... + {:else} + Error de autenticación + {/if} +

+
+ + {#if processing} +
+
+
+

+ Espera un momento mientras completamos tu inicio de sesión con Microsoft... +

+ {:else if error} +
+
+
+ + + +
+
+

+ {error} +

+
+
+
+ +
+ +
+ {/if} +
+
diff --git a/frontend/src/routes/login/+page.svelte b/frontend/src/routes/login/+page.svelte index 8f5600f7..9ece5850 100644 --- a/frontend/src/routes/login/+page.svelte +++ b/frontend/src/routes/login/+page.svelte @@ -1,12 +1,15 @@
@@ -116,5 +125,41 @@

+ + {#if showSSOProviders} +
+
+
+
+
+
+ O continúa con +
+
+ +
+ + + {#if !tenantSlug} +

+ Selecciona un tenant antes de usar login social +

+ {/if} +
+
+ {/if}