@hemia/auth-sdk 0.0.1
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 +86 -0
- package/dist/hemia-auth-sdk.esm.js +496 -0
- package/dist/hemia-auth-sdk.js +505 -0
- package/dist/types/controllers/abstract-auth.controller.d.ts +14 -0
- package/dist/types/errors/index.d.ts +1 -0
- package/dist/types/errors/session.error.d.ts +20 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/ioc.d.ts +4 -0
- package/dist/types/services/auth.service.d.ts +48 -0
- package/dist/types/services/cache.service.d.ts +8 -0
- package/dist/types/services/index.d.ts +2 -0
- package/dist/types/types/auth-config.interface.d.ts +12 -0
- package/dist/types/types/callback-response.interface.d.ts +5 -0
- package/dist/types/types/index.d.ts +10 -0
- package/dist/types/types/jwt-manager.interface.d.ts +3 -0
- package/dist/types/types/login-params.interface.d.ts +5 -0
- package/dist/types/types/session-data.interface.d.ts +8 -0
- package/dist/types/types/session-user-response.interface.d.ts +6 -0
- package/dist/types/types/session-user.interface.d.ts +9 -0
- package/dist/types/types/standard-claims.interface.d.ts +29 -0
- package/dist/types/types/store-state.interface.d.ts +4 -0
- package/dist/types/types/token-response.d.ts +9 -0
- package/dist/types/utils/Generators.d.ts +23 -0
- package/package.json +54 -0
package/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Hemia Auth SDK
|
|
2
|
+
|
|
3
|
+
SDK para gestionar autenticación SSO con PKCE flow, manejo de sesiones y refresh tokens automático.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 📦 Instalación
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @hemia/auth-sdk
|
|
11
|
+
bun add @hemia/auth-sdk
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
bun add @hemia/auth-sdk
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 🚀 Uso Básico
|
|
21
|
+
|
|
22
|
+
### Configuración
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { AuthService, AuthCacheService } from '@hemia/auth-sdk';
|
|
26
|
+
|
|
27
|
+
const config = {
|
|
28
|
+
clientId: 'your-client-id',
|
|
29
|
+
clientSecret: 'your-client-secret',
|
|
30
|
+
redirectUri: 'http://localhost:3000/callback',
|
|
31
|
+
ssoBaseUrl: 'https://sso.hemia.com',
|
|
32
|
+
ssoAuthUrl: '/oauth/authorize',
|
|
33
|
+
ssoTokenEndpoint: '/oauth/token',
|
|
34
|
+
ssoLogoutEndpoint: '/oauth/logout',
|
|
35
|
+
uiBaseUrl: 'http://localhost:3000'
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const storage = new AuthCacheService(redisClient);
|
|
39
|
+
const jwtManager = new JwtManager();
|
|
40
|
+
const authService = new AuthService(config, storage, jwtManager);
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 🎯 Controller Abstracto
|
|
46
|
+
|
|
47
|
+
El SDK incluye un `AbstractAuthController` con endpoints listos:
|
|
48
|
+
|
|
49
|
+
- `GET /login` - Inicia el flujo de autenticación
|
|
50
|
+
- `GET /callback` - Procesa el callback del SSO
|
|
51
|
+
- `GET /me` - Obtiene datos del usuario autenticado
|
|
52
|
+
- `POST /logout` - Cierra la sesión
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
import { AbstractAuthController } from '@hemia/auth-sdk';
|
|
56
|
+
|
|
57
|
+
export class AuthController extends AbstractAuthController {
|
|
58
|
+
constructor(authService: AuthService) {
|
|
59
|
+
super(authService);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 🔒 Manejo de Errores
|
|
67
|
+
|
|
68
|
+
Todos los errores de sesión extienden `SessionError` e incluyen:
|
|
69
|
+
- `code`: Código del error
|
|
70
|
+
- `message`: Mensaje descriptivo
|
|
71
|
+
- `redirectTo`: URL de redirección sugerida
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## 🛠️ Scripts Disponibles
|
|
76
|
+
|
|
77
|
+
| Script | Descripción |
|
|
78
|
+
|--------------|----------------------------------|
|
|
79
|
+
| `npm run build` | Compila el paquete con Rollup |
|
|
80
|
+
| `npm run test` | Ejecuta pruebas con Jest |
|
|
81
|
+
| `npm run clean` | Limpia la carpeta `dist/` |
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## ✨ Generado con Hemia CLI
|
|
86
|
+
|
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
import { Get, Req, Res, Post, HttpError, BadRequestError, CustomHttpError, InternalServerError } from '@hemia/common';
|
|
3
|
+
import { HMNetworkServices } from '@hemia/network-services';
|
|
4
|
+
import { randomBytes, createHash } from 'crypto';
|
|
5
|
+
import { injectable } from 'inversify';
|
|
6
|
+
import { CacheService } from '@hemia/cache-manager';
|
|
7
|
+
|
|
8
|
+
/******************************************************************************
|
|
9
|
+
Copyright (c) Microsoft Corporation.
|
|
10
|
+
|
|
11
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
12
|
+
purpose with or without fee is hereby granted.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
15
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
16
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
17
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
18
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
19
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
20
|
+
PERFORMANCE OF THIS SOFTWARE.
|
|
21
|
+
***************************************************************************** */
|
|
22
|
+
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
function __decorate(decorators, target, key, desc) {
|
|
26
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
27
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
28
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
29
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function __param(paramIndex, decorator) {
|
|
33
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function __metadata(metadataKey, metadataValue) {
|
|
37
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(metadataKey, metadataValue);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
41
|
+
var e = new Error(message);
|
|
42
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
class SessionError extends Error {
|
|
46
|
+
constructor(message, code, redirectTo) {
|
|
47
|
+
super(message);
|
|
48
|
+
this.code = code;
|
|
49
|
+
this.redirectTo = redirectTo;
|
|
50
|
+
this.name = this.constructor.name;
|
|
51
|
+
Error.captureStackTrace(this, this.constructor);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
class SessionNotFoundError extends SessionError {
|
|
55
|
+
constructor(message = 'Sesión no encontrada') {
|
|
56
|
+
super(message, 'SESSION_NOT_FOUND', '/?session=required');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
class SessionExpiredError extends SessionError {
|
|
60
|
+
constructor(message = 'Sesión expirada') {
|
|
61
|
+
super(message, 'SESSION_EXPIRED', '/session-expired?session=expired');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
class SessionInvalidError extends SessionError {
|
|
65
|
+
constructor(message = 'Sesión inválida') {
|
|
66
|
+
super(message, 'SESSION_INVALID', '/?session=invalid');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
class TokenRefreshFailedError extends SessionError {
|
|
70
|
+
constructor(message = 'Error al renovar tokens') {
|
|
71
|
+
super(message, 'TOKEN_REFRESH_FAILED', '/session-expired?session=expired');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
class InvalidTokenFormatError extends SessionError {
|
|
75
|
+
constructor(message = 'Formato de token inválido') {
|
|
76
|
+
super(message, 'INVALID_TOKEN_FORMAT', '/?session=invalid');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Controller Abstracto Reutilizable
|
|
82
|
+
* Gestiona automáticamente Login, Callback, Me y Logout.
|
|
83
|
+
*/
|
|
84
|
+
class AbstractAuthController {
|
|
85
|
+
constructor(authService) {
|
|
86
|
+
this.authService = authService;
|
|
87
|
+
}
|
|
88
|
+
async login(req, res) {
|
|
89
|
+
try {
|
|
90
|
+
const autoParam = typeof req.query.auto === 'string' ? req.query.auto : 'false';
|
|
91
|
+
const { loginUrl, tempState } = this.authService.generateLoginParams(autoParam);
|
|
92
|
+
res.cookie('auth_flow', JSON.stringify(tempState), {
|
|
93
|
+
httpOnly: true,
|
|
94
|
+
secure: process.env.NODE_ENV === 'production',
|
|
95
|
+
maxAge: 300000 // 5 min
|
|
96
|
+
});
|
|
97
|
+
res.redirect(loginUrl);
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
console.error('Login Error:', error);
|
|
101
|
+
res.status(500).send('Login initialization failed');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async callback(req, res) {
|
|
105
|
+
try {
|
|
106
|
+
const { code, state } = req.query;
|
|
107
|
+
const authFlowCookie = req.cookies['auth_flow'];
|
|
108
|
+
if (!authFlowCookie) {
|
|
109
|
+
res.status(400).send('Missing auth flow cookie');
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const storedState = JSON.parse(authFlowCookie);
|
|
113
|
+
const result = await this.authService.handleCallback(code, state, storedState);
|
|
114
|
+
res.cookie('x-session', result.sessionId, {
|
|
115
|
+
httpOnly: true,
|
|
116
|
+
secure: process.env.NODE_ENV === 'production',
|
|
117
|
+
sameSite: 'lax',
|
|
118
|
+
maxAge: result.expiresIn * 1000,
|
|
119
|
+
path: '/'
|
|
120
|
+
});
|
|
121
|
+
res.clearCookie('auth_flow');
|
|
122
|
+
res.redirect(result.redirectUrl);
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
console.error('Callback Error:', error);
|
|
126
|
+
if (error instanceof HttpError) {
|
|
127
|
+
res.status(error.statusCode).json({
|
|
128
|
+
success: false,
|
|
129
|
+
message: error.message,
|
|
130
|
+
error: error.error
|
|
131
|
+
});
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
res.status(500).json({
|
|
135
|
+
success: false,
|
|
136
|
+
message: 'Failed to complete authentication',
|
|
137
|
+
error: error.message
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async me(req, res) {
|
|
142
|
+
const sessionId = req.cookies['x-session'];
|
|
143
|
+
if (!sessionId) {
|
|
144
|
+
return res.status(401).json({
|
|
145
|
+
success: false,
|
|
146
|
+
message: 'No session found',
|
|
147
|
+
data: {
|
|
148
|
+
redirect_to: '/?session=required'
|
|
149
|
+
},
|
|
150
|
+
error: {
|
|
151
|
+
message: 'Session is missing',
|
|
152
|
+
code: 'SESSION_MISSING'
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
const result = await this.authService.getSessionUser(sessionId);
|
|
158
|
+
return res.status(200).json({
|
|
159
|
+
success: true,
|
|
160
|
+
data: result
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
res.clearCookie('x-session', {
|
|
165
|
+
httpOnly: true,
|
|
166
|
+
secure: process.env.NODE_ENV === 'production',
|
|
167
|
+
sameSite: 'lax',
|
|
168
|
+
});
|
|
169
|
+
if (error instanceof SessionError) {
|
|
170
|
+
return res.status(401).json({
|
|
171
|
+
success: false,
|
|
172
|
+
message: error.message,
|
|
173
|
+
data: {
|
|
174
|
+
redirect_to: error.redirectTo || '/login'
|
|
175
|
+
},
|
|
176
|
+
error: {
|
|
177
|
+
message: error.message,
|
|
178
|
+
code: error.code
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
return res.status(500).json({
|
|
184
|
+
success: false,
|
|
185
|
+
message: 'Failed to retrieve session user',
|
|
186
|
+
error: error.message
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
async logout(req, res) {
|
|
192
|
+
const sessionId = req.cookies['x-session'];
|
|
193
|
+
if (sessionId) {
|
|
194
|
+
await this.authService.logout(sessionId);
|
|
195
|
+
}
|
|
196
|
+
res.clearCookie('x-session', {
|
|
197
|
+
httpOnly: true,
|
|
198
|
+
secure: process.env.NODE_ENV === 'production',
|
|
199
|
+
sameSite: 'lax',
|
|
200
|
+
});
|
|
201
|
+
return res.status(200).json({ success: true });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
__decorate([
|
|
205
|
+
Get('/login'),
|
|
206
|
+
__param(0, Req()),
|
|
207
|
+
__param(1, Res()),
|
|
208
|
+
__metadata("design:type", Function),
|
|
209
|
+
__metadata("design:paramtypes", [Object, Object]),
|
|
210
|
+
__metadata("design:returntype", Promise)
|
|
211
|
+
], AbstractAuthController.prototype, "login", null);
|
|
212
|
+
__decorate([
|
|
213
|
+
Get('/callback'),
|
|
214
|
+
__metadata("design:type", Function),
|
|
215
|
+
__metadata("design:paramtypes", [Object, Object]),
|
|
216
|
+
__metadata("design:returntype", Promise)
|
|
217
|
+
], AbstractAuthController.prototype, "callback", null);
|
|
218
|
+
__decorate([
|
|
219
|
+
Get('/me'),
|
|
220
|
+
__param(0, Req()),
|
|
221
|
+
__param(1, Res()),
|
|
222
|
+
__metadata("design:type", Function),
|
|
223
|
+
__metadata("design:paramtypes", [Object, Object]),
|
|
224
|
+
__metadata("design:returntype", Promise)
|
|
225
|
+
], AbstractAuthController.prototype, "me", null);
|
|
226
|
+
__decorate([
|
|
227
|
+
Post('/logout'),
|
|
228
|
+
__param(0, Req()),
|
|
229
|
+
__param(1, Res()),
|
|
230
|
+
__metadata("design:type", Function),
|
|
231
|
+
__metadata("design:paramtypes", [Object, Object]),
|
|
232
|
+
__metadata("design:returntype", Promise)
|
|
233
|
+
], AbstractAuthController.prototype, "logout", null);
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Utilidades para manejo de PKCE y codificación Base64URL
|
|
237
|
+
*/
|
|
238
|
+
class Generators {
|
|
239
|
+
/**
|
|
240
|
+
* Base64URL Encoding
|
|
241
|
+
* Reemplaza la implementación manual con Buffer de Node.js
|
|
242
|
+
*/
|
|
243
|
+
static base64urlEncode(strOrBuffer) {
|
|
244
|
+
const buffer = Buffer.isBuffer(strOrBuffer)
|
|
245
|
+
? strOrBuffer
|
|
246
|
+
: Buffer.from(strOrBuffer);
|
|
247
|
+
return buffer
|
|
248
|
+
.toString('base64')
|
|
249
|
+
.replace(/\+/g, '-')
|
|
250
|
+
.replace(/\//g, '_')
|
|
251
|
+
.replace(/=+$/, '');
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Genera un Code Verifier para el flujo PKCE (OAuth 2.0).
|
|
255
|
+
* Crea una cadena aleatoria de 43 a 128 caracteres, codificada en Base64URL.
|
|
256
|
+
*/
|
|
257
|
+
static generateCodeVerifier() {
|
|
258
|
+
const buffer = randomBytes(32);
|
|
259
|
+
return this.base64urlEncode(buffer);
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Generate Code Challenge (SHA-256)
|
|
263
|
+
*/
|
|
264
|
+
static generateCodeChallenge(verifier) {
|
|
265
|
+
const hash = createHash('sha256')
|
|
266
|
+
.update(verifier)
|
|
267
|
+
.digest();
|
|
268
|
+
return this.base64urlEncode(hash);
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Genera un estado aleatorio para la protección CSRF.
|
|
272
|
+
*/
|
|
273
|
+
static state() {
|
|
274
|
+
const buffer = randomBytes(16);
|
|
275
|
+
return this.base64urlEncode(buffer);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
let AuthCacheService = class AuthCacheService {
|
|
280
|
+
constructor(cacheClient) {
|
|
281
|
+
this.cacheClient = cacheClient;
|
|
282
|
+
}
|
|
283
|
+
async set(key, value, ttlSeconds) {
|
|
284
|
+
await this.cacheClient.setObject(key, value, ttlSeconds);
|
|
285
|
+
}
|
|
286
|
+
async get(key) {
|
|
287
|
+
return await this.cacheClient.getObject(key);
|
|
288
|
+
}
|
|
289
|
+
async delete(key) {
|
|
290
|
+
await this.cacheClient.deleteKey(key);
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
AuthCacheService = __decorate([
|
|
294
|
+
injectable(),
|
|
295
|
+
__metadata("design:paramtypes", [CacheService])
|
|
296
|
+
], AuthCacheService);
|
|
297
|
+
|
|
298
|
+
let AuthService = class AuthService {
|
|
299
|
+
constructor(config, storage, jwtManager) {
|
|
300
|
+
this.config = config;
|
|
301
|
+
this.storage = storage;
|
|
302
|
+
this.jwtManager = jwtManager;
|
|
303
|
+
this.networkServices = new HMNetworkServices(this.config.ssoBaseUrl);
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Genera los parámetros necesarios para iniciar el login SSO
|
|
307
|
+
* @param auto
|
|
308
|
+
* @returns
|
|
309
|
+
*/
|
|
310
|
+
generateLoginParams(auto = 'false') {
|
|
311
|
+
const codeVerifier = Generators.generateCodeVerifier();
|
|
312
|
+
const codeChallenge = Generators.generateCodeChallenge(codeVerifier);
|
|
313
|
+
const state = Generators.state();
|
|
314
|
+
const params = new URLSearchParams({
|
|
315
|
+
response_type: 'code',
|
|
316
|
+
client_id: this.config.clientId,
|
|
317
|
+
redirect_uri: this.config.redirectUri,
|
|
318
|
+
state: state,
|
|
319
|
+
code_challenge: codeChallenge,
|
|
320
|
+
code_challenge_method: 'S256',
|
|
321
|
+
auto: auto
|
|
322
|
+
});
|
|
323
|
+
const loginUrl = `${this.config.ssoBaseUrl}${this.config.ssoAuthUrl}?${params.toString()}`;
|
|
324
|
+
const tempState = {
|
|
325
|
+
state,
|
|
326
|
+
codeVerifier
|
|
327
|
+
};
|
|
328
|
+
return {
|
|
329
|
+
loginUrl,
|
|
330
|
+
tempState
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Maneja el callback del SSO, intercambiando el código por tokens y creando la sesión.
|
|
335
|
+
* @param code Código de autorización recibido del SSO
|
|
336
|
+
* @param incomingState Estado recibido del SSO
|
|
337
|
+
* @param storedState Estado temporal almacenado antes de redirigir al SSO
|
|
338
|
+
* @returns Información de la sesión creada
|
|
339
|
+
*/
|
|
340
|
+
async handleCallback(code, incomingState, storedState) {
|
|
341
|
+
if (incomingState !== storedState.state) {
|
|
342
|
+
throw new BadRequestError('Invalid state parameter', 'invalid_state');
|
|
343
|
+
}
|
|
344
|
+
try {
|
|
345
|
+
const response = await this.networkServices.post(this.config.ssoTokenEndpoint, {
|
|
346
|
+
grantType: 'authorization_code',
|
|
347
|
+
clientId: this.config.clientId,
|
|
348
|
+
clientSecret: this.config.clientSecret,
|
|
349
|
+
code,
|
|
350
|
+
redirectUri: this.config.redirectUri,
|
|
351
|
+
codeVerifier: storedState.codeVerifier
|
|
352
|
+
});
|
|
353
|
+
if (response.status !== 200) {
|
|
354
|
+
throw new CustomHttpError('Token exchange failed', response.status, 'token_exchange_failed');
|
|
355
|
+
}
|
|
356
|
+
if (!response.data.access_token) {
|
|
357
|
+
throw new InternalServerError('No access token received from SSO', 'invalid_token_response');
|
|
358
|
+
}
|
|
359
|
+
const { access_token, refresh_token, id_token, expires_in, session_id } = response.data;
|
|
360
|
+
const sessionId = randomBytes(16).toString('hex');
|
|
361
|
+
const sessionData = {
|
|
362
|
+
accessToken: access_token,
|
|
363
|
+
refreshToken: refresh_token,
|
|
364
|
+
idToken: id_token,
|
|
365
|
+
expiresAt: Date.now() + (expires_in * 1000),
|
|
366
|
+
createdAt: new Date().toISOString(),
|
|
367
|
+
ssoSessionId: session_id
|
|
368
|
+
};
|
|
369
|
+
await this.storage.set(`x-session:${sessionId}`, sessionData, expires_in);
|
|
370
|
+
return {
|
|
371
|
+
sessionId,
|
|
372
|
+
expiresIn: expires_in,
|
|
373
|
+
redirectUrl: `${this.config.uiBaseUrl}`
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
catch (error) {
|
|
377
|
+
console.error('Token Exchange Error:', error);
|
|
378
|
+
throw error;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Obtiene y valida la sesión del usuario a partir del sessionId.
|
|
383
|
+
* Si la sesión está cerca de expirar, intenta refrescar los tokens.
|
|
384
|
+
* @param sessionId Identificador de la sesión
|
|
385
|
+
* @returns Información del usuario o error si la sesión no es válida
|
|
386
|
+
*/
|
|
387
|
+
async getSessionUser(sessionId) {
|
|
388
|
+
const key = `x-session:${sessionId}`;
|
|
389
|
+
let session = await this.storage.get(key);
|
|
390
|
+
if (!session) {
|
|
391
|
+
throw new SessionNotFoundError();
|
|
392
|
+
}
|
|
393
|
+
if (session.expiresAt < Date.now()) {
|
|
394
|
+
throw new SessionExpiredError();
|
|
395
|
+
}
|
|
396
|
+
const timeUntilExpiry = session.expiresAt - Date.now();
|
|
397
|
+
if (timeUntilExpiry < 2 * 60 * 1000) {
|
|
398
|
+
try {
|
|
399
|
+
session = await this.refreshTokens(session, sessionId);
|
|
400
|
+
}
|
|
401
|
+
catch (error) {
|
|
402
|
+
throw new TokenRefreshFailedError();
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
try {
|
|
406
|
+
const userData = await this.decodeIdToken(session.idToken);
|
|
407
|
+
if (!userData) {
|
|
408
|
+
throw new SessionInvalidError();
|
|
409
|
+
}
|
|
410
|
+
return userData;
|
|
411
|
+
}
|
|
412
|
+
catch (e) {
|
|
413
|
+
throw new InvalidTokenFormatError();
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Cierra la sesión del usuario tanto en el SSO como localmente.
|
|
418
|
+
* @param sessionId Identificador de la sesión
|
|
419
|
+
*/
|
|
420
|
+
async logout(sessionId) {
|
|
421
|
+
const key = `x-session:${sessionId}`;
|
|
422
|
+
const session = await this.storage.get(key);
|
|
423
|
+
if (session) {
|
|
424
|
+
try {
|
|
425
|
+
await this.networkServices.post(this.config.ssoLogoutEndpoint, {
|
|
426
|
+
ssoSessionId: session.sessionId
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
catch (e) { /* Silent error */ }
|
|
430
|
+
await this.storage.delete(key);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Decodifica el ID token para extraer la información del usuario.
|
|
435
|
+
* @param idToken Token de identificación JWT
|
|
436
|
+
* @returns Información del usuario o null si no se puede decodificar
|
|
437
|
+
*/
|
|
438
|
+
async decodeIdToken(idToken) {
|
|
439
|
+
try {
|
|
440
|
+
const decode = this.jwtManager.decode(idToken);
|
|
441
|
+
if (!decode) {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
const data = {
|
|
445
|
+
email: decode.email || '',
|
|
446
|
+
name: decode.name || '',
|
|
447
|
+
given_name: decode.given_name,
|
|
448
|
+
family_name: decode.family_name,
|
|
449
|
+
picture: decode.picture,
|
|
450
|
+
...decode
|
|
451
|
+
};
|
|
452
|
+
return data;
|
|
453
|
+
}
|
|
454
|
+
catch (error) {
|
|
455
|
+
throw new Error('Failed to decode ID token');
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Refresca los tokens de la sesión utilizando el refresh token.
|
|
460
|
+
* @param session Datos actuales de la sesión
|
|
461
|
+
* @param sessionId Identificador de la sesión
|
|
462
|
+
* @returns Datos actualizados de la sesión
|
|
463
|
+
*/
|
|
464
|
+
async refreshTokens(session, sessionId) {
|
|
465
|
+
const response = await this.networkServices.post(this.config.ssoTokenEndpoint, {
|
|
466
|
+
grantType: 'refresh_token',
|
|
467
|
+
clientId: this.config.clientId,
|
|
468
|
+
clientSecret: this.config.clientSecret,
|
|
469
|
+
refreshToken: session.refreshToken,
|
|
470
|
+
sessionId: session.ssoSessionId
|
|
471
|
+
});
|
|
472
|
+
const { access_token, refresh_token, id_token, expires_in } = response.data;
|
|
473
|
+
const updatedSession = {
|
|
474
|
+
accessToken: access_token,
|
|
475
|
+
refreshToken: refresh_token || session.refreshToken,
|
|
476
|
+
idToken: id_token || session.idToken,
|
|
477
|
+
expiresAt: Date.now() + (expires_in * 1000),
|
|
478
|
+
sessionId: response.data.session_id || '',
|
|
479
|
+
createdAt: Date.now().toString()
|
|
480
|
+
};
|
|
481
|
+
await this.storage.set(`x-session:${sessionId}`, updatedSession, expires_in);
|
|
482
|
+
return updatedSession;
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
AuthService = __decorate([
|
|
486
|
+
injectable(),
|
|
487
|
+
__metadata("design:paramtypes", [Object, AuthCacheService, Object])
|
|
488
|
+
], AuthService);
|
|
489
|
+
|
|
490
|
+
const registerAuthSdk = (container, config, cacheService, jwtManager) => {
|
|
491
|
+
container.bind(AuthService).toDynamicValue(() => {
|
|
492
|
+
return new AuthService(config, cacheService, jwtManager);
|
|
493
|
+
}).inSingletonScope();
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
export { AbstractAuthController, AuthCacheService, AuthService, InvalidTokenFormatError, SessionError, SessionExpiredError, SessionInvalidError, SessionNotFoundError, TokenRefreshFailedError, registerAuthSdk };
|