@adatechnology/auth-keycloak 0.0.8 → 0.1.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 CHANGED
@@ -10,7 +10,8 @@ e um interceptor opcional. O módulo foi projetado para ser usado junto ao `@ada
10
10
  - `KeycloakModule` — módulo principal. Suporta `KeycloakModule.forRoot(config?)`.
11
11
  - `KEYCLOAK_CLIENT` — provider token para injetar o cliente Keycloak (`@Inject(KEYCLOAK_CLIENT)`).
12
12
  - `KEYCLOAK_HTTP_INTERCEPTOR` — provider token para injetar o interceptor (opcional).
13
- - `Roles` / `RolesGuard` decorator e guard para autorização baseada em roles.
13
+ - `BearerTokenGuard` guard que valida o token Bearer via introspecção no Keycloak (401 em falha).
14
+ - `Roles` / `RolesGuard` — decorator e guard para autorização baseada em roles (403 em falha).
14
15
  - `KeycloakError` — classe de erro tipada com `statusCode` e `details`.
15
16
 
16
17
  ### Instalação
@@ -128,6 +129,35 @@ Resultado no log:
128
129
  [MyController.getToken][InMemoryCacheProvider.set] → token cached
129
130
  ```
130
131
 
132
+ ### BearerTokenGuard — autenticação B2B via introspecção
133
+
134
+ Valida que o header `Authorization: Bearer <token>` contém um token ativo chamando
135
+ `POST /token/introspect` no Keycloak. Use sempre em conjunto com `RolesGuard` em rotas B2B.
136
+
137
+ ```ts
138
+ import { Controller, Headers, Post, UseGuards } from "@nestjs/common";
139
+ import { BearerTokenGuard, Roles, RolesGuard } from "@adatechnology/auth-keycloak";
140
+
141
+ @Controller("orders")
142
+ export class OrdersController {
143
+ @Post()
144
+ @Roles("manage-requests")
145
+ @UseGuards(BearerTokenGuard, RolesGuard)
146
+ create(@Headers("x-user-id") keycloakId: string) {}
147
+ }
148
+ ```
149
+
150
+ **Por que dois guards em sequência?**
151
+
152
+ | Guard | Mecanismo | HTTP? | Falha |
153
+ |---|---|---|---|
154
+ | `BearerTokenGuard` | `POST /token/introspect` ao Keycloak | Sim | 401 — token inativo/expirado/forjado |
155
+ | `RolesGuard` | Decode local do payload JWT | Não | 403 — permissão insuficiente |
156
+
157
+ O `RolesGuard` sozinho **não é seguro** para autenticação: ele apenas decodifica o payload JWT
158
+ sem verificar a assinatura, o que significa que um token forjado passaria. O `BearerTokenGuard`
159
+ é quem garante autenticidade via introspecção.
160
+
131
161
  ### Autorização com @Roles
132
162
 
133
163
  ```ts
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import * as _nestjs_common from '@nestjs/common';
2
2
  import { DynamicModule, CanActivate, ExecutionContext } from '@nestjs/common';
3
3
  import { AxiosRequestConfig, AxiosInstance } from 'axios';
4
+ import { LoggerProviderInterface } from '@adatechnology/logger';
4
5
  import { Reflector } from '@nestjs/core';
5
6
 
6
7
  /**
@@ -87,6 +88,32 @@ declare class KeycloakModule {
87
88
  static forRoot(config: KeycloakConfig, httpConfig?: AxiosRequestConfig | AxiosInstance): DynamicModule;
88
89
  }
89
90
 
91
+ /**
92
+ * Guard that validates the Bearer token in the Authorization header via
93
+ * Keycloak token introspection (POST /token/introspect).
94
+ *
95
+ * Use together with RolesGuard for B2B (service-to-service) routes that trust
96
+ * an X-User-Id header injected by an upstream authenticated service:
97
+ *
98
+ * @example
99
+ * ```ts
100
+ * @Roles('manage-requests')
101
+ * @UseGuards(BearerTokenGuard, RolesGuard)
102
+ * async create(@Headers('x-user-id') keycloakId: string) {}
103
+ * ```
104
+ *
105
+ * Execution order:
106
+ * 1. BearerTokenGuard — validates token is active (HTTP → Keycloak) → 401 on failure
107
+ * 2. RolesGuard — checks roles from decoded JWT payload (local) → 403 on failure
108
+ */
109
+ declare class BearerTokenGuard implements CanActivate {
110
+ private readonly keycloakClient?;
111
+ private readonly logger?;
112
+ constructor(keycloakClient?: KeycloakClientInterface, logger?: LoggerProviderInterface);
113
+ private log;
114
+ canActivate(context: ExecutionContext): Promise<boolean>;
115
+ }
116
+
90
117
  declare const KEYCLOAK_CONFIG = "KEYCLOAK_CONFIG";
91
118
  declare const KEYCLOAK_CLIENT = "KEYCLOAK_CLIENT";
92
119
  declare const KEYCLOAK_HTTP_INTERCEPTOR = "KEYCLOAK_HTTP_INTERCEPTOR";
@@ -110,6 +137,23 @@ type RolesOptions = {
110
137
  */
111
138
  declare function Roles(...args: Array<string | string[] | RolesOptions>): _nestjs_common.CustomDecorator<string>;
112
139
 
140
+ /**
141
+ * Guard that checks whether the current request has the required roles.
142
+ *
143
+ * Supports two auth paths transparently:
144
+ *
145
+ * 1. **Kong path** (user-facing, preferred):
146
+ * Kong validates the token, removes Authorization, and injects:
147
+ * - `X-User-Id` — keycloak sub
148
+ * - `X-User-Roles` — comma-separated realm roles
149
+ * The guard reads roles from `X-User-Roles` header.
150
+ *
151
+ * 2. **B2B path** (service-to-service, e.g. BFF → API):
152
+ * The caller sends a service account JWT in the Authorization header.
153
+ * The guard decodes the JWT locally and reads `realm_access.roles`.
154
+ *
155
+ * Priority: Kong header → JWT fallback.
156
+ */
113
157
  declare class RolesGuard implements CanActivate {
114
158
  private readonly reflector;
115
159
  private readonly config?;
@@ -118,6 +162,104 @@ declare class RolesGuard implements CanActivate {
118
162
  private decodeJwtPayload;
119
163
  }
120
164
 
165
+ /**
166
+ * B2B Guard — service-to-service routes (e.g. BFF → API, Worker → API).
167
+ *
168
+ * The caller must send a valid service account token (client_credentials)
169
+ * in the Authorization header. Delegates full token validation to
170
+ * BearerTokenGuard (Keycloak introspection).
171
+ *
172
+ * The caller is also expected to forward X-User-Id so the API knows which
173
+ * end-user context the call belongs to.
174
+ *
175
+ * Use for routes that are ONLY called by internal services, not by end users.
176
+ *
177
+ * @example
178
+ * ```ts
179
+ * @Post('internal/notify')
180
+ * @Roles('send-notifications')
181
+ * @UseGuards(B2BGuard, RolesGuard)
182
+ * async notify(@AuthUser() keycloakId: string) { ... }
183
+ * ```
184
+ */
185
+ declare class B2BGuard implements CanActivate {
186
+ private readonly bearerTokenGuard;
187
+ constructor(bearerTokenGuard: BearerTokenGuard);
188
+ canActivate(context: ExecutionContext): Promise<boolean>;
189
+ }
190
+
191
+ /**
192
+ * B2C Guard — user-facing routes via Kong.
193
+ *
194
+ * Kong validated the JWT (JWKS local), removed the Authorization header,
195
+ * and injected X-User-Id + X-User-Roles. This guard simply asserts those
196
+ * headers are present — it does NOT re-validate any token.
197
+ *
198
+ * Use for routes that are ONLY called by end users through Kong.
199
+ *
200
+ * @example
201
+ * ```ts
202
+ * @Get('me')
203
+ * @Roles('user-manager')
204
+ * @UseGuards(B2CGuard, RolesGuard)
205
+ * async getMe(@AuthUser() keycloakId: string) { ... }
206
+ * ```
207
+ */
208
+ declare class B2CGuard implements CanActivate {
209
+ canActivate(context: ExecutionContext): boolean;
210
+ }
211
+
212
+ /**
213
+ * ApiAuthGuard — composite guard for routes that accept both paths.
214
+ *
215
+ * Detects which path the request is coming from and delegates accordingly:
216
+ *
217
+ * - **B2B path** (Authorization header present):
218
+ * Service-to-service call (e.g. BFF → API). Delegates to B2BGuard,
219
+ * which validates the service account token via BearerTokenGuard.
220
+ *
221
+ * - **B2C path** (X-User-Id header present, no Authorization):
222
+ * User call routed by Kong. Delegates to B2CGuard,
223
+ * which asserts Kong identity headers are present.
224
+ *
225
+ * Use when the same route must be reachable both from Kong (users) and
226
+ * from internal services (BFF, Worker). If the route is exclusive to one
227
+ * path, prefer B2BGuard or B2CGuard directly for explicit intent.
228
+ *
229
+ * @example
230
+ * ```ts
231
+ * @Get('me')
232
+ * @Roles('user-manager')
233
+ * @UseGuards(ApiAuthGuard, RolesGuard)
234
+ * async getMe(@AuthUser() keycloakId: string) { ... }
235
+ * ```
236
+ */
237
+ declare class ApiAuthGuard implements CanActivate {
238
+ private readonly b2bGuard;
239
+ private readonly b2cGuard;
240
+ constructor(b2bGuard: B2BGuard, b2cGuard: B2CGuard);
241
+ canActivate(context: ExecutionContext): Promise<boolean>;
242
+ }
243
+
244
+ /**
245
+ * Parameter decorator that extracts the authenticated user's Keycloak ID
246
+ * from the `X-User-Id` header injected by Kong after token validation.
247
+ *
248
+ * In the B2B path (service-to-service), the caller is responsible for
249
+ * forwarding the same header.
250
+ *
251
+ * @example
252
+ * ```ts
253
+ * @Get('me')
254
+ * @Roles('user-manager')
255
+ * @UseGuards(RolesGuard)
256
+ * async getMe(@AuthUser() keycloakId: string) {
257
+ * return this.userService.getUserByKeycloakId(keycloakId);
258
+ * }
259
+ * ```
260
+ */
261
+ declare const AuthUser: (...dataOrPipes: unknown[]) => ParameterDecorator;
262
+
121
263
  declare class KeycloakError extends Error {
122
264
  readonly statusCode?: number;
123
265
  readonly details?: unknown;
@@ -129,4 +271,4 @@ declare class KeycloakError extends Error {
129
271
  });
130
272
  }
131
273
 
132
- export { KEYCLOAK_CLIENT, KEYCLOAK_CONFIG, KEYCLOAK_HTTP_INTERCEPTOR, KEYCLOAK_PROVIDER, type KeycloakClientInterface, type KeycloakConfig, KeycloakError, KeycloakModule, type KeycloakProviderInterface, type KeycloakTokenResponse, Roles, RolesGuard };
274
+ export { ApiAuthGuard, AuthUser, B2BGuard, B2CGuard, BearerTokenGuard, KEYCLOAK_CLIENT, KEYCLOAK_CONFIG, KEYCLOAK_HTTP_INTERCEPTOR, KEYCLOAK_PROVIDER, type KeycloakClientInterface, type KeycloakConfig, KeycloakError, KeycloakModule, type KeycloakProviderInterface, type KeycloakTokenResponse, Roles, RolesGuard };
package/dist/index.js CHANGED
@@ -68,7 +68,7 @@ var require_base_app_error = __commonJS({
68
68
  "use strict";
69
69
  Object.defineProperty(exports2, "__esModule", { value: true });
70
70
  exports2.BaseAppError = void 0;
71
- var BaseAppError2 = class extends Error {
71
+ var BaseAppError5 = class extends Error {
72
72
  code;
73
73
  status;
74
74
  context;
@@ -83,7 +83,7 @@ var require_base_app_error = __commonJS({
83
83
  (_a = capturable.captureStackTrace) == null ? void 0 : _a.call(capturable, this, this.constructor);
84
84
  }
85
85
  };
86
- exports2.BaseAppError = BaseAppError2;
86
+ exports2.BaseAppError = BaseAppError5;
87
87
  }
88
88
  });
89
89
 
@@ -268,6 +268,11 @@ var require_dist = __commonJS({
268
268
  // src/index.ts
269
269
  var index_exports = {};
270
270
  __export(index_exports, {
271
+ ApiAuthGuard: () => ApiAuthGuard,
272
+ AuthUser: () => AuthUser,
273
+ B2BGuard: () => B2BGuard,
274
+ B2CGuard: () => B2CGuard,
275
+ BearerTokenGuard: () => BearerTokenGuard,
271
276
  KEYCLOAK_CLIENT: () => KEYCLOAK_CLIENT,
272
277
  KEYCLOAK_CONFIG: () => KEYCLOAK_CONFIG,
273
278
  KEYCLOAK_HTTP_INTERCEPTOR: () => KEYCLOAK_HTTP_INTERCEPTOR,
@@ -280,16 +285,174 @@ __export(index_exports, {
280
285
  module.exports = __toCommonJS(index_exports);
281
286
 
282
287
  // src/keycloak.module.ts
283
- var import_common5 = require("@nestjs/common");
288
+ var import_common6 = require("@nestjs/common");
284
289
  var import_core2 = require("@nestjs/core");
285
- var import_http_client2 = require("@adatechnology/http-client");
286
- var import_logger2 = require("@adatechnology/logger");
290
+ var import_http_client3 = require("@adatechnology/http-client");
291
+ var import_logger3 = require("@adatechnology/logger");
287
292
  var import_cache3 = require("@adatechnology/cache");
288
293
 
289
- // src/keycloak.client.ts
294
+ // src/bearer-token.guard.ts
290
295
  var import_common = require("@nestjs/common");
291
- var import_http_client = require("@adatechnology/http-client");
292
296
  var import_logger = require("@adatechnology/logger");
297
+ var import_http_client = require("@adatechnology/http-client");
298
+ var import_shared = __toESM(require_dist());
299
+
300
+ // src/keycloak.token.ts
301
+ var KEYCLOAK_CONFIG = "KEYCLOAK_CONFIG";
302
+ var KEYCLOAK_CLIENT = "KEYCLOAK_CLIENT";
303
+ var KEYCLOAK_HTTP_INTERCEPTOR = "KEYCLOAK_HTTP_INTERCEPTOR";
304
+ var KEYCLOAK_PROVIDER = "KEYCLOAK_PROVIDER";
305
+
306
+ // package.json
307
+ var package_default = {
308
+ name: "@adatechnology/auth-keycloak",
309
+ version: "0.1.1",
310
+ publishConfig: {
311
+ access: "public"
312
+ },
313
+ main: "dist/index.js",
314
+ module: "dist/index.mjs",
315
+ types: "dist/index.d.ts",
316
+ files: [
317
+ "dist"
318
+ ],
319
+ scripts: {
320
+ build: "rm -rf dist && tsup",
321
+ "build:watch": "tsup --watch",
322
+ check: "tsc -p tsconfig.json --noEmit",
323
+ test: 'echo "no tests"'
324
+ },
325
+ dependencies: {
326
+ "@adatechnology/cache": "workspace:*",
327
+ "@adatechnology/http-client": "workspace:*",
328
+ "@adatechnology/logger": "workspace:*"
329
+ },
330
+ peerDependencies: {
331
+ "@nestjs/common": "^11.0.16",
332
+ "@nestjs/core": "^11"
333
+ },
334
+ devDependencies: {
335
+ "@adatechnology/shared": "workspace:*",
336
+ "@esbuild-plugins/tsconfig-paths": "^0.1.2",
337
+ tsup: "^8.5.1",
338
+ typescript: "^5.2.0"
339
+ }
340
+ };
341
+
342
+ // src/keycloak.constants.ts
343
+ var LIB_NAME = package_default.name;
344
+ var LIB_VERSION = package_default.version;
345
+ var TOKEN_CACHE_KEY = "keycloak:access_token";
346
+ var LOG_CONTEXT = {
347
+ KEYCLOAK_CLIENT: "KeycloakClient",
348
+ BEARER_TOKEN_GUARD: "BearerTokenGuard"
349
+ };
350
+ var HTTP_STATUS = {
351
+ UNAUTHORIZED: 401,
352
+ FORBIDDEN: 403
353
+ };
354
+ var BEARER_ERROR_CODE = {
355
+ MISSING_TOKEN: "UNAUTHORIZED_MISSING_TOKEN",
356
+ KEYCLOAK_NOT_CONFIGURED: "UNAUTHORIZED_KEYCLOAK_NOT_CONFIGURED",
357
+ TOKEN_VALIDATION_FAILED: "UNAUTHORIZED_TOKEN_VALIDATION_FAILED",
358
+ INACTIVE_TOKEN: "UNAUTHORIZED_INACTIVE_TOKEN"
359
+ };
360
+ var ROLES_ERROR_CODE = {
361
+ MISSING_TOKEN: "FORBIDDEN_MISSING_TOKEN",
362
+ INSUFFICIENT_ROLES: "FORBIDDEN_INSUFFICIENT_ROLES"
363
+ };
364
+
365
+ // src/bearer-token.guard.ts
366
+ var BearerTokenGuard = class {
367
+ constructor(keycloakClient, logger) {
368
+ this.keycloakClient = keycloakClient;
369
+ this.logger = logger;
370
+ }
371
+ log(level, message, libMethod, meta) {
372
+ if (!this.logger) return;
373
+ const loggerCtx = (0, import_logger.getContext)();
374
+ const httpCtx = (0, import_http_client.getHttpRequestContext)();
375
+ const logContext = loggerCtx == null ? void 0 : loggerCtx.logContext;
376
+ const requestId = (loggerCtx == null ? void 0 : loggerCtx.requestId) ?? (httpCtx == null ? void 0 : httpCtx.requestId);
377
+ const source = (logContext == null ? void 0 : logContext.className) && (logContext == null ? void 0 : logContext.methodName) ? `${logContext.className}.${logContext.methodName}` : (httpCtx == null ? void 0 : httpCtx.className) && (httpCtx == null ? void 0 : httpCtx.methodName) ? `${httpCtx.className}.${httpCtx.methodName}` : void 0;
378
+ const payload = {
379
+ message,
380
+ context: LOG_CONTEXT.BEARER_TOKEN_GUARD,
381
+ lib: LIB_NAME,
382
+ libVersion: LIB_VERSION,
383
+ libMethod,
384
+ source,
385
+ requestId,
386
+ meta
387
+ };
388
+ if (level === "debug") this.logger.debug(payload);
389
+ else if (level === "info") this.logger.info(payload);
390
+ else if (level === "warn") this.logger.warn(payload);
391
+ else if (level === "error") this.logger.error(payload);
392
+ }
393
+ async canActivate(context) {
394
+ var _a, _b;
395
+ const method = "canActivate";
396
+ this.log("debug", `${method} - Start`, method);
397
+ const request = context.switchToHttp().getRequest();
398
+ const authorization = ((_a = request.headers) == null ? void 0 : _a.authorization) ?? ((_b = request.headers) == null ? void 0 : _b.Authorization);
399
+ if (!(authorization == null ? void 0 : authorization.startsWith("Bearer "))) {
400
+ this.log("warn", `${method} - Missing or invalid Authorization header`, method);
401
+ throw new import_shared.BaseAppError({
402
+ message: "Missing or invalid Authorization header",
403
+ status: HTTP_STATUS.UNAUTHORIZED,
404
+ code: BEARER_ERROR_CODE.MISSING_TOKEN,
405
+ context: {}
406
+ });
407
+ }
408
+ if (!this.keycloakClient) {
409
+ this.log("error", `${method} - Keycloak client not configured`, method);
410
+ throw new import_shared.BaseAppError({
411
+ message: "Keycloak client not configured",
412
+ status: HTTP_STATUS.UNAUTHORIZED,
413
+ code: BEARER_ERROR_CODE.KEYCLOAK_NOT_CONFIGURED,
414
+ context: {}
415
+ });
416
+ }
417
+ const token = authorization.slice(7);
418
+ let isValid;
419
+ try {
420
+ isValid = await this.keycloakClient.validateToken(token);
421
+ } catch (err) {
422
+ const detail = err instanceof Error ? err.message : String(err);
423
+ this.log("error", `${method} - Token validation failed`, method, { detail });
424
+ throw new import_shared.BaseAppError({
425
+ message: "Token validation failed",
426
+ status: HTTP_STATUS.UNAUTHORIZED,
427
+ code: BEARER_ERROR_CODE.TOKEN_VALIDATION_FAILED,
428
+ context: { detail }
429
+ });
430
+ }
431
+ if (!isValid) {
432
+ this.log("warn", `${method} - Inactive or expired token`, method);
433
+ throw new import_shared.BaseAppError({
434
+ message: "Inactive or expired token",
435
+ status: HTTP_STATUS.UNAUTHORIZED,
436
+ code: BEARER_ERROR_CODE.INACTIVE_TOKEN,
437
+ context: {}
438
+ });
439
+ }
440
+ this.log("debug", `${method} - Token valid`, method);
441
+ return true;
442
+ }
443
+ };
444
+ BearerTokenGuard = __decorateClass([
445
+ (0, import_common.Injectable)(),
446
+ __decorateParam(0, (0, import_common.Optional)()),
447
+ __decorateParam(0, (0, import_common.Inject)(KEYCLOAK_CLIENT)),
448
+ __decorateParam(1, (0, import_common.Optional)()),
449
+ __decorateParam(1, (0, import_common.Inject)(import_logger.LOGGER_PROVIDER))
450
+ ], BearerTokenGuard);
451
+
452
+ // src/keycloak.client.ts
453
+ var import_common2 = require("@nestjs/common");
454
+ var import_http_client2 = require("@adatechnology/http-client");
455
+ var import_logger2 = require("@adatechnology/logger");
293
456
  var import_cache = require("@adatechnology/cache");
294
457
  var import_cache2 = require("@adatechnology/cache");
295
458
 
@@ -309,9 +472,6 @@ var KeycloakError = class _KeycloakError extends Error {
309
472
  };
310
473
 
311
474
  // src/keycloak.client.ts
312
- var LIB_NAME = "@adatechnology/auth-keycloak";
313
- var LIB_VERSION = "0.0.7";
314
- var TOKEN_CACHE_KEY = "keycloak:access_token";
315
475
  function extractErrorInfo(err) {
316
476
  var _a, _b, _c, _d, _e;
317
477
  const statusCode = (err == null ? void 0 : err.status) ?? ((_a = err == null ? void 0 : err.response) == null ? void 0 : _a.status);
@@ -347,14 +507,14 @@ var KeycloakClient = class {
347
507
  tokenPromise = null;
348
508
  log(level, message, libMethod, meta) {
349
509
  if (!this.logger) return;
350
- const loggerCtx = (0, import_logger.getContext)();
351
- const httpCtx = (0, import_http_client.getHttpRequestContext)();
510
+ const loggerCtx = (0, import_logger2.getContext)();
511
+ const httpCtx = (0, import_http_client2.getHttpRequestContext)();
352
512
  const logContext = loggerCtx == null ? void 0 : loggerCtx.logContext;
353
513
  const requestId = (loggerCtx == null ? void 0 : loggerCtx.requestId) ?? (httpCtx == null ? void 0 : httpCtx.requestId);
354
514
  const source = (logContext == null ? void 0 : logContext.className) && (logContext == null ? void 0 : logContext.methodName) ? `${logContext.className}.${logContext.methodName}` : (httpCtx == null ? void 0 : httpCtx.className) && (httpCtx == null ? void 0 : httpCtx.methodName) ? `${httpCtx.className}.${httpCtx.methodName}` : void 0;
355
515
  const payload = {
356
516
  message,
357
- context: "KeycloakClient",
517
+ context: LOG_CONTEXT.KEYCLOAK_CLIENT,
358
518
  lib: LIB_NAME,
359
519
  libVersion: LIB_VERSION,
360
520
  libMethod,
@@ -413,7 +573,7 @@ var KeycloakClient = class {
413
573
  data: body,
414
574
  config: {
415
575
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
416
- logContext: { className: "KeycloakClient", methodName: method }
576
+ logContext: { className: LOG_CONTEXT.KEYCLOAK_CLIENT, methodName: method }
417
577
  }
418
578
  });
419
579
  this.log("info", `${method} - Success for user: ${username}`, method);
@@ -451,7 +611,7 @@ var KeycloakClient = class {
451
611
  data,
452
612
  config: {
453
613
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
454
- logContext: { className: "KeycloakClient", methodName: method }
614
+ logContext: { className: LOG_CONTEXT.KEYCLOAK_CLIENT, methodName: method }
455
615
  }
456
616
  });
457
617
  this.log("debug", `${method} - Success`, method);
@@ -483,7 +643,7 @@ var KeycloakClient = class {
483
643
  data,
484
644
  config: {
485
645
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
486
- logContext: { className: "KeycloakClient", methodName: method }
646
+ logContext: { className: LOG_CONTEXT.KEYCLOAK_CLIENT, methodName: method }
487
647
  }
488
648
  });
489
649
  const ttlSeconds = this.config.tokenCacheTtl ? Math.floor(this.config.tokenCacheTtl / 1e3) : response.data.expires_in - 60;
@@ -517,7 +677,7 @@ var KeycloakClient = class {
517
677
  data,
518
678
  config: {
519
679
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
520
- logContext: { className: "KeycloakClient", methodName: method }
680
+ logContext: { className: LOG_CONTEXT.KEYCLOAK_CLIENT, methodName: method }
521
681
  }
522
682
  });
523
683
  const active = ((_a = response.data) == null ? void 0 : _a.active) === true;
@@ -542,7 +702,7 @@ var KeycloakClient = class {
542
702
  url: userInfoUrl,
543
703
  config: {
544
704
  headers: { Authorization: `Bearer ${token}` },
545
- logContext: { className: "KeycloakClient", methodName: method }
705
+ logContext: { className: LOG_CONTEXT.KEYCLOAK_CLIENT, methodName: method }
546
706
  }
547
707
  });
548
708
  this.log("debug", `${method} - Success`, method);
@@ -566,16 +726,16 @@ var KeycloakClient = class {
566
726
  }
567
727
  };
568
728
  KeycloakClient = __decorateClass([
569
- (0, import_common.Injectable)(),
570
- __decorateParam(1, (0, import_common.Inject)(import_http_client.HTTP_PROVIDER)),
571
- __decorateParam(2, (0, import_common.Optional)()),
572
- __decorateParam(2, (0, import_common.Inject)(import_logger.LOGGER_PROVIDER)),
573
- __decorateParam(3, (0, import_common.Optional)()),
574
- __decorateParam(3, (0, import_common.Inject)(import_cache2.CACHE_PROVIDER))
729
+ (0, import_common2.Injectable)(),
730
+ __decorateParam(1, (0, import_common2.Inject)(import_http_client2.HTTP_PROVIDER)),
731
+ __decorateParam(2, (0, import_common2.Optional)()),
732
+ __decorateParam(2, (0, import_common2.Inject)(import_logger2.LOGGER_PROVIDER)),
733
+ __decorateParam(3, (0, import_common2.Optional)()),
734
+ __decorateParam(3, (0, import_common2.Inject)(import_cache2.CACHE_PROVIDER))
575
735
  ], KeycloakClient);
576
736
 
577
737
  // src/keycloak.http.interceptor.ts
578
- var import_common2 = require("@nestjs/common");
738
+ var import_common3 = require("@nestjs/common");
579
739
  var KeycloakHttpInterceptor = class {
580
740
  constructor() {
581
741
  }
@@ -587,15 +747,15 @@ var KeycloakHttpInterceptor = class {
587
747
  }
588
748
  };
589
749
  KeycloakHttpInterceptor = __decorateClass([
590
- (0, import_common2.Injectable)()
750
+ (0, import_common3.Injectable)()
591
751
  ], KeycloakHttpInterceptor);
592
752
 
593
753
  // src/roles.guard.ts
594
- var import_common4 = require("@nestjs/common");
754
+ var import_common5 = require("@nestjs/common");
595
755
  var import_core = require("@nestjs/core");
596
756
 
597
757
  // src/roles.decorator.ts
598
- var import_common3 = require("@nestjs/common");
758
+ var import_common4 = require("@nestjs/common");
599
759
  var ROLES_META_KEY = "roles";
600
760
  function Roles(...args) {
601
761
  let payload;
@@ -609,66 +769,65 @@ function Roles(...args) {
609
769
  }
610
770
  payload.mode = payload.mode ?? "any";
611
771
  payload.type = payload.type ?? "both";
612
- return (0, import_common3.SetMetadata)(ROLES_META_KEY, payload);
772
+ return (0, import_common4.SetMetadata)(ROLES_META_KEY, payload);
613
773
  }
614
774
 
615
- // src/keycloak.token.ts
616
- var KEYCLOAK_CONFIG = "KEYCLOAK_CONFIG";
617
- var KEYCLOAK_CLIENT = "KEYCLOAK_CLIENT";
618
- var KEYCLOAK_HTTP_INTERCEPTOR = "KEYCLOAK_HTTP_INTERCEPTOR";
619
- var KEYCLOAK_PROVIDER = "KEYCLOAK_PROVIDER";
620
-
621
775
  // src/roles.guard.ts
622
- var import_shared = __toESM(require_dist());
776
+ var import_shared2 = __toESM(require_dist());
623
777
  var RolesGuard = class {
624
778
  constructor(reflector, config) {
625
779
  this.reflector = reflector;
626
780
  this.config = config;
627
781
  }
628
782
  canActivate(context) {
629
- var _a, _b, _c, _d, _e, _f, _g, _h;
783
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
630
784
  const meta = this.reflector.get(ROLES_META_KEY, context.getHandler()) || this.reflector.get(ROLES_META_KEY, context.getClass());
631
785
  if (!meta || !meta.roles || meta.roles.length === 0) return true;
632
786
  const req = context.switchToHttp().getRequest();
633
- const authHeader = ((_a = req.headers) == null ? void 0 : _a.authorization) || ((_b = req.headers) == null ? void 0 : _b.Authorization);
634
- const token = authHeader ? String(authHeader).split(" ")[1] : (_c = req.query) == null ? void 0 : _c.token;
635
- if (!token)
636
- throw new import_shared.BaseAppError({
637
- message: "Authorization token not provided",
638
- status: 403,
639
- code: "FORBIDDEN_MISSING_TOKEN",
640
- context: {}
641
- });
642
- const payload = this.decodeJwtPayload(token);
787
+ const required = meta.roles;
643
788
  const availableRoles = /* @__PURE__ */ new Set();
644
- if (((_d = payload == null ? void 0 : payload.realm_access) == null ? void 0 : _d.roles) && Array.isArray(payload.realm_access.roles)) {
645
- payload.realm_access.roles.forEach((r) => availableRoles.add(r));
646
- }
647
- const clientId = (_f = (_e = this.config) == null ? void 0 : _e.credentials) == null ? void 0 : _f.clientId;
648
- if (clientId && ((_h = (_g = payload == null ? void 0 : payload.resource_access) == null ? void 0 : _g[clientId]) == null ? void 0 : _h.roles)) {
649
- payload.resource_access[clientId].roles.forEach(
650
- (r) => availableRoles.add(r)
651
- );
652
- }
653
- if (meta.type === "both" && (payload == null ? void 0 : payload.resource_access)) {
654
- Object.values(payload.resource_access).forEach((entry) => {
655
- if ((entry == null ? void 0 : entry.roles) && Array.isArray(entry.roles)) {
656
- entry.roles.forEach(
657
- (r) => availableRoles.add(r)
658
- );
659
- }
660
- });
789
+ const kongRolesHeader = ((_a = req.headers) == null ? void 0 : _a["x-user-roles"]) || ((_b = req.headers) == null ? void 0 : _b["X-User-Roles"]);
790
+ if (kongRolesHeader) {
791
+ kongRolesHeader.split(",").map((r) => r.trim()).filter(Boolean).forEach((r) => availableRoles.add(r));
792
+ } else {
793
+ const authHeader = ((_c = req.headers) == null ? void 0 : _c.authorization) || ((_d = req.headers) == null ? void 0 : _d.Authorization);
794
+ const token = authHeader ? String(authHeader).split(" ")[1] : (_e = req.query) == null ? void 0 : _e.token;
795
+ if (!token) {
796
+ throw new import_shared2.BaseAppError({
797
+ message: "Authorization token not provided",
798
+ status: HTTP_STATUS.FORBIDDEN,
799
+ code: ROLES_ERROR_CODE.MISSING_TOKEN,
800
+ context: {}
801
+ });
802
+ }
803
+ const payload = this.decodeJwtPayload(token);
804
+ if (((_f = payload == null ? void 0 : payload.realm_access) == null ? void 0 : _f.roles) && Array.isArray(payload.realm_access.roles)) {
805
+ payload.realm_access.roles.forEach((r) => availableRoles.add(r));
806
+ }
807
+ const clientId = (_h = (_g = this.config) == null ? void 0 : _g.credentials) == null ? void 0 : _h.clientId;
808
+ if (clientId && ((_j = (_i = payload == null ? void 0 : payload.resource_access) == null ? void 0 : _i[clientId]) == null ? void 0 : _j.roles)) {
809
+ payload.resource_access[clientId].roles.forEach(
810
+ (r) => availableRoles.add(r)
811
+ );
812
+ }
813
+ if (meta.type === "both" && (payload == null ? void 0 : payload.resource_access)) {
814
+ Object.values(payload.resource_access).forEach((entry) => {
815
+ if ((entry == null ? void 0 : entry.roles) && Array.isArray(entry.roles)) {
816
+ entry.roles.forEach((r) => availableRoles.add(r));
817
+ }
818
+ });
819
+ }
661
820
  }
662
- const required = meta.roles || [];
663
821
  const hasMatch = required.map((r) => availableRoles.has(r));
664
822
  const result = meta.mode === "all" ? hasMatch.every(Boolean) : hasMatch.some(Boolean);
665
- if (!result)
666
- throw new import_shared.BaseAppError({
823
+ if (!result) {
824
+ throw new import_shared2.BaseAppError({
667
825
  message: "Insufficient roles",
668
- status: 403,
669
- code: "FORBIDDEN_INSUFFICIENT_ROLES",
826
+ status: HTTP_STATUS.FORBIDDEN,
827
+ code: ROLES_ERROR_CODE.INSUFFICIENT_ROLES,
670
828
  context: { required }
671
829
  });
830
+ }
672
831
  return true;
673
832
  }
674
833
  decodeJwtPayload(token) {
@@ -680,16 +839,16 @@ var RolesGuard = class {
680
839
  if (!BufferCtor) return {};
681
840
  const decoded = BufferCtor.from(payload, "base64").toString("utf8");
682
841
  return JSON.parse(decoded);
683
- } catch (e) {
842
+ } catch {
684
843
  return {};
685
844
  }
686
845
  }
687
846
  };
688
847
  RolesGuard = __decorateClass([
689
- (0, import_common4.Injectable)(),
690
- __decorateParam(0, (0, import_common4.Inject)(import_core.Reflector)),
691
- __decorateParam(1, (0, import_common4.Optional)()),
692
- __decorateParam(1, (0, import_common4.Inject)(KEYCLOAK_CONFIG))
848
+ (0, import_common5.Injectable)(),
849
+ __decorateParam(0, (0, import_common5.Inject)(import_core.Reflector)),
850
+ __decorateParam(1, (0, import_common5.Optional)()),
851
+ __decorateParam(1, (0, import_common5.Inject)(KEYCLOAK_CONFIG))
693
852
  ], RolesGuard);
694
853
 
695
854
  // src/keycloak.module.ts
@@ -699,7 +858,7 @@ var KeycloakModule = class {
699
858
  module: KeycloakModule,
700
859
  global: true,
701
860
  imports: [
702
- import_http_client2.HttpModule.forRoot(
861
+ import_http_client3.HttpModule.forRoot(
703
862
  httpConfig || { baseURL: config.baseUrl, timeout: 5e3 },
704
863
  {
705
864
  logging: {
@@ -719,8 +878,8 @@ var KeycloakModule = class {
719
878
  useFactory: (cfg, httpProvider, logger, cacheProvider) => new KeycloakClient(cfg, httpProvider, logger, cacheProvider),
720
879
  inject: [
721
880
  KEYCLOAK_CONFIG,
722
- import_http_client2.HTTP_PROVIDER,
723
- { token: import_logger2.LOGGER_PROVIDER, optional: true },
881
+ import_http_client3.HTTP_PROVIDER,
882
+ { token: import_logger3.LOGGER_PROVIDER, optional: true },
724
883
  { token: import_cache3.CACHE_PROVIDER, optional: true }
725
884
  ]
726
885
  },
@@ -732,7 +891,8 @@ var KeycloakModule = class {
732
891
  provide: KEYCLOAK_HTTP_INTERCEPTOR,
733
892
  useFactory: () => new KeycloakHttpInterceptor()
734
893
  },
735
- RolesGuard
894
+ RolesGuard,
895
+ BearerTokenGuard
736
896
  ],
737
897
  exports: [
738
898
  import_core2.Reflector,
@@ -740,16 +900,99 @@ var KeycloakModule = class {
740
900
  KEYCLOAK_PROVIDER,
741
901
  KEYCLOAK_HTTP_INTERCEPTOR,
742
902
  KEYCLOAK_CONFIG,
743
- RolesGuard
903
+ RolesGuard,
904
+ BearerTokenGuard
744
905
  ]
745
906
  };
746
907
  }
747
908
  };
748
909
  KeycloakModule = __decorateClass([
749
- (0, import_common5.Module)({})
910
+ (0, import_common6.Module)({})
750
911
  ], KeycloakModule);
912
+
913
+ // src/b2b.guard.ts
914
+ var import_common7 = require("@nestjs/common");
915
+ var B2BGuard = class {
916
+ constructor(bearerTokenGuard) {
917
+ this.bearerTokenGuard = bearerTokenGuard;
918
+ }
919
+ canActivate(context) {
920
+ return Promise.resolve(this.bearerTokenGuard.canActivate(context));
921
+ }
922
+ };
923
+ B2BGuard = __decorateClass([
924
+ (0, import_common7.Injectable)()
925
+ ], B2BGuard);
926
+
927
+ // src/b2c.guard.ts
928
+ var import_common8 = require("@nestjs/common");
929
+ var import_shared3 = __toESM(require_dist());
930
+ var B2CGuard = class {
931
+ canActivate(context) {
932
+ var _a, _b;
933
+ const request = context.switchToHttp().getRequest();
934
+ const userId = ((_a = request.headers) == null ? void 0 : _a["x-user-id"]) ?? ((_b = request.headers) == null ? void 0 : _b["X-User-Id"]);
935
+ if (userId) return true;
936
+ throw new import_shared3.BaseAppError({
937
+ message: "Missing Kong identity headers (X-User-Id). Route requires user authentication via Kong.",
938
+ status: HTTP_STATUS.UNAUTHORIZED,
939
+ code: BEARER_ERROR_CODE.MISSING_TOKEN,
940
+ context: {}
941
+ });
942
+ }
943
+ };
944
+ B2CGuard = __decorateClass([
945
+ (0, import_common8.Injectable)()
946
+ ], B2CGuard);
947
+
948
+ // src/api-auth.guard.ts
949
+ var import_common9 = require("@nestjs/common");
950
+ var import_shared4 = __toESM(require_dist());
951
+ var ApiAuthGuard = class {
952
+ constructor(b2bGuard, b2cGuard) {
953
+ this.b2bGuard = b2bGuard;
954
+ this.b2cGuard = b2cGuard;
955
+ }
956
+ async canActivate(context) {
957
+ var _a, _b, _c, _d;
958
+ const request = context.switchToHttp().getRequest();
959
+ const authHeader = ((_a = request.headers) == null ? void 0 : _a.authorization) ?? ((_b = request.headers) == null ? void 0 : _b.Authorization);
960
+ if (authHeader == null ? void 0 : authHeader.toLowerCase().startsWith("bearer ")) {
961
+ return this.b2bGuard.canActivate(context);
962
+ }
963
+ const userId = ((_c = request.headers) == null ? void 0 : _c["x-user-id"]) ?? ((_d = request.headers) == null ? void 0 : _d["X-User-Id"]);
964
+ if (userId) {
965
+ return this.b2cGuard.canActivate(context);
966
+ }
967
+ throw new import_shared4.BaseAppError({
968
+ message: "Unauthorized: missing Authorization header (B2B) or Kong identity headers (B2C)",
969
+ status: HTTP_STATUS.UNAUTHORIZED,
970
+ code: BEARER_ERROR_CODE.MISSING_TOKEN,
971
+ context: {}
972
+ });
973
+ }
974
+ };
975
+ ApiAuthGuard = __decorateClass([
976
+ (0, import_common9.Injectable)()
977
+ ], ApiAuthGuard);
978
+
979
+ // src/auth-user.decorator.ts
980
+ var import_common10 = require("@nestjs/common");
981
+ var AuthUser = (0, import_common10.createParamDecorator)(
982
+ (_data, ctx) => {
983
+ var _a, _b;
984
+ const request = ctx.switchToHttp().getRequest();
985
+ const raw = ((_a = request.headers) == null ? void 0 : _a["x-user-id"]) ?? ((_b = request.headers) == null ? void 0 : _b["X-User-Id"]);
986
+ return Array.isArray(raw) ? raw[0] : raw ?? "";
987
+ }
988
+ );
751
989
  // Annotate the CommonJS export names for ESM import in node:
752
990
  0 && (module.exports = {
991
+ ApiAuthGuard,
992
+ AuthUser,
993
+ B2BGuard,
994
+ B2CGuard,
995
+ BearerTokenGuard,
753
996
  KEYCLOAK_CLIENT,
754
997
  KEYCLOAK_CONFIG,
755
998
  KEYCLOAK_HTTP_INTERCEPTOR,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adatechnology/auth-keycloak",
3
- "version": "0.0.8",
3
+ "version": "0.1.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -11,9 +11,9 @@
11
11
  "dist"
12
12
  ],
13
13
  "dependencies": {
14
- "@adatechnology/cache": "0.0.7",
15
- "@adatechnology/http-client": "0.0.8",
16
- "@adatechnology/logger": "0.0.6"
14
+ "@adatechnology/cache": "0.0.8",
15
+ "@adatechnology/http-client": "0.0.9",
16
+ "@adatechnology/logger": "0.0.7"
17
17
  },
18
18
  "peerDependencies": {
19
19
  "@nestjs/common": "^11.0.16",