@bigso/auth-sdk 0.3.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +188 -0
- package/dist/index.cjs +371 -0
- package/dist/index.d.cts +66 -0
- package/dist/index.d.ts +66 -0
- package/dist/index.js +344 -0
- package/package.json +3 -2
package/README.md
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# @bigso/auth-sdk
|
|
2
|
+
|
|
3
|
+
SDK oficial de autenticación para Bigso SSO, compatible con el estándar SSO v2.3. Este paquete permite integrar aplicaciones web con el flujo de autenticación basado en iframe seguro, utilizando PKCE, JWS firmados y comunicación mediante postMessage.
|
|
4
|
+
|
|
5
|
+
## 🚀 Características
|
|
6
|
+
|
|
7
|
+
✅ **Flujo seguro** con PKCE obligatorio y JWS firmado.
|
|
8
|
+
|
|
9
|
+
✅ **Compatible con SSO v2.3** (whitelist de origins, kid en JWS, validación de nonce, timeout reactivo).
|
|
10
|
+
|
|
11
|
+
✅ **Manejo automático de estados** (state, nonce, verifier, requestId).
|
|
12
|
+
|
|
13
|
+
✅ **Verificación de firma JWS** en el frontend usando `jose` y JWKS remoto.
|
|
14
|
+
|
|
15
|
+
✅ **Timeout reactivo configurable**, activado tras `sso-ready`.
|
|
16
|
+
|
|
17
|
+
✅ **Soporte para redirect_uri y tenant_hint**.
|
|
18
|
+
|
|
19
|
+
✅ **Manejo de errores** incluyendo `version_mismatch` con fallback automático.
|
|
20
|
+
|
|
21
|
+
✅ **API asíncrona** basada en promesas.
|
|
22
|
+
|
|
23
|
+
✅ **Sistema de eventos** (ready, success, error, fallback, debug).
|
|
24
|
+
|
|
25
|
+
✅ **Ligero** (~6 kB) y con tipos TypeScript.
|
|
26
|
+
|
|
27
|
+
## 📦 Instalación
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install @bigso/auth-sdk
|
|
31
|
+
# o
|
|
32
|
+
yarn add @bigso/auth-sdk
|
|
33
|
+
# o
|
|
34
|
+
pnpm add @bigso/auth-sdk
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## 🧪 Uso básico
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import { BigsoAuth } from '@bigso/auth-sdk';
|
|
41
|
+
|
|
42
|
+
const auth = new BigsoAuth({
|
|
43
|
+
clientId: 'tu-client-id',
|
|
44
|
+
ssoOrigin: 'https://sso.tudominio.com',
|
|
45
|
+
jwksUrl: 'https://sso.tudominio.com/.well-known/jwks.json',
|
|
46
|
+
timeout: 5000, // opcional, por defecto 5000ms
|
|
47
|
+
debug: true, // opcional, logs de depuración
|
|
48
|
+
redirectUri: 'https://miapp.com/callback', // opcional
|
|
49
|
+
tenantHint: 'mi-tenant' // opcional
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
auth.on('ready', () => console.log('✅ Iframe listo'));
|
|
53
|
+
auth.on('success', (payload) => {
|
|
54
|
+
console.log('✅ Autenticación exitosa', payload);
|
|
55
|
+
// Envía el signed_payload al backend para canjear el código
|
|
56
|
+
enviarAlBackend(payload.signed_payload);
|
|
57
|
+
});
|
|
58
|
+
auth.on('error', (error) => console.error('❌ Error:', error));
|
|
59
|
+
auth.on('fallback', () => console.log('⚠️ Fallback por redirección activado'));
|
|
60
|
+
|
|
61
|
+
auth.login().catch(err => console.error('Login falló', err));
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## 📚 API Reference
|
|
65
|
+
|
|
66
|
+
### `new BigsoAuth(options)`
|
|
67
|
+
Crea una nueva instancia del cliente de autenticación.
|
|
68
|
+
|
|
69
|
+
#### Opciones de configuración
|
|
70
|
+
| Parámetro | Tipo | Obligatorio | Por defecto | Descripción |
|
|
71
|
+
| :--- | :--- | :---: | :---: | :--- |
|
|
72
|
+
| `clientId` | `string` | ✅ | — | Client ID registrado en el SSO. |
|
|
73
|
+
| `ssoOrigin` | `string` | ✅ | — | Origen del SSO (ej. `https://sso.bigso.co`). |
|
|
74
|
+
| `jwksUrl` | `string` | ✅ | — | URL del JWKS para verificar firmas (ej. `/.well-known/jwks.json`). |
|
|
75
|
+
| `timeout` | `number` | ❌ | `5000` | Tiempo máximo de espera tras `sso-ready` (milisegundos). |
|
|
76
|
+
| `debug` | `boolean` | ❌ | `false` | Activa logs de depuración en consola. |
|
|
77
|
+
| `redirectUri` | `string` | ❌ | `''` | URI de redirección registrada (se valida exactamente en el SSO). |
|
|
78
|
+
| `tenantHint` | `string` | ❌ | `''` | Sugerencia de tenant para flujos multi-tenant. |
|
|
79
|
+
|
|
80
|
+
### `auth.login()`
|
|
81
|
+
Inicia el flujo de autenticación. Devuelve una promesa que se resuelve con el payload decodificado del JWS (solo para información; la validación final debe realizarse en el backend).
|
|
82
|
+
|
|
83
|
+
**Retorna**: `Promise<any>` – Payload del JWS (contiene `code`, `state`, `nonce`, `iss`, etc.).
|
|
84
|
+
|
|
85
|
+
**Rechaza**: `Error` o payload de error del iframe.
|
|
86
|
+
|
|
87
|
+
### `auth.abort()`
|
|
88
|
+
Cancela el flujo de autenticación en curso, eliminando el iframe y rechazando la promesa.
|
|
89
|
+
|
|
90
|
+
### `auth.on(event, handler)`
|
|
91
|
+
Registra un manejador para los eventos del SDK.
|
|
92
|
+
|
|
93
|
+
#### Eventos disponibles:
|
|
94
|
+
| Evento | Descripción | Parámetros |
|
|
95
|
+
| :--- | :--- | :--- |
|
|
96
|
+
| `ready` | Se emite cuando el iframe está listo y se ha enviado `sso-init`. | — |
|
|
97
|
+
| `success` | Se emite tras verificar exitosamente la firma JWS y validar `state`/`nonce` en el frontend. | `payload: any` (payload del JWS) |
|
|
98
|
+
| `error` | Se emite cuando ocurre un error (incluyendo `version_mismatch` antes del fallback automático). | `error: Error | SsoErrorPayload` |
|
|
99
|
+
| `fallback` | Se emite justo antes de redirigir a la URL de fallback (por timeout o `version_mismatch`). | — |
|
|
100
|
+
| `debug` | Se emite cuando `debug: true` para logs internos. | `args: any[]` |
|
|
101
|
+
|
|
102
|
+
## ⚙️ Ejemplos avanzados
|
|
103
|
+
|
|
104
|
+
### Personalizar el timeout
|
|
105
|
+
```typescript
|
|
106
|
+
const auth = new BigsoAuth({
|
|
107
|
+
clientId: 'abc123',
|
|
108
|
+
ssoOrigin: 'https://sso.bigso.co',
|
|
109
|
+
jwksUrl: 'https://sso.bigso.co/.well-known/jwks.json',
|
|
110
|
+
timeout: 10000 // 10 segundos
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Usar `redirect_uri` y `tenant_hint`
|
|
115
|
+
```typescript
|
|
116
|
+
const auth = new BigsoAuth({
|
|
117
|
+
clientId: 'abc123',
|
|
118
|
+
ssoOrigin: 'https://sso.bigso.co',
|
|
119
|
+
jwksUrl: 'https://sso.bigso.co/.well-known/jwks.json',
|
|
120
|
+
redirectUri: 'https://admin.miapp.com/callback',
|
|
121
|
+
tenantHint: 'enterprise'
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Manejo de fallback personalizado
|
|
126
|
+
Puedes escuchar el evento `fallback` para ejecutar tu propia lógica antes de la redirección automática:
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
auth.on('fallback', () => {
|
|
130
|
+
console.log('Mostrando spinner o mensaje...');
|
|
131
|
+
// Por defecto, el SDK redirige a /authorize.
|
|
132
|
+
// Si quieres evitar la redirección automática, puedes sobrescribir el comportamiento
|
|
133
|
+
// pero no es recomendable ya que es el mecanismo de último recurso.
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## 🔒 Consideraciones de seguridad
|
|
138
|
+
|
|
139
|
+
- **Whitelist de origins**: El SDK asume que el SSO Core está configurado correctamente con la whitelist de origins para cada `client_id` y que responde con `Content-Security-Policy: frame-ancestors`. El SDK no puede controlar esto; es responsabilidad del administrador del SSO.
|
|
140
|
+
- **Validación en backend**: La verificación del JWS en el frontend es solo una capa adicional de integridad. El backend debe validar la firma, el nonce, el state y canjear el código usando PKCE antes de emitir tokens.
|
|
141
|
+
- **nonce y state**: El SDK genera valores aleatorios seguros (`crypto.randomUUID`) y los valida en el frontend. El backend debe realizar la misma validación para prevenir ataques de replay.
|
|
142
|
+
- **Prevención de replay**: El SDK no incluye lógica de deduplicación de JWS en el frontend; esto debe implementarse en el backend usando `jti` si es necesario.
|
|
143
|
+
- **Timeout**: El timeout se inicia solo después de `sso-ready`, evitando falsos positivos. Si se alcanza, el SDK ejecuta un fallback a redirección (endpoint `/authorize`).
|
|
144
|
+
- **Versiones**: El SDK usa `v: '2.3'`. Si el iframe responde con `version_mismatch`, se activa el fallback automático.
|
|
145
|
+
|
|
146
|
+
## 🛠️ Desarrollo
|
|
147
|
+
|
|
148
|
+
### Construcción
|
|
149
|
+
```bash
|
|
150
|
+
npm run build # genera dist/ con formatos ESM, CJS y types
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Pruebas
|
|
154
|
+
```bash
|
|
155
|
+
npm test # ejecuta vitest
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Linting
|
|
159
|
+
```bash
|
|
160
|
+
npm run lint
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## 📝 Changelog
|
|
164
|
+
|
|
165
|
+
### v0.4.0 (2026-03-23)
|
|
166
|
+
Protocolo actualizado a SSO v2.3
|
|
167
|
+
- Mensaje `sso-init` con `v: '2.3'`.
|
|
168
|
+
- Timeout reactivo (se inicia tras `sso-ready`).
|
|
169
|
+
- Validación de `requestId` en respuestas.
|
|
170
|
+
- Soporte para `redirect_uri`, `tenant_hint`, `timeout_ms`.
|
|
171
|
+
- Manejo de error `version_mismatch` con fallback automático.
|
|
172
|
+
- Validación de `nonce` en el frontend tras verificar JWS.
|
|
173
|
+
|
|
174
|
+
**Mejoras internas:**
|
|
175
|
+
- Método `abort()` para cancelar operación.
|
|
176
|
+
- Evento `debug` opcional.
|
|
177
|
+
- Documentación completa.
|
|
178
|
+
|
|
179
|
+
**Breaking changes:** Ninguno, pero se recomienda actualizar el backend para validar `nonce` si no lo hacía antes.
|
|
180
|
+
|
|
181
|
+
### v0.2.0 (anterior)
|
|
182
|
+
- Implementación inicial con v2.2.
|
|
183
|
+
|
|
184
|
+
## 📄 Licencia
|
|
185
|
+
MIT © Bigso
|
|
186
|
+
|
|
187
|
+
## 🤝 Contribuciones
|
|
188
|
+
Por favor, abre un issue o pull request en el repositorio oficial.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
BigsoAuth: () => BigsoAuth
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(index_exports);
|
|
26
|
+
|
|
27
|
+
// src/utils/crypto.ts
|
|
28
|
+
async function sha256Base64Url(input) {
|
|
29
|
+
const encoder = new TextEncoder();
|
|
30
|
+
const data = encoder.encode(input);
|
|
31
|
+
const digest = await crypto.subtle.digest("SHA-256", data);
|
|
32
|
+
return base64Url(new Uint8Array(digest));
|
|
33
|
+
}
|
|
34
|
+
function generateVerifier(length = 32) {
|
|
35
|
+
const array = new Uint8Array(length);
|
|
36
|
+
crypto.getRandomValues(array);
|
|
37
|
+
return base64Url(array);
|
|
38
|
+
}
|
|
39
|
+
function base64Url(bytes) {
|
|
40
|
+
return btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
41
|
+
}
|
|
42
|
+
function generateRandomId() {
|
|
43
|
+
return crypto.randomUUID();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/utils/events.ts
|
|
47
|
+
var EventEmitter = class {
|
|
48
|
+
constructor() {
|
|
49
|
+
this.events = {};
|
|
50
|
+
}
|
|
51
|
+
on(event, handler) {
|
|
52
|
+
if (!this.events[event]) this.events[event] = [];
|
|
53
|
+
this.events[event].push(handler);
|
|
54
|
+
}
|
|
55
|
+
off(event, handler) {
|
|
56
|
+
if (!this.events[event]) return;
|
|
57
|
+
this.events[event] = this.events[event].filter((h) => h !== handler);
|
|
58
|
+
}
|
|
59
|
+
emit(event, data) {
|
|
60
|
+
this.events[event]?.forEach((fn) => fn(data));
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// src/utils/jws.ts
|
|
65
|
+
var import_jose = require("jose");
|
|
66
|
+
async function verifySignedPayload(token, jwksUrl, expectedAudience) {
|
|
67
|
+
const JWKS = (0, import_jose.createRemoteJWKSet)(new URL(jwksUrl));
|
|
68
|
+
const { payload } = await (0, import_jose.jwtVerify)(token, JWKS, {
|
|
69
|
+
audience: expectedAudience
|
|
70
|
+
});
|
|
71
|
+
return payload;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/core/auth.ts
|
|
75
|
+
var BigsoAuth = class extends EventEmitter {
|
|
76
|
+
constructor(options) {
|
|
77
|
+
super();
|
|
78
|
+
this.authCompleted = false;
|
|
79
|
+
this.requestId = generateRandomId();
|
|
80
|
+
this.loginInProgress = false;
|
|
81
|
+
this.options = {
|
|
82
|
+
timeout: 5e3,
|
|
83
|
+
// por defecto 5s (estándar v2.3)
|
|
84
|
+
debug: false,
|
|
85
|
+
redirectUri: "",
|
|
86
|
+
tenantHint: "",
|
|
87
|
+
theme: "light",
|
|
88
|
+
...options
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Inicia el flujo de autenticación.
|
|
93
|
+
* @returns Promise que resuelve con el payload decodificado del JWS (solo para información; el backend debe validar)
|
|
94
|
+
*/
|
|
95
|
+
async login() {
|
|
96
|
+
if (this.loginInProgress) {
|
|
97
|
+
this.debug("login() ya en curso, ignorando llamada duplicada");
|
|
98
|
+
return Promise.reject(new Error("Login already in progress"));
|
|
99
|
+
}
|
|
100
|
+
this.loginInProgress = true;
|
|
101
|
+
this.authCompleted = false;
|
|
102
|
+
const state = generateRandomId();
|
|
103
|
+
const nonce = generateRandomId();
|
|
104
|
+
const verifier = generateVerifier();
|
|
105
|
+
const requestId = this.requestId;
|
|
106
|
+
sessionStorage.setItem("sso_ctx", JSON.stringify({ state, nonce, verifier, requestId }));
|
|
107
|
+
this.createUI();
|
|
108
|
+
return new Promise((resolve, reject) => {
|
|
109
|
+
this.abortController = new AbortController();
|
|
110
|
+
const { signal } = this.abortController;
|
|
111
|
+
const cleanup = () => {
|
|
112
|
+
if (this.timeoutId) clearTimeout(this.timeoutId);
|
|
113
|
+
if (this.messageListener) window.removeEventListener("message", this.messageListener);
|
|
114
|
+
this.iframe?.remove();
|
|
115
|
+
this.iframe = void 0;
|
|
116
|
+
this.authCompleted = true;
|
|
117
|
+
this.loginInProgress = false;
|
|
118
|
+
};
|
|
119
|
+
this.messageListener = async (event) => {
|
|
120
|
+
if (event.origin !== this.options.ssoOrigin) {
|
|
121
|
+
this.debug("Ignorado mensaje de origen no autorizado:", event.origin);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const msg = event.data;
|
|
125
|
+
this.debug("Mensaje recibido:", msg);
|
|
126
|
+
if (msg.requestId && msg.requestId !== requestId) {
|
|
127
|
+
this.debug("requestId no coincide, ignorado");
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (msg.type === "sso-ready") {
|
|
131
|
+
this.debug("sso-ready recibido, iniciando timeout y enviando sso-init");
|
|
132
|
+
this.timeoutId = window.setTimeout(() => {
|
|
133
|
+
if (!this.authCompleted) {
|
|
134
|
+
this.debug("Timeout alcanzado, activando fallback");
|
|
135
|
+
this.closeUI();
|
|
136
|
+
cleanup();
|
|
137
|
+
this.emit("fallback");
|
|
138
|
+
window.location.href = this.buildFallbackUrl();
|
|
139
|
+
reject(new Error("Timeout"));
|
|
140
|
+
}
|
|
141
|
+
}, this.options.timeout);
|
|
142
|
+
const codeChallenge = await sha256Base64Url(verifier);
|
|
143
|
+
const initPayload = {
|
|
144
|
+
state,
|
|
145
|
+
nonce,
|
|
146
|
+
code_challenge: codeChallenge,
|
|
147
|
+
code_challenge_method: "S256",
|
|
148
|
+
origin: window.location.origin,
|
|
149
|
+
...this.options.redirectUri && { redirect_uri: this.options.redirectUri },
|
|
150
|
+
...this.options.tenantHint && { tenant_hint: this.options.tenantHint },
|
|
151
|
+
timeout_ms: this.options.timeout
|
|
152
|
+
// pasar el timeout configurado (opcional)
|
|
153
|
+
};
|
|
154
|
+
this.iframe?.contentWindow?.postMessage({
|
|
155
|
+
v: "2.3",
|
|
156
|
+
// versión del protocolo (estándar v2.3)
|
|
157
|
+
source: "@app/widget",
|
|
158
|
+
type: "sso-init",
|
|
159
|
+
requestId: this.requestId,
|
|
160
|
+
payload: initPayload
|
|
161
|
+
}, this.options.ssoOrigin);
|
|
162
|
+
this.emit("ready");
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (msg.type === "sso-success") {
|
|
166
|
+
this.debug("sso-success recibido");
|
|
167
|
+
clearTimeout(this.timeoutId);
|
|
168
|
+
try {
|
|
169
|
+
const payload = msg.payload;
|
|
170
|
+
const ctx = JSON.parse(sessionStorage.getItem("sso_ctx") || "{}");
|
|
171
|
+
if (payload.state !== ctx.state) {
|
|
172
|
+
throw new Error("Invalid state");
|
|
173
|
+
}
|
|
174
|
+
const decoded = await verifySignedPayload(
|
|
175
|
+
payload.signed_payload,
|
|
176
|
+
this.options.jwksUrl,
|
|
177
|
+
window.location.origin
|
|
178
|
+
// aud esperado
|
|
179
|
+
);
|
|
180
|
+
if (decoded.nonce !== ctx.nonce) {
|
|
181
|
+
throw new Error("Invalid nonce");
|
|
182
|
+
}
|
|
183
|
+
this.debug("JWS v\xE1lido, payload:", decoded);
|
|
184
|
+
this.closeUI();
|
|
185
|
+
cleanup();
|
|
186
|
+
this.emit("success", decoded);
|
|
187
|
+
resolve(decoded);
|
|
188
|
+
} catch (err) {
|
|
189
|
+
this.debug("Error en sso-success:", err);
|
|
190
|
+
this.closeUI();
|
|
191
|
+
cleanup();
|
|
192
|
+
this.emit("error", err);
|
|
193
|
+
reject(err);
|
|
194
|
+
}
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (msg.type === "sso-error") {
|
|
198
|
+
const errorPayload = msg.payload;
|
|
199
|
+
this.debug("sso-error recibido:", errorPayload);
|
|
200
|
+
clearTimeout(this.timeoutId);
|
|
201
|
+
this.closeUI();
|
|
202
|
+
cleanup();
|
|
203
|
+
if (errorPayload.code === "version_mismatch") {
|
|
204
|
+
this.emit("error", errorPayload);
|
|
205
|
+
window.location.href = this.buildFallbackUrl();
|
|
206
|
+
reject(new Error(`Version mismatch: expected ${errorPayload.expected_version}`));
|
|
207
|
+
} else {
|
|
208
|
+
this.emit("error", errorPayload);
|
|
209
|
+
reject(errorPayload);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (msg.type === "sso-close") {
|
|
213
|
+
this.debug("sso-close recibido");
|
|
214
|
+
this.closeUI();
|
|
215
|
+
cleanup();
|
|
216
|
+
reject(new Error("Login cancelled by user"));
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
window.addEventListener("message", this.messageListener);
|
|
220
|
+
signal.addEventListener("abort", () => {
|
|
221
|
+
this.debug("Operaci\xF3n abortada");
|
|
222
|
+
this.closeUI();
|
|
223
|
+
cleanup();
|
|
224
|
+
reject(new Error("Login aborted"));
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
/** Cancela el flujo de autenticación en curso */
|
|
229
|
+
abort() {
|
|
230
|
+
this.abortController?.abort();
|
|
231
|
+
}
|
|
232
|
+
// ─── UI Management ───────────────────────────────────────────────
|
|
233
|
+
/**
|
|
234
|
+
* Crea (o reutiliza) el overlay con Shadow DOM y el iframe visible.
|
|
235
|
+
* Patrón tomado del CDN widget v1: Shadow DOM para aislar estilos.
|
|
236
|
+
*/
|
|
237
|
+
createUI() {
|
|
238
|
+
if (!this.hostEl) {
|
|
239
|
+
this.hostEl = document.createElement("div");
|
|
240
|
+
this.hostEl.id = "bigso-auth-host";
|
|
241
|
+
this.shadowRoot = this.hostEl.attachShadow({ mode: "open" });
|
|
242
|
+
const style = document.createElement("style");
|
|
243
|
+
style.textContent = this.getOverlayStyles();
|
|
244
|
+
this.shadowRoot.appendChild(style);
|
|
245
|
+
this.overlayEl = document.createElement("div");
|
|
246
|
+
this.overlayEl.className = "sso-overlay";
|
|
247
|
+
const closeBtn = document.createElement("button");
|
|
248
|
+
closeBtn.className = "sso-close-btn";
|
|
249
|
+
closeBtn.innerHTML = "×";
|
|
250
|
+
closeBtn.setAttribute("aria-label", "Cerrar modal");
|
|
251
|
+
closeBtn.addEventListener("click", () => this.abort());
|
|
252
|
+
this.overlayEl.appendChild(closeBtn);
|
|
253
|
+
this.overlayEl.addEventListener("click", (event) => {
|
|
254
|
+
if (event.target === this.overlayEl) {
|
|
255
|
+
this.abort();
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
this.shadowRoot.appendChild(this.overlayEl);
|
|
259
|
+
document.body.appendChild(this.hostEl);
|
|
260
|
+
}
|
|
261
|
+
this.iframe = document.createElement("iframe");
|
|
262
|
+
this.iframe.className = "sso-frame";
|
|
263
|
+
this.iframe.src = `${this.options.ssoOrigin}/auth/sign-in?v=2.3&client_id=${this.options.clientId}&embedded=true&app_id=${this.options.clientId}&theme=${this.options.theme}`;
|
|
264
|
+
this.iframe.setAttribute("title", "SSO Login");
|
|
265
|
+
this.overlayEl.appendChild(this.iframe);
|
|
266
|
+
this.debug("Iframe creado", this.iframe.src);
|
|
267
|
+
this.overlayEl.classList.remove("sso-closing");
|
|
268
|
+
this.overlayEl.style.display = "flex";
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Cierra el overlay con animación suave (fadeOut + slideDown).
|
|
272
|
+
* El overlay persiste en el DOM (solo se oculta).
|
|
273
|
+
*/
|
|
274
|
+
closeUI() {
|
|
275
|
+
if (!this.overlayEl || this.overlayEl.style.display === "none") return;
|
|
276
|
+
this.overlayEl.classList.add("sso-closing");
|
|
277
|
+
setTimeout(() => {
|
|
278
|
+
if (this.overlayEl) {
|
|
279
|
+
this.overlayEl.style.display = "none";
|
|
280
|
+
this.overlayEl.classList.remove("sso-closing");
|
|
281
|
+
}
|
|
282
|
+
}, 200);
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Estilos CSS encapsulados dentro del Shadow DOM.
|
|
286
|
+
* Migrados del widget CDN v1 con las mismas animaciones y responsive.
|
|
287
|
+
*/
|
|
288
|
+
getOverlayStyles() {
|
|
289
|
+
return `
|
|
290
|
+
.sso-overlay {
|
|
291
|
+
position: fixed;
|
|
292
|
+
inset: 0;
|
|
293
|
+
display: none;
|
|
294
|
+
justify-content: center;
|
|
295
|
+
align-items: center;
|
|
296
|
+
background: rgba(0, 0, 0, 0.6);
|
|
297
|
+
z-index: 999999;
|
|
298
|
+
backdrop-filter: blur(4px);
|
|
299
|
+
-webkit-backdrop-filter: blur(4px);
|
|
300
|
+
animation: fadeIn 0.2s ease;
|
|
301
|
+
}
|
|
302
|
+
.sso-frame {
|
|
303
|
+
width: 370px;
|
|
304
|
+
height: 350px;
|
|
305
|
+
border: none;
|
|
306
|
+
border-radius: 16px;
|
|
307
|
+
background: var(--card-bg, #fff);
|
|
308
|
+
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.3);
|
|
309
|
+
animation: slideUp 0.3s ease;
|
|
310
|
+
}
|
|
311
|
+
@media (max-width: 480px), (max-height: 480px) {
|
|
312
|
+
.sso-frame {
|
|
313
|
+
width: 100%;
|
|
314
|
+
height: 100%;
|
|
315
|
+
border-radius: 0;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
.sso-close-btn {
|
|
319
|
+
position: absolute;
|
|
320
|
+
top: 12px;
|
|
321
|
+
right: 12px;
|
|
322
|
+
width: 32px;
|
|
323
|
+
height: 32px;
|
|
324
|
+
background: rgba(0, 0, 0, 0.4);
|
|
325
|
+
color: white;
|
|
326
|
+
border: none;
|
|
327
|
+
border-radius: 50%;
|
|
328
|
+
font-size: 24px;
|
|
329
|
+
line-height: 1;
|
|
330
|
+
cursor: pointer;
|
|
331
|
+
display: flex;
|
|
332
|
+
align-items: center;
|
|
333
|
+
justify-content: center;
|
|
334
|
+
z-index: 1000000;
|
|
335
|
+
transition: background 0.2s;
|
|
336
|
+
}
|
|
337
|
+
.sso-close-btn:hover {
|
|
338
|
+
background: rgba(0, 0, 0, 0.8);
|
|
339
|
+
}
|
|
340
|
+
.sso-overlay.sso-closing {
|
|
341
|
+
animation: fadeOut 0.2s ease forwards;
|
|
342
|
+
}
|
|
343
|
+
.sso-overlay.sso-closing .sso-frame {
|
|
344
|
+
animation: slideDown 0.2s ease forwards;
|
|
345
|
+
}
|
|
346
|
+
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
|
347
|
+
@keyframes slideUp { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
|
|
348
|
+
@keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } }
|
|
349
|
+
@keyframes slideDown { from { transform: translateY(0); opacity: 1; } to { transform: translateY(20px); opacity: 0; } }
|
|
350
|
+
`;
|
|
351
|
+
}
|
|
352
|
+
// ─── Helpers ──────────────────────────────────────────────────────
|
|
353
|
+
buildFallbackUrl() {
|
|
354
|
+
const url = new URL(`${this.options.ssoOrigin}/authorize`);
|
|
355
|
+
url.searchParams.set("client_id", this.options.clientId);
|
|
356
|
+
url.searchParams.set("response_type", "code");
|
|
357
|
+
url.searchParams.set("redirect_uri", this.options.redirectUri || window.location.origin);
|
|
358
|
+
url.searchParams.set("state", generateRandomId());
|
|
359
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
360
|
+
return url.toString();
|
|
361
|
+
}
|
|
362
|
+
debug(...args) {
|
|
363
|
+
if (this.options.debug) {
|
|
364
|
+
console.log("[BigsoAuth]", ...args);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
369
|
+
0 && (module.exports = {
|
|
370
|
+
BigsoAuth
|
|
371
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
declare class EventEmitter {
|
|
2
|
+
private events;
|
|
3
|
+
on(event: string, handler: Function): void;
|
|
4
|
+
off(event: string, handler: Function): void;
|
|
5
|
+
emit(event: string, data?: any): void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface BigsoAuthOptions {
|
|
9
|
+
/** Client ID registrado en el SSO */
|
|
10
|
+
clientId: string;
|
|
11
|
+
/** Origen del SSO (ej: https://sso.bigso.co) */
|
|
12
|
+
ssoOrigin: string;
|
|
13
|
+
/** URL del JWKS para verificar firmas (ej: https://sso.bigso.co/.well-known/jwks.json) */
|
|
14
|
+
jwksUrl: string;
|
|
15
|
+
/** Timeout en milisegundos (por defecto 5000) */
|
|
16
|
+
timeout?: number;
|
|
17
|
+
/** Activar logs de depuración */
|
|
18
|
+
debug?: boolean;
|
|
19
|
+
/** URI de redirección registrada (opcional, si se requiere validación exacta) */
|
|
20
|
+
redirectUri?: string;
|
|
21
|
+
/** Sugerencia de tenant (opcional) */
|
|
22
|
+
tenantHint?: string;
|
|
23
|
+
/** Tema visual del iframe ('light' | 'dark', por defecto 'light') */
|
|
24
|
+
theme?: 'light' | 'dark';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
declare class BigsoAuth extends EventEmitter {
|
|
28
|
+
private options;
|
|
29
|
+
private iframe?;
|
|
30
|
+
private authCompleted;
|
|
31
|
+
private requestId;
|
|
32
|
+
private timeoutId?;
|
|
33
|
+
private messageListener?;
|
|
34
|
+
private abortController?;
|
|
35
|
+
private hostEl?;
|
|
36
|
+
private shadowRoot?;
|
|
37
|
+
private overlayEl?;
|
|
38
|
+
private loginInProgress;
|
|
39
|
+
constructor(options: BigsoAuthOptions);
|
|
40
|
+
/**
|
|
41
|
+
* Inicia el flujo de autenticación.
|
|
42
|
+
* @returns Promise que resuelve con el payload decodificado del JWS (solo para información; el backend debe validar)
|
|
43
|
+
*/
|
|
44
|
+
login(): Promise<any>;
|
|
45
|
+
/** Cancela el flujo de autenticación en curso */
|
|
46
|
+
abort(): void;
|
|
47
|
+
/**
|
|
48
|
+
* Crea (o reutiliza) el overlay con Shadow DOM y el iframe visible.
|
|
49
|
+
* Patrón tomado del CDN widget v1: Shadow DOM para aislar estilos.
|
|
50
|
+
*/
|
|
51
|
+
private createUI;
|
|
52
|
+
/**
|
|
53
|
+
* Cierra el overlay con animación suave (fadeOut + slideDown).
|
|
54
|
+
* El overlay persiste en el DOM (solo se oculta).
|
|
55
|
+
*/
|
|
56
|
+
private closeUI;
|
|
57
|
+
/**
|
|
58
|
+
* Estilos CSS encapsulados dentro del Shadow DOM.
|
|
59
|
+
* Migrados del widget CDN v1 con las mismas animaciones y responsive.
|
|
60
|
+
*/
|
|
61
|
+
private getOverlayStyles;
|
|
62
|
+
private buildFallbackUrl;
|
|
63
|
+
private debug;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export { BigsoAuth, type BigsoAuthOptions };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
declare class EventEmitter {
|
|
2
|
+
private events;
|
|
3
|
+
on(event: string, handler: Function): void;
|
|
4
|
+
off(event: string, handler: Function): void;
|
|
5
|
+
emit(event: string, data?: any): void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface BigsoAuthOptions {
|
|
9
|
+
/** Client ID registrado en el SSO */
|
|
10
|
+
clientId: string;
|
|
11
|
+
/** Origen del SSO (ej: https://sso.bigso.co) */
|
|
12
|
+
ssoOrigin: string;
|
|
13
|
+
/** URL del JWKS para verificar firmas (ej: https://sso.bigso.co/.well-known/jwks.json) */
|
|
14
|
+
jwksUrl: string;
|
|
15
|
+
/** Timeout en milisegundos (por defecto 5000) */
|
|
16
|
+
timeout?: number;
|
|
17
|
+
/** Activar logs de depuración */
|
|
18
|
+
debug?: boolean;
|
|
19
|
+
/** URI de redirección registrada (opcional, si se requiere validación exacta) */
|
|
20
|
+
redirectUri?: string;
|
|
21
|
+
/** Sugerencia de tenant (opcional) */
|
|
22
|
+
tenantHint?: string;
|
|
23
|
+
/** Tema visual del iframe ('light' | 'dark', por defecto 'light') */
|
|
24
|
+
theme?: 'light' | 'dark';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
declare class BigsoAuth extends EventEmitter {
|
|
28
|
+
private options;
|
|
29
|
+
private iframe?;
|
|
30
|
+
private authCompleted;
|
|
31
|
+
private requestId;
|
|
32
|
+
private timeoutId?;
|
|
33
|
+
private messageListener?;
|
|
34
|
+
private abortController?;
|
|
35
|
+
private hostEl?;
|
|
36
|
+
private shadowRoot?;
|
|
37
|
+
private overlayEl?;
|
|
38
|
+
private loginInProgress;
|
|
39
|
+
constructor(options: BigsoAuthOptions);
|
|
40
|
+
/**
|
|
41
|
+
* Inicia el flujo de autenticación.
|
|
42
|
+
* @returns Promise que resuelve con el payload decodificado del JWS (solo para información; el backend debe validar)
|
|
43
|
+
*/
|
|
44
|
+
login(): Promise<any>;
|
|
45
|
+
/** Cancela el flujo de autenticación en curso */
|
|
46
|
+
abort(): void;
|
|
47
|
+
/**
|
|
48
|
+
* Crea (o reutiliza) el overlay con Shadow DOM y el iframe visible.
|
|
49
|
+
* Patrón tomado del CDN widget v1: Shadow DOM para aislar estilos.
|
|
50
|
+
*/
|
|
51
|
+
private createUI;
|
|
52
|
+
/**
|
|
53
|
+
* Cierra el overlay con animación suave (fadeOut + slideDown).
|
|
54
|
+
* El overlay persiste en el DOM (solo se oculta).
|
|
55
|
+
*/
|
|
56
|
+
private closeUI;
|
|
57
|
+
/**
|
|
58
|
+
* Estilos CSS encapsulados dentro del Shadow DOM.
|
|
59
|
+
* Migrados del widget CDN v1 con las mismas animaciones y responsive.
|
|
60
|
+
*/
|
|
61
|
+
private getOverlayStyles;
|
|
62
|
+
private buildFallbackUrl;
|
|
63
|
+
private debug;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export { BigsoAuth, type BigsoAuthOptions };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
// src/utils/crypto.ts
|
|
2
|
+
async function sha256Base64Url(input) {
|
|
3
|
+
const encoder = new TextEncoder();
|
|
4
|
+
const data = encoder.encode(input);
|
|
5
|
+
const digest = await crypto.subtle.digest("SHA-256", data);
|
|
6
|
+
return base64Url(new Uint8Array(digest));
|
|
7
|
+
}
|
|
8
|
+
function generateVerifier(length = 32) {
|
|
9
|
+
const array = new Uint8Array(length);
|
|
10
|
+
crypto.getRandomValues(array);
|
|
11
|
+
return base64Url(array);
|
|
12
|
+
}
|
|
13
|
+
function base64Url(bytes) {
|
|
14
|
+
return btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
15
|
+
}
|
|
16
|
+
function generateRandomId() {
|
|
17
|
+
return crypto.randomUUID();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// src/utils/events.ts
|
|
21
|
+
var EventEmitter = class {
|
|
22
|
+
constructor() {
|
|
23
|
+
this.events = {};
|
|
24
|
+
}
|
|
25
|
+
on(event, handler) {
|
|
26
|
+
if (!this.events[event]) this.events[event] = [];
|
|
27
|
+
this.events[event].push(handler);
|
|
28
|
+
}
|
|
29
|
+
off(event, handler) {
|
|
30
|
+
if (!this.events[event]) return;
|
|
31
|
+
this.events[event] = this.events[event].filter((h) => h !== handler);
|
|
32
|
+
}
|
|
33
|
+
emit(event, data) {
|
|
34
|
+
this.events[event]?.forEach((fn) => fn(data));
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// src/utils/jws.ts
|
|
39
|
+
import { jwtVerify, createRemoteJWKSet } from "jose";
|
|
40
|
+
async function verifySignedPayload(token, jwksUrl, expectedAudience) {
|
|
41
|
+
const JWKS = createRemoteJWKSet(new URL(jwksUrl));
|
|
42
|
+
const { payload } = await jwtVerify(token, JWKS, {
|
|
43
|
+
audience: expectedAudience
|
|
44
|
+
});
|
|
45
|
+
return payload;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/core/auth.ts
|
|
49
|
+
var BigsoAuth = class extends EventEmitter {
|
|
50
|
+
constructor(options) {
|
|
51
|
+
super();
|
|
52
|
+
this.authCompleted = false;
|
|
53
|
+
this.requestId = generateRandomId();
|
|
54
|
+
this.loginInProgress = false;
|
|
55
|
+
this.options = {
|
|
56
|
+
timeout: 5e3,
|
|
57
|
+
// por defecto 5s (estándar v2.3)
|
|
58
|
+
debug: false,
|
|
59
|
+
redirectUri: "",
|
|
60
|
+
tenantHint: "",
|
|
61
|
+
theme: "light",
|
|
62
|
+
...options
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Inicia el flujo de autenticación.
|
|
67
|
+
* @returns Promise que resuelve con el payload decodificado del JWS (solo para información; el backend debe validar)
|
|
68
|
+
*/
|
|
69
|
+
async login() {
|
|
70
|
+
if (this.loginInProgress) {
|
|
71
|
+
this.debug("login() ya en curso, ignorando llamada duplicada");
|
|
72
|
+
return Promise.reject(new Error("Login already in progress"));
|
|
73
|
+
}
|
|
74
|
+
this.loginInProgress = true;
|
|
75
|
+
this.authCompleted = false;
|
|
76
|
+
const state = generateRandomId();
|
|
77
|
+
const nonce = generateRandomId();
|
|
78
|
+
const verifier = generateVerifier();
|
|
79
|
+
const requestId = this.requestId;
|
|
80
|
+
sessionStorage.setItem("sso_ctx", JSON.stringify({ state, nonce, verifier, requestId }));
|
|
81
|
+
this.createUI();
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
this.abortController = new AbortController();
|
|
84
|
+
const { signal } = this.abortController;
|
|
85
|
+
const cleanup = () => {
|
|
86
|
+
if (this.timeoutId) clearTimeout(this.timeoutId);
|
|
87
|
+
if (this.messageListener) window.removeEventListener("message", this.messageListener);
|
|
88
|
+
this.iframe?.remove();
|
|
89
|
+
this.iframe = void 0;
|
|
90
|
+
this.authCompleted = true;
|
|
91
|
+
this.loginInProgress = false;
|
|
92
|
+
};
|
|
93
|
+
this.messageListener = async (event) => {
|
|
94
|
+
if (event.origin !== this.options.ssoOrigin) {
|
|
95
|
+
this.debug("Ignorado mensaje de origen no autorizado:", event.origin);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const msg = event.data;
|
|
99
|
+
this.debug("Mensaje recibido:", msg);
|
|
100
|
+
if (msg.requestId && msg.requestId !== requestId) {
|
|
101
|
+
this.debug("requestId no coincide, ignorado");
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (msg.type === "sso-ready") {
|
|
105
|
+
this.debug("sso-ready recibido, iniciando timeout y enviando sso-init");
|
|
106
|
+
this.timeoutId = window.setTimeout(() => {
|
|
107
|
+
if (!this.authCompleted) {
|
|
108
|
+
this.debug("Timeout alcanzado, activando fallback");
|
|
109
|
+
this.closeUI();
|
|
110
|
+
cleanup();
|
|
111
|
+
this.emit("fallback");
|
|
112
|
+
window.location.href = this.buildFallbackUrl();
|
|
113
|
+
reject(new Error("Timeout"));
|
|
114
|
+
}
|
|
115
|
+
}, this.options.timeout);
|
|
116
|
+
const codeChallenge = await sha256Base64Url(verifier);
|
|
117
|
+
const initPayload = {
|
|
118
|
+
state,
|
|
119
|
+
nonce,
|
|
120
|
+
code_challenge: codeChallenge,
|
|
121
|
+
code_challenge_method: "S256",
|
|
122
|
+
origin: window.location.origin,
|
|
123
|
+
...this.options.redirectUri && { redirect_uri: this.options.redirectUri },
|
|
124
|
+
...this.options.tenantHint && { tenant_hint: this.options.tenantHint },
|
|
125
|
+
timeout_ms: this.options.timeout
|
|
126
|
+
// pasar el timeout configurado (opcional)
|
|
127
|
+
};
|
|
128
|
+
this.iframe?.contentWindow?.postMessage({
|
|
129
|
+
v: "2.3",
|
|
130
|
+
// versión del protocolo (estándar v2.3)
|
|
131
|
+
source: "@app/widget",
|
|
132
|
+
type: "sso-init",
|
|
133
|
+
requestId: this.requestId,
|
|
134
|
+
payload: initPayload
|
|
135
|
+
}, this.options.ssoOrigin);
|
|
136
|
+
this.emit("ready");
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (msg.type === "sso-success") {
|
|
140
|
+
this.debug("sso-success recibido");
|
|
141
|
+
clearTimeout(this.timeoutId);
|
|
142
|
+
try {
|
|
143
|
+
const payload = msg.payload;
|
|
144
|
+
const ctx = JSON.parse(sessionStorage.getItem("sso_ctx") || "{}");
|
|
145
|
+
if (payload.state !== ctx.state) {
|
|
146
|
+
throw new Error("Invalid state");
|
|
147
|
+
}
|
|
148
|
+
const decoded = await verifySignedPayload(
|
|
149
|
+
payload.signed_payload,
|
|
150
|
+
this.options.jwksUrl,
|
|
151
|
+
window.location.origin
|
|
152
|
+
// aud esperado
|
|
153
|
+
);
|
|
154
|
+
if (decoded.nonce !== ctx.nonce) {
|
|
155
|
+
throw new Error("Invalid nonce");
|
|
156
|
+
}
|
|
157
|
+
this.debug("JWS v\xE1lido, payload:", decoded);
|
|
158
|
+
this.closeUI();
|
|
159
|
+
cleanup();
|
|
160
|
+
this.emit("success", decoded);
|
|
161
|
+
resolve(decoded);
|
|
162
|
+
} catch (err) {
|
|
163
|
+
this.debug("Error en sso-success:", err);
|
|
164
|
+
this.closeUI();
|
|
165
|
+
cleanup();
|
|
166
|
+
this.emit("error", err);
|
|
167
|
+
reject(err);
|
|
168
|
+
}
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (msg.type === "sso-error") {
|
|
172
|
+
const errorPayload = msg.payload;
|
|
173
|
+
this.debug("sso-error recibido:", errorPayload);
|
|
174
|
+
clearTimeout(this.timeoutId);
|
|
175
|
+
this.closeUI();
|
|
176
|
+
cleanup();
|
|
177
|
+
if (errorPayload.code === "version_mismatch") {
|
|
178
|
+
this.emit("error", errorPayload);
|
|
179
|
+
window.location.href = this.buildFallbackUrl();
|
|
180
|
+
reject(new Error(`Version mismatch: expected ${errorPayload.expected_version}`));
|
|
181
|
+
} else {
|
|
182
|
+
this.emit("error", errorPayload);
|
|
183
|
+
reject(errorPayload);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (msg.type === "sso-close") {
|
|
187
|
+
this.debug("sso-close recibido");
|
|
188
|
+
this.closeUI();
|
|
189
|
+
cleanup();
|
|
190
|
+
reject(new Error("Login cancelled by user"));
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
window.addEventListener("message", this.messageListener);
|
|
194
|
+
signal.addEventListener("abort", () => {
|
|
195
|
+
this.debug("Operaci\xF3n abortada");
|
|
196
|
+
this.closeUI();
|
|
197
|
+
cleanup();
|
|
198
|
+
reject(new Error("Login aborted"));
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
/** Cancela el flujo de autenticación en curso */
|
|
203
|
+
abort() {
|
|
204
|
+
this.abortController?.abort();
|
|
205
|
+
}
|
|
206
|
+
// ─── UI Management ───────────────────────────────────────────────
|
|
207
|
+
/**
|
|
208
|
+
* Crea (o reutiliza) el overlay con Shadow DOM y el iframe visible.
|
|
209
|
+
* Patrón tomado del CDN widget v1: Shadow DOM para aislar estilos.
|
|
210
|
+
*/
|
|
211
|
+
createUI() {
|
|
212
|
+
if (!this.hostEl) {
|
|
213
|
+
this.hostEl = document.createElement("div");
|
|
214
|
+
this.hostEl.id = "bigso-auth-host";
|
|
215
|
+
this.shadowRoot = this.hostEl.attachShadow({ mode: "open" });
|
|
216
|
+
const style = document.createElement("style");
|
|
217
|
+
style.textContent = this.getOverlayStyles();
|
|
218
|
+
this.shadowRoot.appendChild(style);
|
|
219
|
+
this.overlayEl = document.createElement("div");
|
|
220
|
+
this.overlayEl.className = "sso-overlay";
|
|
221
|
+
const closeBtn = document.createElement("button");
|
|
222
|
+
closeBtn.className = "sso-close-btn";
|
|
223
|
+
closeBtn.innerHTML = "×";
|
|
224
|
+
closeBtn.setAttribute("aria-label", "Cerrar modal");
|
|
225
|
+
closeBtn.addEventListener("click", () => this.abort());
|
|
226
|
+
this.overlayEl.appendChild(closeBtn);
|
|
227
|
+
this.overlayEl.addEventListener("click", (event) => {
|
|
228
|
+
if (event.target === this.overlayEl) {
|
|
229
|
+
this.abort();
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
this.shadowRoot.appendChild(this.overlayEl);
|
|
233
|
+
document.body.appendChild(this.hostEl);
|
|
234
|
+
}
|
|
235
|
+
this.iframe = document.createElement("iframe");
|
|
236
|
+
this.iframe.className = "sso-frame";
|
|
237
|
+
this.iframe.src = `${this.options.ssoOrigin}/auth/sign-in?v=2.3&client_id=${this.options.clientId}&embedded=true&app_id=${this.options.clientId}&theme=${this.options.theme}`;
|
|
238
|
+
this.iframe.setAttribute("title", "SSO Login");
|
|
239
|
+
this.overlayEl.appendChild(this.iframe);
|
|
240
|
+
this.debug("Iframe creado", this.iframe.src);
|
|
241
|
+
this.overlayEl.classList.remove("sso-closing");
|
|
242
|
+
this.overlayEl.style.display = "flex";
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Cierra el overlay con animación suave (fadeOut + slideDown).
|
|
246
|
+
* El overlay persiste en el DOM (solo se oculta).
|
|
247
|
+
*/
|
|
248
|
+
closeUI() {
|
|
249
|
+
if (!this.overlayEl || this.overlayEl.style.display === "none") return;
|
|
250
|
+
this.overlayEl.classList.add("sso-closing");
|
|
251
|
+
setTimeout(() => {
|
|
252
|
+
if (this.overlayEl) {
|
|
253
|
+
this.overlayEl.style.display = "none";
|
|
254
|
+
this.overlayEl.classList.remove("sso-closing");
|
|
255
|
+
}
|
|
256
|
+
}, 200);
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Estilos CSS encapsulados dentro del Shadow DOM.
|
|
260
|
+
* Migrados del widget CDN v1 con las mismas animaciones y responsive.
|
|
261
|
+
*/
|
|
262
|
+
getOverlayStyles() {
|
|
263
|
+
return `
|
|
264
|
+
.sso-overlay {
|
|
265
|
+
position: fixed;
|
|
266
|
+
inset: 0;
|
|
267
|
+
display: none;
|
|
268
|
+
justify-content: center;
|
|
269
|
+
align-items: center;
|
|
270
|
+
background: rgba(0, 0, 0, 0.6);
|
|
271
|
+
z-index: 999999;
|
|
272
|
+
backdrop-filter: blur(4px);
|
|
273
|
+
-webkit-backdrop-filter: blur(4px);
|
|
274
|
+
animation: fadeIn 0.2s ease;
|
|
275
|
+
}
|
|
276
|
+
.sso-frame {
|
|
277
|
+
width: 370px;
|
|
278
|
+
height: 350px;
|
|
279
|
+
border: none;
|
|
280
|
+
border-radius: 16px;
|
|
281
|
+
background: var(--card-bg, #fff);
|
|
282
|
+
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.3);
|
|
283
|
+
animation: slideUp 0.3s ease;
|
|
284
|
+
}
|
|
285
|
+
@media (max-width: 480px), (max-height: 480px) {
|
|
286
|
+
.sso-frame {
|
|
287
|
+
width: 100%;
|
|
288
|
+
height: 100%;
|
|
289
|
+
border-radius: 0;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
.sso-close-btn {
|
|
293
|
+
position: absolute;
|
|
294
|
+
top: 12px;
|
|
295
|
+
right: 12px;
|
|
296
|
+
width: 32px;
|
|
297
|
+
height: 32px;
|
|
298
|
+
background: rgba(0, 0, 0, 0.4);
|
|
299
|
+
color: white;
|
|
300
|
+
border: none;
|
|
301
|
+
border-radius: 50%;
|
|
302
|
+
font-size: 24px;
|
|
303
|
+
line-height: 1;
|
|
304
|
+
cursor: pointer;
|
|
305
|
+
display: flex;
|
|
306
|
+
align-items: center;
|
|
307
|
+
justify-content: center;
|
|
308
|
+
z-index: 1000000;
|
|
309
|
+
transition: background 0.2s;
|
|
310
|
+
}
|
|
311
|
+
.sso-close-btn:hover {
|
|
312
|
+
background: rgba(0, 0, 0, 0.8);
|
|
313
|
+
}
|
|
314
|
+
.sso-overlay.sso-closing {
|
|
315
|
+
animation: fadeOut 0.2s ease forwards;
|
|
316
|
+
}
|
|
317
|
+
.sso-overlay.sso-closing .sso-frame {
|
|
318
|
+
animation: slideDown 0.2s ease forwards;
|
|
319
|
+
}
|
|
320
|
+
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
|
321
|
+
@keyframes slideUp { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
|
|
322
|
+
@keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } }
|
|
323
|
+
@keyframes slideDown { from { transform: translateY(0); opacity: 1; } to { transform: translateY(20px); opacity: 0; } }
|
|
324
|
+
`;
|
|
325
|
+
}
|
|
326
|
+
// ─── Helpers ──────────────────────────────────────────────────────
|
|
327
|
+
buildFallbackUrl() {
|
|
328
|
+
const url = new URL(`${this.options.ssoOrigin}/authorize`);
|
|
329
|
+
url.searchParams.set("client_id", this.options.clientId);
|
|
330
|
+
url.searchParams.set("response_type", "code");
|
|
331
|
+
url.searchParams.set("redirect_uri", this.options.redirectUri || window.location.origin);
|
|
332
|
+
url.searchParams.set("state", generateRandomId());
|
|
333
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
334
|
+
return url.toString();
|
|
335
|
+
}
|
|
336
|
+
debug(...args) {
|
|
337
|
+
if (this.options.debug) {
|
|
338
|
+
console.log("[BigsoAuth]", ...args);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
export {
|
|
343
|
+
BigsoAuth
|
|
344
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bigso/auth-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "SDK de autenticación para SSO v2.3 con iframe seguro",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"registry": "https://registry.npmjs.org/",
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"build": "tsup src/index.ts --format esm,cjs --dts",
|
|
17
17
|
"dev": "tsup src/index.ts --watch",
|
|
18
18
|
"lint": "eslint .",
|
|
19
|
-
"test": "vitest"
|
|
19
|
+
"test": "vitest",
|
|
20
|
+
"release": "git tag v$npm_package_version && git push origin v$npm_package_version"
|
|
20
21
|
},
|
|
21
22
|
"dependencies": {
|
|
22
23
|
"jose": "^5.0.0"
|