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
|
DEBUG=True
|
||||||
ENVIRONMENT=development
|
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 -----
|
# ----- Frontend -----
|
||||||
NODE_ENV=development
|
NODE_ENV=development
|
||||||
PUBLIC_API_URL=http://localhost:8000
|
PUBLIC_API_URL=http://localhost:8000
|
||||||
|
PUBLIC_KEYCLOAK_REALM=master
|
||||||
PUBLIC_KEYCLOAK_URL=http://localhost:8080
|
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"
|
"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,
|
UserInfoResponseDTO,
|
||||||
LogoutRequestDTO,
|
LogoutRequestDTO,
|
||||||
RegisterRequestDTO,
|
RegisterRequestDTO,
|
||||||
RegisterResponseDTO
|
RegisterResponseDTO,
|
||||||
|
ExchangeCodeRequestDTO
|
||||||
)
|
)
|
||||||
from .service import AuthService
|
from .service import AuthService
|
||||||
|
|
||||||
@@ -101,6 +102,24 @@ async def logout(
|
|||||||
return service.logout(logout_data)
|
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")
|
@router.get("/health")
|
||||||
async def auth_health():
|
async def auth_health():
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -275,3 +275,78 @@ class AuthService:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Registration error: {str(e)}")
|
logger.error(f"Registration error: {str(e)}")
|
||||||
raise HTTPException(status_code=500, detail="Registration error")
|
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}
|
- ENVIRONMENT=${ENVIRONMENT:-development}
|
||||||
- PYTHONUNBUFFERED=1
|
- PYTHONUNBUFFERED=1
|
||||||
- PYTHONDONTWRITEBYTECODE=1
|
- PYTHONDONTWRITEBYTECODE=1
|
||||||
- CORE_DB_HOST=postgres-a76
|
- CORE_DB_HOST=${CORE_DB_HOST:-postgres-a76}
|
||||||
- CORE_DB_PORT=5432
|
- CORE_DB_PORT=${CORE_DB_PORT:-5432}
|
||||||
- CORE_DB_NAME=anexo76_core
|
- CORE_DB_NAME=${CORE_DB_NAME:-anexo76_core}
|
||||||
- CORE_DB_USER=postgres
|
- CORE_DB_USER=${CORE_DB_USER:-postgres}
|
||||||
- CORE_DB_PASSWORD=${POSTGRES_APP_PASSWORD:-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_REALM=${KEYCLOAK_REALM:-master}
|
||||||
- KEYCLOAK_CLIENT_ID=${KEYCLOAK_CLIENT_ID:-anexo76-backend}
|
- KEYCLOAK_CLIENT_ID=${KEYCLOAK_CLIENT_ID:-anexo76-backend}
|
||||||
- KEYCLOAK_CLIENT_SECRET=${KEYCLOAK_CLIENT_SECRET:-dev-secret}
|
- 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
|
# 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_URL=http://localhost:8080
|
||||||
PUBLIC_KEYCLOAK_REALM=master
|
PUBLIC_KEYCLOAK_REALM=master
|
||||||
PUBLIC_KEYCLOAK_CLIENT_ID=anexo76-frontend
|
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">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { login } from '$lib/auth';
|
import { login } from '$lib/auth';
|
||||||
|
import { loginWithProvider } from '$lib/sso';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
let username = $state('demo');
|
let username = $state('demo');
|
||||||
let password = $state('demo123');
|
let password = $state('demo123');
|
||||||
let tenantSlug = $state('aduanasoft');
|
let tenantSlug = $state('aduanasoft');
|
||||||
let error = $state('');
|
let error = $state('');
|
||||||
let loading = $state(false);
|
let loading = $state(false);
|
||||||
|
let showSSOProviders = $state(true); // Puedes hacer esto dinámico consultando los providers disponibles
|
||||||
|
|
||||||
async function handleSubmit(e: Event) {
|
async function handleSubmit(e: Event) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -27,6 +30,12 @@
|
|||||||
error = result.error || 'Error de autenticación';
|
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>
|
</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="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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user