feat(auth): implement OAuth2 exchange code flow for Microsoft login, update environment configurations, and enhance frontend SSO handling
This commit is contained in:
@@ -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
44
azure.crt
Normal 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-----
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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():
|
||||
"""
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
214
docs/MICROSOFT_SSO_SETUP.md
Normal 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
|
||||
219
docs/VERIFICAR_MICROSOFT_CONFIG.md
Normal file
219
docs/VERIFICAR_MICROSOFT_CONFIG.md
Normal 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".
|
||||
@@ -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
|
||||
|
||||
140
frontend/src/routes/auth/callback/+page.svelte
Normal file
140
frontend/src/routes/auth/callback/+page.svelte
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user