@jmlq/auth-plugin-jose 0.0.1-alpha.7 → 0.0.1-alpha.9

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.
@@ -1,4 +1,4 @@
1
- import { type ITokenServicePort } from "@jmlq/auth";
1
+ import { JoseTokenService } from "../../infrastructure/services";
2
2
  import type { CreateAuthErrorFn } from "../../infrastructure/services/types";
3
3
  import { JoseTokenServiceOptions } from "../types";
4
4
  /**
@@ -11,4 +11,4 @@ import { JoseTokenServiceOptions } from "../types";
11
11
  */
12
12
  export declare function createJoseTokenService(options: JoseTokenServiceOptions, deps: {
13
13
  createAuthError: CreateAuthErrorFn;
14
- }): ITokenServicePort;
14
+ }): JoseTokenService;
@@ -1,10 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createJoseTokenService = createJoseTokenService;
4
- const auth_1 = require("@jmlq/auth");
5
4
  const services_1 = require("../../infrastructure/services");
6
5
  const internal_1 = require("./internal");
7
- const types_1 = require("../types");
8
6
  /**
9
7
  * Factory para construir un `ITokenServicePort` basado en `jose`.
10
8
  *
@@ -16,21 +14,13 @@ const types_1 = require("../types");
16
14
  function createJoseTokenService(options, deps) {
17
15
  (0, internal_1.assert)(options, "JoseTokenServiceOptions is required");
18
16
  (0, internal_1.assert)(deps?.createAuthError, "createAuthError dependency is required");
19
- const keyMaterial = (0, internal_1.validateKeyMaterial)(options.keyMaterial);
20
- // Normalización mínima local (string trim -> undefined) OK en application
21
- const issuer = (0, auth_1.readNonEmptyString)(options.issuer);
22
- // IMPORTANTE:
23
- // - clockSkewSeconds y defaultExpiresIn se delegan a infraestructura para
24
- // aplicar normalización estándar desde @jmlq/auth.
25
- const clockSkewSeconds = options.clockSkewSeconds;
26
- const defaultExpiresIn = options.defaultExpiresIn;
27
17
  return new services_1.JoseTokenService({
28
18
  options: {
29
- keyMaterial,
30
- issuer,
31
- clockSkewSeconds,
32
- defaultExpiresIn,
33
- getExpirationPolicy: types_1.DEFAULT_GET_EXPIRATION_POLICY,
19
+ keyMaterial: options.keyMaterial,
20
+ issuer: options.issuer,
21
+ audience: options.audience,
22
+ clockSkewSeconds: options.clockSkewSeconds,
23
+ defaultExpiresIn: options.defaultExpiresIn,
34
24
  },
35
25
  createAuthError: deps.createAuthError,
36
26
  });
@@ -1,5 +1,3 @@
1
1
  export * from "./jose-token-service-options.type";
2
2
  export * from "./jose-key-material.type";
3
3
  export * from "./default-expires-in.type";
4
- export * from "./get-expiration-policy.type";
5
- export { DEFAULT_GET_EXPIRATION_POLICY } from "./get-expiration-policy.type";
@@ -14,11 +14,6 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.DEFAULT_GET_EXPIRATION_POLICY = void 0;
18
17
  __exportStar(require("./jose-token-service-options.type"), exports);
19
18
  __exportStar(require("./jose-key-material.type"), exports);
20
19
  __exportStar(require("./default-expires-in.type"), exports);
21
- __exportStar(require("./get-expiration-policy.type"), exports);
22
- // Constantes públicas
23
- var get_expiration_policy_type_1 = require("./get-expiration-policy.type");
24
- Object.defineProperty(exports, "DEFAULT_GET_EXPIRATION_POLICY", { enumerable: true, get: function () { return get_expiration_policy_type_1.DEFAULT_GET_EXPIRATION_POLICY; } });
@@ -1,6 +1,5 @@
1
1
  import type { JoseKeyMaterial } from "./jose-key-material.type";
2
2
  import type { DefaultExpiresIn } from "./default-expires-in.type";
3
- import type { GetExpirationPolicy } from "./get-expiration-policy.type";
4
3
  /**
5
4
  * Opciones públicas para construir el token service basado en jose.
6
5
  * Responsabilidad: configuración neutral, sin acoplar al consumidor a jose.
@@ -8,6 +7,14 @@ import type { GetExpirationPolicy } from "./get-expiration-policy.type";
8
7
  export interface JoseTokenServiceOptions {
9
8
  keyMaterial: JoseKeyMaterial;
10
9
  issuer?: string;
10
+ /**
11
+ * Audience opcional para firmar (claim `aud`).
12
+ * Puede ser string o string[].
13
+ *
14
+ * Nota: esto NO es validación de audience contra el token entrante.
15
+ * Es solo configuración para emisión (firmado).
16
+ */
17
+ audience?: string | string[];
11
18
  /**
12
19
  * Clock skew en segundos para validaciones temporales (iat/nbf/exp).
13
20
  */
@@ -16,9 +23,4 @@ export interface JoseTokenServiceOptions {
16
23
  * Expiraciones default si el token no define otra (por kind).
17
24
  */
18
25
  defaultExpiresIn?: DefaultExpiresIn;
19
- /**
20
- * Política acordada del plugin.
21
- * Se fuerza a "verify-first" desde la factory.
22
- */
23
- getExpirationPolicy?: GetExpirationPolicy;
24
26
  }
@@ -2,3 +2,4 @@ export * from "./normalize-key-material";
2
2
  export * from "./jwt-expiration-reader";
3
3
  export * from "./get-alg-from-key-material";
4
4
  export * from "./build-jose-ctx";
5
+ export * from "./strip-sensitive-custom-claims";
@@ -18,3 +18,4 @@ __exportStar(require("./normalize-key-material"), exports);
18
18
  __exportStar(require("./jwt-expiration-reader"), exports);
19
19
  __exportStar(require("./get-alg-from-key-material"), exports);
20
20
  __exportStar(require("./build-jose-ctx"), exports);
21
+ __exportStar(require("./strip-sensitive-custom-claims"), exports);
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Elimina claims sensibles que NO deben viajar en JWT.
3
+ * - permissions: deben resolverse en BDD en cada request.
4
+ */
5
+ export declare function stripSensitiveCustomClaims(input: Record<string, unknown>): Record<string, unknown>;
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.stripSensitiveCustomClaims = stripSensitiveCustomClaims;
4
+ /**
5
+ * Elimina claims sensibles que NO deben viajar en JWT.
6
+ * - permissions: deben resolverse en BDD en cada request.
7
+ */
8
+ function stripSensitiveCustomClaims(input) {
9
+ const out = {};
10
+ for (const [k, v] of Object.entries(input)) {
11
+ if (k === "permissions")
12
+ continue;
13
+ out[k] = v;
14
+ }
15
+ return out;
16
+ }
@@ -1,4 +1,4 @@
1
- import type { ITokenServicePort, IJWTPayload, IGenerateAccessTokenProps, IGenerateRefreshTokenProps } from "@jmlq/auth";
1
+ import { type IGenerateAccessTokenProps, type IGenerateRefreshTokenProps, type IJWTPayload, type ITokenServicePort } from "@jmlq/auth";
2
2
  import type { JoseTokenServiceOptions } from "../../application/types";
3
3
  import type { CreateAuthErrorFn } from "./types";
4
4
  /**
@@ -6,20 +6,24 @@ import type { CreateAuthErrorFn } from "./types";
6
6
  *
7
7
  * Responsabilidad única:
8
8
  * - Firmar/verificar JWT
9
- * - Delegar validación de payload al core (normalizeJwtPayload)
9
+ * - Delegar validación/normalización del payload al core (normalizeJwtPayload)
10
10
  *
11
11
  * Clean Architecture (decisión del proyecto):
12
- * - Aquí (infra) sí consumimos utilidades estándar desde @jmlq/auth,
13
- * para evitar duplicidad en cada plugin.
12
+ * - En infraestructura sí consumimos utilidades estándar desde @jmlq/auth
13
+ * (reglas canónicas), para evitar duplicidad en cada plugin.
14
14
  */
15
15
  export declare class JoseTokenService implements ITokenServicePort {
16
16
  private readonly options;
17
17
  private readonly createAuthError;
18
+ private readonly keyMaterial;
18
19
  /**
19
20
  * Config normalizada (estandarizada) para uso interno.
20
- * Se deriva 1 sola vez para evitar repetir lógica.
21
+ * Se deriva 1 sola vez para evitar repetir lógica y asegurar consistencia.
21
22
  */
22
23
  private readonly eff;
24
+ /**
25
+ * Cache lazy de llaves normalizadas (evita repetir trabajo por request).
26
+ */
23
27
  private keysPromise;
24
28
  constructor(cfg: {
25
29
  options: JoseTokenServiceOptions;
@@ -29,9 +33,37 @@ export declare class JoseTokenService implements ITokenServicePort {
29
33
  generateRefreshToken(props: IGenerateRefreshTokenProps): Promise<string>;
30
34
  verifyAccessToken(token: string): Promise<IJWTPayload>;
31
35
  verifyRefreshToken(token: string): Promise<IJWTPayload>;
36
+ /**
37
+ * Obtiene la fecha de expiración del token.
38
+ *
39
+ * Estrategia:
40
+ * - Primero intenta verificarlo como access.
41
+ * - Si falla con error “retryable”, intenta verificarlo como refresh.
42
+ * - Si está expirado pero se puede leer exp vía decode (sin verificación),
43
+ * retorna esa exp para fines informativos.
44
+ */
32
45
  getTokenExpiration(token: string): Promise<Date>;
46
+ /**
47
+ * Construye contexto mínimo (no sensible) para mapear errores.
48
+ *
49
+ * ¿Por qué existe?
50
+ * - Hace diagnósticos más fáciles (operación, tokenKind, alg, issuer).
51
+ * - Evita exponer token, claims completos o material criptográfico.
52
+ *
53
+ * Nota:
54
+ * - No incluimos audience en el contexto porque la audience en este plugin
55
+ * es SOLO para emisión y no participa en verificación (por decisión de diseño).
56
+ */
33
57
  private ctx;
58
+ /**
59
+ * Wrapper estándar:
60
+ * - Ejecuta fn
61
+ * - En caso de error, lo traduce a AuthDomainError usando el mapper del plugin
62
+ */
34
63
  private withAuthError;
64
+ /**
65
+ * Normaliza/carga llaves de forma lazy y cacheada.
66
+ */
35
67
  private getNormalizedKeys;
36
68
  private generateToken;
37
69
  private verifyToken;
@@ -1,29 +1,38 @@
1
1
  "use strict";
2
+ // src/infrastructure/services/jose-token.service.ts
2
3
  Object.defineProperty(exports, "__esModule", { value: true });
3
4
  exports.JoseTokenService = void 0;
4
- const auth_1 = require("@jmlq/auth");
5
5
  const jose_1 = require("jose");
6
+ const auth_1 = require("@jmlq/auth");
7
+ const internal_1 = require("../../application/factories/internal");
6
8
  const mappers_1 = require("../mappers");
7
- const internal_1 = require("./internal");
9
+ const internal_2 = require("./internal");
8
10
  /**
9
11
  * Implementación de `ITokenServicePort` usando la librería `jose`.
10
12
  *
11
13
  * Responsabilidad única:
12
14
  * - Firmar/verificar JWT
13
- * - Delegar validación de payload al core (normalizeJwtPayload)
15
+ * - Delegar validación/normalización del payload al core (normalizeJwtPayload)
14
16
  *
15
17
  * Clean Architecture (decisión del proyecto):
16
- * - Aquí (infra) sí consumimos utilidades estándar desde @jmlq/auth,
17
- * para evitar duplicidad en cada plugin.
18
+ * - En infraestructura sí consumimos utilidades estándar desde @jmlq/auth
19
+ * (reglas canónicas), para evitar duplicidad en cada plugin.
18
20
  */
19
21
  class JoseTokenService {
20
22
  constructor(cfg) {
23
+ /**
24
+ * Cache lazy de llaves normalizadas (evita repetir trabajo por request).
25
+ */
21
26
  this.keysPromise = null;
22
27
  this.options = cfg.options;
23
28
  this.createAuthError = cfg.createAuthError;
24
- // Normalización estándar (core) aplicada en infraestructura
29
+ // Valida estructura de keyMaterial (alg / secret / keys etc.).
30
+ this.keyMaterial = (0, internal_1.validateKeyMaterial)(this.options.keyMaterial);
31
+ // Normalización canónica (core) aplicada en infraestructura.
32
+ // Nota: si el usuario envía strings con espacios, se estandariza aquí.
25
33
  this.eff = {
26
- issuer: cfg.options.issuer,
34
+ issuer: (0, auth_1.readNonEmptyString)(cfg.options.issuer), // " a " => "a", " " => undefined
35
+ audience: (0, auth_1.optionalAudience)(cfg.options.audience), // trim / dedupe+sort / errores canónicos
27
36
  clockSkewSeconds: (0, auth_1.normalizeClockSkewSeconds)(cfg.options.clockSkewSeconds),
28
37
  defaultExpiresIn: (0, auth_1.normalizeDefaultExpiresIn)(cfg.options.defaultExpiresIn),
29
38
  };
@@ -43,15 +52,16 @@ class JoseTokenService {
43
52
  async verifyRefreshToken(token) {
44
53
  return this.verifyToken("refresh", "verifyRefreshToken", token);
45
54
  }
55
+ /**
56
+ * Obtiene la fecha de expiración del token.
57
+ *
58
+ * Estrategia:
59
+ * - Primero intenta verificarlo como access.
60
+ * - Si falla con error “retryable”, intenta verificarlo como refresh.
61
+ * - Si está expirado pero se puede leer exp vía decode (sin verificación),
62
+ * retorna esa exp para fines informativos.
63
+ */
46
64
  async getTokenExpiration(token) {
47
- const policy = this.options.getExpirationPolicy ?? "verify-first";
48
- if (policy !== "verify-first") {
49
- throw this.createAuthError({
50
- code: "JWT_ERROR",
51
- message: "Unsupported expiration policy",
52
- meta: { operation: "getTokenExpiration", tokenKind: "unknown" },
53
- });
54
- }
55
65
  const firstTry = await this.tryGetExpirationByVerify("access", token);
56
66
  if (firstTry.ok)
57
67
  return firstTry.value;
@@ -65,14 +75,29 @@ class JoseTokenService {
65
75
  // ---------------------------------------------------------------------------
66
76
  // Internals - ctx + error wrapper
67
77
  // ---------------------------------------------------------------------------
78
+ /**
79
+ * Construye contexto mínimo (no sensible) para mapear errores.
80
+ *
81
+ * ¿Por qué existe?
82
+ * - Hace diagnósticos más fáciles (operación, tokenKind, alg, issuer).
83
+ * - Evita exponer token, claims completos o material criptográfico.
84
+ *
85
+ * Nota:
86
+ * - No incluimos audience en el contexto porque la audience en este plugin
87
+ * es SOLO para emisión y no participa en verificación (por decisión de diseño).
88
+ */
68
89
  ctx(operation, kind) {
69
- const alg = (0, internal_1.getAlgFromKeyMaterial)(this.options.keyMaterial);
70
- // FASE 3: sin audience en meta
71
- return (0, internal_1.buildJoseCtx)(operation, kind, {
90
+ const alg = (0, internal_2.getAlgFromKeyMaterial)(this.keyMaterial);
91
+ return (0, internal_2.buildJoseCtx)(operation, kind, {
72
92
  issuer: this.eff.issuer,
73
93
  alg,
74
94
  });
75
95
  }
96
+ /**
97
+ * Wrapper estándar:
98
+ * - Ejecuta fn
99
+ * - En caso de error, lo traduce a AuthDomainError usando el mapper del plugin
100
+ */
76
101
  async withAuthError(operation, kind, fn) {
77
102
  try {
78
103
  return await fn();
@@ -84,9 +109,12 @@ class JoseTokenService {
84
109
  // ---------------------------------------------------------------------------
85
110
  // Internals - keys
86
111
  // ---------------------------------------------------------------------------
112
+ /**
113
+ * Normaliza/carga llaves de forma lazy y cacheada.
114
+ */
87
115
  getNormalizedKeys() {
88
116
  if (!this.keysPromise) {
89
- this.keysPromise = (0, internal_1.normalizeKeyMaterial)(this.options.keyMaterial);
117
+ this.keysPromise = (0, internal_2.normalizeKeyMaterial)(this.keyMaterial);
90
118
  }
91
119
  return this.keysPromise;
92
120
  }
@@ -95,6 +123,7 @@ class JoseTokenService {
95
123
  // ---------------------------------------------------------------------------
96
124
  async generateToken(kind, operation, props) {
97
125
  return this.withAuthError(operation, kind, async () => {
126
+ // 1) Expiración: usar regla canónica del core (props > default).
98
127
  const expiresInFromProps = (0, auth_1.readExpiresIn)(props.expiresIn);
99
128
  const defaultExpiresIn = kind === "access"
100
129
  ? this.eff.defaultExpiresIn?.accessToken
@@ -104,9 +133,12 @@ class JoseTokenService {
104
133
  defaultExpiresIn,
105
134
  operation,
106
135
  });
107
- const roles = props.user.roles ?? [];
108
- const customClaims = (0, auth_1.readCustomClaims)(props.customClaims) ??
109
- {};
136
+ // 2) Claims estándar
137
+ // - NO emitimos `roles` en el JWT.
138
+ // - Mantenemos customClaims, pero filtramos `permissions`.
139
+ const customClaimsInput = (0, auth_1.readCustomClaims)(props.customClaims) ?? {};
140
+ const customClaims = (0, internal_2.stripSensitiveCustomClaims)(customClaimsInput);
141
+ // 3) Session Id (sid)
110
142
  const sid = (0, auth_1.readSessionId)(props.sessionId);
111
143
  if (!sid) {
112
144
  throw this.createAuthError({
@@ -115,15 +147,25 @@ class JoseTokenService {
115
147
  meta: { operation, tokenKind: kind, field: "sessionId" },
116
148
  });
117
149
  }
150
+ // 4) Firmar
118
151
  const keys = await this.getNormalizedKeys();
119
- const jwt = new jose_1.SignJWT({ roles, customClaims, sid })
152
+ const body = { sid };
153
+ if (Object.keys(customClaims).length > 0) {
154
+ body.customClaims = customClaims;
155
+ }
156
+ const jwt = new jose_1.SignJWT(body)
120
157
  .setProtectedHeader({ alg: keys.alg })
121
158
  .setSubject(props.user.id)
122
159
  .setJti((0, auth_1.createJwtId)())
123
160
  .setIssuedAt()
124
161
  .setExpirationTime(expiresIn);
162
+ // Issuer opcional (si viene configurado y es no-vacío)
125
163
  if (this.eff.issuer)
126
164
  jwt.setIssuer(this.eff.issuer);
165
+ // Audience opcional SOLO para emisión (claim `aud`).
166
+ // Importante: NO se usa en verificación para no volverlo requisito.
167
+ if (this.eff.audience)
168
+ jwt.setAudience(this.eff.audience);
127
169
  return keys.alg === "HS256"
128
170
  ? jwt.sign(keys.secret)
129
171
  : jwt.sign(keys.privateKey);
@@ -133,11 +175,21 @@ class JoseTokenService {
133
175
  return this.withAuthError(operation, kind, async () => {
134
176
  const keys = await this.getNormalizedKeys();
135
177
  const key = keys.alg === "HS256" ? keys.secret : keys.publicKey;
136
- // FASE 2/3: jwtVerify sin audience; clockSkew ya normalizado
178
+ /**
179
+ * Verificación:
180
+ * - issuer: se aplica si está configurado (si no, se omite)
181
+ * - clockTolerance: ya normalizado (clock skew seguro)
182
+ *
183
+ * Nota: NO pasamos `audience` a jwtVerify.
184
+ * - La audiencia en este plugin es SOLO para emisión.
185
+ * - Pasarla aquí convertiría audience en una restricción (“must match”)
186
+ * y rompería compatibilidad con tokens válidos sin `aud`.
187
+ */
137
188
  const result = await (0, jose_1.jwtVerify)(token, key, {
138
189
  issuer: this.eff.issuer,
139
190
  clockTolerance: this.eff.clockSkewSeconds,
140
191
  });
192
+ // Delegar a la regla canónica del core: valida + normaliza payload.
141
193
  return (0, auth_1.normalizeJwtPayload)(result.payload);
142
194
  });
143
195
  }
@@ -147,11 +199,15 @@ class JoseTokenService {
147
199
  return { ok: true, value: (0, auth_1.toDateFromUnixSeconds)(payload.exp) };
148
200
  }
149
201
  catch (err) {
202
+ // Si el error ya es un AuthDomainError, aplicamos reglas de retry/fallback.
150
203
  if (auth_1.AuthDomainError.isAuthError(err)) {
204
+ // Caso especial: si expiró, podemos leer exp por decode (sin verificar)
205
+ // para devolver la fecha de expiración con fines informativos.
151
206
  if (err.code === "TOKEN_EXPIRED") {
152
- const exp = (0, internal_1.tryReadExpByDecode)(token);
153
- if (exp !== null)
207
+ const exp = (0, internal_2.tryReadExpByDecode)(token);
208
+ if (exp !== null) {
154
209
  return { ok: true, value: (0, auth_1.toDateFromUnixSeconds)(exp) };
210
+ }
155
211
  }
156
212
  return {
157
213
  ok: false,
@@ -159,6 +215,7 @@ class JoseTokenService {
159
215
  error: err,
160
216
  };
161
217
  }
218
+ // Si es un error no estándar, lo envolvemos para mantener consistencia.
162
219
  const wrapped = (0, mappers_1.toAuthDomainError)(this.createAuthError, err, this.ctx("getTokenExpiration", kind));
163
220
  return { ok: false, retryable: false, error: wrapped };
164
221
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
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-alpha.7",
4
+ "version": "0.0.1-alpha.9",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "scripts": {
@@ -29,7 +29,7 @@
29
29
  "author": "MLahuasi",
30
30
  "license": "MIT",
31
31
  "dependencies": {
32
- "@jmlq/auth": "^0.0.1-alpha.27",
32
+ "@jmlq/auth": "^0.0.1-alpha.30",
33
33
  "jose": "^6.1.3"
34
34
  },
35
35
  "devDependencies": {
@@ -1,9 +0,0 @@
1
- /**
2
- * Política de expiración del plugin.
3
- * - "verify-first": verifica el token y si no tiene exp, aplica default.
4
- */
5
- export type GetExpirationPolicy = "verify-first";
6
- /**
7
- * Política por defecto del plugin (estándar acordado).
8
- */
9
- export declare const DEFAULT_GET_EXPIRATION_POLICY: GetExpirationPolicy;
@@ -1,7 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DEFAULT_GET_EXPIRATION_POLICY = void 0;
4
- /**
5
- * Política por defecto del plugin (estándar acordado).
6
- */
7
- exports.DEFAULT_GET_EXPIRATION_POLICY = "verify-first";