@adatechnology/auth-keycloak 0.0.2 → 0.0.6

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
@@ -72,6 +72,52 @@ Notas
72
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
73
  - O interceptor `KeycloakHttpInterceptor` é fornecido caso queira integrar com outras camadas que aceitem interceptors.
74
74
 
75
+ ## Autorização (decorator @Roles)
76
+
77
+ O pacote agora fornece um decorator `@Roles()` e um `RolesGuard` para uso nas rotas do NestJS. Exemplos:
78
+
79
+ ```ts
80
+ import { Controller, Get, UseGuards } from "@nestjs/common";
81
+ import { Roles } from "@adatechnology/auth-keycloak";
82
+ import { RolesGuard } from "@adatechnology/auth-keycloak";
83
+
84
+ @Controller("secure")
85
+ @UseGuards(RolesGuard)
86
+ export class SecureController {
87
+ @Get("admin")
88
+ @Roles("admin") // aceita um ou mais roles (OR por padrão)
89
+ adminOnly() {
90
+ return { ok: true };
91
+ }
92
+
93
+ @Get("team")
94
+ @Roles({ roles: ["manager", "lead"], mode: "all" }) // requer ambos (AND)
95
+ teamOnly() {
96
+ return { ok: true };
97
+ }
98
+ }
99
+ ```
100
+
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
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:
106
+
107
+ ```ts
108
+ import { KeycloakError } from "@adatechnology/auth-keycloak";
109
+
110
+ try {
111
+ await keycloakClient.getUserInfo(token);
112
+ } catch (e) {
113
+ if (e instanceof KeycloakError) {
114
+ // tratar problema específico de Keycloak
115
+ console.error(e.statusCode, e.details);
116
+ }
117
+ throw e;
118
+ }
119
+ ```
120
+
75
121
  Contribuições
76
122
 
77
123
  Relate issues/PRs no repositório principal. Mantenha compatibilidade com o padrão usado pelo `HttpModule`.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
- import { DynamicModule } from '@nestjs/common';
1
+ import * as _nestjs_common from '@nestjs/common';
2
+ import { DynamicModule, CanActivate, ExecutionContext } from '@nestjs/common';
2
3
  import { AxiosRequestConfig, AxiosInstance } from 'axios';
4
+ import { Reflector } from '@nestjs/core';
3
5
 
4
6
  /**
5
7
  * Keycloak token response
@@ -10,7 +12,7 @@ interface KeycloakTokenResponse {
10
12
  refresh_expires_in: number;
11
13
  refresh_token: string;
12
14
  token_type: string;
13
- 'not-before-policy': number;
15
+ "not-before-policy": number;
14
16
  session_state: string;
15
17
  scope: string;
16
18
  }
@@ -22,7 +24,7 @@ interface KeycloakCredentials {
22
24
  clientSecret: string;
23
25
  username?: string;
24
26
  password?: string;
25
- grantType: 'client_credentials' | 'password';
27
+ grantType: "client_credentials" | "password";
26
28
  }
27
29
  /**
28
30
  * Keycloak configuration
@@ -31,6 +33,16 @@ interface KeycloakConfig {
31
33
  baseUrl: string;
32
34
  realm: string;
33
35
  credentials: KeycloakCredentials;
36
+ /**
37
+ * Optional scopes to request when fetching tokens. Can be a space-separated string or array of scopes.
38
+ * Defaults to ['openid', 'profile', 'email'] when omitted.
39
+ */
40
+ scopes?: string | string[];
41
+ /**
42
+ * Optional token cache TTL in milliseconds. If provided, KeycloakClient will use this value to
43
+ * determine how long to cache the access token instead of deriving TTL from the token's expires_in.
44
+ */
45
+ tokenCacheTtl?: number;
34
46
  }
35
47
  /**
36
48
  * Keycloak client interface
@@ -51,7 +63,7 @@ interface KeycloakClientInterface {
51
63
  /**
52
64
  * Get user info
53
65
  */
54
- getUserInfo(token: string): Promise<any>;
66
+ getUserInfo(token: string): Promise<Record<string, unknown>>;
55
67
  }
56
68
 
57
69
  declare class KeycloakModule {
@@ -62,4 +74,41 @@ declare const KEYCLOAK_CONFIG = "KEYCLOAK_CONFIG";
62
74
  declare const KEYCLOAK_CLIENT = "KEYCLOAK_CLIENT";
63
75
  declare const KEYCLOAK_HTTP_INTERCEPTOR = "KEYCLOAK_HTTP_INTERCEPTOR";
64
76
 
65
- export { KEYCLOAK_CLIENT, KEYCLOAK_CONFIG, KEYCLOAK_HTTP_INTERCEPTOR, type KeycloakClientInterface, type KeycloakConfig, KeycloakModule, type KeycloakTokenResponse };
77
+ type RolesMode = "any" | "all";
78
+ type RolesType = "realm" | "client" | "both";
79
+ type RolesOptions = {
80
+ roles: string[];
81
+ mode?: RolesMode;
82
+ type?: RolesType;
83
+ };
84
+ /**
85
+ * Decorator to declare required roles for a route or controller.
86
+ * Accepts either a list of strings or a single options object.
87
+ * Examples:
88
+ * @Roles('admin')
89
+ * @Roles('admin','editor')
90
+ * @Roles(['admin','editor'])
91
+ * @Roles({ roles: ['a','b'], mode: 'all', type: 'client' })
92
+ */
93
+ declare function Roles(...args: Array<string | string[] | RolesOptions>): _nestjs_common.CustomDecorator<string>;
94
+
95
+ declare class RolesGuard implements CanActivate {
96
+ private readonly reflector;
97
+ private readonly config?;
98
+ constructor(reflector: Reflector, config?: KeycloakConfig);
99
+ canActivate(context: ExecutionContext): boolean | Promise<boolean>;
100
+ private decodeJwtPayload;
101
+ }
102
+
103
+ declare class KeycloakError extends Error {
104
+ readonly statusCode?: number;
105
+ readonly details?: unknown;
106
+ readonly keycloakError?: string;
107
+ constructor(message: string, opts?: {
108
+ statusCode?: number;
109
+ details?: unknown;
110
+ keycloakError?: string;
111
+ });
112
+ }
113
+
114
+ export { KEYCLOAK_CLIENT, KEYCLOAK_CONFIG, KEYCLOAK_HTTP_INTERCEPTOR, type KeycloakClientInterface, type KeycloakConfig, KeycloakError, KeycloakModule, type KeycloakTokenResponse, Roles, RolesGuard };
package/dist/index.js CHANGED
@@ -1,7 +1,12 @@
1
+ var __create = Object.create;
1
2
  var __defProp = Object.defineProperty;
2
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
4
6
  var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __commonJS = (cb, mod) => function __require() {
8
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
9
+ };
5
10
  var __export = (target, all) => {
6
11
  for (var name in all)
7
12
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -14,6 +19,14 @@ var __copyProps = (to, from, except, desc) => {
14
19
  }
15
20
  return to;
16
21
  };
22
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
23
+ // If the importer is in node compatibility mode or this is not an ESM
24
+ // file that has been converted to a CommonJS file using a Babel-
25
+ // compatible transform (i.e. "__esModule" has not been set), then set
26
+ // "default" to the CommonJS "module.exports" for node compatibility.
27
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
28
+ mod
29
+ ));
17
30
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
18
31
  var __decorateClass = (decorators, target, key, kind) => {
19
32
  var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
@@ -25,41 +38,345 @@ var __decorateClass = (decorators, target, key, kind) => {
25
38
  };
26
39
  var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
27
40
 
41
+ // ../shared/dist/types.js
42
+ var require_types = __commonJS({
43
+ "../shared/dist/types.js"(exports2) {
44
+ "use strict";
45
+ Object.defineProperty(exports2, "__esModule", { value: true });
46
+ }
47
+ });
48
+
49
+ // ../shared/dist/utils.js
50
+ var require_utils = __commonJS({
51
+ "../shared/dist/utils.js"(exports2) {
52
+ "use strict";
53
+ Object.defineProperty(exports2, "__esModule", { value: true });
54
+ exports2.noop = noop;
55
+ exports2.prefixWith = prefixWith;
56
+ function noop() {
57
+ return void 0;
58
+ }
59
+ function prefixWith(prefix, value) {
60
+ return `${prefix}-${value}`;
61
+ }
62
+ }
63
+ });
64
+
65
+ // ../shared/dist/errors/base-app-error.js
66
+ var require_base_app_error = __commonJS({
67
+ "../shared/dist/errors/base-app-error.js"(exports2) {
68
+ "use strict";
69
+ Object.defineProperty(exports2, "__esModule", { value: true });
70
+ exports2.BaseAppError = void 0;
71
+ var BaseAppError2 = class extends Error {
72
+ code;
73
+ status;
74
+ context;
75
+ constructor(params) {
76
+ var _a;
77
+ super(params.message);
78
+ this.name = new.target.name;
79
+ this.status = params.status;
80
+ this.code = params.code;
81
+ this.context = params.context;
82
+ const capturable = Error;
83
+ (_a = capturable.captureStackTrace) == null ? void 0 : _a.call(capturable, this, this.constructor);
84
+ }
85
+ };
86
+ exports2.BaseAppError = BaseAppError2;
87
+ }
88
+ });
89
+
90
+ // ../shared/dist/errors/errors.constants.js
91
+ var require_errors_constants = __commonJS({
92
+ "../shared/dist/errors/errors.constants.js"(exports2) {
93
+ "use strict";
94
+ Object.defineProperty(exports2, "__esModule", { value: true });
95
+ exports2.SHARED_INTERNAL_FRAME_RE = exports2.SHARED_ERROR_MESSAGES = exports2.SHARED_ERRORS = void 0;
96
+ exports2.SHARED_ERRORS = {
97
+ DEFAULT_STATUS: 502,
98
+ INTERNAL_STATUS: 500
99
+ };
100
+ exports2.SHARED_ERROR_MESSAGES = {
101
+ UPSTREAM_ERROR: "Upstream error",
102
+ NO_RESPONSE: "No response from upstream service",
103
+ UNEXPECTED_ERROR: "Unexpected error",
104
+ MAPPING_FAILURE: "Error mapping failure"
105
+ };
106
+ exports2.SHARED_INTERNAL_FRAME_RE = /node_modules|internal|\(internal|axios|packages\/http|@adatechnology\/http-client/;
107
+ }
108
+ });
109
+
110
+ // ../shared/dist/errors/error-mapper.service.js
111
+ var require_error_mapper_service = __commonJS({
112
+ "../shared/dist/errors/error-mapper.service.js"(exports2) {
113
+ "use strict";
114
+ var __decorate = exports2 && exports2.__decorate || function(decorators, target, key, desc) {
115
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
116
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
117
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
118
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
119
+ };
120
+ Object.defineProperty(exports2, "__esModule", { value: true });
121
+ exports2.ErrorMapperService = void 0;
122
+ var common_1 = require("@nestjs/common");
123
+ var base_app_error_1 = require_base_app_error();
124
+ var errors_constants_1 = require_errors_constants();
125
+ var ErrorMapperService = class ErrorMapperService {
126
+ /**
127
+ * Map an upstream/internal error to a BaseAppError with normalized fields.
128
+ * Keeps a small context to help tracing origin without leaking secrets.
129
+ */
130
+ mapUpstreamError(err) {
131
+ if (err instanceof base_app_error_1.BaseAppError)
132
+ return err;
133
+ try {
134
+ const obj = err ?? void 0;
135
+ const context = {};
136
+ if (obj && typeof obj.stack === "string") {
137
+ const frames = this.parseStack(obj.stack);
138
+ if (frames.length) {
139
+ context.stack = frames;
140
+ const origin = frames.find((f) => !this.isInternalFrame(f.file));
141
+ if (origin)
142
+ context.origin = origin;
143
+ }
144
+ }
145
+ if (obj && typeof obj.config === "object" && obj.config !== null) {
146
+ const cfg = obj.config;
147
+ context.url = typeof cfg.url === "string" ? cfg.url : typeof cfg.baseURL === "string" ? cfg.baseURL : void 0;
148
+ context.method = typeof cfg.method === "string" ? cfg.method : void 0;
149
+ }
150
+ if (obj && typeof obj.response === "object" && obj.response !== null) {
151
+ const resp = obj.response;
152
+ const status = typeof resp.status === "number" ? resp.status : errors_constants_1.SHARED_ERRORS.DEFAULT_STATUS;
153
+ const data = resp.data;
154
+ const message = data && typeof data.message === "string" ? data.message : typeof obj.message === "string" ? obj.message : errors_constants_1.SHARED_ERROR_MESSAGES.UPSTREAM_ERROR;
155
+ const code = typeof obj.code === "string" ? obj.code : void 0;
156
+ return new base_app_error_1.BaseAppError({
157
+ message,
158
+ status,
159
+ code,
160
+ context
161
+ });
162
+ }
163
+ if (obj && typeof obj.request === "object") {
164
+ const code = typeof obj.code === "string" ? obj.code : void 0;
165
+ return new base_app_error_1.BaseAppError({
166
+ message: errors_constants_1.SHARED_ERROR_MESSAGES.NO_RESPONSE,
167
+ status: errors_constants_1.SHARED_ERRORS.DEFAULT_STATUS,
168
+ code,
169
+ context
170
+ });
171
+ }
172
+ return new base_app_error_1.BaseAppError({
173
+ message: obj && typeof obj.message === "string" ? obj.message : errors_constants_1.SHARED_ERROR_MESSAGES.UNEXPECTED_ERROR,
174
+ status: errors_constants_1.SHARED_ERRORS.INTERNAL_STATUS,
175
+ code: typeof (obj == null ? void 0 : obj.code) === "string" ? obj.code : void 0,
176
+ context
177
+ });
178
+ } catch (e) {
179
+ return new base_app_error_1.BaseAppError({
180
+ message: errors_constants_1.SHARED_ERROR_MESSAGES.MAPPING_FAILURE,
181
+ status: errors_constants_1.SHARED_ERRORS.INTERNAL_STATUS,
182
+ context: { original: String(err) }
183
+ });
184
+ }
185
+ }
186
+ parseStack(stack) {
187
+ const lines = stack.split("\n").map((l) => l.trim()).filter(Boolean);
188
+ const frames = [];
189
+ const re = /^at\s+(?:(.*?)\s+\()?(.*?):(\d+):(\d+)\)?$/;
190
+ for (const line of lines) {
191
+ const m = re.exec(line);
192
+ if (m) {
193
+ const fn = m[1] || void 0;
194
+ const file = m[2];
195
+ const lineNum = parseInt(m[3], 10);
196
+ const colNum = parseInt(m[4], 10);
197
+ frames.push({ fn, file, line: lineNum, column: colNum });
198
+ }
199
+ }
200
+ return frames;
201
+ }
202
+ isInternalFrame(file) {
203
+ if (!file)
204
+ return false;
205
+ return errors_constants_1.SHARED_INTERNAL_FRAME_RE.test(file);
206
+ }
207
+ };
208
+ exports2.ErrorMapperService = ErrorMapperService;
209
+ exports2.ErrorMapperService = ErrorMapperService = __decorate([
210
+ (0, common_1.Injectable)()
211
+ ], ErrorMapperService);
212
+ }
213
+ });
214
+
215
+ // ../shared/dist/errors/index.js
216
+ var require_errors = __commonJS({
217
+ "../shared/dist/errors/index.js"(exports2) {
218
+ "use strict";
219
+ var __createBinding = exports2 && exports2.__createBinding || (Object.create ? (function(o, m, k, k2) {
220
+ if (k2 === void 0) k2 = k;
221
+ var desc = Object.getOwnPropertyDescriptor(m, k);
222
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
223
+ desc = { enumerable: true, get: function() {
224
+ return m[k];
225
+ } };
226
+ }
227
+ Object.defineProperty(o, k2, desc);
228
+ }) : (function(o, m, k, k2) {
229
+ if (k2 === void 0) k2 = k;
230
+ o[k2] = m[k];
231
+ }));
232
+ var __exportStar = exports2 && exports2.__exportStar || function(m, exports3) {
233
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports3, p)) __createBinding(exports3, m, p);
234
+ };
235
+ Object.defineProperty(exports2, "__esModule", { value: true });
236
+ __exportStar(require_base_app_error(), exports2);
237
+ __exportStar(require_error_mapper_service(), exports2);
238
+ }
239
+ });
240
+
241
+ // ../shared/dist/index.js
242
+ var require_dist = __commonJS({
243
+ "../shared/dist/index.js"(exports2) {
244
+ "use strict";
245
+ var __createBinding = exports2 && exports2.__createBinding || (Object.create ? (function(o, m, k, k2) {
246
+ if (k2 === void 0) k2 = k;
247
+ var desc = Object.getOwnPropertyDescriptor(m, k);
248
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
249
+ desc = { enumerable: true, get: function() {
250
+ return m[k];
251
+ } };
252
+ }
253
+ Object.defineProperty(o, k2, desc);
254
+ }) : (function(o, m, k, k2) {
255
+ if (k2 === void 0) k2 = k;
256
+ o[k2] = m[k];
257
+ }));
258
+ var __exportStar = exports2 && exports2.__exportStar || function(m, exports3) {
259
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports3, p)) __createBinding(exports3, m, p);
260
+ };
261
+ Object.defineProperty(exports2, "__esModule", { value: true });
262
+ __exportStar(require_types(), exports2);
263
+ __exportStar(require_utils(), exports2);
264
+ __exportStar(require_errors(), exports2);
265
+ }
266
+ });
267
+
28
268
  // src/index.ts
29
269
  var index_exports = {};
30
270
  __export(index_exports, {
31
271
  KEYCLOAK_CLIENT: () => KEYCLOAK_CLIENT,
32
272
  KEYCLOAK_CONFIG: () => KEYCLOAK_CONFIG,
33
273
  KEYCLOAK_HTTP_INTERCEPTOR: () => KEYCLOAK_HTTP_INTERCEPTOR,
34
- KeycloakModule: () => KeycloakModule
274
+ KeycloakError: () => KeycloakError,
275
+ KeycloakModule: () => KeycloakModule,
276
+ Roles: () => Roles,
277
+ RolesGuard: () => RolesGuard
35
278
  });
36
279
  module.exports = __toCommonJS(index_exports);
37
280
 
38
281
  // src/keycloak.module.ts
39
- var import_common3 = require("@nestjs/common");
282
+ var import_common5 = require("@nestjs/common");
283
+ var import_core2 = require("@nestjs/core");
40
284
  var import_http_client2 = require("@adatechnology/http-client");
285
+ var import_logger2 = require("@adatechnology/logger");
41
286
 
42
287
  // src/keycloak.client.ts
43
288
  var import_common = require("@nestjs/common");
44
289
  var import_http_client = require("@adatechnology/http-client");
290
+ var import_logger = require("@adatechnology/logger");
291
+
292
+ // src/errors/keycloak-error.ts
293
+ var KeycloakError = class _KeycloakError extends Error {
294
+ statusCode;
295
+ details;
296
+ keycloakError;
297
+ constructor(message, opts) {
298
+ super(message);
299
+ this.name = "KeycloakError";
300
+ this.statusCode = opts == null ? void 0 : opts.statusCode;
301
+ this.details = opts == null ? void 0 : opts.details;
302
+ this.keycloakError = opts == null ? void 0 : opts.keycloakError;
303
+ Object.setPrototypeOf(this, _KeycloakError.prototype);
304
+ }
305
+ };
306
+
307
+ // src/keycloak.client.ts
308
+ var LIB_NAME = "@adatechnology/auth-keycloak";
309
+ var LIB_VERSION = "0.0.2";
310
+ function extractErrorInfo(err) {
311
+ var _a, _b, _c, _d, _e;
312
+ const statusCode = (err == null ? void 0 : err.status) ?? ((_a = err == null ? void 0 : err.response) == null ? void 0 : _a.status);
313
+ const details = ((_b = err == null ? void 0 : err.response) == null ? void 0 : _b.data) ?? ((_c = err == null ? void 0 : err.context) == null ? void 0 : _c.data) ?? (err == null ? void 0 : err.context) ?? (err == null ? void 0 : err.details);
314
+ const errorCode = (err == null ? void 0 : err.code) ?? ((_e = (_d = err == null ? void 0 : err.response) == null ? void 0 : _d.data) == null ? void 0 : _e.error);
315
+ let keycloakError = void 0;
316
+ if (details && typeof details === "object" && details !== null) {
317
+ const raw = details.error;
318
+ if (typeof raw === "string") {
319
+ keycloakError = raw;
320
+ } else if (raw) {
321
+ try {
322
+ keycloakError = JSON.stringify(raw);
323
+ } catch {
324
+ keycloakError = String(raw);
325
+ }
326
+ }
327
+ }
328
+ return {
329
+ statusCode,
330
+ details: details ?? (err == null ? void 0 : err.message),
331
+ keycloakError: keycloakError ?? (errorCode ? `NETWORK_ERROR_${String(errorCode)}` : void 0)
332
+ };
333
+ }
45
334
  var KeycloakClient = class {
46
- constructor(config, httpProvider) {
335
+ constructor(config, httpProvider, logger) {
47
336
  this.config = config;
48
337
  this.httpProvider = httpProvider;
338
+ this.logger = logger;
49
339
  }
50
340
  tokenCache = null;
51
341
  tokenPromise = null;
342
+ log(level, message, libMethod, meta) {
343
+ if (!this.logger) return;
344
+ const httpCtx = (0, import_http_client.getHttpRequestContext)();
345
+ const requestId = httpCtx == null ? void 0 : httpCtx.requestId;
346
+ const source = (httpCtx == null ? void 0 : httpCtx.className) && (httpCtx == null ? void 0 : httpCtx.methodName) ? `${httpCtx.className}.${httpCtx.methodName}` : void 0;
347
+ const payload = {
348
+ message,
349
+ context: "KeycloakClient",
350
+ lib: LIB_NAME,
351
+ libVersion: LIB_VERSION,
352
+ libMethod,
353
+ source,
354
+ requestId,
355
+ meta
356
+ };
357
+ if (level === "debug") this.logger.debug(payload);
358
+ else if (level === "info") this.logger.info(payload);
359
+ else if (level === "warn") this.logger.warn(payload);
360
+ else if (level === "error") this.logger.error(payload);
361
+ }
52
362
  async getAccessToken() {
363
+ const method = "getAccessToken";
364
+ this.log("debug", `${method} - Start`, method);
53
365
  const now = Date.now();
54
366
  if (this.tokenCache && now < this.tokenCache.expiresAt) {
367
+ this.log("debug", `${method} - Returning cached token`, method);
55
368
  return this.tokenCache.token;
56
369
  }
57
- if (this.tokenPromise) return this.tokenPromise;
370
+ if (this.tokenPromise) {
371
+ this.log("debug", `${method} - Waiting for existing token request`, method);
372
+ return this.tokenPromise;
373
+ }
58
374
  this.tokenPromise = (async () => {
59
375
  try {
60
376
  const tokenResponse = await this.requestToken();
61
- const expiresAt = Date.now() + (tokenResponse.expires_in - 60) * 1e3;
377
+ const expiresAt = this.config.tokenCacheTtl ? Date.now() + this.config.tokenCacheTtl : Date.now() + (tokenResponse.expires_in - 60) * 1e3;
62
378
  this.tokenCache = { token: tokenResponse.access_token, expiresAt };
379
+ this.log("debug", `${method} - Token obtained and cached`, method);
63
380
  return tokenResponse.access_token;
64
381
  } finally {
65
382
  this.tokenPromise = null;
@@ -67,22 +384,45 @@ var KeycloakClient = class {
67
384
  })();
68
385
  return this.tokenPromise;
69
386
  }
70
- async getTokenWithCredentials(username, password) {
387
+ async getTokenWithCredentials(params) {
388
+ const method = "getTokenWithCredentials";
389
+ const { username } = params;
390
+ this.log("info", `${method} - Start for user: ${username}`, method);
391
+ const { password } = params;
71
392
  const tokenUrl = `${this.config.baseUrl}/realms/${this.config.realm}/protocol/openid-connect/token`;
72
- const data = new URLSearchParams();
73
- data.append("client_id", this.config.credentials.clientId);
74
- data.append("grant_type", "password");
75
- data.append("username", username);
76
- data.append("password", password);
393
+ const body = new URLSearchParams();
394
+ body.append("client_id", this.config.credentials.clientId);
395
+ body.append("grant_type", "password");
396
+ body.append("username", username);
397
+ body.append("password", password);
77
398
  if (this.config.credentials.clientSecret) {
78
- data.append("client_secret", this.config.credentials.clientSecret);
399
+ body.append("client_secret", this.config.credentials.clientSecret);
400
+ }
401
+ body.append("scope", KeycloakClient.scopesToString(this.config.scopes));
402
+ try {
403
+ const response = await this.httpProvider.post({
404
+ url: tokenUrl,
405
+ data: body,
406
+ config: {
407
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
408
+ logContext: { className: "KeycloakClient", methodName: method }
409
+ }
410
+ });
411
+ this.log("info", `${method} - Success for user: ${username}`, method);
412
+ return response.data;
413
+ } catch (err) {
414
+ const { statusCode, details, keycloakError } = extractErrorInfo(err);
415
+ this.log("error", `${method} - Failed for user: ${username}`, method, { statusCode, keycloakError });
416
+ throw new KeycloakError("Failed to obtain token with credentials", {
417
+ statusCode,
418
+ details,
419
+ keycloakError
420
+ });
79
421
  }
80
- const response = await this.httpProvider.post(tokenUrl, data, {
81
- headers: { "Content-Type": "application/x-www-form-urlencoded" }
82
- });
83
- return response.data;
84
422
  }
85
423
  async requestToken() {
424
+ const method = "requestToken";
425
+ this.log("debug", `${method} - Start`, method);
86
426
  const tokenUrl = `${this.config.baseUrl}/realms/${this.config.realm}/protocol/openid-connect/token`;
87
427
  const data = new URLSearchParams();
88
428
  data.append("client_id", this.config.credentials.clientId);
@@ -94,18 +434,33 @@ var KeycloakClient = class {
94
434
  if (this.config.credentials.username && this.config.credentials.password) {
95
435
  data.append("username", this.config.credentials.username);
96
436
  data.append("password", this.config.credentials.password);
437
+ data.append("scope", KeycloakClient.scopesToString(this.config.scopes));
97
438
  }
98
439
  }
99
- const response = await this.httpProvider.post(
100
- tokenUrl,
101
- data,
102
- {
103
- headers: { "Content-Type": "application/x-www-form-urlencoded" }
104
- }
105
- );
106
- return response.data;
440
+ try {
441
+ const response = await this.httpProvider.post({
442
+ url: tokenUrl,
443
+ data,
444
+ config: {
445
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
446
+ logContext: { className: "KeycloakClient", methodName: method }
447
+ }
448
+ });
449
+ this.log("debug", `${method} - Success`, method);
450
+ return response.data;
451
+ } catch (err) {
452
+ const { statusCode, details, keycloakError } = extractErrorInfo(err);
453
+ this.log("error", `${method} - Failed`, method, { statusCode, keycloakError });
454
+ throw new KeycloakError("Failed to request token", {
455
+ statusCode,
456
+ details,
457
+ keycloakError
458
+ });
459
+ }
107
460
  }
108
461
  async refreshToken(refreshToken) {
462
+ const method = "refreshToken";
463
+ this.log("debug", `${method} - Start`, method);
109
464
  const tokenUrl = `${this.config.baseUrl}/realms/${this.config.realm}/protocol/openid-connect/token`;
110
465
  const data = new URLSearchParams();
111
466
  data.append("client_id", this.config.credentials.clientId);
@@ -114,18 +469,33 @@ var KeycloakClient = class {
114
469
  if (this.config.credentials.clientSecret) {
115
470
  data.append("client_secret", this.config.credentials.clientSecret);
116
471
  }
117
- const response = await this.httpProvider.post(
118
- tokenUrl,
119
- data,
120
- {
121
- headers: { "Content-Type": "application/x-www-form-urlencoded" }
122
- }
123
- );
124
- const expiresAt = Date.now() + (response.data.expires_in - 60) * 1e3;
125
- this.tokenCache = { token: response.data.access_token, expiresAt };
126
- return response.data;
472
+ try {
473
+ const response = await this.httpProvider.post({
474
+ url: tokenUrl,
475
+ data,
476
+ config: {
477
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
478
+ logContext: { className: "KeycloakClient", methodName: method }
479
+ }
480
+ });
481
+ const expiresAt = this.config.tokenCacheTtl ? Date.now() + this.config.tokenCacheTtl : Date.now() + (response.data.expires_in - 60) * 1e3;
482
+ this.tokenCache = { token: response.data.access_token, expiresAt };
483
+ this.log("debug", `${method} - Success`, method);
484
+ return response.data;
485
+ } catch (err) {
486
+ const { statusCode, details, keycloakError } = extractErrorInfo(err);
487
+ this.log("error", `${method} - Failed`, method, { statusCode, keycloakError });
488
+ throw new KeycloakError("Failed to refresh token", {
489
+ statusCode,
490
+ details,
491
+ keycloakError
492
+ });
493
+ }
127
494
  }
128
495
  async validateToken(token) {
496
+ var _a;
497
+ const method = "validateToken";
498
+ this.log("debug", `${method} - Start`, method);
129
499
  try {
130
500
  const introspectUrl = `${this.config.baseUrl}/realms/${this.config.realm}/protocol/openid-connect/token/introspect`;
131
501
  const data = new URLSearchParams();
@@ -134,20 +504,50 @@ var KeycloakClient = class {
134
504
  if (this.config.credentials.clientSecret) {
135
505
  data.append("client_secret", this.config.credentials.clientSecret);
136
506
  }
137
- const response = await this.httpProvider.post(introspectUrl, data, {
138
- headers: { "Content-Type": "application/x-www-form-urlencoded" }
507
+ const response = await this.httpProvider.post({
508
+ url: introspectUrl,
509
+ data,
510
+ config: {
511
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
512
+ logContext: { className: "KeycloakClient", methodName: method }
513
+ }
139
514
  });
140
- return response.data.active === true;
515
+ const active = ((_a = response.data) == null ? void 0 : _a.active) === true;
516
+ this.log("debug", `${method} - Success (Active: ${active})`, method);
517
+ return active;
141
518
  } catch (error) {
142
- return false;
519
+ const { statusCode, details, keycloakError } = extractErrorInfo(error);
520
+ this.log("error", `${method} - Failed`, method, { statusCode, keycloakError });
521
+ throw new KeycloakError("Token introspection failed", {
522
+ statusCode,
523
+ details,
524
+ keycloakError
525
+ });
143
526
  }
144
527
  }
145
528
  async getUserInfo(token) {
529
+ const method = "getUserInfo";
530
+ this.log("debug", `${method} - Start`, method);
146
531
  const userInfoUrl = `${this.config.baseUrl}/realms/${this.config.realm}/protocol/openid-connect/userinfo`;
147
- const response = await this.httpProvider.get(userInfoUrl, {
148
- headers: { Authorization: `Bearer ${token}` }
149
- });
150
- return response.data;
532
+ try {
533
+ const response = await this.httpProvider.get({
534
+ url: userInfoUrl,
535
+ config: {
536
+ headers: { Authorization: `Bearer ${token}` },
537
+ logContext: { className: "KeycloakClient", methodName: method }
538
+ }
539
+ });
540
+ this.log("debug", `${method} - Success`, method);
541
+ return response.data;
542
+ } catch (err) {
543
+ const { statusCode, details, keycloakError } = extractErrorInfo(err);
544
+ this.log("error", `${method} - Failed`, method, { statusCode, keycloakError });
545
+ throw new KeycloakError("Failed to retrieve userinfo", {
546
+ statusCode,
547
+ details,
548
+ keycloakError
549
+ });
550
+ }
151
551
  }
152
552
  clearTokenCache() {
153
553
  this.tokenCache = null;
@@ -156,68 +556,188 @@ var KeycloakClient = class {
156
556
  if (!token || typeof token !== "string") return "";
157
557
  return token.length <= visibleChars ? token : `${token.slice(0, visibleChars)}...`;
158
558
  }
559
+ static scopesToString(scopes) {
560
+ if (!scopes) return "openid profile email";
561
+ return Array.isArray(scopes) ? scopes.join(" ") : String(scopes);
562
+ }
159
563
  };
160
564
  KeycloakClient = __decorateClass([
161
565
  (0, import_common.Injectable)(),
162
- __decorateParam(1, (0, import_common.Inject)(import_http_client.HTTP_PROVIDER))
566
+ __decorateParam(1, (0, import_common.Inject)(import_http_client.HTTP_PROVIDER)),
567
+ __decorateParam(2, (0, import_common.Optional)()),
568
+ __decorateParam(2, (0, import_common.Inject)(import_logger.LOGGER_PROVIDER))
163
569
  ], KeycloakClient);
164
570
 
165
571
  // src/keycloak.http.interceptor.ts
166
572
  var import_common2 = require("@nestjs/common");
167
-
168
- // src/keycloak.token.ts
169
- var KEYCLOAK_CONFIG = "KEYCLOAK_CONFIG";
170
- var KEYCLOAK_CLIENT = "KEYCLOAK_CLIENT";
171
- var KEYCLOAK_HTTP_INTERCEPTOR = "KEYCLOAK_HTTP_INTERCEPTOR";
172
-
173
- // src/keycloak.http.interceptor.ts
174
573
  var KeycloakHttpInterceptor = class {
175
- constructor(keycloakClient) {
176
- this.keycloakClient = keycloakClient;
574
+ constructor() {
177
575
  }
178
576
  intercept(context, next) {
179
577
  const request = context.switchToHttp().getRequest();
180
- if (request.url && !request.url.includes("keycloak")) {
578
+ if (typeof request.url === "string" && !request.url.includes("keycloak")) {
181
579
  }
182
580
  return next.handle();
183
581
  }
184
582
  };
185
583
  KeycloakHttpInterceptor = __decorateClass([
186
- (0, import_common2.Injectable)(),
187
- __decorateParam(0, (0, import_common2.Inject)(KEYCLOAK_CLIENT))
584
+ (0, import_common2.Injectable)()
188
585
  ], KeycloakHttpInterceptor);
189
586
 
587
+ // src/roles.guard.ts
588
+ var import_common4 = require("@nestjs/common");
589
+ var import_core = require("@nestjs/core");
590
+
591
+ // src/roles.decorator.ts
592
+ var import_common3 = require("@nestjs/common");
593
+ var ROLES_META_KEY = "roles";
594
+ function Roles(...args) {
595
+ let payload;
596
+ if (args.length === 1 && typeof args[0] === "object" && !Array.isArray(args[0])) {
597
+ payload = args[0];
598
+ } else {
599
+ const roles = [].concat(
600
+ ...args.map((a) => Array.isArray(a) ? a : String(a))
601
+ );
602
+ payload = { roles };
603
+ }
604
+ payload.mode = payload.mode ?? "any";
605
+ payload.type = payload.type ?? "both";
606
+ return (0, import_common3.SetMetadata)(ROLES_META_KEY, payload);
607
+ }
608
+
609
+ // src/keycloak.token.ts
610
+ var KEYCLOAK_CONFIG = "KEYCLOAK_CONFIG";
611
+ var KEYCLOAK_CLIENT = "KEYCLOAK_CLIENT";
612
+ var KEYCLOAK_HTTP_INTERCEPTOR = "KEYCLOAK_HTTP_INTERCEPTOR";
613
+
614
+ // src/roles.guard.ts
615
+ var import_shared = __toESM(require_dist());
616
+ var RolesGuard = class {
617
+ constructor(reflector, config) {
618
+ this.reflector = reflector;
619
+ this.config = config;
620
+ }
621
+ canActivate(context) {
622
+ var _a, _b, _c, _d, _e, _f, _g, _h;
623
+ const meta = this.reflector.get(ROLES_META_KEY, context.getHandler()) || this.reflector.get(ROLES_META_KEY, context.getClass());
624
+ if (!meta || !meta.roles || meta.roles.length === 0) return true;
625
+ const req = context.switchToHttp().getRequest();
626
+ const authHeader = ((_a = req.headers) == null ? void 0 : _a.authorization) || ((_b = req.headers) == null ? void 0 : _b.Authorization);
627
+ const token = authHeader ? String(authHeader).split(" ")[1] : (_c = req.query) == null ? void 0 : _c.token;
628
+ if (!token)
629
+ throw new import_shared.BaseAppError({
630
+ message: "Authorization token not provided",
631
+ status: 403,
632
+ code: "FORBIDDEN_MISSING_TOKEN",
633
+ context: {}
634
+ });
635
+ const payload = this.decodeJwtPayload(token);
636
+ const availableRoles = /* @__PURE__ */ new Set();
637
+ if (((_d = payload == null ? void 0 : payload.realm_access) == null ? void 0 : _d.roles) && Array.isArray(payload.realm_access.roles)) {
638
+ payload.realm_access.roles.forEach((r) => availableRoles.add(r));
639
+ }
640
+ const clientId = (_f = (_e = this.config) == null ? void 0 : _e.credentials) == null ? void 0 : _f.clientId;
641
+ if (clientId && ((_h = (_g = payload == null ? void 0 : payload.resource_access) == null ? void 0 : _g[clientId]) == null ? void 0 : _h.roles)) {
642
+ payload.resource_access[clientId].roles.forEach(
643
+ (r) => availableRoles.add(r)
644
+ );
645
+ }
646
+ if (meta.type === "both" && (payload == null ? void 0 : payload.resource_access)) {
647
+ Object.values(payload.resource_access).forEach((entry) => {
648
+ if ((entry == null ? void 0 : entry.roles) && Array.isArray(entry.roles)) {
649
+ entry.roles.forEach(
650
+ (r) => availableRoles.add(r)
651
+ );
652
+ }
653
+ });
654
+ }
655
+ const required = meta.roles || [];
656
+ const hasMatch = required.map((r) => availableRoles.has(r));
657
+ const result = meta.mode === "all" ? hasMatch.every(Boolean) : hasMatch.some(Boolean);
658
+ if (!result)
659
+ throw new import_shared.BaseAppError({
660
+ message: "Insufficient roles",
661
+ status: 403,
662
+ code: "FORBIDDEN_INSUFFICIENT_ROLES",
663
+ context: { required }
664
+ });
665
+ return true;
666
+ }
667
+ decodeJwtPayload(token) {
668
+ try {
669
+ const parts = token.split(".");
670
+ if (parts.length < 2) return {};
671
+ const payload = parts[1];
672
+ const BufferCtor = globalThis.Buffer;
673
+ if (!BufferCtor) return {};
674
+ const decoded = BufferCtor.from(payload, "base64").toString("utf8");
675
+ return JSON.parse(decoded);
676
+ } catch (e) {
677
+ return {};
678
+ }
679
+ }
680
+ };
681
+ RolesGuard = __decorateClass([
682
+ (0, import_common4.Injectable)(),
683
+ __decorateParam(0, (0, import_common4.Inject)(import_core.Reflector)),
684
+ __decorateParam(1, (0, import_common4.Optional)()),
685
+ __decorateParam(1, (0, import_common4.Inject)(KEYCLOAK_CONFIG))
686
+ ], RolesGuard);
687
+
190
688
  // src/keycloak.module.ts
191
689
  var KeycloakModule = class {
192
690
  static forRoot(config, httpConfig) {
193
691
  return {
194
692
  module: KeycloakModule,
195
693
  global: true,
196
- imports: [import_http_client2.HttpModule.forRoot(httpConfig)],
694
+ imports: [
695
+ import_http_client2.HttpModule.forRoot(
696
+ httpConfig || { baseURL: config.baseUrl, timeout: 5e3 },
697
+ {
698
+ logging: {
699
+ enabled: true,
700
+ includeBody: true,
701
+ context: "KeycloakHttpClient",
702
+ environments: ["development", "test"]
703
+ }
704
+ }
705
+ )
706
+ ],
197
707
  providers: [
708
+ { provide: import_core2.Reflector, useClass: import_core2.Reflector },
198
709
  { provide: KEYCLOAK_CONFIG, useValue: config },
199
710
  {
200
711
  provide: KEYCLOAK_CLIENT,
201
- useFactory: (cfg, httpProvider) => new KeycloakClient(cfg, httpProvider),
202
- inject: [KEYCLOAK_CONFIG, import_http_client2.HTTP_PROVIDER]
712
+ useFactory: (cfg, httpProvider, logger) => new KeycloakClient(cfg, httpProvider, logger),
713
+ inject: [KEYCLOAK_CONFIG, import_http_client2.HTTP_PROVIDER, { token: import_logger2.LOGGER_PROVIDER, optional: true }]
203
714
  },
204
715
  {
205
716
  provide: KEYCLOAK_HTTP_INTERCEPTOR,
206
- useFactory: (client) => new KeycloakHttpInterceptor(client),
207
- inject: [KEYCLOAK_CLIENT]
208
- }
717
+ useFactory: () => new KeycloakHttpInterceptor()
718
+ },
719
+ RolesGuard
209
720
  ],
210
- exports: [KEYCLOAK_CLIENT, KEYCLOAK_HTTP_INTERCEPTOR]
721
+ exports: [
722
+ import_core2.Reflector,
723
+ KEYCLOAK_CLIENT,
724
+ KEYCLOAK_HTTP_INTERCEPTOR,
725
+ KEYCLOAK_CONFIG,
726
+ RolesGuard
727
+ ]
211
728
  };
212
729
  }
213
730
  };
214
731
  KeycloakModule = __decorateClass([
215
- (0, import_common3.Module)({})
732
+ (0, import_common5.Module)({})
216
733
  ], KeycloakModule);
217
734
  // Annotate the CommonJS export names for ESM import in node:
218
735
  0 && (module.exports = {
219
736
  KEYCLOAK_CLIENT,
220
737
  KEYCLOAK_CONFIG,
221
738
  KEYCLOAK_HTTP_INTERCEPTOR,
222
- KeycloakModule
739
+ KeycloakError,
740
+ KeycloakModule,
741
+ Roles,
742
+ RolesGuard
223
743
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adatechnology/auth-keycloak",
3
- "version": "0.0.2",
3
+ "version": "0.0.6",
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.2"
14
+ "@adatechnology/http-client": "0.0.6",
15
+ "@adatechnology/logger": "0.0.5"
15
16
  },
16
17
  "peerDependencies": {
17
18
  "@nestjs/common": "^11.0.16",
@@ -20,10 +21,11 @@
20
21
  "devDependencies": {
21
22
  "@esbuild-plugins/tsconfig-paths": "^0.1.2",
22
23
  "tsup": "^8.5.1",
23
- "typescript": "^5.2.0"
24
+ "typescript": "^5.2.0",
25
+ "@adatechnology/shared": "0.0.2"
24
26
  },
25
27
  "scripts": {
26
- "build": "tsup",
28
+ "build": "rm -rf dist && tsup",
27
29
  "build:watch": "tsup --watch",
28
30
  "check": "tsc -p tsconfig.json --noEmit",
29
31
  "test": "echo \"no tests\""
package/dist/index.d.mts DELETED
@@ -1,65 +0,0 @@
1
- import { DynamicModule } from '@nestjs/common';
2
- import { AxiosRequestConfig, AxiosInstance } from 'axios';
3
-
4
- /**
5
- * Keycloak token response
6
- */
7
- interface KeycloakTokenResponse {
8
- access_token: string;
9
- expires_in: number;
10
- refresh_expires_in: number;
11
- refresh_token: string;
12
- token_type: string;
13
- 'not-before-policy': number;
14
- session_state: string;
15
- scope: string;
16
- }
17
- /**
18
- * Keycloak client credentials
19
- */
20
- interface KeycloakCredentials {
21
- clientId: string;
22
- clientSecret: string;
23
- username?: string;
24
- password?: string;
25
- grantType: 'client_credentials' | 'password';
26
- }
27
- /**
28
- * Keycloak configuration
29
- */
30
- interface KeycloakConfig {
31
- baseUrl: string;
32
- realm: string;
33
- credentials: KeycloakCredentials;
34
- }
35
- /**
36
- * Keycloak client interface
37
- */
38
- interface KeycloakClientInterface {
39
- /**
40
- * Get access token
41
- */
42
- getAccessToken(): Promise<string>;
43
- /**
44
- * Refresh access token
45
- */
46
- refreshToken(refreshToken: string): Promise<KeycloakTokenResponse>;
47
- /**
48
- * Validate token
49
- */
50
- validateToken(token: string): Promise<boolean>;
51
- /**
52
- * Get user info
53
- */
54
- getUserInfo(token: string): Promise<any>;
55
- }
56
-
57
- declare class KeycloakModule {
58
- static forRoot(config: KeycloakConfig, httpConfig?: AxiosRequestConfig | AxiosInstance): DynamicModule;
59
- }
60
-
61
- declare const KEYCLOAK_CONFIG = "KEYCLOAK_CONFIG";
62
- declare const KEYCLOAK_CLIENT = "KEYCLOAK_CLIENT";
63
- declare const KEYCLOAK_HTTP_INTERCEPTOR = "KEYCLOAK_HTTP_INTERCEPTOR";
64
-
65
- export { KEYCLOAK_CLIENT, KEYCLOAK_CONFIG, KEYCLOAK_HTTP_INTERCEPTOR, type KeycloakClientInterface, type KeycloakConfig, KeycloakModule, type KeycloakTokenResponse };
package/dist/index.mjs DELETED
@@ -1,200 +0,0 @@
1
- var __defProp = Object.defineProperty;
2
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
- var __decorateClass = (decorators, target, key, kind) => {
4
- var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
5
- for (var i = decorators.length - 1, decorator; i >= 0; i--)
6
- if (decorator = decorators[i])
7
- result = (kind ? decorator(target, key, result) : decorator(result)) || result;
8
- if (kind && result) __defProp(target, key, result);
9
- return result;
10
- };
11
- var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
12
-
13
- // src/keycloak.module.ts
14
- import { Module } from "@nestjs/common";
15
- import { HTTP_PROVIDER as HTTP_PROVIDER2, HttpModule } from "@adatechnology/http-client";
16
-
17
- // src/keycloak.client.ts
18
- import { Inject, Injectable } from "@nestjs/common";
19
- import { HTTP_PROVIDER } from "@adatechnology/http-client";
20
- var KeycloakClient = class {
21
- constructor(config, httpProvider) {
22
- this.config = config;
23
- this.httpProvider = httpProvider;
24
- }
25
- tokenCache = null;
26
- tokenPromise = null;
27
- async getAccessToken() {
28
- const now = Date.now();
29
- if (this.tokenCache && now < this.tokenCache.expiresAt) {
30
- return this.tokenCache.token;
31
- }
32
- if (this.tokenPromise) return this.tokenPromise;
33
- this.tokenPromise = (async () => {
34
- try {
35
- const tokenResponse = await this.requestToken();
36
- const expiresAt = Date.now() + (tokenResponse.expires_in - 60) * 1e3;
37
- this.tokenCache = { token: tokenResponse.access_token, expiresAt };
38
- return tokenResponse.access_token;
39
- } finally {
40
- this.tokenPromise = null;
41
- }
42
- })();
43
- return this.tokenPromise;
44
- }
45
- async getTokenWithCredentials(username, password) {
46
- const tokenUrl = `${this.config.baseUrl}/realms/${this.config.realm}/protocol/openid-connect/token`;
47
- const data = new URLSearchParams();
48
- data.append("client_id", this.config.credentials.clientId);
49
- data.append("grant_type", "password");
50
- data.append("username", username);
51
- data.append("password", password);
52
- if (this.config.credentials.clientSecret) {
53
- data.append("client_secret", this.config.credentials.clientSecret);
54
- }
55
- const response = await this.httpProvider.post(tokenUrl, data, {
56
- headers: { "Content-Type": "application/x-www-form-urlencoded" }
57
- });
58
- return response.data;
59
- }
60
- async requestToken() {
61
- const tokenUrl = `${this.config.baseUrl}/realms/${this.config.realm}/protocol/openid-connect/token`;
62
- const data = new URLSearchParams();
63
- data.append("client_id", this.config.credentials.clientId);
64
- data.append("grant_type", this.config.credentials.grantType);
65
- if (this.config.credentials.clientSecret) {
66
- data.append("client_secret", this.config.credentials.clientSecret);
67
- }
68
- if (this.config.credentials.grantType === "password") {
69
- if (this.config.credentials.username && this.config.credentials.password) {
70
- data.append("username", this.config.credentials.username);
71
- data.append("password", this.config.credentials.password);
72
- }
73
- }
74
- const response = await this.httpProvider.post(
75
- tokenUrl,
76
- data,
77
- {
78
- headers: { "Content-Type": "application/x-www-form-urlencoded" }
79
- }
80
- );
81
- return response.data;
82
- }
83
- async refreshToken(refreshToken) {
84
- const tokenUrl = `${this.config.baseUrl}/realms/${this.config.realm}/protocol/openid-connect/token`;
85
- const data = new URLSearchParams();
86
- data.append("client_id", this.config.credentials.clientId);
87
- data.append("grant_type", "refresh_token");
88
- data.append("refresh_token", refreshToken);
89
- if (this.config.credentials.clientSecret) {
90
- data.append("client_secret", this.config.credentials.clientSecret);
91
- }
92
- const response = await this.httpProvider.post(
93
- tokenUrl,
94
- data,
95
- {
96
- headers: { "Content-Type": "application/x-www-form-urlencoded" }
97
- }
98
- );
99
- const expiresAt = Date.now() + (response.data.expires_in - 60) * 1e3;
100
- this.tokenCache = { token: response.data.access_token, expiresAt };
101
- return response.data;
102
- }
103
- async validateToken(token) {
104
- try {
105
- const introspectUrl = `${this.config.baseUrl}/realms/${this.config.realm}/protocol/openid-connect/token/introspect`;
106
- const data = new URLSearchParams();
107
- data.append("token", token);
108
- data.append("client_id", this.config.credentials.clientId);
109
- if (this.config.credentials.clientSecret) {
110
- data.append("client_secret", this.config.credentials.clientSecret);
111
- }
112
- const response = await this.httpProvider.post(introspectUrl, data, {
113
- headers: { "Content-Type": "application/x-www-form-urlencoded" }
114
- });
115
- return response.data.active === true;
116
- } catch (error) {
117
- return false;
118
- }
119
- }
120
- async getUserInfo(token) {
121
- const userInfoUrl = `${this.config.baseUrl}/realms/${this.config.realm}/protocol/openid-connect/userinfo`;
122
- const response = await this.httpProvider.get(userInfoUrl, {
123
- headers: { Authorization: `Bearer ${token}` }
124
- });
125
- return response.data;
126
- }
127
- clearTokenCache() {
128
- this.tokenCache = null;
129
- }
130
- static maskToken(token, visibleChars = 8) {
131
- if (!token || typeof token !== "string") return "";
132
- return token.length <= visibleChars ? token : `${token.slice(0, visibleChars)}...`;
133
- }
134
- };
135
- KeycloakClient = __decorateClass([
136
- Injectable(),
137
- __decorateParam(1, Inject(HTTP_PROVIDER))
138
- ], KeycloakClient);
139
-
140
- // src/keycloak.http.interceptor.ts
141
- import {
142
- Inject as Inject2,
143
- Injectable as Injectable2
144
- } from "@nestjs/common";
145
-
146
- // src/keycloak.token.ts
147
- var KEYCLOAK_CONFIG = "KEYCLOAK_CONFIG";
148
- var KEYCLOAK_CLIENT = "KEYCLOAK_CLIENT";
149
- var KEYCLOAK_HTTP_INTERCEPTOR = "KEYCLOAK_HTTP_INTERCEPTOR";
150
-
151
- // src/keycloak.http.interceptor.ts
152
- var KeycloakHttpInterceptor = class {
153
- constructor(keycloakClient) {
154
- this.keycloakClient = keycloakClient;
155
- }
156
- intercept(context, next) {
157
- const request = context.switchToHttp().getRequest();
158
- if (request.url && !request.url.includes("keycloak")) {
159
- }
160
- return next.handle();
161
- }
162
- };
163
- KeycloakHttpInterceptor = __decorateClass([
164
- Injectable2(),
165
- __decorateParam(0, Inject2(KEYCLOAK_CLIENT))
166
- ], KeycloakHttpInterceptor);
167
-
168
- // src/keycloak.module.ts
169
- var KeycloakModule = class {
170
- static forRoot(config, httpConfig) {
171
- return {
172
- module: KeycloakModule,
173
- global: true,
174
- imports: [HttpModule.forRoot(httpConfig)],
175
- providers: [
176
- { provide: KEYCLOAK_CONFIG, useValue: config },
177
- {
178
- provide: KEYCLOAK_CLIENT,
179
- useFactory: (cfg, httpProvider) => new KeycloakClient(cfg, httpProvider),
180
- inject: [KEYCLOAK_CONFIG, HTTP_PROVIDER2]
181
- },
182
- {
183
- provide: KEYCLOAK_HTTP_INTERCEPTOR,
184
- useFactory: (client) => new KeycloakHttpInterceptor(client),
185
- inject: [KEYCLOAK_CLIENT]
186
- }
187
- ],
188
- exports: [KEYCLOAK_CLIENT, KEYCLOAK_HTTP_INTERCEPTOR]
189
- };
190
- }
191
- };
192
- KeycloakModule = __decorateClass([
193
- Module({})
194
- ], KeycloakModule);
195
- export {
196
- KEYCLOAK_CLIENT,
197
- KEYCLOAK_CONFIG,
198
- KEYCLOAK_HTTP_INTERCEPTOR,
199
- KeycloakModule
200
- };