@adatechnology/auth-keycloak 0.0.7 → 0.0.8

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,35 @@ 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
+ - `Roles` / `RolesGuard` — decorator e guard para autorização baseada em roles.
14
+ - `KeycloakError` — classe de erro tipada com `statusCode` e `details`.
13
15
 
14
- Instalação
16
+ ### Instalação
15
17
 
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
18
+ ```bash
19
+ # Este pacote já declara dependências de workspace para http-client, logger e cache.
20
+ # Em um monorepo PNPM/Turbo os pacotes são resolvidos automaticamente.
21
+ ```
19
22
 
20
- - Configuração via código (recomendado quando quiser injetar configuração manualmente):
23
+ ### Uso básico
21
24
 
22
25
  ```ts
23
26
  import { Module } from "@nestjs/common";
24
- import { HttpModule } from "@adatechnology/http-client";
25
27
  import { KeycloakModule } from "@adatechnology/auth-keycloak";
26
28
 
27
29
  @Module({
28
30
  imports: [
29
- HttpModule.forRoot({ baseURL: "https://pokeapi.co/api/v2", timeout: 5000 }),
30
31
  KeycloakModule.forRoot({
31
32
  baseUrl: "https://keycloak.example.com",
32
- realm: "BACKEND",
33
+ realm: "myrealm",
33
34
  credentials: {
34
- clientId: "backend-api",
35
- clientSecret: "backend-api-secret",
35
+ clientId: "my-client",
36
+ clientSecret: "my-secret",
36
37
  grantType: "client_credentials",
37
38
  },
38
39
  }),
@@ -41,68 +42,125 @@ import { KeycloakModule } from "@adatechnology/auth-keycloak";
41
42
  export class AppModule {}
42
43
  ```
43
44
 
44
- - Configuração via `ConfigService` / variáveis de ambiente (padrão quando não passar `forRoot`):
45
+ ### Injeção do cliente
45
46
 
46
- As variáveis usadas pelo módulo interno são:
47
+ ```ts
48
+ import { Inject } from '@nestjs/common';
49
+ import { KEYCLOAK_CLIENT } from '@adatechnology/auth-keycloak';
50
+ import type { KeycloakClientInterface } from '@adatechnology/auth-keycloak';
47
51
 
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`)
52
+ constructor(
53
+ @Inject(KEYCLOAK_CLIENT) private readonly keycloakClient: KeycloakClientInterface,
54
+ ) {}
55
+ ```
56
+
57
+ ### API do cliente
52
58
 
53
- API rápida (via token)
59
+ | Método | Descrição |
60
+ |---|---|
61
+ | `getAccessToken()` | Obtém token com cache automático e deduplicação de requisições |
62
+ | `getTokenWithCredentials({ username, password })` | Login com credenciais (resource-owner password grant) |
63
+ | `refreshToken(refreshToken)` | Renova token e atualiza o cache interno |
64
+ | `validateToken(token)` | Introspecção via endpoint `/token/introspect` |
65
+ | `getUserInfo(token)` | Retorna claims via endpoint `/userinfo` |
66
+ | `clearTokenCache()` | Remove o token do cache (útil para forçar renovação) |
54
67
 
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.
68
+ ### Cache de token
59
69
 
60
- Exemplo de injeção no NestJS:
70
+ O `KeycloakClient` usa `@adatechnology/cache` para armazenar o access token obtido via `client_credentials`.
71
+ Por padrão é criado um `InMemoryCacheProvider` local. Você pode substituir por Redis ou qualquer implementação
72
+ de `CacheProviderInterface` injetando o provider `CACHE_PROVIDER` no contexto do módulo:
61
73
 
62
74
  ```ts
63
- import { Inject } from '@nestjs/common';
64
- import { KEYCLOAK_CLIENT } from '@adatechnology/auth-keycloak';
65
- import type { KeycloakClientInterface } from '@adatechnology/auth-keycloak';
75
+ import { Module } from "@nestjs/common";
76
+ import { CacheModule } from "@adatechnology/cache";
77
+ import { KeycloakModule } from "@adatechnology/auth-keycloak";
78
+
79
+ @Module({
80
+ imports: [
81
+ // Registra CACHE_PROVIDER como Redis — KeycloakClient o usará automaticamente
82
+ CacheModule.forRoot({
83
+ type: 'redis',
84
+ redis: { host: 'localhost', port: 6379 },
85
+ }),
86
+ KeycloakModule.forRoot({ ... }),
87
+ ],
88
+ })
89
+ export class AppModule {}
90
+ ```
66
91
 
67
- constructor(@Inject(KEYCLOAK_CLIENT) private readonly keycloakClient: KeycloakClientInterface) {}
92
+ Se `CACHE_PROVIDER` não for registrado no módulo, o `KeycloakClient` cria um `InMemoryCacheProvider`
93
+ interno sem necessidade de configuração adicional.
94
+
95
+ O TTL do cache é derivado do campo `expires_in` do token (com 60 segundos de margem). Você pode
96
+ sobrescrever com a opção `tokenCacheTtl` (em **milissegundos**):
97
+
98
+ ```ts
99
+ KeycloakModule.forRoot({
100
+ ...
101
+ tokenCacheTtl: 60_000, // força TTL de 60 s independente do token
102
+ })
68
103
  ```
69
104
 
70
- Notas
105
+ ### Propagação de contexto de log (cascade)
71
106
 
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.
107
+ O cliente o `logContext` do `AsyncLocalStorage` da lib `@adatechnology/logger`. Para que os logs
108
+ de downstream (keycloak cache) mostrem `className.methodName` da origem correta, use `runWithContext`
109
+ no controller:
74
110
 
75
- ## Autorização (decorator @Roles)
111
+ ```ts
112
+ import { getContext, runWithContext } from '@adatechnology/logger';
76
113
 
77
- O pacote agora fornece um decorator `@Roles()` e um `RolesGuard` para uso nas rotas do NestJS. Exemplos:
114
+ // No controller
115
+ private withCtx<T>(logContext: object, fn: () => Promise<T>): Promise<T> {
116
+ return runWithContext({ ...(getContext() ?? {}), logContext }, fn);
117
+ }
118
+
119
+ async getToken() {
120
+ const logContext = { className: 'MyController', methodName: 'getToken' };
121
+ return this.withCtx(logContext, () => this.keycloakService.getAccessToken());
122
+ }
123
+ ```
124
+
125
+ Resultado no log:
126
+ ```
127
+ [MyController.getToken][KeycloakClient.getAccessToken] → cache miss → request token
128
+ [MyController.getToken][InMemoryCacheProvider.set] → token cached
129
+ ```
130
+
131
+ ### Autorização com @Roles
78
132
 
79
133
  ```ts
80
134
  import { Controller, Get, UseGuards } from "@nestjs/common";
81
- import { Roles } from "@adatechnology/auth-keycloak";
82
- import { RolesGuard } from "@adatechnology/auth-keycloak";
135
+ import { Roles, RolesGuard } from "@adatechnology/auth-keycloak";
83
136
 
84
137
  @Controller("secure")
85
- @UseGuards(RolesGuard)
86
138
  export class SecureController {
139
+ @Get("public")
140
+ @UseGuards(RolesGuard)
141
+ public() {
142
+ return { ok: true };
143
+ }
144
+
87
145
  @Get("admin")
88
- @Roles("admin") // aceita um ou mais roles (OR por padrão)
146
+ @UseGuards(RolesGuard)
147
+ @Roles("admin")
89
148
  adminOnly() {
90
149
  return { ok: true };
91
150
  }
92
151
 
93
152
  @Get("team")
94
- @Roles({ roles: ["manager", "lead"], mode: "all" }) // requer ambos (AND)
153
+ @UseGuards(RolesGuard)
154
+ @Roles({ roles: ["manager", "lead"], mode: "all" }) // AND — requer ambas as roles
95
155
  teamOnly() {
96
156
  return { ok: true };
97
157
  }
98
158
  }
99
159
  ```
100
160
 
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' }`.
161
+ O `RolesGuard` extrai roles de `realm_access.roles` e `resource_access[clientId].roles` do JWT.
102
162
 
103
- ## Erros
104
-
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:
163
+ ### Tratamento de erros
106
164
 
107
165
  ```ts
108
166
  import { KeycloakError } from "@adatechnology/auth-keycloak";
@@ -111,17 +169,31 @@ try {
111
169
  await keycloakClient.getUserInfo(token);
112
170
  } catch (e) {
113
171
  if (e instanceof KeycloakError) {
114
- // tratar problema específico de Keycloak
115
- console.error(e.statusCode, e.details);
172
+ console.error(e.statusCode, e.details, e.keycloakError);
116
173
  }
117
174
  throw e;
118
175
  }
119
176
  ```
120
177
 
121
- Contribuições
178
+ ### Variáveis de ambiente (referência)
179
+
180
+ | Variável | Padrão |
181
+ |---|---|
182
+ | `KEYCLOAK_BASE_URL` | `http://localhost:8081` |
183
+ | `KEYCLOAK_REALM` | `BACKEND` |
184
+ | `KEYCLOAK_CLIENT_ID` | `backend-api` |
185
+ | `KEYCLOAK_CLIENT_SECRET` | `backend-api-secret` |
186
+
187
+ ### Notas
188
+
189
+ - Este módulo depende de `@adatechnology/http-client` para chamadas HTTP ao Keycloak.
190
+ - O interceptor `KeycloakHttpInterceptor` pode ser registrado como `APP_INTERCEPTOR` para integração global.
191
+ - `clearTokenCache()` é assíncrono desde a versão `0.0.7` (retorna `Promise<void>`).
192
+
193
+ ### Contribuições
122
194
 
123
195
  Relate issues/PRs no repositório principal. Mantenha compatibilidade com o padrão usado pelo `HttpModule`.
124
196
 
125
- Licença
197
+ ### Licença
126
198
 
127
199
  MIT
package/dist/index.d.ts CHANGED
@@ -75,7 +75,7 @@ interface KeycloakClientInterface {
75
75
  /**
76
76
  * Clear the internal access token cache maintained by the client.
77
77
  */
78
- clearTokenCache(): void;
78
+ clearTokenCache(): Promise<void>;
79
79
  }
80
80
  /**
81
81
  * Provider-facing interface type to be used when injecting the keycloak provider token.
package/dist/index.js CHANGED
@@ -284,11 +284,14 @@ var import_common5 = require("@nestjs/common");
284
284
  var import_core2 = require("@nestjs/core");
285
285
  var import_http_client2 = require("@adatechnology/http-client");
286
286
  var import_logger2 = require("@adatechnology/logger");
287
+ var import_cache3 = require("@adatechnology/cache");
287
288
 
288
289
  // src/keycloak.client.ts
289
290
  var import_common = require("@nestjs/common");
290
291
  var import_http_client = require("@adatechnology/http-client");
291
292
  var import_logger = require("@adatechnology/logger");
293
+ var import_cache = require("@adatechnology/cache");
294
+ var import_cache2 = require("@adatechnology/cache");
292
295
 
293
296
  // src/errors/keycloak-error.ts
294
297
  var KeycloakError = class _KeycloakError extends Error {
@@ -307,7 +310,8 @@ var KeycloakError = class _KeycloakError extends Error {
307
310
 
308
311
  // src/keycloak.client.ts
309
312
  var LIB_NAME = "@adatechnology/auth-keycloak";
310
- var LIB_VERSION = "0.0.2";
313
+ var LIB_VERSION = "0.0.7";
314
+ var TOKEN_CACHE_KEY = "keycloak:access_token";
311
315
  function extractErrorInfo(err) {
312
316
  var _a, _b, _c, _d, _e;
313
317
  const statusCode = (err == null ? void 0 : err.status) ?? ((_a = err == null ? void 0 : err.response) == null ? void 0 : _a.status);
@@ -333,18 +337,21 @@ function extractErrorInfo(err) {
333
337
  };
334
338
  }
335
339
  var KeycloakClient = class {
336
- constructor(config, httpProvider, logger) {
340
+ constructor(config, httpProvider, logger, cacheProvider) {
337
341
  this.config = config;
338
342
  this.httpProvider = httpProvider;
339
343
  this.logger = logger;
344
+ this.cacheProvider = cacheProvider ?? new import_cache.InMemoryCacheProvider(logger);
340
345
  }
341
- tokenCache = null;
346
+ cacheProvider;
342
347
  tokenPromise = null;
343
348
  log(level, message, libMethod, meta) {
344
349
  if (!this.logger) return;
350
+ const loggerCtx = (0, import_logger.getContext)();
345
351
  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;
352
+ const logContext = loggerCtx == null ? void 0 : loggerCtx.logContext;
353
+ const requestId = (loggerCtx == null ? void 0 : loggerCtx.requestId) ?? (httpCtx == null ? void 0 : httpCtx.requestId);
354
+ 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
355
  const payload = {
349
356
  message,
350
357
  context: "KeycloakClient",
@@ -363,10 +370,10 @@ var KeycloakClient = class {
363
370
  async getAccessToken() {
364
371
  const method = "getAccessToken";
365
372
  this.log("debug", `${method} - Start`, method);
366
- const now = Date.now();
367
- if (this.tokenCache && now < this.tokenCache.expiresAt) {
373
+ const cached = await this.cacheProvider.get(TOKEN_CACHE_KEY);
374
+ if (cached) {
368
375
  this.log("debug", `${method} - Returning cached token`, method);
369
- return this.tokenCache.token;
376
+ return cached;
370
377
  }
371
378
  if (this.tokenPromise) {
372
379
  this.log("debug", `${method} - Waiting for existing token request`, method);
@@ -375,8 +382,8 @@ var KeycloakClient = class {
375
382
  this.tokenPromise = (async () => {
376
383
  try {
377
384
  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 };
385
+ const ttlSeconds = this.config.tokenCacheTtl ? Math.floor(this.config.tokenCacheTtl / 1e3) : tokenResponse.expires_in - 60;
386
+ await this.cacheProvider.set(TOKEN_CACHE_KEY, tokenResponse.access_token, ttlSeconds);
380
387
  this.log("debug", `${method} - Token obtained and cached`, method);
381
388
  return tokenResponse.access_token;
382
389
  } finally {
@@ -479,8 +486,8 @@ var KeycloakClient = class {
479
486
  logContext: { className: "KeycloakClient", methodName: method }
480
487
  }
481
488
  });
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 };
489
+ const ttlSeconds = this.config.tokenCacheTtl ? Math.floor(this.config.tokenCacheTtl / 1e3) : response.data.expires_in - 60;
490
+ await this.cacheProvider.set(TOKEN_CACHE_KEY, response.data.access_token, ttlSeconds);
484
491
  this.log("debug", `${method} - Success`, method);
485
492
  return response.data;
486
493
  } catch (err) {
@@ -550,12 +557,8 @@ var KeycloakClient = class {
550
557
  });
551
558
  }
552
559
  }
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)}...`;
560
+ async clearTokenCache() {
561
+ await this.cacheProvider.del(TOKEN_CACHE_KEY);
559
562
  }
560
563
  static scopesToString(scopes) {
561
564
  if (!scopes) return "openid profile email";
@@ -566,7 +569,9 @@ KeycloakClient = __decorateClass([
566
569
  (0, import_common.Injectable)(),
567
570
  __decorateParam(1, (0, import_common.Inject)(import_http_client.HTTP_PROVIDER)),
568
571
  __decorateParam(2, (0, import_common.Optional)()),
569
- __decorateParam(2, (0, import_common.Inject)(import_logger.LOGGER_PROVIDER))
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))
570
575
  ], KeycloakClient);
571
576
 
572
577
  // src/keycloak.http.interceptor.ts
@@ -711,11 +716,12 @@ var KeycloakModule = class {
711
716
  { provide: KEYCLOAK_CONFIG, useValue: config },
712
717
  {
713
718
  provide: KEYCLOAK_CLIENT,
714
- useFactory: (cfg, httpProvider, logger) => new KeycloakClient(cfg, httpProvider, logger),
719
+ useFactory: (cfg, httpProvider, logger, cacheProvider) => new KeycloakClient(cfg, httpProvider, logger, cacheProvider),
715
720
  inject: [
716
721
  KEYCLOAK_CONFIG,
717
722
  import_http_client2.HTTP_PROVIDER,
718
- { token: import_logger2.LOGGER_PROVIDER, optional: true }
723
+ { token: import_logger2.LOGGER_PROVIDER, optional: true },
724
+ { token: import_cache3.CACHE_PROVIDER, optional: true }
719
725
  ]
720
726
  },
721
727
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adatechnology/auth-keycloak",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -11,7 +11,8 @@
11
11
  "dist"
12
12
  ],
13
13
  "dependencies": {
14
- "@adatechnology/http-client": "0.0.7",
14
+ "@adatechnology/cache": "0.0.7",
15
+ "@adatechnology/http-client": "0.0.8",
15
16
  "@adatechnology/logger": "0.0.6"
16
17
  },
17
18
  "peerDependencies": {