@jmlq/auth-plugin-jose 0.0.1-alpha.9 → 0.0.1-beta.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.es.md +197 -0
- package/README.md +134 -165
- package/dist/infrastructure/mappers/errors/ensure-auth-error-code.d.ts +11 -0
- package/dist/infrastructure/mappers/errors/ensure-auth-error-code.js +20 -0
- package/dist/infrastructure/mappers/errors/index.d.ts +1 -0
- package/dist/infrastructure/mappers/errors/index.js +17 -0
- package/dist/infrastructure/mappers/jose-error.mapper.d.ts +4 -1
- package/dist/infrastructure/mappers/jose-error.mapper.js +30 -5
- package/dist/infrastructure/services/jose-token.service.js +51 -17
- package/package.json +8 -10
package/README.es.md
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# @jmlq/auth-plugin-jose 🧩
|
|
2
|
+
|
|
3
|
+
## 🎯 Objetivo
|
|
4
|
+
|
|
5
|
+
`@jmlq/auth-plugin-jose` es el plugin de infraestructura que implementa `ITokenServicePort` de `@jmlq/auth` usando la librería `jose`.
|
|
6
|
+
|
|
7
|
+
Su responsabilidad real es:
|
|
8
|
+
|
|
9
|
+
- generar access tokens y refresh tokens,
|
|
10
|
+
- verificar tokens firmados,
|
|
11
|
+
- normalizar configuración criptográfica,
|
|
12
|
+
- traducir errores técnicos de `jose` a errores compatibles con el core.
|
|
13
|
+
|
|
14
|
+
El punto de entrada recomendado es `createJoseTokenService(...)`.
|
|
15
|
+
|
|
16
|
+
## ⭐ Importancia
|
|
17
|
+
|
|
18
|
+
Este plugin permite que el core `@jmlq/auth` siga siendo independiente de la tecnología JWT concreta.
|
|
19
|
+
|
|
20
|
+
En la práctica:
|
|
21
|
+
|
|
22
|
+
- el core depende de `ITokenServicePort`,
|
|
23
|
+
- este plugin implementa ese contrato,
|
|
24
|
+
- el host decide si usa `HS256`, `RS256` o `ES256`,
|
|
25
|
+
- la lógica de negocio permanece en `@jmlq/auth`.
|
|
26
|
+
|
|
27
|
+
## 🏗️ Arquitectura (visión rápida)
|
|
28
|
+
|
|
29
|
+
- `createJoseTokenService(...)` valida precondiciones mínimas y construye `JoseTokenService`.
|
|
30
|
+
- `JoseTokenService` encapsula `SignJWT`, `jwtVerify`, `decodeJwt`, `importPKCS8` e `importSPKI`.
|
|
31
|
+
- `jose-error.mapper.ts` traduce errores técnicos a códigos del core.
|
|
32
|
+
- `audience` se usa **solo para emisión**; en verificación no se pasa a `jwtVerify`.
|
|
33
|
+
|
|
34
|
+
➡️ Ver detalle en: [architecture.md](./docs/es/architecture.md)
|
|
35
|
+
|
|
36
|
+
## 🔧 Implementación
|
|
37
|
+
|
|
38
|
+
### 5.1 Instalación
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm i @jmlq/auth @jmlq/auth-plugin-jose jose
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 5.2 Dependencias
|
|
45
|
+
|
|
46
|
+
Dependencias directas del paquete:
|
|
47
|
+
|
|
48
|
+
- `@jmlq/auth`
|
|
49
|
+
- `jose`
|
|
50
|
+
|
|
51
|
+
### 5.3 Quickstart (implementación rápida)
|
|
52
|
+
|
|
53
|
+
Uso real recomendado desde infrastructure:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { createJoseTokenService } from "@jmlq/auth-plugin-jose";
|
|
57
|
+
|
|
58
|
+
const tokenService = createJoseTokenService(
|
|
59
|
+
{
|
|
60
|
+
keyMaterial: {
|
|
61
|
+
alg: "RS256",
|
|
62
|
+
privateKey: process.env.JWT_PRIVATE_KEY!,
|
|
63
|
+
publicKey: process.env.JWT_PUBLIC_KEY!,
|
|
64
|
+
},
|
|
65
|
+
issuer: process.env.JWT_ISSUER,
|
|
66
|
+
audience: process.env.JWT_AUDIENCE,
|
|
67
|
+
clockSkewSeconds: Number(process.env.JWT_CLOCK_SKEW_SECONDS ?? 5),
|
|
68
|
+
defaultExpiresIn: {
|
|
69
|
+
accessToken: "15m",
|
|
70
|
+
refreshToken: "7d",
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
createAuthError,
|
|
75
|
+
},
|
|
76
|
+
);
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
También es válido con `HS256`:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
const tokenService = createJoseTokenService(
|
|
83
|
+
{
|
|
84
|
+
keyMaterial: {
|
|
85
|
+
alg: "HS256",
|
|
86
|
+
secret: process.env.JWT_SECRET!,
|
|
87
|
+
},
|
|
88
|
+
issuer: process.env.JWT_ISSUER,
|
|
89
|
+
audience: process.env.JWT_AUDIENCE,
|
|
90
|
+
defaultExpiresIn: {
|
|
91
|
+
accessToken: "15m",
|
|
92
|
+
refreshToken: "7d",
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
{ createAuthError },
|
|
96
|
+
);
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### 5.4 Variables de entorno (.env) 📦
|
|
100
|
+
|
|
101
|
+
El plugin **no lee `.env` directamente**.
|
|
102
|
+
El host resuelve variables y construye `JoseTokenServiceOptions`.
|
|
103
|
+
|
|
104
|
+
Variables típicas y coherentes con esta integración:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
process.env.JWT_SECRET;
|
|
108
|
+
process.env.JWT_PRIVATE_KEY;
|
|
109
|
+
process.env.JWT_PUBLIC_KEY;
|
|
110
|
+
process.env.JWT_ISSUER;
|
|
111
|
+
process.env.JWT_AUDIENCE;
|
|
112
|
+
process.env.JWT_CLOCK_SKEW_SECONDS;
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### 5.5 Helpers y funcionalidades clave
|
|
116
|
+
|
|
117
|
+
#### `createJoseTokenService(...)`
|
|
118
|
+
|
|
119
|
+
Firma real:
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
createJoseTokenService(
|
|
123
|
+
options: JoseTokenServiceOptions,
|
|
124
|
+
deps: { createAuthError: CreateAuthErrorFn },
|
|
125
|
+
)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Responsabilidades reales:
|
|
129
|
+
|
|
130
|
+
- validar `keyMaterial`,
|
|
131
|
+
- exigir `createAuthError`,
|
|
132
|
+
- construir una implementación concreta de `ITokenServicePort`.
|
|
133
|
+
|
|
134
|
+
#### `JoseTokenServiceOptions`
|
|
135
|
+
|
|
136
|
+
Configuración pública real:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
export interface JoseTokenServiceOptions {
|
|
140
|
+
keyMaterial: JoseKeyMaterial;
|
|
141
|
+
issuer?: string;
|
|
142
|
+
audience?: string | string[];
|
|
143
|
+
clockSkewSeconds?: number;
|
|
144
|
+
defaultExpiresIn?: {
|
|
145
|
+
accessToken?: string;
|
|
146
|
+
refreshToken?: string;
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
#### `JoseKeyMaterial`
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
type JoseKeyMaterial =
|
|
155
|
+
| { alg: "HS256"; secret: string }
|
|
156
|
+
| { alg: "RS256" | "ES256"; privateKey: string; publicKey: string };
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
#### Validación real de material criptográfico
|
|
160
|
+
|
|
161
|
+
El plugin valida:
|
|
162
|
+
|
|
163
|
+
- `HS256` requiere `secret`
|
|
164
|
+
- `RS256` / `ES256` requieren `privateKey` y `publicKey`
|
|
165
|
+
|
|
166
|
+
#### Audience: comportamiento importante
|
|
167
|
+
|
|
168
|
+
Regla real implementada en el plugin:
|
|
169
|
+
|
|
170
|
+
- `audience` se usa para **firmar** (`jwt.setAudience(...)`)
|
|
171
|
+
- `audience` **no se pasa** a `jwtVerify(...)`
|
|
172
|
+
|
|
173
|
+
Esto evita convertir `aud` en requisito obligatorio durante verificación.
|
|
174
|
+
|
|
175
|
+
## ✅ Checklist (pasos rápidos)
|
|
176
|
+
|
|
177
|
+
- [Instalar](#51-instalación)
|
|
178
|
+
- [Definir `keyMaterial` según algoritmo](./docs/es/configuration.md#configuración-del-keymaterial)
|
|
179
|
+
- [Construir el servicio con `createJoseTokenService`](./docs/configuration.md#construcción-del-token-service)
|
|
180
|
+
- [Integrarlo en infrastructure del host](./docs/es/integration-express.md#integración-en-infrastructure-del-host)
|
|
181
|
+
- [Verificar reglas de `audience` y clock skew](./docs/es/troubleshooting.md#1-el-token-se-firma-con-aud-pero-falla-la-verificación-esperada)
|
|
182
|
+
- [Revisar troubleshooting](./docs/es/troubleshooting.md)
|
|
183
|
+
|
|
184
|
+
## 📌 Menú
|
|
185
|
+
|
|
186
|
+
- [Arquitectura](./docs/es/architecture.md)
|
|
187
|
+
- [Configuración](./docs/es/configuration.md)
|
|
188
|
+
- [Integración Express](./docs/es/integration-express.md)
|
|
189
|
+
- [Troubleshooting](./docs/es/troubleshooting.md)
|
|
190
|
+
|
|
191
|
+
## 🔗 Referencias
|
|
192
|
+
|
|
193
|
+
- [`@jmlq/auth`](https://github.com/MLahuasi/jmlq-auth#readme)
|
|
194
|
+
|
|
195
|
+
## ⬅️ 🌐 Ecosistema
|
|
196
|
+
|
|
197
|
+
- [`@jmlq`](https://github.com/MLahuasi/jmlq-ecosystem#readme)
|
package/README.md
CHANGED
|
@@ -1,228 +1,197 @@
|
|
|
1
|
-
# @jmlq/auth-plugin-jose
|
|
1
|
+
# @jmlq/auth-plugin-jose 🧩
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 🎯 Objective
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
`@jmlq/auth-plugin-jose` is the infrastructure plugin that implements `ITokenServicePort` from `@jmlq/auth` using the `jose` library.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
Su única responsabilidad es **implementar los ports de tokens definidos por el core** y traducir detalles técnicos de `jose` a contratos estables del dominio.
|
|
7
|
+
Its real responsibility is:
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
- generate access tokens and refresh tokens,
|
|
10
|
+
- verify signed tokens,
|
|
11
|
+
- normalize cryptographic configuration,
|
|
12
|
+
- translate technical errors from `jose` into core-compatible errors.
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
- Soporte para algoritmos HS256, RS256 y ES256
|
|
14
|
-
- Normalización y validación de claims estándar (`iss`, `aud`, `exp`, `nbf`, `iat`, `sub`, `jti`)
|
|
15
|
-
- Validación de expiración y clock skew
|
|
16
|
-
- Manejo de claves simétricas y asimétricas
|
|
17
|
-
- Traducción de errores técnicos de `jose` a errores del core
|
|
14
|
+
The recommended entry point is `createJoseTokenService(...)`.
|
|
18
15
|
|
|
19
|
-
|
|
16
|
+
## ⭐ Importance
|
|
20
17
|
|
|
21
|
-
|
|
18
|
+
This plugin allows the `@jmlq/auth` core to remain independent from the specific JWT technology.
|
|
22
19
|
|
|
23
|
-
|
|
24
|
-
Core de autenticación (domain + application).
|
|
25
|
-
Define contratos, errores y casos de uso.
|
|
20
|
+
In practice:
|
|
26
21
|
|
|
27
|
-
-
|
|
28
|
-
|
|
29
|
-
|
|
22
|
+
- the core depends on `ITokenServicePort`,
|
|
23
|
+
- this plugin implements that contract,
|
|
24
|
+
- the host decides whether to use `HS256`, `RS256`, or `ES256`,
|
|
25
|
+
- business logic remains in `@jmlq/auth`.
|
|
30
26
|
|
|
31
|
-
|
|
27
|
+
## 🏗️ Architecture (quick view)
|
|
32
28
|
|
|
33
|
-
|
|
29
|
+
- `createJoseTokenService(...)` validates minimal preconditions and builds `JoseTokenService`.
|
|
30
|
+
- `JoseTokenService` encapsulates `SignJWT`, `jwtVerify`, `decodeJwt`, `importPKCS8`, and `importSPKI`.
|
|
31
|
+
- `jose-error.mapper.ts` translates technical errors into core error codes.
|
|
32
|
+
- `audience` is used **only for issuing**; it is not passed to `jwtVerify` during verification.
|
|
34
33
|
|
|
35
|
-
|
|
34
|
+
➡️ See details in: [architecture.md](./docs/en/architecture.md)
|
|
36
35
|
|
|
37
|
-
|
|
36
|
+
## 🔧 Implementation
|
|
38
37
|
|
|
39
|
-
|
|
40
|
-
Factory de alto nivel que construye una implementación de `ITokenServicePort`
|
|
41
|
-
compatible con `@jmlq/auth`.
|
|
42
|
-
|
|
43
|
-
Responsabilidades:
|
|
44
|
-
|
|
45
|
-
- Validar configuración técnica mínima (keys, algoritmo, opciones)
|
|
46
|
-
- Normalizar issuer, audience, expiración y clock skew
|
|
47
|
-
- Garantizar una política de expiración consistente
|
|
48
|
-
- Encapsular completamente el uso de `jose`
|
|
49
|
-
|
|
50
|
-
> Es el **único punto recomendado de entrada** para consumidores del plugin.
|
|
51
|
-
|
|
52
|
-
---
|
|
53
|
-
|
|
54
|
-
### Tipos de configuración
|
|
55
|
-
|
|
56
|
-
- **JoseTokenServiceOptions**
|
|
57
|
-
Define la configuración necesaria para construir el servicio de tokens:
|
|
58
|
-
- `algorithm`: Algoritmo JWT (`HS256`, `RS256`, `ES256`)
|
|
59
|
-
- `keys`: Material criptográfico (secret o key pair)
|
|
60
|
-
- `issuer`: Emisor esperado (`iss`)
|
|
61
|
-
- `audience`: Audiencia permitida (`aud`)
|
|
62
|
-
- `defaultExpiresIn`: Expiración por defecto (ej: `"15m"`)
|
|
63
|
-
- `clockSkewSeconds`: Tolerancia de desajuste de reloj
|
|
64
|
-
|
|
65
|
-
Estos tipos existen para **configuración explícita y tipada**, sin estado global.
|
|
66
|
-
|
|
67
|
-
---
|
|
68
|
-
|
|
69
|
-
## Estrategia de errores (OBLIGATORIA)
|
|
70
|
-
|
|
71
|
-
- El plugin **NO expone errores propios**
|
|
72
|
-
|
|
73
|
-
Reglas:
|
|
74
|
-
|
|
75
|
-
- Todos los errores de `jose` se traducen a `AuthDomainError`
|
|
76
|
-
- No se definen nuevos códigos de error
|
|
77
|
-
- El error original se conserva como `cause` o `meta`
|
|
78
|
-
- La aplicación **solo maneja errores del core**
|
|
79
|
-
|
|
80
|
-
Ejemplos de mapeo:
|
|
81
|
-
|
|
82
|
-
| Error `jose` | Error del core |
|
|
83
|
-
| ---------------------- | ------------------------- |
|
|
84
|
-
| Token expirado | `TOKEN_EXPIRED` |
|
|
85
|
-
| Firma inválida | `SIGNATURE_INVALID` |
|
|
86
|
-
| Token malformado | `TOKEN_MALFORMED` |
|
|
87
|
-
| Algoritmo no soportado | `ALGORITHM_UNSUPPORTED` |
|
|
88
|
-
| Claims inválidos | `CLAIMS_VALIDATION_ERROR` |
|
|
89
|
-
|
|
90
|
-
---
|
|
91
|
-
|
|
92
|
-
## 📦 Instalación
|
|
38
|
+
### 5.1 Installation
|
|
93
39
|
|
|
94
40
|
```bash
|
|
95
|
-
npm
|
|
41
|
+
npm i @jmlq/auth @jmlq/auth-plugin-jose jose
|
|
96
42
|
```
|
|
97
43
|
|
|
98
|
-
|
|
44
|
+
### 5.2 Dependencies
|
|
99
45
|
|
|
100
|
-
|
|
101
|
-
- `jose` >= `5.x`
|
|
46
|
+
Direct dependencies of the package:
|
|
102
47
|
|
|
103
|
-
|
|
48
|
+
- `@jmlq/auth`
|
|
49
|
+
- `jose`
|
|
104
50
|
|
|
105
|
-
|
|
51
|
+
### 5.3 Quickstart (quick implementation)
|
|
106
52
|
|
|
107
|
-
|
|
53
|
+
Recommended real usage from infrastructure:
|
|
108
54
|
|
|
109
55
|
```ts
|
|
110
56
|
import { createJoseTokenService } from "@jmlq/auth-plugin-jose";
|
|
111
57
|
|
|
112
|
-
const tokenService = createJoseTokenService(
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
58
|
+
const tokenService = createJoseTokenService(
|
|
59
|
+
{
|
|
60
|
+
keyMaterial: {
|
|
61
|
+
alg: "RS256",
|
|
62
|
+
privateKey: process.env.JWT_PRIVATE_KEY!,
|
|
63
|
+
publicKey: process.env.JWT_PUBLIC_KEY!,
|
|
64
|
+
},
|
|
65
|
+
issuer: process.env.JWT_ISSUER,
|
|
66
|
+
audience: process.env.JWT_AUDIENCE,
|
|
67
|
+
clockSkewSeconds: Number(process.env.JWT_CLOCK_SKEW_SECONDS ?? 5),
|
|
68
|
+
defaultExpiresIn: {
|
|
69
|
+
accessToken: "15m",
|
|
70
|
+
refreshToken: "7d",
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
createAuthError,
|
|
75
|
+
},
|
|
76
|
+
);
|
|
120
77
|
```
|
|
121
78
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
### Generar un access token
|
|
79
|
+
Also valid with `HS256`:
|
|
125
80
|
|
|
126
81
|
```ts
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
82
|
+
const tokenService = createJoseTokenService(
|
|
83
|
+
{
|
|
84
|
+
keyMaterial: {
|
|
85
|
+
alg: "HS256",
|
|
86
|
+
secret: process.env.JWT_SECRET!,
|
|
87
|
+
},
|
|
88
|
+
issuer: process.env.JWT_ISSUER,
|
|
89
|
+
audience: process.env.JWT_AUDIENCE,
|
|
90
|
+
defaultExpiresIn: {
|
|
91
|
+
accessToken: "15m",
|
|
92
|
+
refreshToken: "7d",
|
|
93
|
+
},
|
|
131
94
|
},
|
|
132
|
-
}
|
|
95
|
+
{ createAuthError },
|
|
96
|
+
);
|
|
133
97
|
```
|
|
134
98
|
|
|
135
|
-
|
|
99
|
+
### 5.4 Environment variables (.env) 📦
|
|
136
100
|
|
|
137
|
-
|
|
101
|
+
The plugin **does not read `.env` directly**.
|
|
102
|
+
The host resolves variables and builds `JoseTokenServiceOptions`.
|
|
138
103
|
|
|
139
|
-
|
|
140
|
-
const result = await tokenService.verifyAccessToken(token);
|
|
104
|
+
Typical variables consistent with this integration:
|
|
141
105
|
|
|
142
|
-
|
|
106
|
+
```ts
|
|
107
|
+
process.env.JWT_SECRET;
|
|
108
|
+
process.env.JWT_PRIVATE_KEY;
|
|
109
|
+
process.env.JWT_PUBLIC_KEY;
|
|
110
|
+
process.env.JWT_ISSUER;
|
|
111
|
+
process.env.JWT_AUDIENCE;
|
|
112
|
+
process.env.JWT_CLOCK_SKEW_SECONDS;
|
|
143
113
|
```
|
|
144
114
|
|
|
145
|
-
|
|
115
|
+
### 5.5 Helpers and key functionalities
|
|
146
116
|
|
|
147
|
-
|
|
148
|
-
- Nunca se lanza un error de `jose`
|
|
117
|
+
#### `createJoseTokenService(...)`
|
|
149
118
|
|
|
150
|
-
|
|
119
|
+
Real signature:
|
|
151
120
|
|
|
152
|
-
|
|
121
|
+
```ts
|
|
122
|
+
createJoseTokenService(
|
|
123
|
+
options: JoseTokenServiceOptions,
|
|
124
|
+
deps: { createAuthError: CreateAuthErrorFn },
|
|
125
|
+
)
|
|
126
|
+
```
|
|
153
127
|
|
|
154
|
-
|
|
128
|
+
Real responsibilities:
|
|
155
129
|
|
|
156
|
-
|
|
157
|
-
|
|
130
|
+
- validate `keyMaterial`,
|
|
131
|
+
- require `createAuthError`,
|
|
132
|
+
- build a concrete implementation of `ITokenServicePort`.
|
|
158
133
|
|
|
159
|
-
|
|
134
|
+
#### `JoseTokenServiceOptions`
|
|
160
135
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
defaultExpiresIn
|
|
170
|
-
|
|
136
|
+
Real public configuration:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
export interface JoseTokenServiceOptions {
|
|
140
|
+
keyMaterial: JoseKeyMaterial;
|
|
141
|
+
issuer?: string;
|
|
142
|
+
audience?: string | string[];
|
|
143
|
+
clockSkewSeconds?: number;
|
|
144
|
+
defaultExpiresIn?: {
|
|
145
|
+
accessToken?: string;
|
|
146
|
+
refreshToken?: string;
|
|
147
|
+
};
|
|
148
|
+
}
|
|
171
149
|
```
|
|
172
150
|
|
|
173
|
-
|
|
151
|
+
#### `JoseKeyMaterial`
|
|
174
152
|
|
|
175
|
-
|
|
153
|
+
```ts
|
|
154
|
+
type JoseKeyMaterial =
|
|
155
|
+
| { alg: "HS256"; secret: string }
|
|
156
|
+
| { alg: "RS256" | "ES256"; privateKey: string; publicKey: string };
|
|
157
|
+
```
|
|
176
158
|
|
|
177
|
-
|
|
159
|
+
#### Real cryptographic material validation
|
|
178
160
|
|
|
179
|
-
|
|
161
|
+
The plugin validates:
|
|
180
162
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
JWT_ISSUER=auth-api
|
|
184
|
-
JWT_AUDIENCE=web-app
|
|
185
|
-
JWT_DEFAULT_EXPIRES_IN=15m
|
|
186
|
-
JWT_CLOCK_SKEW_SECONDS=5
|
|
163
|
+
- `HS256` requires `secret`
|
|
164
|
+
- `RS256` / `ES256` require `privateKey` and `publicKey`
|
|
187
165
|
|
|
188
|
-
|
|
189
|
-
JWT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----..."
|
|
166
|
+
#### Audience: important behavior
|
|
190
167
|
|
|
191
|
-
|
|
168
|
+
Real rule implemented in the plugin:
|
|
192
169
|
|
|
193
|
-
|
|
170
|
+
- `audience` is used for **signing** (`jwt.setAudience(...)`)
|
|
171
|
+
- `audience` is **not passed** to `jwtVerify(...)`
|
|
194
172
|
|
|
195
|
-
|
|
173
|
+
This prevents turning `aud` into a mandatory requirement during verification.
|
|
196
174
|
|
|
197
|
-
##
|
|
175
|
+
## ✅ Checklist (quick steps)
|
|
198
176
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
│ │ └─ jose-token-service.options.ts
|
|
206
|
-
│ └─ internal/
|
|
207
|
-
│ └─ *.util.ts
|
|
208
|
-
│
|
|
209
|
-
├─ infrastructure/
|
|
210
|
-
│ ├─ services/
|
|
211
|
-
│ │ └─ jose-token.service.ts
|
|
212
|
-
│ └─ mappers/
|
|
213
|
-
│ └─ jose-error.mapper.ts
|
|
214
|
-
│
|
|
215
|
-
└─ index.ts
|
|
177
|
+
- [Install](#51-installation)
|
|
178
|
+
- [Define `keyMaterial` according to algorithm](./docs/en/configuration.md#configuring-keymaterial)
|
|
179
|
+
- [Build the service with `createJoseTokenService`](./docs/en/configuration.md#building-the-token-service)
|
|
180
|
+
- [Integrate it into the host infrastructure](./docs/en/integration-express.md#integration-in-host-infrastructure)
|
|
181
|
+
- [Verify `audience` and clock skew rules](./docs/en/troubleshooting.md#1-token-is-signed-with-aud-but-verification-fails-as-expected)
|
|
182
|
+
- [Review troubleshooting](./docs/en/troubleshooting.md)
|
|
216
183
|
|
|
217
|
-
|
|
184
|
+
## 📌 Menu
|
|
218
185
|
|
|
219
|
-
|
|
186
|
+
- [Architecture](./docs/en/architecture.md)
|
|
187
|
+
- [Configuration](./docs/en/configuration.md)
|
|
188
|
+
- [Express Integration](./docs/en/integration-express.md)
|
|
189
|
+
- [Troubleshooting](./docs/en/troubleshooting.md)
|
|
220
190
|
|
|
221
|
-
##
|
|
191
|
+
## 🔗 References
|
|
222
192
|
|
|
223
|
-
-
|
|
224
|
-
- **[📚 Ejemplos de Código](./examples/)** - Casos de uso reales y implementaciones
|
|
193
|
+
- [`@jmlq/auth`](https://github.com/MLahuasi/jmlq-auth#readme)
|
|
225
194
|
|
|
226
|
-
##
|
|
195
|
+
## ⬅️ 🌐 Ecosystem
|
|
227
196
|
|
|
228
|
-
|
|
197
|
+
- [`@jmlq`](https://github.com/MLahuasi/jmlq-ecosystem#readme)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AuthErrorCode } from "@jmlq/auth";
|
|
2
|
+
/**
|
|
3
|
+
* Convierte un Error (posiblemente sin code) en uno con `code: AuthErrorCode`
|
|
4
|
+
* de forma segura y mínima, SIN cambiar el tipo del error original.
|
|
5
|
+
*
|
|
6
|
+
* - Si ya tiene code string, lo usa.
|
|
7
|
+
* - Si no, usa un fallback dado (por ej. "JWT_ERROR").
|
|
8
|
+
*/
|
|
9
|
+
export declare function ensureAuthErrorCode(err: Error, fallback: AuthErrorCode): Error & {
|
|
10
|
+
code: AuthErrorCode;
|
|
11
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ensureAuthErrorCode = ensureAuthErrorCode;
|
|
4
|
+
/**
|
|
5
|
+
* Convierte un Error (posiblemente sin code) en uno con `code: AuthErrorCode`
|
|
6
|
+
* de forma segura y mínima, SIN cambiar el tipo del error original.
|
|
7
|
+
*
|
|
8
|
+
* - Si ya tiene code string, lo usa.
|
|
9
|
+
* - Si no, usa un fallback dado (por ej. "JWT_ERROR").
|
|
10
|
+
*/
|
|
11
|
+
function ensureAuthErrorCode(err, fallback) {
|
|
12
|
+
const e = err;
|
|
13
|
+
const code = typeof e.code === "string" ? e.code : undefined;
|
|
14
|
+
// Importante: aquí NO validamos que code pertenezca al union.
|
|
15
|
+
// Solo garantizamos que al menos sea un AuthErrorCode conocido o fallback.
|
|
16
|
+
const safeCode = code ?? fallback;
|
|
17
|
+
// Mutación mínima: añadimos code si falta
|
|
18
|
+
e.code = safeCode;
|
|
19
|
+
return e;
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./ensure-auth-error-code";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./ensure-auth-error-code"), exports);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AuthErrorCode } from "@jmlq/auth";
|
|
1
2
|
import type { JoseErrorContext, MappedAuthError } from "./types";
|
|
2
3
|
export declare function mapJoseErrorToAuthError(err: unknown, ctx: JoseErrorContext): MappedAuthError;
|
|
3
4
|
export declare function toAuthDomainError<TAuthError extends Error>(createAuthError: (args: {
|
|
@@ -5,4 +6,6 @@ export declare function toAuthDomainError<TAuthError extends Error>(createAuthEr
|
|
|
5
6
|
message: string;
|
|
6
7
|
cause?: unknown;
|
|
7
8
|
meta?: Record<string, unknown>;
|
|
8
|
-
}) => TAuthError, err: unknown, ctx: JoseErrorContext):
|
|
9
|
+
}) => TAuthError, err: unknown, ctx: JoseErrorContext): Error & {
|
|
10
|
+
code: AuthErrorCode;
|
|
11
|
+
};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
//src/infrastructure/mappers/jose-error.mapper.ts
|
|
3
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
3
|
exports.mapJoseErrorToAuthError = mapJoseErrorToAuthError;
|
|
5
4
|
exports.toAuthDomainError = toAuthDomainError;
|
|
6
5
|
const auth_1 = require("@jmlq/auth");
|
|
6
|
+
const errors_1 = require("./errors");
|
|
7
7
|
/**
|
|
8
8
|
* Mapper de errores de `jose` → error “entendible” por el core (@jmlq/auth).
|
|
9
9
|
*
|
|
@@ -29,20 +29,32 @@ function buildSafeMeta(joseErrorName, ctx) {
|
|
|
29
29
|
alg: ctx.alg,
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Tabla: err.name (jose) -> AuthErrorCode (core)
|
|
34
|
+
*/
|
|
32
35
|
const JOSE_NAME_TO_AUTH_CODE = {
|
|
36
|
+
// Tiempo
|
|
33
37
|
JWTExpired: "TOKEN_EXPIRED",
|
|
34
38
|
JWTNotBefore: "TOKEN_NOT_YET_VALID",
|
|
35
39
|
JWTNotYetValid: "TOKEN_NOT_YET_VALID",
|
|
40
|
+
// Firma
|
|
36
41
|
JWSSignatureVerificationFailed: "SIGNATURE_INVALID",
|
|
37
|
-
|
|
38
|
-
|
|
42
|
+
// Estructura/serialización JWS inválida
|
|
43
|
+
JWSInvalid: "TOKEN_MALFORMED",
|
|
44
|
+
JWSMalformed: "TOKEN_MALFORMED",
|
|
45
|
+
// Error genérico JWS (no asumir firma)
|
|
46
|
+
JWSError: "TOKEN_INVALID",
|
|
47
|
+
// Claims inválidos (issuer, exp, nbf, etc. dependiendo de jose)
|
|
39
48
|
JWTClaimValidationFailed: "CLAIMS_VALIDATION_ERROR",
|
|
49
|
+
// Token inválido (pero no necesariamente “malformed”)
|
|
40
50
|
JWTInvalid: "TOKEN_INVALID",
|
|
51
|
+
// Token malformado
|
|
41
52
|
JWTMalformed: "TOKEN_MALFORMED",
|
|
42
|
-
JWSMalformed: "TOKEN_MALFORMED",
|
|
43
53
|
JOSEError: "TOKEN_MALFORMED",
|
|
54
|
+
// Algoritmo / soporte
|
|
44
55
|
JOSENotSupported: "ALGORITHM_UNSUPPORTED",
|
|
45
56
|
JWTAlgorithmNotAllowed: "ALGORITHM_UNSUPPORTED",
|
|
57
|
+
// Key material
|
|
46
58
|
JWKInvalid: "KEY_MISMATCH",
|
|
47
59
|
JWKInvalidFormat: "KEY_MISMATCH",
|
|
48
60
|
};
|
|
@@ -54,6 +66,8 @@ const JOSE_NAME_TO_AUTH_CODE = {
|
|
|
54
66
|
* - Por eso es Partial<Record<AuthErrorCode, string>> + fallback.
|
|
55
67
|
*/
|
|
56
68
|
const AUTH_CODE_TO_MESSAGE = {
|
|
69
|
+
JWT_EMPTY: "Invalid empty JWT",
|
|
70
|
+
JWT_MALFORMED: "JWT is malformed",
|
|
57
71
|
TOKEN_INVALID: "Token is invalid",
|
|
58
72
|
TOKEN_EXPIRED: "Token has expired",
|
|
59
73
|
TOKEN_MALFORMED: "Token is malformed",
|
|
@@ -74,6 +88,14 @@ function mapJoseErrorToAuthError(err, ctx) {
|
|
|
74
88
|
const message = AUTH_CODE_TO_MESSAGE[code] ??
|
|
75
89
|
// fallback defensivo (no dependemos de message de jose)
|
|
76
90
|
"JWT operation failed";
|
|
91
|
+
// console.log({
|
|
92
|
+
// package: "@jmlq/auth-plugin-jose",
|
|
93
|
+
// name,
|
|
94
|
+
// meta,
|
|
95
|
+
// code,
|
|
96
|
+
// message,
|
|
97
|
+
// err,
|
|
98
|
+
// });
|
|
77
99
|
return {
|
|
78
100
|
code,
|
|
79
101
|
message,
|
|
@@ -82,14 +104,17 @@ function mapJoseErrorToAuthError(err, ctx) {
|
|
|
82
104
|
};
|
|
83
105
|
}
|
|
84
106
|
function toAuthDomainError(createAuthError, err, ctx) {
|
|
107
|
+
// Si ya es error del core, conservarlo (NO remapear)
|
|
85
108
|
if (auth_1.AuthDomainError.isAuthError(err)) {
|
|
86
109
|
return err;
|
|
87
110
|
}
|
|
88
111
|
const mapped = mapJoseErrorToAuthError(err, ctx);
|
|
89
|
-
|
|
112
|
+
const created = createAuthError({
|
|
90
113
|
code: mapped.code,
|
|
91
114
|
message: mapped.message,
|
|
92
115
|
cause: mapped.cause,
|
|
93
116
|
meta: mapped.meta,
|
|
94
117
|
});
|
|
118
|
+
// Garantizamos code usable por `isRetryableAuthCode`
|
|
119
|
+
return (0, errors_1.ensureAuthErrorCode)(created, mapped.code);
|
|
95
120
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
// src/infrastructure/services/jose-token.service.ts
|
|
3
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
3
|
exports.JoseTokenService = void 0;
|
|
5
4
|
const jose_1 = require("jose");
|
|
@@ -103,7 +102,19 @@ class JoseTokenService {
|
|
|
103
102
|
return await fn();
|
|
104
103
|
}
|
|
105
104
|
catch (err) {
|
|
106
|
-
|
|
105
|
+
// 1) Si ya viene como error del core, NO tocarlo
|
|
106
|
+
if (auth_1.AuthDomainError.isAuthError(err)) {
|
|
107
|
+
throw err;
|
|
108
|
+
}
|
|
109
|
+
// 2) Si es error de jose u otro, mapear SIEMPRE
|
|
110
|
+
const authErr = (0, mappers_1.toAuthDomainError)(this.createAuthError, err, this.ctx(operation, kind));
|
|
111
|
+
// 3) (Opcional) Si tienes retry logic, que dependa solo de code del core
|
|
112
|
+
// y NO de códigos internos de jose.
|
|
113
|
+
// Ej: si no quieres reintentos, elimina esto.
|
|
114
|
+
if ((0, auth_1.isRetryableAuthCode)(authErr.code)) {
|
|
115
|
+
throw authErr;
|
|
116
|
+
}
|
|
117
|
+
throw authErr;
|
|
107
118
|
}
|
|
108
119
|
}
|
|
109
120
|
// ---------------------------------------------------------------------------
|
|
@@ -173,24 +184,47 @@ class JoseTokenService {
|
|
|
173
184
|
}
|
|
174
185
|
async verifyToken(kind, operation, token) {
|
|
175
186
|
return this.withAuthError(operation, kind, async () => {
|
|
176
|
-
const keys = await this.getNormalizedKeys();
|
|
177
|
-
const key = keys.alg === "HS256" ? keys.secret : keys.publicKey;
|
|
178
187
|
/**
|
|
179
|
-
*
|
|
180
|
-
* -
|
|
181
|
-
* -
|
|
188
|
+
* Validación estructural temprana (core):
|
|
189
|
+
* - JWT_EMPTY si token está vacío
|
|
190
|
+
* - JWT_MALFORMED si no tiene 3 segmentos
|
|
182
191
|
*
|
|
183
|
-
* Nota:
|
|
184
|
-
* -
|
|
185
|
-
* -
|
|
186
|
-
* y rompería compatibilidad con tokens válidos sin `aud`.
|
|
192
|
+
* Nota:
|
|
193
|
+
* - Esto evita que `jose` arroje errores “ruidosos” por formato inválido.
|
|
194
|
+
* - Estos errores YA son AuthDomainError y deben conservar su code.
|
|
187
195
|
*/
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
196
|
+
(0, auth_1.assertJwtStructure)(token);
|
|
197
|
+
const keys = await this.getNormalizedKeys();
|
|
198
|
+
const key = keys.alg === "HS256" ? keys.secret : keys.publicKey;
|
|
199
|
+
try {
|
|
200
|
+
/**
|
|
201
|
+
* Verificación:
|
|
202
|
+
* - issuer: se aplica si está configurado (si no, se omite)
|
|
203
|
+
* - clockTolerance: ya normalizado (clock skew seguro)
|
|
204
|
+
*
|
|
205
|
+
* Nota: NO pasamos `audience` a jwtVerify.
|
|
206
|
+
* - La audiencia en este plugin es SOLO para emisión.
|
|
207
|
+
* - Pasarla aquí convertiría audience en una restricción (“must match”)
|
|
208
|
+
* y rompería compatibilidad con tokens válidos sin `aud`.
|
|
209
|
+
*/
|
|
210
|
+
const result = await (0, jose_1.jwtVerify)(token, key, {
|
|
211
|
+
issuer: this.eff.issuer,
|
|
212
|
+
clockTolerance: this.eff.clockSkewSeconds,
|
|
213
|
+
});
|
|
214
|
+
// Delegar a la regla canónica del core: valida + normaliza payload.
|
|
215
|
+
return (0, auth_1.normalizeJwtPayload)(result.payload);
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
/**
|
|
219
|
+
* Importante:
|
|
220
|
+
* - NO relanzar error crudo de jose.
|
|
221
|
+
* - Convertirlo a AuthDomainError con code estable (TOKEN_INVALID, SIGNATURE_INVALID, etc.)
|
|
222
|
+
* usando el mapper oficial.
|
|
223
|
+
*
|
|
224
|
+
* Esto evita que el host termine con INTERNAL + alert emails para 401 esperados.
|
|
225
|
+
*/
|
|
226
|
+
throw (0, mappers_1.toAuthDomainError)(this.createAuthError, err, this.ctx(operation, kind));
|
|
227
|
+
}
|
|
194
228
|
});
|
|
195
229
|
}
|
|
196
230
|
async tryGetExpirationByVerify(kind, token) {
|
package/package.json
CHANGED
|
@@ -1,23 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jmlq/auth-plugin-jose",
|
|
3
3
|
"description": "Infrastructure plugin that integrates the jose library with @jmlq/auth, providing JWT token generation and verification following Clean Architecture principles.",
|
|
4
|
-
"version": "0.0.1-
|
|
4
|
+
"version": "0.0.1-beta.1",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/MLahuasi/jmlq-auth-plugin-jose.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/MLahuasi/jmlq-auth-plugin-jose#readme",
|
|
7
12
|
"scripts": {
|
|
8
13
|
"dev": "rimraf dist && mkdir dist && tsc -p tsconfig.json",
|
|
9
14
|
"build": "rimraf dist && mkdir dist && tsc -p tsconfig.build.json",
|
|
10
15
|
"prepublishOnly": "npm run build",
|
|
11
|
-
"package:script": "node scripts/package-script.mjs",
|
|
12
16
|
"test": "jest --passWithNoTests",
|
|
13
17
|
"test:watch": "jest --watch",
|
|
14
|
-
"test:coverage": "jest --coverage"
|
|
15
|
-
"example:services": "tsx examples/index.example.ts services",
|
|
16
|
-
"example:service-helpers": "tsx examples/index.example.ts service-helpers",
|
|
17
|
-
"example:factories": "tsx examples/index.example.ts factories",
|
|
18
|
-
"example:factory-helpers": "tsx examples/index.example.ts factory-helpers",
|
|
19
|
-
"example:help": "tsx examples/index.example.ts help",
|
|
20
|
-
"example:all": "tsx examples/index.example.ts"
|
|
18
|
+
"test:coverage": "jest --coverage"
|
|
21
19
|
},
|
|
22
20
|
"keywords": [
|
|
23
21
|
"jwt",
|
|
@@ -29,7 +27,7 @@
|
|
|
29
27
|
"author": "MLahuasi",
|
|
30
28
|
"license": "MIT",
|
|
31
29
|
"dependencies": {
|
|
32
|
-
"@jmlq/auth": "
|
|
30
|
+
"@jmlq/auth": "beta",
|
|
33
31
|
"jose": "^6.1.3"
|
|
34
32
|
},
|
|
35
33
|
"devDependencies": {
|