@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 +46 -0
- package/dist/index.d.ts +54 -5
- package/dist/index.js +584 -64
- package/package.json +6 -4
- package/dist/index.d.mts +0 -65
- package/dist/index.mjs +0 -200
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
|
|
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
|
-
|
|
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:
|
|
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<
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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)
|
|
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(
|
|
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
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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(
|
|
138
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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(
|
|
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: [
|
|
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: (
|
|
207
|
-
|
|
208
|
-
|
|
717
|
+
useFactory: () => new KeycloakHttpInterceptor()
|
|
718
|
+
},
|
|
719
|
+
RolesGuard
|
|
209
720
|
],
|
|
210
|
-
exports: [
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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": "
|
|
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
|
-
};
|