@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 +120 -48
- package/dist/index.d.ts +1 -1
- package/dist/index.js +27 -21
- package/package.json +3 -2
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?)
|
|
11
|
-
- `KEYCLOAK_CLIENT` — provider token para injetar o cliente Keycloak (
|
|
12
|
-
- `KEYCLOAK_HTTP_INTERCEPTOR` — provider token para injetar o interceptor (
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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: "
|
|
33
|
+
realm: "myrealm",
|
|
33
34
|
credentials: {
|
|
34
|
-
clientId: "
|
|
35
|
-
clientSecret: "
|
|
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
|
-
|
|
45
|
+
### Injeção do cliente
|
|
45
46
|
|
|
46
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
constructor(
|
|
53
|
+
@Inject(KEYCLOAK_CLIENT) private readonly keycloakClient: KeycloakClientInterface,
|
|
54
|
+
) {}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### API do cliente
|
|
52
58
|
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
64
|
-
import {
|
|
65
|
-
import
|
|
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
|
-
|
|
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
|
-
|
|
105
|
+
### Propagação de contexto de log (cascade)
|
|
71
106
|
|
|
72
|
-
|
|
73
|
-
|
|
107
|
+
O cliente lê 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
|
-
|
|
111
|
+
```ts
|
|
112
|
+
import { getContext, runWithContext } from '@adatechnology/logger';
|
|
76
113
|
|
|
77
|
-
|
|
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
|
-
@
|
|
146
|
+
@UseGuards(RolesGuard)
|
|
147
|
+
@Roles("admin")
|
|
89
148
|
adminOnly() {
|
|
90
149
|
return { ok: true };
|
|
91
150
|
}
|
|
92
151
|
|
|
93
152
|
@Get("team")
|
|
94
|
-
@
|
|
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
|
|
161
|
+
O `RolesGuard` extrai roles de `realm_access.roles` e `resource_access[clientId].roles` do JWT.
|
|
102
162
|
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
347
|
-
const
|
|
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
|
|
367
|
-
if (
|
|
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
|
|
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
|
|
379
|
-
this.
|
|
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
|
|
483
|
-
this.
|
|
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.
|
|
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.
|
|
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/
|
|
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": {
|