@adatechnology/auth-keycloak 0.0.7 → 0.1.0

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
@@ -5,34 +5,36 @@ Módulo Keycloak para autenticação de clients e usuários, seguindo o padrão
5
5
  Este pacote fornece um cliente leve para interagir com o Keycloak (obter/refresh de tokens, introspecção, userinfo)
6
6
  e um interceptor opcional. O módulo foi projetado para ser usado junto ao `@adatechnology/http-client`.
7
7
 
8
- Principais exportações
8
+ ### Principais exportações
9
9
 
10
- - `KeycloakModule` — módulo principal. Suporta `KeycloakModule.forRoot(config?)` (padrão dinâmico).
11
- - `KEYCLOAK_CLIENT` — provider token para injetar o cliente Keycloak (use `@Inject(KEYCLOAK_CLIENT)`).
12
- - `KEYCLOAK_HTTP_INTERCEPTOR` — provider token para injetar o interceptor (se necessário).
10
+ - `KeycloakModule` — módulo principal. Suporta `KeycloakModule.forRoot(config?)`.
11
+ - `KEYCLOAK_CLIENT` — provider token para injetar o cliente Keycloak (`@Inject(KEYCLOAK_CLIENT)`).
12
+ - `KEYCLOAK_HTTP_INTERCEPTOR` — provider token para injetar o interceptor (opcional).
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).
15
+ - `KeycloakError` — classe de erro tipada com `statusCode` e `details`.
13
16
 
14
- Instalação
17
+ ### Instalação
15
18
 
16
- Este pacote já declara dependência interna de workspace para `@adatechnology/http-client`. Em um monorepo PNPM/Turbo o pacote é resolvido automaticamente.
17
-
18
- Uso
19
+ ```bash
20
+ # Este pacote já declara dependências de workspace para http-client, logger e cache.
21
+ # Em um monorepo PNPM/Turbo os pacotes são resolvidos automaticamente.
22
+ ```
19
23
 
20
- - Configuração via código (recomendado quando quiser injetar configuração manualmente):
24
+ ### Uso básico
21
25
 
22
26
  ```ts
23
27
  import { Module } from "@nestjs/common";
24
- import { HttpModule } from "@adatechnology/http-client";
25
28
  import { KeycloakModule } from "@adatechnology/auth-keycloak";
26
29
 
27
30
  @Module({
28
31
  imports: [
29
- HttpModule.forRoot({ baseURL: "https://pokeapi.co/api/v2", timeout: 5000 }),
30
32
  KeycloakModule.forRoot({
31
33
  baseUrl: "https://keycloak.example.com",
32
- realm: "BACKEND",
34
+ realm: "myrealm",
33
35
  credentials: {
34
- clientId: "backend-api",
35
- clientSecret: "backend-api-secret",
36
+ clientId: "my-client",
37
+ clientSecret: "my-secret",
36
38
  grantType: "client_credentials",
37
39
  },
38
40
  }),
@@ -41,68 +43,154 @@ import { KeycloakModule } from "@adatechnology/auth-keycloak";
41
43
  export class AppModule {}
42
44
  ```
43
45
 
44
- - Configuração via `ConfigService` / variáveis de ambiente (padrão quando não passar `forRoot`):
46
+ ### Injeção do cliente
45
47
 
46
- As variáveis usadas pelo módulo interno são:
48
+ ```ts
49
+ import { Inject } from '@nestjs/common';
50
+ import { KEYCLOAK_CLIENT } from '@adatechnology/auth-keycloak';
51
+ import type { KeycloakClientInterface } from '@adatechnology/auth-keycloak';
47
52
 
48
- - `KEYCLOAK_BASE_URL` (padrão: `http://localhost:8081`)
49
- - `KEYCLOAK_REALM` (padrão: `BACKEND`)
50
- - `KEYCLOAK_CLIENT_ID` (padrão: `backend-api`)
51
- - `KEYCLOAK_CLIENT_SECRET` (padrão: `backend-api-secret`)
53
+ constructor(
54
+ @Inject(KEYCLOAK_CLIENT) private readonly keycloakClient: KeycloakClientInterface,
55
+ ) {}
56
+ ```
52
57
 
53
- API rápida (via token)
58
+ ### API do cliente
54
59
 
55
- - `KEYCLOAK_CLIENT.getAccessToken()` obtém token com cache e deduplicação de requisições.
56
- - `KEYCLOAK_CLIENT.refreshToken(refreshToken)` — renova token.
57
- - `KEYCLOAK_CLIENT.validateToken(token)` introspecção no Keycloak.
58
- - `KEYCLOAK_CLIENT.getUserInfo(token)` retorna userinfo.
60
+ | Método | Descrição |
61
+ |---|---|
62
+ | `getAccessToken()` | Obtém token com cache automático e deduplicação de requisições |
63
+ | `getTokenWithCredentials({ username, password })` | Login com credenciais (resource-owner password grant) |
64
+ | `refreshToken(refreshToken)` | Renova token e atualiza o cache interno |
65
+ | `validateToken(token)` | Introspecção via endpoint `/token/introspect` |
66
+ | `getUserInfo(token)` | Retorna claims via endpoint `/userinfo` |
67
+ | `clearTokenCache()` | Remove o token do cache (útil para forçar renovação) |
59
68
 
60
- Exemplo de injeção no NestJS:
69
+ ### Cache de token
70
+
71
+ O `KeycloakClient` usa `@adatechnology/cache` para armazenar o access token obtido via `client_credentials`.
72
+ Por padrão é criado um `InMemoryCacheProvider` local. Você pode substituir por Redis ou qualquer implementação
73
+ de `CacheProviderInterface` injetando o provider `CACHE_PROVIDER` no contexto do módulo:
61
74
 
62
75
  ```ts
63
- import { Inject } from '@nestjs/common';
64
- import { KEYCLOAK_CLIENT } from '@adatechnology/auth-keycloak';
65
- import type { KeycloakClientInterface } from '@adatechnology/auth-keycloak';
76
+ import { Module } from "@nestjs/common";
77
+ import { CacheModule } from "@adatechnology/cache";
78
+ import { KeycloakModule } from "@adatechnology/auth-keycloak";
66
79
 
67
- constructor(@Inject(KEYCLOAK_CLIENT) private readonly keycloakClient: KeycloakClientInterface) {}
80
+ @Module({
81
+ imports: [
82
+ // Registra CACHE_PROVIDER como Redis — KeycloakClient o usará automaticamente
83
+ CacheModule.forRoot({
84
+ type: 'redis',
85
+ redis: { host: 'localhost', port: 6379 },
86
+ }),
87
+ KeycloakModule.forRoot({ ... }),
88
+ ],
89
+ })
90
+ export class AppModule {}
68
91
  ```
69
92
 
70
- Notas
93
+ Se `CACHE_PROVIDER` não for registrado no módulo, o `KeycloakClient` cria um `InMemoryCacheProvider`
94
+ interno sem necessidade de configuração adicional.
71
95
 
72
- - Este módulo depende de `@adatechnology/http-client` (provider `HTTP_PROVIDER`) para realizar chamadas HTTP ao Keycloak. Configure o `HttpModule` conforme necessário na aplicação que consome este pacote.
73
- - O interceptor `KeycloakHttpInterceptor` é fornecido caso queira integrar com outras camadas que aceitem interceptors.
96
+ O TTL do cache é derivado do campo `expires_in` do token (com 60 segundos de margem). Você pode
97
+ sobrescrever com a opção `tokenCacheTtl` (em **milissegundos**):
74
98
 
75
- ## Autorização (decorator @Roles)
99
+ ```ts
100
+ KeycloakModule.forRoot({
101
+ ...
102
+ tokenCacheTtl: 60_000, // força TTL de 60 s independente do token
103
+ })
104
+ ```
105
+
106
+ ### Propagação de contexto de log (cascade)
76
107
 
77
- O pacote agora fornece um decorator `@Roles()` e um `RolesGuard` para uso nas rotas do NestJS. Exemplos:
108
+ O cliente o `logContext` do `AsyncLocalStorage` da lib `@adatechnology/logger`. Para que os logs
109
+ de downstream (keycloak → cache) mostrem `className.methodName` da origem correta, use `runWithContext`
110
+ no controller:
111
+
112
+ ```ts
113
+ import { getContext, runWithContext } from '@adatechnology/logger';
114
+
115
+ // No controller
116
+ private withCtx<T>(logContext: object, fn: () => Promise<T>): Promise<T> {
117
+ return runWithContext({ ...(getContext() ?? {}), logContext }, fn);
118
+ }
119
+
120
+ async getToken() {
121
+ const logContext = { className: 'MyController', methodName: 'getToken' };
122
+ return this.withCtx(logContext, () => this.keycloakService.getAccessToken());
123
+ }
124
+ ```
125
+
126
+ Resultado no log:
127
+ ```
128
+ [MyController.getToken][KeycloakClient.getAccessToken] → cache miss → request token
129
+ [MyController.getToken][InMemoryCacheProvider.set] → token cached
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
+
161
+ ### Autorização com @Roles
78
162
 
79
163
  ```ts
80
164
  import { Controller, Get, UseGuards } from "@nestjs/common";
81
- import { Roles } from "@adatechnology/auth-keycloak";
82
- import { RolesGuard } from "@adatechnology/auth-keycloak";
165
+ import { Roles, RolesGuard } from "@adatechnology/auth-keycloak";
83
166
 
84
167
  @Controller("secure")
85
- @UseGuards(RolesGuard)
86
168
  export class SecureController {
169
+ @Get("public")
170
+ @UseGuards(RolesGuard)
171
+ public() {
172
+ return { ok: true };
173
+ }
174
+
87
175
  @Get("admin")
88
- @Roles("admin") // aceita um ou mais roles (OR por padrão)
176
+ @UseGuards(RolesGuard)
177
+ @Roles("admin")
89
178
  adminOnly() {
90
179
  return { ok: true };
91
180
  }
92
181
 
93
182
  @Get("team")
94
- @Roles({ roles: ["manager", "lead"], mode: "all" }) // requer ambos (AND)
183
+ @UseGuards(RolesGuard)
184
+ @Roles({ roles: ["manager", "lead"], mode: "all" }) // AND — requer ambas as roles
95
185
  teamOnly() {
96
186
  return { ok: true };
97
187
  }
98
188
  }
99
189
  ```
100
190
 
101
- O `RolesGuard` extrai roles do payload do JWT (claims `realm_access.roles` e `resource_access[clientId].roles`). Por padrão o decorator verifica ambos (realm e client). Você pode ajustar o comportamento usando as opções `{ type: 'realm'|'client'|'both' }`.
102
-
103
- ## Erros
191
+ O `RolesGuard` extrai roles de `realm_access.roles` e `resource_access[clientId].roles` do JWT.
104
192
 
105
- O pacote exporta `KeycloakError` (classe) que é usada para representar falhas nas chamadas HTTP ao Keycloak. A classe contém `statusCode` e `details` para permitir um tratamento declarativo dos erros na aplicação que consome a biblioteca. Exemplo:
193
+ ### Tratamento de erros
106
194
 
107
195
  ```ts
108
196
  import { KeycloakError } from "@adatechnology/auth-keycloak";
@@ -111,17 +199,31 @@ try {
111
199
  await keycloakClient.getUserInfo(token);
112
200
  } catch (e) {
113
201
  if (e instanceof KeycloakError) {
114
- // tratar problema específico de Keycloak
115
- console.error(e.statusCode, e.details);
202
+ console.error(e.statusCode, e.details, e.keycloakError);
116
203
  }
117
204
  throw e;
118
205
  }
119
206
  ```
120
207
 
121
- Contribuições
208
+ ### Variáveis de ambiente (referência)
209
+
210
+ | Variável | Padrão |
211
+ |---|---|
212
+ | `KEYCLOAK_BASE_URL` | `http://localhost:8081` |
213
+ | `KEYCLOAK_REALM` | `BACKEND` |
214
+ | `KEYCLOAK_CLIENT_ID` | `backend-api` |
215
+ | `KEYCLOAK_CLIENT_SECRET` | `backend-api-secret` |
216
+
217
+ ### Notas
218
+
219
+ - Este módulo depende de `@adatechnology/http-client` para chamadas HTTP ao Keycloak.
220
+ - O interceptor `KeycloakHttpInterceptor` pode ser registrado como `APP_INTERCEPTOR` para integração global.
221
+ - `clearTokenCache()` é assíncrono desde a versão `0.0.7` (retorna `Promise<void>`).
222
+
223
+ ### Contribuições
122
224
 
123
225
  Relate issues/PRs no repositório principal. Mantenha compatibilidade com o padrão usado pelo `HttpModule`.
124
226
 
125
- Licença
227
+ ### Licença
126
228
 
127
229
  MIT
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
  /**
@@ -75,7 +76,7 @@ interface KeycloakClientInterface {
75
76
  /**
76
77
  * Clear the internal access token cache maintained by the client.
77
78
  */
78
- clearTokenCache(): void;
79
+ clearTokenCache(): Promise<void>;
79
80
  }
80
81
  /**
81
82
  * Provider-facing interface type to be used when injecting the keycloak provider token.
@@ -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";
@@ -129,4 +156,4 @@ declare class KeycloakError extends Error {
129
156
  });
130
157
  }
131
158
 
132
- export { KEYCLOAK_CLIENT, KEYCLOAK_CONFIG, KEYCLOAK_HTTP_INTERCEPTOR, KEYCLOAK_PROVIDER, type KeycloakClientInterface, type KeycloakConfig, KeycloakError, KeycloakModule, type KeycloakProviderInterface, type KeycloakTokenResponse, Roles, RolesGuard };
159
+ export { 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 BaseAppError3 = 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 = BaseAppError3;
87
87
  }
88
88
  });
89
89
 
@@ -268,6 +268,7 @@ var require_dist = __commonJS({
268
268
  // src/index.ts
269
269
  var index_exports = {};
270
270
  __export(index_exports, {
271
+ BearerTokenGuard: () => BearerTokenGuard,
271
272
  KEYCLOAK_CLIENT: () => KEYCLOAK_CLIENT,
272
273
  KEYCLOAK_CONFIG: () => KEYCLOAK_CONFIG,
273
274
  KEYCLOAK_HTTP_INTERCEPTOR: () => KEYCLOAK_HTTP_INTERCEPTOR,
@@ -280,15 +281,176 @@ __export(index_exports, {
280
281
  module.exports = __toCommonJS(index_exports);
281
282
 
282
283
  // src/keycloak.module.ts
283
- var import_common5 = require("@nestjs/common");
284
+ var import_common6 = require("@nestjs/common");
284
285
  var import_core2 = require("@nestjs/core");
285
- var import_http_client2 = require("@adatechnology/http-client");
286
- var import_logger2 = require("@adatechnology/logger");
286
+ var import_http_client3 = require("@adatechnology/http-client");
287
+ var import_logger3 = require("@adatechnology/logger");
288
+ var import_cache3 = require("@adatechnology/cache");
287
289
 
288
- // src/keycloak.client.ts
290
+ // src/bearer-token.guard.ts
289
291
  var import_common = require("@nestjs/common");
290
- var import_http_client = require("@adatechnology/http-client");
291
292
  var import_logger = require("@adatechnology/logger");
293
+ var import_http_client = require("@adatechnology/http-client");
294
+ var import_shared = __toESM(require_dist());
295
+
296
+ // src/keycloak.token.ts
297
+ var KEYCLOAK_CONFIG = "KEYCLOAK_CONFIG";
298
+ var KEYCLOAK_CLIENT = "KEYCLOAK_CLIENT";
299
+ var KEYCLOAK_HTTP_INTERCEPTOR = "KEYCLOAK_HTTP_INTERCEPTOR";
300
+ var KEYCLOAK_PROVIDER = "KEYCLOAK_PROVIDER";
301
+
302
+ // package.json
303
+ var package_default = {
304
+ name: "@adatechnology/auth-keycloak",
305
+ version: "0.1.0",
306
+ publishConfig: {
307
+ access: "public"
308
+ },
309
+ main: "dist/index.js",
310
+ module: "dist/index.mjs",
311
+ types: "dist/index.d.ts",
312
+ files: [
313
+ "dist"
314
+ ],
315
+ scripts: {
316
+ build: "rm -rf dist && tsup",
317
+ "build:watch": "tsup --watch",
318
+ check: "tsc -p tsconfig.json --noEmit",
319
+ test: 'echo "no tests"'
320
+ },
321
+ dependencies: {
322
+ "@adatechnology/cache": "workspace:*",
323
+ "@adatechnology/http-client": "workspace:*",
324
+ "@adatechnology/logger": "workspace:*"
325
+ },
326
+ peerDependencies: {
327
+ "@nestjs/common": "^11.0.16",
328
+ "@nestjs/core": "^11"
329
+ },
330
+ devDependencies: {
331
+ "@adatechnology/shared": "workspace:*",
332
+ "@esbuild-plugins/tsconfig-paths": "^0.1.2",
333
+ tsup: "^8.5.1",
334
+ typescript: "^5.2.0"
335
+ }
336
+ };
337
+
338
+ // src/keycloak.constants.ts
339
+ var LIB_NAME = package_default.name;
340
+ var LIB_VERSION = package_default.version;
341
+ var TOKEN_CACHE_KEY = "keycloak:access_token";
342
+ var LOG_CONTEXT = {
343
+ KEYCLOAK_CLIENT: "KeycloakClient",
344
+ BEARER_TOKEN_GUARD: "BearerTokenGuard"
345
+ };
346
+ var HTTP_STATUS = {
347
+ UNAUTHORIZED: 401,
348
+ FORBIDDEN: 403
349
+ };
350
+ var BEARER_ERROR_CODE = {
351
+ MISSING_TOKEN: "UNAUTHORIZED_MISSING_TOKEN",
352
+ KEYCLOAK_NOT_CONFIGURED: "UNAUTHORIZED_KEYCLOAK_NOT_CONFIGURED",
353
+ TOKEN_VALIDATION_FAILED: "UNAUTHORIZED_TOKEN_VALIDATION_FAILED",
354
+ INACTIVE_TOKEN: "UNAUTHORIZED_INACTIVE_TOKEN"
355
+ };
356
+ var ROLES_ERROR_CODE = {
357
+ MISSING_TOKEN: "FORBIDDEN_MISSING_TOKEN",
358
+ INSUFFICIENT_ROLES: "FORBIDDEN_INSUFFICIENT_ROLES"
359
+ };
360
+
361
+ // src/bearer-token.guard.ts
362
+ var BearerTokenGuard = class {
363
+ constructor(keycloakClient, logger) {
364
+ this.keycloakClient = keycloakClient;
365
+ this.logger = logger;
366
+ }
367
+ log(level, message, libMethod, meta) {
368
+ if (!this.logger) return;
369
+ const loggerCtx = (0, import_logger.getContext)();
370
+ const httpCtx = (0, import_http_client.getHttpRequestContext)();
371
+ const logContext = loggerCtx == null ? void 0 : loggerCtx.logContext;
372
+ const requestId = (loggerCtx == null ? void 0 : loggerCtx.requestId) ?? (httpCtx == null ? void 0 : httpCtx.requestId);
373
+ 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;
374
+ const payload = {
375
+ message,
376
+ context: LOG_CONTEXT.BEARER_TOKEN_GUARD,
377
+ lib: LIB_NAME,
378
+ libVersion: LIB_VERSION,
379
+ libMethod,
380
+ source,
381
+ requestId,
382
+ meta
383
+ };
384
+ if (level === "debug") this.logger.debug(payload);
385
+ else if (level === "info") this.logger.info(payload);
386
+ else if (level === "warn") this.logger.warn(payload);
387
+ else if (level === "error") this.logger.error(payload);
388
+ }
389
+ async canActivate(context) {
390
+ var _a, _b;
391
+ const method = "canActivate";
392
+ this.log("debug", `${method} - Start`, method);
393
+ const request = context.switchToHttp().getRequest();
394
+ const authorization = ((_a = request.headers) == null ? void 0 : _a.authorization) ?? ((_b = request.headers) == null ? void 0 : _b.Authorization);
395
+ if (!(authorization == null ? void 0 : authorization.startsWith("Bearer "))) {
396
+ this.log("warn", `${method} - Missing or invalid Authorization header`, method);
397
+ throw new import_shared.BaseAppError({
398
+ message: "Missing or invalid Authorization header",
399
+ status: HTTP_STATUS.UNAUTHORIZED,
400
+ code: BEARER_ERROR_CODE.MISSING_TOKEN,
401
+ context: {}
402
+ });
403
+ }
404
+ if (!this.keycloakClient) {
405
+ this.log("error", `${method} - Keycloak client not configured`, method);
406
+ throw new import_shared.BaseAppError({
407
+ message: "Keycloak client not configured",
408
+ status: HTTP_STATUS.UNAUTHORIZED,
409
+ code: BEARER_ERROR_CODE.KEYCLOAK_NOT_CONFIGURED,
410
+ context: {}
411
+ });
412
+ }
413
+ const token = authorization.slice(7);
414
+ let isValid;
415
+ try {
416
+ isValid = await this.keycloakClient.validateToken(token);
417
+ } catch (err) {
418
+ const detail = err instanceof Error ? err.message : String(err);
419
+ this.log("error", `${method} - Token validation failed`, method, { detail });
420
+ throw new import_shared.BaseAppError({
421
+ message: "Token validation failed",
422
+ status: HTTP_STATUS.UNAUTHORIZED,
423
+ code: BEARER_ERROR_CODE.TOKEN_VALIDATION_FAILED,
424
+ context: { detail }
425
+ });
426
+ }
427
+ if (!isValid) {
428
+ this.log("warn", `${method} - Inactive or expired token`, method);
429
+ throw new import_shared.BaseAppError({
430
+ message: "Inactive or expired token",
431
+ status: HTTP_STATUS.UNAUTHORIZED,
432
+ code: BEARER_ERROR_CODE.INACTIVE_TOKEN,
433
+ context: {}
434
+ });
435
+ }
436
+ this.log("debug", `${method} - Token valid`, method);
437
+ return true;
438
+ }
439
+ };
440
+ BearerTokenGuard = __decorateClass([
441
+ (0, import_common.Injectable)(),
442
+ __decorateParam(0, (0, import_common.Optional)()),
443
+ __decorateParam(0, (0, import_common.Inject)(KEYCLOAK_CLIENT)),
444
+ __decorateParam(1, (0, import_common.Optional)()),
445
+ __decorateParam(1, (0, import_common.Inject)(import_logger.LOGGER_PROVIDER))
446
+ ], BearerTokenGuard);
447
+
448
+ // src/keycloak.client.ts
449
+ var import_common2 = require("@nestjs/common");
450
+ var import_http_client2 = require("@adatechnology/http-client");
451
+ var import_logger2 = require("@adatechnology/logger");
452
+ var import_cache = require("@adatechnology/cache");
453
+ var import_cache2 = require("@adatechnology/cache");
292
454
 
293
455
  // src/errors/keycloak-error.ts
294
456
  var KeycloakError = class _KeycloakError extends Error {
@@ -306,8 +468,6 @@ var KeycloakError = class _KeycloakError extends Error {
306
468
  };
307
469
 
308
470
  // src/keycloak.client.ts
309
- var LIB_NAME = "@adatechnology/auth-keycloak";
310
- var LIB_VERSION = "0.0.2";
311
471
  function extractErrorInfo(err) {
312
472
  var _a, _b, _c, _d, _e;
313
473
  const statusCode = (err == null ? void 0 : err.status) ?? ((_a = err == null ? void 0 : err.response) == null ? void 0 : _a.status);
@@ -333,21 +493,24 @@ function extractErrorInfo(err) {
333
493
  };
334
494
  }
335
495
  var KeycloakClient = class {
336
- constructor(config, httpProvider, logger) {
496
+ constructor(config, httpProvider, logger, cacheProvider) {
337
497
  this.config = config;
338
498
  this.httpProvider = httpProvider;
339
499
  this.logger = logger;
500
+ this.cacheProvider = cacheProvider ?? new import_cache.InMemoryCacheProvider(logger);
340
501
  }
341
- tokenCache = null;
502
+ cacheProvider;
342
503
  tokenPromise = null;
343
504
  log(level, message, libMethod, meta) {
344
505
  if (!this.logger) return;
345
- const httpCtx = (0, import_http_client.getHttpRequestContext)();
346
- const requestId = httpCtx == null ? void 0 : httpCtx.requestId;
347
- const source = (httpCtx == null ? void 0 : httpCtx.className) && (httpCtx == null ? void 0 : httpCtx.methodName) ? `${httpCtx.className}.${httpCtx.methodName}` : void 0;
506
+ const loggerCtx = (0, import_logger2.getContext)();
507
+ const httpCtx = (0, import_http_client2.getHttpRequestContext)();
508
+ const logContext = loggerCtx == null ? void 0 : loggerCtx.logContext;
509
+ const requestId = (loggerCtx == null ? void 0 : loggerCtx.requestId) ?? (httpCtx == null ? void 0 : httpCtx.requestId);
510
+ 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;
348
511
  const payload = {
349
512
  message,
350
- context: "KeycloakClient",
513
+ context: LOG_CONTEXT.KEYCLOAK_CLIENT,
351
514
  lib: LIB_NAME,
352
515
  libVersion: LIB_VERSION,
353
516
  libMethod,
@@ -363,10 +526,10 @@ var KeycloakClient = class {
363
526
  async getAccessToken() {
364
527
  const method = "getAccessToken";
365
528
  this.log("debug", `${method} - Start`, method);
366
- const now = Date.now();
367
- if (this.tokenCache && now < this.tokenCache.expiresAt) {
529
+ const cached = await this.cacheProvider.get(TOKEN_CACHE_KEY);
530
+ if (cached) {
368
531
  this.log("debug", `${method} - Returning cached token`, method);
369
- return this.tokenCache.token;
532
+ return cached;
370
533
  }
371
534
  if (this.tokenPromise) {
372
535
  this.log("debug", `${method} - Waiting for existing token request`, method);
@@ -375,8 +538,8 @@ var KeycloakClient = class {
375
538
  this.tokenPromise = (async () => {
376
539
  try {
377
540
  const tokenResponse = await this.requestToken();
378
- const expiresAt = this.config.tokenCacheTtl ? Date.now() + this.config.tokenCacheTtl : Date.now() + (tokenResponse.expires_in - 60) * 1e3;
379
- this.tokenCache = { token: tokenResponse.access_token, expiresAt };
541
+ const ttlSeconds = this.config.tokenCacheTtl ? Math.floor(this.config.tokenCacheTtl / 1e3) : tokenResponse.expires_in - 60;
542
+ await this.cacheProvider.set(TOKEN_CACHE_KEY, tokenResponse.access_token, ttlSeconds);
380
543
  this.log("debug", `${method} - Token obtained and cached`, method);
381
544
  return tokenResponse.access_token;
382
545
  } finally {
@@ -406,7 +569,7 @@ var KeycloakClient = class {
406
569
  data: body,
407
570
  config: {
408
571
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
409
- logContext: { className: "KeycloakClient", methodName: method }
572
+ logContext: { className: LOG_CONTEXT.KEYCLOAK_CLIENT, methodName: method }
410
573
  }
411
574
  });
412
575
  this.log("info", `${method} - Success for user: ${username}`, method);
@@ -444,7 +607,7 @@ var KeycloakClient = class {
444
607
  data,
445
608
  config: {
446
609
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
447
- logContext: { className: "KeycloakClient", methodName: method }
610
+ logContext: { className: LOG_CONTEXT.KEYCLOAK_CLIENT, methodName: method }
448
611
  }
449
612
  });
450
613
  this.log("debug", `${method} - Success`, method);
@@ -476,11 +639,11 @@ var KeycloakClient = class {
476
639
  data,
477
640
  config: {
478
641
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
479
- logContext: { className: "KeycloakClient", methodName: method }
642
+ logContext: { className: LOG_CONTEXT.KEYCLOAK_CLIENT, methodName: method }
480
643
  }
481
644
  });
482
- const expiresAt = this.config.tokenCacheTtl ? Date.now() + this.config.tokenCacheTtl : Date.now() + (response.data.expires_in - 60) * 1e3;
483
- this.tokenCache = { token: response.data.access_token, expiresAt };
645
+ const ttlSeconds = this.config.tokenCacheTtl ? Math.floor(this.config.tokenCacheTtl / 1e3) : response.data.expires_in - 60;
646
+ await this.cacheProvider.set(TOKEN_CACHE_KEY, response.data.access_token, ttlSeconds);
484
647
  this.log("debug", `${method} - Success`, method);
485
648
  return response.data;
486
649
  } catch (err) {
@@ -510,7 +673,7 @@ var KeycloakClient = class {
510
673
  data,
511
674
  config: {
512
675
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
513
- logContext: { className: "KeycloakClient", methodName: method }
676
+ logContext: { className: LOG_CONTEXT.KEYCLOAK_CLIENT, methodName: method }
514
677
  }
515
678
  });
516
679
  const active = ((_a = response.data) == null ? void 0 : _a.active) === true;
@@ -535,7 +698,7 @@ var KeycloakClient = class {
535
698
  url: userInfoUrl,
536
699
  config: {
537
700
  headers: { Authorization: `Bearer ${token}` },
538
- logContext: { className: "KeycloakClient", methodName: method }
701
+ logContext: { className: LOG_CONTEXT.KEYCLOAK_CLIENT, methodName: method }
539
702
  }
540
703
  });
541
704
  this.log("debug", `${method} - Success`, method);
@@ -550,12 +713,8 @@ var KeycloakClient = class {
550
713
  });
551
714
  }
552
715
  }
553
- clearTokenCache() {
554
- this.tokenCache = null;
555
- }
556
- static maskToken(token, visibleChars = 8) {
557
- if (!token || typeof token !== "string") return "";
558
- return token.length <= visibleChars ? token : `${token.slice(0, visibleChars)}...`;
716
+ async clearTokenCache() {
717
+ await this.cacheProvider.del(TOKEN_CACHE_KEY);
559
718
  }
560
719
  static scopesToString(scopes) {
561
720
  if (!scopes) return "openid profile email";
@@ -563,14 +722,16 @@ var KeycloakClient = class {
563
722
  }
564
723
  };
565
724
  KeycloakClient = __decorateClass([
566
- (0, import_common.Injectable)(),
567
- __decorateParam(1, (0, import_common.Inject)(import_http_client.HTTP_PROVIDER)),
568
- __decorateParam(2, (0, import_common.Optional)()),
569
- __decorateParam(2, (0, import_common.Inject)(import_logger.LOGGER_PROVIDER))
725
+ (0, import_common2.Injectable)(),
726
+ __decorateParam(1, (0, import_common2.Inject)(import_http_client2.HTTP_PROVIDER)),
727
+ __decorateParam(2, (0, import_common2.Optional)()),
728
+ __decorateParam(2, (0, import_common2.Inject)(import_logger2.LOGGER_PROVIDER)),
729
+ __decorateParam(3, (0, import_common2.Optional)()),
730
+ __decorateParam(3, (0, import_common2.Inject)(import_cache2.CACHE_PROVIDER))
570
731
  ], KeycloakClient);
571
732
 
572
733
  // src/keycloak.http.interceptor.ts
573
- var import_common2 = require("@nestjs/common");
734
+ var import_common3 = require("@nestjs/common");
574
735
  var KeycloakHttpInterceptor = class {
575
736
  constructor() {
576
737
  }
@@ -582,15 +743,15 @@ var KeycloakHttpInterceptor = class {
582
743
  }
583
744
  };
584
745
  KeycloakHttpInterceptor = __decorateClass([
585
- (0, import_common2.Injectable)()
746
+ (0, import_common3.Injectable)()
586
747
  ], KeycloakHttpInterceptor);
587
748
 
588
749
  // src/roles.guard.ts
589
- var import_common4 = require("@nestjs/common");
750
+ var import_common5 = require("@nestjs/common");
590
751
  var import_core = require("@nestjs/core");
591
752
 
592
753
  // src/roles.decorator.ts
593
- var import_common3 = require("@nestjs/common");
754
+ var import_common4 = require("@nestjs/common");
594
755
  var ROLES_META_KEY = "roles";
595
756
  function Roles(...args) {
596
757
  let payload;
@@ -604,17 +765,11 @@ function Roles(...args) {
604
765
  }
605
766
  payload.mode = payload.mode ?? "any";
606
767
  payload.type = payload.type ?? "both";
607
- return (0, import_common3.SetMetadata)(ROLES_META_KEY, payload);
768
+ return (0, import_common4.SetMetadata)(ROLES_META_KEY, payload);
608
769
  }
609
770
 
610
- // src/keycloak.token.ts
611
- var KEYCLOAK_CONFIG = "KEYCLOAK_CONFIG";
612
- var KEYCLOAK_CLIENT = "KEYCLOAK_CLIENT";
613
- var KEYCLOAK_HTTP_INTERCEPTOR = "KEYCLOAK_HTTP_INTERCEPTOR";
614
- var KEYCLOAK_PROVIDER = "KEYCLOAK_PROVIDER";
615
-
616
771
  // src/roles.guard.ts
617
- var import_shared = __toESM(require_dist());
772
+ var import_shared2 = __toESM(require_dist());
618
773
  var RolesGuard = class {
619
774
  constructor(reflector, config) {
620
775
  this.reflector = reflector;
@@ -628,10 +783,10 @@ var RolesGuard = class {
628
783
  const authHeader = ((_a = req.headers) == null ? void 0 : _a.authorization) || ((_b = req.headers) == null ? void 0 : _b.Authorization);
629
784
  const token = authHeader ? String(authHeader).split(" ")[1] : (_c = req.query) == null ? void 0 : _c.token;
630
785
  if (!token)
631
- throw new import_shared.BaseAppError({
786
+ throw new import_shared2.BaseAppError({
632
787
  message: "Authorization token not provided",
633
- status: 403,
634
- code: "FORBIDDEN_MISSING_TOKEN",
788
+ status: HTTP_STATUS.FORBIDDEN,
789
+ code: ROLES_ERROR_CODE.MISSING_TOKEN,
635
790
  context: {}
636
791
  });
637
792
  const payload = this.decodeJwtPayload(token);
@@ -658,10 +813,10 @@ var RolesGuard = class {
658
813
  const hasMatch = required.map((r) => availableRoles.has(r));
659
814
  const result = meta.mode === "all" ? hasMatch.every(Boolean) : hasMatch.some(Boolean);
660
815
  if (!result)
661
- throw new import_shared.BaseAppError({
816
+ throw new import_shared2.BaseAppError({
662
817
  message: "Insufficient roles",
663
- status: 403,
664
- code: "FORBIDDEN_INSUFFICIENT_ROLES",
818
+ status: HTTP_STATUS.FORBIDDEN,
819
+ code: ROLES_ERROR_CODE.INSUFFICIENT_ROLES,
665
820
  context: { required }
666
821
  });
667
822
  return true;
@@ -681,10 +836,10 @@ var RolesGuard = class {
681
836
  }
682
837
  };
683
838
  RolesGuard = __decorateClass([
684
- (0, import_common4.Injectable)(),
685
- __decorateParam(0, (0, import_common4.Inject)(import_core.Reflector)),
686
- __decorateParam(1, (0, import_common4.Optional)()),
687
- __decorateParam(1, (0, import_common4.Inject)(KEYCLOAK_CONFIG))
839
+ (0, import_common5.Injectable)(),
840
+ __decorateParam(0, (0, import_common5.Inject)(import_core.Reflector)),
841
+ __decorateParam(1, (0, import_common5.Optional)()),
842
+ __decorateParam(1, (0, import_common5.Inject)(KEYCLOAK_CONFIG))
688
843
  ], RolesGuard);
689
844
 
690
845
  // src/keycloak.module.ts
@@ -694,7 +849,7 @@ var KeycloakModule = class {
694
849
  module: KeycloakModule,
695
850
  global: true,
696
851
  imports: [
697
- import_http_client2.HttpModule.forRoot(
852
+ import_http_client3.HttpModule.forRoot(
698
853
  httpConfig || { baseURL: config.baseUrl, timeout: 5e3 },
699
854
  {
700
855
  logging: {
@@ -711,11 +866,12 @@ var KeycloakModule = class {
711
866
  { provide: KEYCLOAK_CONFIG, useValue: config },
712
867
  {
713
868
  provide: KEYCLOAK_CLIENT,
714
- useFactory: (cfg, httpProvider, logger) => new KeycloakClient(cfg, httpProvider, logger),
869
+ useFactory: (cfg, httpProvider, logger, cacheProvider) => new KeycloakClient(cfg, httpProvider, logger, cacheProvider),
715
870
  inject: [
716
871
  KEYCLOAK_CONFIG,
717
- import_http_client2.HTTP_PROVIDER,
718
- { token: import_logger2.LOGGER_PROVIDER, optional: true }
872
+ import_http_client3.HTTP_PROVIDER,
873
+ { token: import_logger3.LOGGER_PROVIDER, optional: true },
874
+ { token: import_cache3.CACHE_PROVIDER, optional: true }
719
875
  ]
720
876
  },
721
877
  {
@@ -726,7 +882,8 @@ var KeycloakModule = class {
726
882
  provide: KEYCLOAK_HTTP_INTERCEPTOR,
727
883
  useFactory: () => new KeycloakHttpInterceptor()
728
884
  },
729
- RolesGuard
885
+ RolesGuard,
886
+ BearerTokenGuard
730
887
  ],
731
888
  exports: [
732
889
  import_core2.Reflector,
@@ -734,16 +891,18 @@ var KeycloakModule = class {
734
891
  KEYCLOAK_PROVIDER,
735
892
  KEYCLOAK_HTTP_INTERCEPTOR,
736
893
  KEYCLOAK_CONFIG,
737
- RolesGuard
894
+ RolesGuard,
895
+ BearerTokenGuard
738
896
  ]
739
897
  };
740
898
  }
741
899
  };
742
900
  KeycloakModule = __decorateClass([
743
- (0, import_common5.Module)({})
901
+ (0, import_common6.Module)({})
744
902
  ], KeycloakModule);
745
903
  // Annotate the CommonJS export names for ESM import in node:
746
904
  0 && (module.exports = {
905
+ BearerTokenGuard,
747
906
  KEYCLOAK_CLIENT,
748
907
  KEYCLOAK_CONFIG,
749
908
  KEYCLOAK_HTTP_INTERCEPTOR,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adatechnology/auth-keycloak",
3
- "version": "0.0.7",
3
+ "version": "0.1.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -11,8 +11,9 @@
11
11
  "dist"
12
12
  ],
13
13
  "dependencies": {
14
- "@adatechnology/http-client": "0.0.7",
15
- "@adatechnology/logger": "0.0.6"
14
+ "@adatechnology/cache": "0.0.8",
15
+ "@adatechnology/http-client": "0.0.9",
16
+ "@adatechnology/logger": "0.0.7"
16
17
  },
17
18
  "peerDependencies": {
18
19
  "@nestjs/common": "^11.0.16",