npm package: @hemia/jwt-manager
Description: Gestor de JWT seguro y extensible para aplicaciones Node.js
Author: Hemia Technologies
Latest version: 0.0.6

# @hemia/jwt-manager

Una clase completa y robusta para la generación, validación y decodificación de tokens JWT con soporte completo para estándares **OIDC (OpenID Connect)** y **OAuth 2.0**. Ideal para gestionar autenticación y autorización basada en tokens dentro de aplicaciones Node.js modernas.

---

## ✨ Características principales

* ✅ **Estándares OIDC/OAuth 2.0**: Claims estándar y tokens compatibles con especificaciones oficiales
* 🔒 **Seguridad mejorada**: Algoritmos explícitos, validación de issuer/audience, clock skew tolerance
* 🎯 **Tres tipos de tokens**: ID Token, Access Token y Refresh Token
* 🔑 **Gestión de permisos**: Sistema de scopes para autorización granular
* 📝 **Claims estándar**: Soporte completo para profile, email, phone, address claims
* 🔄 **Retrocompatible**: Los métodos existentes siguen funcionando
* 🛡️ **Manejo de errores detallado**: Identificación específica de tipos de error
* 🚀 **TypeScript nativo**: Tipado completo y autocomplete

---

## 📦 Instalación

```bash
npm install @hemia/jwt-manager
```

---

## 🚀 Inicio rápido

```typescript
import { JwtManager } from '@hemia/jwt-manager';

const jwtManager = new JwtManager();

// Crear un ID Token (OIDC)
const idToken = jwtManager.createIdToken({
    sub: 'user_12345',
    name: 'Cristian Mendez',
    email: 'cristian@hemia.com',
    email_verified: true
}, '1h');

// Crear un Access Token con permisos
const accessToken = jwtManager.createAccessToken(
    'user_12345',
    ['read:products', 'write:orders'],
    '15m'
);

// Validar token
const payload = jwtManager.verify(accessToken);
if (payload) {
    console.log('✅ Token válido:', payload);
}

// Verificar permisos
const hasPermission = jwtManager.hasScope(accessToken, 'read:products');
console.log('Tiene permiso:', hasPermission);
```

---

## 📖 API Reference

### 🎫 Métodos de Creación de Tokens

#### `createIdToken(claims, expiresIn?)`

Crea un **ID Token** según estándar OIDC con información de identidad del usuario.

```typescript
const idToken = jwtManager.createIdToken({
    sub: 'user_12345',              // Requerido
    name: 'Cristian Mendez',
    email: 'cristian@hemia.com',
    email_verified: true,
    picture: 'https://example.com/avatar.jpg',
    preferred_username: 'cristianm',
    locale: 'es-MX'
}, '1h');

// Token contiene:
// {
//   sub, name, email, email_verified, picture, preferred_username, locale,
//   iss: 'hemia-app',
//   aud: 'hemia-api',
//   iat: 1703779200,
//   exp: 1703782800
// }
```

#### `createAccessToken(sub, scopes, expiresIn?)`

Crea un **Access Token** OAuth 2.0 con scopes para autorización.

```typescript
const accessToken = jwtManager.createAccessToken(
    'user_12345',
    ['read:products', 'write:orders', 'admin:users'],
    '15m'  // Access tokens suelen ser de corta duración
);

// Token contiene:
// {
//   sub: 'user_12345',
//   scope: 'read:products write:orders admin:users',
//   iss: 'hemia-app',
//   aud: 'hemia-api',
//   iat: 1703779200,
//   exp: 1703780100
// }
```

#### `createRefreshToken(sub, expiresIn?)`

Crea un **Refresh Token** para renovar access tokens expirados.

```typescript
const refreshToken = jwtManager.createRefreshToken('user_12345', '30d');

// Token contiene:
// {
//   sub: 'user_12345',
//   type: 'refresh',
//   jti: '1703779200-xyz123abc',  // JWT ID único para revocación
//   iss: 'hemia-app',
//   aud: 'hemia-api',
//   iat: 1703779200,
//   exp: 1706371200
// }
```

#### `createToken(payload, expiresIn?, options?)`

Crea un JWT genérico con payload personalizado (usa clave por defecto).

```typescript
const token = jwtManager.createToken(
    { userId: 'abc123', role: 'admin' },
    '2h',
    { issuer: 'my-app' }  // Opcional
);
```

#### `createTokenWithSecret(payload, secretKey, expiresIn?, options?)`

Crea un JWT genérico con una clave secreta específica.

```typescript
const token = jwtManager.createTokenWithSecret(
    { data: 'sensitive' },
    'custom-secret-key',
    '1h'
);
```

#### `createCleanCredentialsToken(operative?, expiresIn?)`

Crea un token especial para operaciones sin credenciales completas.

```typescript
const token = jwtManager.createCleanCredentialsToken(Operatives.CATALOG, '2h');
```

---

### ✅ Métodos de Validación

#### `verify(token, secretKey?, options?)`

Valida un JWT y retorna el payload si es válido.

```typescript
const payload = jwtManager.verify(token);

if (payload) {
    console.log('✅ Token válido');
    console.log('Usuario:', payload.sub);
    console.log('Email:', payload.email);
} else {
    console.log('❌ Token inválido o expirado');
}

// Con secret key personalizada
const payload2 = jwtManager.verify(token, 'custom-secret');
```

#### `verifyDetailed(token, secretKey?, options?)`

Valida un JWT y retorna resultado detallado con información de error.

```typescript
const result = jwtManager.verifyDetailed(token);

console.log('Válido:', result.valid);

if (result.valid) {
    console.log('Payload:', result.payload);
} else {
    console.log('Error:', result.error);
    console.log('Tipo de error:', result.errorType);
    // errorType: 'expired' | 'invalid' | 'not_before' | 'malformed' | 'signature_invalid'
}
```

---

### 🔍 Métodos de Decodificación y Consulta

#### `decode(token, complete?)`

Decodifica un JWT **sin validarlo** (no verifica firma ni expiración).

```typescript
// Solo payload
const payload = jwtManager.decode(token);

// Con header incluido
const full = jwtManager.decode(token, true);
console.log('Header:', full.header);    // { alg: 'HS256', typ: 'JWT' }
console.log('Payload:', full.payload);
```

#### `getClaims(token)`

Extrae solo los **claims estándar OIDC** de un token.

```typescript
const claims = jwtManager.getClaims(token);
// Retorna solo: sub, name, email, email_verified, picture, etc.
// NO incluye: iss, aud, iat, exp (que son JWT claims)
```

#### `hasScope(token, requiredScope)`

Verifica si un token tiene un scope/permiso específico.

```typescript
const canRead = jwtManager.hasScope(token, 'read:products');
const canDelete = jwtManager.hasScope(token, 'delete:products');

if (canRead) {
    console.log('✅ Usuario puede leer productos');
}
```

#### `getJwtKey()`

Obtiene la clave secreta configurada.

```typescript
const secretKey = jwtManager.getJwtKey();
```

---

### 🔄 Métodos Legacy (Deprecated)

Estos métodos siguen funcionando para mantener compatibilidad hacia atrás:

```typescript
// ⚠️ Deprecated - Usa createToken() en su lugar
jwtManager.getTokenWithoutKey(payload, expiresIn, options);

// ⚠️ Deprecated - Usa createTokenWithSecret() en su lugar
jwtManager.getTokenWithKey(payload, secretKey, expiresIn, options);

// ⚠️ Deprecated - Usa createCleanCredentialsToken() en su lugar
jwtManager.getTokenCleanCredentials(operative, expiresIn);

// ⚠️ Deprecated - Usa verify() en su lugar
jwtManager.validateToken(token, secretKey, options);

// ⚠️ Deprecated - Usa verifyDetailed() en su lugar
jwtManager.validateTokenDetailed(token, secretKey, options);

// ⚠️ Deprecated - Usa getClaims() en su lugar
jwtManager.getStandardClaims(token);
```

---

## 🔧 Configuración

### Variables de entorno

```env
# Clave secreta para firmar tokens (REQUERIDO en producción)
JWT_SECRET=tu_clave_super_secreta_aqui

# Emisor de los tokens (quien genera el token)
JWT_ISSUER=hemia-app

# Audiencia de los tokens (para quién es el token)
JWT_AUDIENCE=hemia-api
```

> ⚠️ **Importante:** Nunca uses una clave por defecto en producción. Define `JWT_SECRET` siempre en entornos seguros.

---

## 💡 Ejemplos de Uso

### Ejemplo 1: Autenticación completa

```typescript
import { JwtManager } from '@hemia/jwt-manager';

const jwtManager = new JwtManager();

// 1. Usuario inicia sesión
const idToken = jwtManager.createIdToken({
    sub: 'user_12345',
    name: 'Cristian Mendez',
    email: 'cristian@hemia.com',
    email_verified: true
}, '1h');

const accessToken = jwtManager.createAccessToken(
    'user_12345',
    ['read:profile', 'read:products', 'write:orders'],
    '15m'
);

const refreshToken = jwtManager.createRefreshToken('user_12345', '30d');

// 2. Enviar tokens al cliente
res.json({ idToken, accessToken, refreshToken });

// 3. Cliente usa accessToken en requests
// Authorization: Bearer <accessToken>

// 4. Validar token en cada request
const payload = jwtManager.verify(accessToken);
if (!payload) {
    return res.status(401).json({ error: 'Token inválido' });
}

// 5. Verificar permisos específicos
if (!jwtManager.hasScope(accessToken, 'write:orders')) {
    return res.status(403).json({ error: 'Sin permisos' });
}
```

### Ejemplo 2: Middleware de autenticación (Express)

```typescript
import { Request, Response, NextFunction } from 'express';
import { JwtManager } from '@hemia/jwt-manager';

const jwtManager = new JwtManager();

// Middleware de autenticación
export const authenticateToken = (req: Request, res: Response, next: NextFunction) => {
    const authHeader = req.headers['authorization'];
    const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN

    if (!token) {
        return res.status(401).json({ error: 'Token no proporcionado' });
    }

    const payload = jwtManager.verify(token);

    if (!payload) {
        return res.status(403).json({ error: 'Token inválido o expirado' });
    }

    req.user = payload;
    next();
};

// Middleware de autorización por scope
export const requireScope = (requiredScope: string) => {
    return (req: Request, res: Response, next: NextFunction) => {
        const authHeader = req.headers['authorization'];
        const token = authHeader && authHeader.split(' ')[1];

        if (!token || !jwtManager.hasScope(token, requiredScope)) {
            return res.status(403).json({ 
                error: `Permiso denegado. Se requiere: ${requiredScope}` 
            });
        }

        next();
    };
};

// Uso en rutas
app.get('/profile', authenticateToken, (req, res) => {
    res.json({ user: req.user });
});

app.get('/products', authenticateToken, requireScope('read:products'), (req, res) => {
    res.json({ products: [...] });
});

app.post('/orders', authenticateToken, requireScope('write:orders'), (req, res) => {
    res.json({ success: true });
});
```

### Ejemplo 3: Manejo de errores detallado

```typescript
const result = jwtManager.verifyDetailed(token);

if (!result.valid) {
    switch (result.errorType) {
        case 'expired':
            // Token expirado - solicitar refresh
            return res.status(401).json({ 
                error: 'Token expirado',
                code: 'TOKEN_EXPIRED',
                message: 'Por favor renueva tu sesión'
            });
        
        case 'signature_invalid':
            // Firma inválida - posible manipulación
            console.error('⚠️ Token con firma inválida detectado');
            return res.status(403).json({ 
                error: 'Token inválido',
                code: 'INVALID_SIGNATURE' 
            });
        
        case 'not_before':
            // Token aún no válido
            return res.status(403).json({ 
                error: 'Token no válido aún',
                code: 'TOKEN_NOT_YET_VALID' 
            });
        
        default:
            return res.status(403).json({ 
                error: 'Token inválido',
                code: 'INVALID_TOKEN' 
            });
    }
}

// Token válido
console.log('Usuario autenticado:', result.payload.sub);
```

### Ejemplo 4: Renovar tokens con Refresh Token

```typescript
app.post('/auth/refresh', async (req, res) => {
    const { refreshToken } = req.body;

    // Validar refresh token
    const payload = jwtManager.verify(refreshToken);

    if (!payload || payload.type !== 'refresh') {
        return res.status(401).json({ error: 'Refresh token inválido' });
    }

    // Opcional: Verificar que el jti no esté en lista negra
    // const isRevoked = await checkIfRevoked(payload.jti);
    // if (isRevoked) return res.status(401).json({ error: 'Token revocado' });

    // Generar nuevos tokens
    const newAccessToken = jwtManager.createAccessToken(
        payload.sub,
        ['read:profile', 'read:products'],
        '15m'
    );

    const newRefreshToken = jwtManager.createRefreshToken(payload.sub, '30d');

    res.json({ 
        accessToken: newAccessToken, 
        refreshToken: newRefreshToken 
    });
});
```

---

## 📚 TypeScript Types

```typescript
import { 
    JwtManager, 
    StandardClaims, 
    TokenPayload, 
    TokenValidationResult,
    TokenType 
} from '@hemia/jwt-manager';

// StandardClaims (OIDC)
interface StandardClaims {
    sub: string;                    // Subject (required)
    name?: string;
    given_name?: string;
    family_name?: string;
    email?: string;
    email_verified?: boolean;
    picture?: string;
    // ... más claims
}

// TokenValidationResult
interface TokenValidationResult {
    valid: boolean;
    payload?: any;
    error?: string;
    errorType?: 'expired' | 'invalid' | 'not_before' | 'malformed' | 'signature_invalid';
}

// TokenType
enum TokenType {
    ID_TOKEN = 'id_token',
    ACCESS_TOKEN = 'access_token',
    REFRESH_TOKEN = 'refresh_token'
}
```

---

## 🔒 Mejoras de Seguridad Implementadas

### ✅ Algoritmo Explícito
Previene ataques de confusión de algoritmo especificando siempre `HS256` o `RS256`.

### ✅ Validación de Issuer y Audience
Verifica que el token fue emitido por quien dice serlo y para la audiencia correcta.

### ✅ Clock Skew Tolerance
30 segundos de tolerancia para diferencias de reloj entre servidores.

### ✅ JWT ID (jti) para Refresh Tokens
Permite implementar revocación de tokens mediante lista negra.

### ✅ Lista Blanca de Algoritmos
Solo acepta `HS256` y `RS256`, rechazando algoritmos inseguros como `none`.

---

## 📖 Documentación Adicional

Para más ejemplos y documentación detallada, consulta [USAGE_EXAMPLES.md](./USAGE_EXAMPLES.md).

---

## 🌐 Recursos

- [OpenID Connect Standard Claims](https://www.cerberauth.com/blog/openid-connect-standard-claims/)
- [RFC 7519 - JWT](https://tools.ietf.org/html/rfc7519)
- [OAuth 2.0 Scopes](https://oauth.net/2/scope/)
- [OIDC Specification](https://openid.net/specs/openid-connect-core-1_0.html)

---

## 📄 Licencia

MIT — Desarrollado por [Hemia Technologies](https://hemia.mx)