@adatechnology/auth-keycloak 0.0.2 → 0.0.3

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`.
@@ -0,0 +1,11 @@
1
+ export declare class KeycloakError extends Error {
2
+ readonly statusCode?: number;
3
+ readonly details?: unknown;
4
+ readonly keycloakError?: string;
5
+ constructor(message: string, opts?: {
6
+ statusCode?: number;
7
+ details?: unknown;
8
+ keycloakError?: string;
9
+ });
10
+ }
11
+ export default KeycloakError;
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.KeycloakError = void 0;
4
+ class KeycloakError extends Error {
5
+ statusCode;
6
+ details;
7
+ keycloakError;
8
+ constructor(message, opts) {
9
+ super(message);
10
+ this.name = "KeycloakError";
11
+ this.statusCode = opts?.statusCode;
12
+ this.details = opts?.details;
13
+ this.keycloakError = opts?.keycloakError;
14
+ // maintain proper prototype chain
15
+ Object.setPrototypeOf(this, KeycloakError.prototype);
16
+ }
17
+ }
18
+ exports.KeycloakError = KeycloakError;
19
+ exports.default = KeycloakError;
20
+ //# sourceMappingURL=keycloak-error.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keycloak-error.js","sourceRoot":"","sources":["../../src/errors/keycloak-error.ts"],"names":[],"mappings":";;;AAAA,MAAa,aAAc,SAAQ,KAAK;IACtB,UAAU,CAAU;IACpB,OAAO,CAAW;IAClB,aAAa,CAAU;IAEvC,YACE,OAAe,EACf,IAAyE;QAEzE,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC;QAC5B,IAAI,CAAC,UAAU,GAAG,IAAI,EAAE,UAAU,CAAC;QACnC,IAAI,CAAC,OAAO,GAAG,IAAI,EAAE,OAAO,CAAC;QAC7B,IAAI,CAAC,aAAa,GAAG,IAAI,EAAE,aAAa,CAAC;QACzC,kCAAkC;QAClC,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,aAAa,CAAC,SAAS,CAAC,CAAC;IACvD,CAAC;CACF;AAjBD,sCAiBC;AAED,kBAAe,aAAa,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,65 +1,6 @@
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 };
1
+ export { KeycloakModule } from "./keycloak.module";
2
+ export { KEYCLOAK_CONFIG, KEYCLOAK_CLIENT, KEYCLOAK_HTTP_INTERCEPTOR, } from "./keycloak.token";
3
+ export type { KeycloakConfig, KeycloakClientInterface, KeycloakTokenResponse, } from "./keycloak.interface";
4
+ export { Roles } from "./roles.decorator";
5
+ export { RolesGuard } from "./roles.guard";
6
+ export { KeycloakError } from "./errors/keycloak-error";
package/dist/index.js CHANGED
@@ -1,223 +1,16 @@
1
- var __defProp = Object.defineProperty;
2
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
- var __getOwnPropNames = Object.getOwnPropertyNames;
4
- var __hasOwnProp = Object.prototype.hasOwnProperty;
5
- var __export = (target, all) => {
6
- for (var name in all)
7
- __defProp(target, name, { get: all[name], enumerable: true });
8
- };
9
- var __copyProps = (to, from, except, desc) => {
10
- if (from && typeof from === "object" || typeof from === "function") {
11
- for (let key of __getOwnPropNames(from))
12
- if (!__hasOwnProp.call(to, key) && key !== except)
13
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
- }
15
- return to;
16
- };
17
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
18
- var __decorateClass = (decorators, target, key, kind) => {
19
- var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
20
- for (var i = decorators.length - 1, decorator; i >= 0; i--)
21
- if (decorator = decorators[i])
22
- result = (kind ? decorator(target, key, result) : decorator(result)) || result;
23
- if (kind && result) __defProp(target, key, result);
24
- return result;
25
- };
26
- var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
27
-
28
- // src/index.ts
29
- var index_exports = {};
30
- __export(index_exports, {
31
- KEYCLOAK_CLIENT: () => KEYCLOAK_CLIENT,
32
- KEYCLOAK_CONFIG: () => KEYCLOAK_CONFIG,
33
- KEYCLOAK_HTTP_INTERCEPTOR: () => KEYCLOAK_HTTP_INTERCEPTOR,
34
- KeycloakModule: () => KeycloakModule
35
- });
36
- module.exports = __toCommonJS(index_exports);
37
-
38
- // src/keycloak.module.ts
39
- var import_common3 = require("@nestjs/common");
40
- var import_http_client2 = require("@adatechnology/http-client");
41
-
42
- // src/keycloak.client.ts
43
- var import_common = require("@nestjs/common");
44
- var import_http_client = require("@adatechnology/http-client");
45
- var KeycloakClient = class {
46
- constructor(config, httpProvider) {
47
- this.config = config;
48
- this.httpProvider = httpProvider;
49
- }
50
- tokenCache = null;
51
- tokenPromise = null;
52
- async getAccessToken() {
53
- const now = Date.now();
54
- if (this.tokenCache && now < this.tokenCache.expiresAt) {
55
- return this.tokenCache.token;
56
- }
57
- if (this.tokenPromise) return this.tokenPromise;
58
- this.tokenPromise = (async () => {
59
- try {
60
- const tokenResponse = await this.requestToken();
61
- const expiresAt = Date.now() + (tokenResponse.expires_in - 60) * 1e3;
62
- this.tokenCache = { token: tokenResponse.access_token, expiresAt };
63
- return tokenResponse.access_token;
64
- } finally {
65
- this.tokenPromise = null;
66
- }
67
- })();
68
- return this.tokenPromise;
69
- }
70
- async getTokenWithCredentials(username, password) {
71
- 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);
77
- if (this.config.credentials.clientSecret) {
78
- data.append("client_secret", this.config.credentials.clientSecret);
79
- }
80
- const response = await this.httpProvider.post(tokenUrl, data, {
81
- headers: { "Content-Type": "application/x-www-form-urlencoded" }
82
- });
83
- return response.data;
84
- }
85
- async requestToken() {
86
- const tokenUrl = `${this.config.baseUrl}/realms/${this.config.realm}/protocol/openid-connect/token`;
87
- const data = new URLSearchParams();
88
- data.append("client_id", this.config.credentials.clientId);
89
- data.append("grant_type", this.config.credentials.grantType);
90
- if (this.config.credentials.clientSecret) {
91
- data.append("client_secret", this.config.credentials.clientSecret);
92
- }
93
- if (this.config.credentials.grantType === "password") {
94
- if (this.config.credentials.username && this.config.credentials.password) {
95
- data.append("username", this.config.credentials.username);
96
- data.append("password", this.config.credentials.password);
97
- }
98
- }
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;
107
- }
108
- async refreshToken(refreshToken) {
109
- const tokenUrl = `${this.config.baseUrl}/realms/${this.config.realm}/protocol/openid-connect/token`;
110
- const data = new URLSearchParams();
111
- data.append("client_id", this.config.credentials.clientId);
112
- data.append("grant_type", "refresh_token");
113
- data.append("refresh_token", refreshToken);
114
- if (this.config.credentials.clientSecret) {
115
- data.append("client_secret", this.config.credentials.clientSecret);
116
- }
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;
127
- }
128
- async validateToken(token) {
129
- try {
130
- const introspectUrl = `${this.config.baseUrl}/realms/${this.config.realm}/protocol/openid-connect/token/introspect`;
131
- const data = new URLSearchParams();
132
- data.append("token", token);
133
- data.append("client_id", this.config.credentials.clientId);
134
- if (this.config.credentials.clientSecret) {
135
- data.append("client_secret", this.config.credentials.clientSecret);
136
- }
137
- const response = await this.httpProvider.post(introspectUrl, data, {
138
- headers: { "Content-Type": "application/x-www-form-urlencoded" }
139
- });
140
- return response.data.active === true;
141
- } catch (error) {
142
- return false;
143
- }
144
- }
145
- async getUserInfo(token) {
146
- 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;
151
- }
152
- clearTokenCache() {
153
- this.tokenCache = null;
154
- }
155
- static maskToken(token, visibleChars = 8) {
156
- if (!token || typeof token !== "string") return "";
157
- return token.length <= visibleChars ? token : `${token.slice(0, visibleChars)}...`;
158
- }
159
- };
160
- KeycloakClient = __decorateClass([
161
- (0, import_common.Injectable)(),
162
- __decorateParam(1, (0, import_common.Inject)(import_http_client.HTTP_PROVIDER))
163
- ], KeycloakClient);
164
-
165
- // src/keycloak.http.interceptor.ts
166
- 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
- var KeycloakHttpInterceptor = class {
175
- constructor(keycloakClient) {
176
- this.keycloakClient = keycloakClient;
177
- }
178
- intercept(context, next) {
179
- const request = context.switchToHttp().getRequest();
180
- if (request.url && !request.url.includes("keycloak")) {
181
- }
182
- return next.handle();
183
- }
184
- };
185
- KeycloakHttpInterceptor = __decorateClass([
186
- (0, import_common2.Injectable)(),
187
- __decorateParam(0, (0, import_common2.Inject)(KEYCLOAK_CLIENT))
188
- ], KeycloakHttpInterceptor);
189
-
190
- // src/keycloak.module.ts
191
- var KeycloakModule = class {
192
- static forRoot(config, httpConfig) {
193
- return {
194
- module: KeycloakModule,
195
- global: true,
196
- imports: [import_http_client2.HttpModule.forRoot(httpConfig)],
197
- providers: [
198
- { provide: KEYCLOAK_CONFIG, useValue: config },
199
- {
200
- provide: KEYCLOAK_CLIENT,
201
- useFactory: (cfg, httpProvider) => new KeycloakClient(cfg, httpProvider),
202
- inject: [KEYCLOAK_CONFIG, import_http_client2.HTTP_PROVIDER]
203
- },
204
- {
205
- provide: KEYCLOAK_HTTP_INTERCEPTOR,
206
- useFactory: (client) => new KeycloakHttpInterceptor(client),
207
- inject: [KEYCLOAK_CLIENT]
208
- }
209
- ],
210
- exports: [KEYCLOAK_CLIENT, KEYCLOAK_HTTP_INTERCEPTOR]
211
- };
212
- }
213
- };
214
- KeycloakModule = __decorateClass([
215
- (0, import_common3.Module)({})
216
- ], KeycloakModule);
217
- // Annotate the CommonJS export names for ESM import in node:
218
- 0 && (module.exports = {
219
- KEYCLOAK_CLIENT,
220
- KEYCLOAK_CONFIG,
221
- KEYCLOAK_HTTP_INTERCEPTOR,
222
- KeycloakModule
223
- });
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.KeycloakError = exports.RolesGuard = exports.Roles = exports.KEYCLOAK_HTTP_INTERCEPTOR = exports.KEYCLOAK_CLIENT = exports.KEYCLOAK_CONFIG = exports.KeycloakModule = void 0;
4
+ var keycloak_module_1 = require("./keycloak.module");
5
+ Object.defineProperty(exports, "KeycloakModule", { enumerable: true, get: function () { return keycloak_module_1.KeycloakModule; } });
6
+ var keycloak_token_1 = require("./keycloak.token");
7
+ Object.defineProperty(exports, "KEYCLOAK_CONFIG", { enumerable: true, get: function () { return keycloak_token_1.KEYCLOAK_CONFIG; } });
8
+ Object.defineProperty(exports, "KEYCLOAK_CLIENT", { enumerable: true, get: function () { return keycloak_token_1.KEYCLOAK_CLIENT; } });
9
+ Object.defineProperty(exports, "KEYCLOAK_HTTP_INTERCEPTOR", { enumerable: true, get: function () { return keycloak_token_1.KEYCLOAK_HTTP_INTERCEPTOR; } });
10
+ var roles_decorator_1 = require("./roles.decorator");
11
+ Object.defineProperty(exports, "Roles", { enumerable: true, get: function () { return roles_decorator_1.Roles; } });
12
+ var roles_guard_1 = require("./roles.guard");
13
+ Object.defineProperty(exports, "RolesGuard", { enumerable: true, get: function () { return roles_guard_1.RolesGuard; } });
14
+ var keycloak_error_1 = require("./errors/keycloak-error");
15
+ Object.defineProperty(exports, "KeycloakError", { enumerable: true, get: function () { return keycloak_error_1.KeycloakError; } });
16
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,qDAAmD;AAA1C,iHAAA,cAAc,OAAA;AACvB,mDAI0B;AAHxB,iHAAA,eAAe,OAAA;AACf,iHAAA,eAAe,OAAA;AACf,2HAAA,yBAAyB,OAAA;AAO3B,qDAA0C;AAAjC,wGAAA,KAAK,OAAA;AACd,6CAA2C;AAAlC,yGAAA,UAAU,OAAA;AACnB,0DAAwD;AAA/C,+GAAA,aAAa,OAAA"}
@@ -0,0 +1,27 @@
1
+ import type { HttpProviderInterface } from "@adatechnology/http-client";
2
+ import { LoggerProviderInterface } from "@adatechnology/logger";
3
+ import type { KeycloakClientInterface, KeycloakConfig, KeycloakTokenResponse } from "./keycloak.interface";
4
+ /**
5
+ * Minimal Keycloak client implementation without external shared infra dependencies.
6
+ */
7
+ export declare class KeycloakClient implements KeycloakClientInterface {
8
+ private readonly config;
9
+ private readonly httpProvider;
10
+ private readonly logger?;
11
+ private tokenCache;
12
+ private tokenPromise?;
13
+ constructor(config: KeycloakConfig, httpProvider: HttpProviderInterface, logger?: LoggerProviderInterface);
14
+ private log;
15
+ getAccessToken(): Promise<string>;
16
+ getTokenWithCredentials(params: {
17
+ username: string;
18
+ password: string;
19
+ }): Promise<KeycloakTokenResponse>;
20
+ private requestToken;
21
+ refreshToken(refreshToken: string): Promise<KeycloakTokenResponse>;
22
+ validateToken(token: string): Promise<boolean>;
23
+ getUserInfo(token: string): Promise<Record<string, unknown>>;
24
+ clearTokenCache(): void;
25
+ private static maskToken;
26
+ private static scopesToString;
27
+ }