feat(auth): implement OAuth2 exchange code flow for Microsoft login, update environment configurations, and enhance frontend SSO handling

This commit is contained in:
2025-10-20 19:25:29 -05:00
parent f460c2b8aa
commit 1961296593
11 changed files with 793 additions and 7 deletions

View File

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

44
azure.crt Normal file
View File

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

View File

@@ -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"
}
}

View File

@@ -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():
"""

View File

@@ -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")

View File

@@ -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}

214
docs/MICROSOFT_SSO_SETUP.md Normal file
View File

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

View File

@@ -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".

View File

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

View File

@@ -0,0 +1,140 @@
<script lang="ts">
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { exchangeCodeForTokens, decodeJWT } from '$lib/sso';
import { authStore } from '$lib/auth';
import { browser } from '$app/environment';
let error = $state('');
let processing = $state(true);
onMount(async () => {
if (!browser) return;
try {
// Obtener el código y state de los query params
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
const state = params.get('state');
const errorParam = params.get('error');
const errorDescription = params.get('error_description');
if (errorParam) {
error = errorDescription || errorParam;
processing = false;
return;
}
if (!code) {
error = 'No se recibió código de autorización';
processing = false;
return;
}
// Intercambiar código por tokens
const redirectUri = `${window.location.origin}/auth/callback`;
const tokens = await exchangeCodeForTokens(code, redirectUri);
// Decodificar el access token para obtener información del usuario
const userInfo = decodeJWT(tokens.access_token);
if (!userInfo) {
error = 'No se pudo decodificar el token';
processing = false;
return;
}
// Recuperar tenant_slug del state si existe
let tenantSlug = 'default';
if (state) {
try {
const stateObj = JSON.parse(state);
tenantSlug = stateObj.tenant_slug || 'default';
} catch (e) {
console.warn('No se pudo parsear el state:', e);
}
}
// Guardar tokens y usuario en el store
authStore.setAuthenticated(true);
authStore.setLoading(false);
authStore.setUser({
id: userInfo.sub,
username: userInfo.preferred_username || userInfo.email,
email: userInfo.email,
name: userInfo.name || `${userInfo.given_name || ''} ${userInfo.family_name || ''}`.trim(),
tenantId: userInfo.tenant_id,
roles: userInfo.realm_access?.roles || []
});
authStore.setTokens(tokens.access_token, tokens.refresh_token);
// Guardar en localStorage
if (browser) {
localStorage.setItem('access_token', tokens.access_token);
localStorage.setItem('refresh_token', tokens.refresh_token);
localStorage.setItem('user', JSON.stringify(userInfo));
}
// Redirigir a la página principal
await goto('/');
} catch (err: any) {
console.error('Error en callback SSO:', err);
error = err.message || 'Error procesando autenticación';
processing = false;
}
});
function goToLogin() {
goto('/login');
}
</script>
<div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div class="max-w-md w-full space-y-8">
<div>
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">
{#if processing}
Procesando autenticación...
{:else}
Error de autenticación
{/if}
</h2>
</div>
{#if processing}
<div class="flex justify-center">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
</div>
<p class="text-center text-sm text-gray-600">
Espera un momento mientras completamos tu inicio de sesión con Microsoft...
</p>
{:else if error}
<div class="rounded-md bg-red-50 p-4">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-red-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-red-800">
{error}
</h3>
</div>
</div>
</div>
<div>
<button
type="button"
onclick={goToLogin}
class="w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
Volver al login
</button>
</div>
{/if}
</div>
</div>

View File

@@ -1,12 +1,15 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { login } from '$lib/auth';
import { loginWithProvider } from '$lib/sso';
import { onMount } from 'svelte';
let username = $state('demo');
let password = $state('demo123');
let tenantSlug = $state('aduanasoft');
let error = $state('');
let loading = $state(false);
let showSSOProviders = $state(true); // Puedes hacer esto dinámico consultando los providers disponibles
async function handleSubmit(e: Event) {
e.preventDefault();
@@ -27,6 +30,12 @@
error = result.error || 'Error de autenticación';
}
}
function handleMicrosoftLogin() {
// Guardamos el tenant_slug en el state para recuperarlo después del callback
const state = JSON.stringify({ tenant_slug: tenantSlug });
loginWithProvider('microsoft', undefined, state);
}
</script>
<div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
@@ -116,5 +125,41 @@
</p>
</div>
</form>
{#if showSSOProviders}
<div class="mt-6">
<div class="relative">
<div class="absolute inset-0 flex items-center">
<div class="w-full border-t border-gray-300"></div>
</div>
<div class="relative flex justify-center text-sm">
<span class="px-2 bg-gray-50 text-gray-500">O continúa con</span>
</div>
</div>
<div class="mt-6">
<button
type="button"
onclick={handleMicrosoftLogin}
disabled={loading || !tenantSlug}
class="w-full inline-flex justify-center items-center gap-2 py-2 px-4 border border-gray-300 rounded-md shadow-sm bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
<svg class="w-5 h-5" viewBox="0 0 23 23" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0H11V11H0V0Z" fill="#F25022"/>
<path d="M12 0H23V11H12V0Z" fill="#7FBA00"/>
<path d="M0 12H11V23H0V12Z" fill="#00A4EF"/>
<path d="M12 12H23V23H12V12Z" fill="#FFB900"/>
</svg>
Iniciar sesión con Microsoft
</button>
{#if !tenantSlug}
<p class="mt-2 text-xs text-red-600 text-center">
Selecciona un tenant antes de usar login social
</p>
{/if}
</div>
</div>
{/if}
</div>
</div>