@adatechnology/auth-keycloak 0.1.0 → 0.1.1
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/dist/index.d.ts +116 -1
- package/dist/index.js +124 -34
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -137,6 +137,23 @@ type RolesOptions = {
|
|
|
137
137
|
*/
|
|
138
138
|
declare function Roles(...args: Array<string | string[] | RolesOptions>): _nestjs_common.CustomDecorator<string>;
|
|
139
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Guard that checks whether the current request has the required roles.
|
|
142
|
+
*
|
|
143
|
+
* Supports two auth paths transparently:
|
|
144
|
+
*
|
|
145
|
+
* 1. **Kong path** (user-facing, preferred):
|
|
146
|
+
* Kong validates the token, removes Authorization, and injects:
|
|
147
|
+
* - `X-User-Id` — keycloak sub
|
|
148
|
+
* - `X-User-Roles` — comma-separated realm roles
|
|
149
|
+
* The guard reads roles from `X-User-Roles` header.
|
|
150
|
+
*
|
|
151
|
+
* 2. **B2B path** (service-to-service, e.g. BFF → API):
|
|
152
|
+
* The caller sends a service account JWT in the Authorization header.
|
|
153
|
+
* The guard decodes the JWT locally and reads `realm_access.roles`.
|
|
154
|
+
*
|
|
155
|
+
* Priority: Kong header → JWT fallback.
|
|
156
|
+
*/
|
|
140
157
|
declare class RolesGuard implements CanActivate {
|
|
141
158
|
private readonly reflector;
|
|
142
159
|
private readonly config?;
|
|
@@ -145,6 +162,104 @@ declare class RolesGuard implements CanActivate {
|
|
|
145
162
|
private decodeJwtPayload;
|
|
146
163
|
}
|
|
147
164
|
|
|
165
|
+
/**
|
|
166
|
+
* B2B Guard — service-to-service routes (e.g. BFF → API, Worker → API).
|
|
167
|
+
*
|
|
168
|
+
* The caller must send a valid service account token (client_credentials)
|
|
169
|
+
* in the Authorization header. Delegates full token validation to
|
|
170
|
+
* BearerTokenGuard (Keycloak introspection).
|
|
171
|
+
*
|
|
172
|
+
* The caller is also expected to forward X-User-Id so the API knows which
|
|
173
|
+
* end-user context the call belongs to.
|
|
174
|
+
*
|
|
175
|
+
* Use for routes that are ONLY called by internal services, not by end users.
|
|
176
|
+
*
|
|
177
|
+
* @example
|
|
178
|
+
* ```ts
|
|
179
|
+
* @Post('internal/notify')
|
|
180
|
+
* @Roles('send-notifications')
|
|
181
|
+
* @UseGuards(B2BGuard, RolesGuard)
|
|
182
|
+
* async notify(@AuthUser() keycloakId: string) { ... }
|
|
183
|
+
* ```
|
|
184
|
+
*/
|
|
185
|
+
declare class B2BGuard implements CanActivate {
|
|
186
|
+
private readonly bearerTokenGuard;
|
|
187
|
+
constructor(bearerTokenGuard: BearerTokenGuard);
|
|
188
|
+
canActivate(context: ExecutionContext): Promise<boolean>;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* B2C Guard — user-facing routes via Kong.
|
|
193
|
+
*
|
|
194
|
+
* Kong validated the JWT (JWKS local), removed the Authorization header,
|
|
195
|
+
* and injected X-User-Id + X-User-Roles. This guard simply asserts those
|
|
196
|
+
* headers are present — it does NOT re-validate any token.
|
|
197
|
+
*
|
|
198
|
+
* Use for routes that are ONLY called by end users through Kong.
|
|
199
|
+
*
|
|
200
|
+
* @example
|
|
201
|
+
* ```ts
|
|
202
|
+
* @Get('me')
|
|
203
|
+
* @Roles('user-manager')
|
|
204
|
+
* @UseGuards(B2CGuard, RolesGuard)
|
|
205
|
+
* async getMe(@AuthUser() keycloakId: string) { ... }
|
|
206
|
+
* ```
|
|
207
|
+
*/
|
|
208
|
+
declare class B2CGuard implements CanActivate {
|
|
209
|
+
canActivate(context: ExecutionContext): boolean;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* ApiAuthGuard — composite guard for routes that accept both paths.
|
|
214
|
+
*
|
|
215
|
+
* Detects which path the request is coming from and delegates accordingly:
|
|
216
|
+
*
|
|
217
|
+
* - **B2B path** (Authorization header present):
|
|
218
|
+
* Service-to-service call (e.g. BFF → API). Delegates to B2BGuard,
|
|
219
|
+
* which validates the service account token via BearerTokenGuard.
|
|
220
|
+
*
|
|
221
|
+
* - **B2C path** (X-User-Id header present, no Authorization):
|
|
222
|
+
* User call routed by Kong. Delegates to B2CGuard,
|
|
223
|
+
* which asserts Kong identity headers are present.
|
|
224
|
+
*
|
|
225
|
+
* Use when the same route must be reachable both from Kong (users) and
|
|
226
|
+
* from internal services (BFF, Worker). If the route is exclusive to one
|
|
227
|
+
* path, prefer B2BGuard or B2CGuard directly for explicit intent.
|
|
228
|
+
*
|
|
229
|
+
* @example
|
|
230
|
+
* ```ts
|
|
231
|
+
* @Get('me')
|
|
232
|
+
* @Roles('user-manager')
|
|
233
|
+
* @UseGuards(ApiAuthGuard, RolesGuard)
|
|
234
|
+
* async getMe(@AuthUser() keycloakId: string) { ... }
|
|
235
|
+
* ```
|
|
236
|
+
*/
|
|
237
|
+
declare class ApiAuthGuard implements CanActivate {
|
|
238
|
+
private readonly b2bGuard;
|
|
239
|
+
private readonly b2cGuard;
|
|
240
|
+
constructor(b2bGuard: B2BGuard, b2cGuard: B2CGuard);
|
|
241
|
+
canActivate(context: ExecutionContext): Promise<boolean>;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Parameter decorator that extracts the authenticated user's Keycloak ID
|
|
246
|
+
* from the `X-User-Id` header injected by Kong after token validation.
|
|
247
|
+
*
|
|
248
|
+
* In the B2B path (service-to-service), the caller is responsible for
|
|
249
|
+
* forwarding the same header.
|
|
250
|
+
*
|
|
251
|
+
* @example
|
|
252
|
+
* ```ts
|
|
253
|
+
* @Get('me')
|
|
254
|
+
* @Roles('user-manager')
|
|
255
|
+
* @UseGuards(RolesGuard)
|
|
256
|
+
* async getMe(@AuthUser() keycloakId: string) {
|
|
257
|
+
* return this.userService.getUserByKeycloakId(keycloakId);
|
|
258
|
+
* }
|
|
259
|
+
* ```
|
|
260
|
+
*/
|
|
261
|
+
declare const AuthUser: (...dataOrPipes: unknown[]) => ParameterDecorator;
|
|
262
|
+
|
|
148
263
|
declare class KeycloakError extends Error {
|
|
149
264
|
readonly statusCode?: number;
|
|
150
265
|
readonly details?: unknown;
|
|
@@ -156,4 +271,4 @@ declare class KeycloakError extends Error {
|
|
|
156
271
|
});
|
|
157
272
|
}
|
|
158
273
|
|
|
159
|
-
export { BearerTokenGuard, KEYCLOAK_CLIENT, KEYCLOAK_CONFIG, KEYCLOAK_HTTP_INTERCEPTOR, KEYCLOAK_PROVIDER, type KeycloakClientInterface, type KeycloakConfig, KeycloakError, KeycloakModule, type KeycloakProviderInterface, type KeycloakTokenResponse, Roles, RolesGuard };
|
|
274
|
+
export { ApiAuthGuard, AuthUser, B2BGuard, B2CGuard, BearerTokenGuard, KEYCLOAK_CLIENT, KEYCLOAK_CONFIG, KEYCLOAK_HTTP_INTERCEPTOR, KEYCLOAK_PROVIDER, type KeycloakClientInterface, type KeycloakConfig, KeycloakError, KeycloakModule, type KeycloakProviderInterface, type KeycloakTokenResponse, Roles, RolesGuard };
|
package/dist/index.js
CHANGED
|
@@ -68,7 +68,7 @@ var require_base_app_error = __commonJS({
|
|
|
68
68
|
"use strict";
|
|
69
69
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
70
70
|
exports2.BaseAppError = void 0;
|
|
71
|
-
var
|
|
71
|
+
var BaseAppError5 = class extends Error {
|
|
72
72
|
code;
|
|
73
73
|
status;
|
|
74
74
|
context;
|
|
@@ -83,7 +83,7 @@ var require_base_app_error = __commonJS({
|
|
|
83
83
|
(_a = capturable.captureStackTrace) == null ? void 0 : _a.call(capturable, this, this.constructor);
|
|
84
84
|
}
|
|
85
85
|
};
|
|
86
|
-
exports2.BaseAppError =
|
|
86
|
+
exports2.BaseAppError = BaseAppError5;
|
|
87
87
|
}
|
|
88
88
|
});
|
|
89
89
|
|
|
@@ -268,6 +268,10 @@ var require_dist = __commonJS({
|
|
|
268
268
|
// src/index.ts
|
|
269
269
|
var index_exports = {};
|
|
270
270
|
__export(index_exports, {
|
|
271
|
+
ApiAuthGuard: () => ApiAuthGuard,
|
|
272
|
+
AuthUser: () => AuthUser,
|
|
273
|
+
B2BGuard: () => B2BGuard,
|
|
274
|
+
B2CGuard: () => B2CGuard,
|
|
271
275
|
BearerTokenGuard: () => BearerTokenGuard,
|
|
272
276
|
KEYCLOAK_CLIENT: () => KEYCLOAK_CLIENT,
|
|
273
277
|
KEYCLOAK_CONFIG: () => KEYCLOAK_CONFIG,
|
|
@@ -302,7 +306,7 @@ var KEYCLOAK_PROVIDER = "KEYCLOAK_PROVIDER";
|
|
|
302
306
|
// package.json
|
|
303
307
|
var package_default = {
|
|
304
308
|
name: "@adatechnology/auth-keycloak",
|
|
305
|
-
version: "0.1.
|
|
309
|
+
version: "0.1.1",
|
|
306
310
|
publishConfig: {
|
|
307
311
|
access: "public"
|
|
308
312
|
},
|
|
@@ -776,49 +780,54 @@ var RolesGuard = class {
|
|
|
776
780
|
this.config = config;
|
|
777
781
|
}
|
|
778
782
|
canActivate(context) {
|
|
779
|
-
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
783
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
|
|
780
784
|
const meta = this.reflector.get(ROLES_META_KEY, context.getHandler()) || this.reflector.get(ROLES_META_KEY, context.getClass());
|
|
781
785
|
if (!meta || !meta.roles || meta.roles.length === 0) return true;
|
|
782
786
|
const req = context.switchToHttp().getRequest();
|
|
783
|
-
const
|
|
784
|
-
const token = authHeader ? String(authHeader).split(" ")[1] : (_c = req.query) == null ? void 0 : _c.token;
|
|
785
|
-
if (!token)
|
|
786
|
-
throw new import_shared2.BaseAppError({
|
|
787
|
-
message: "Authorization token not provided",
|
|
788
|
-
status: HTTP_STATUS.FORBIDDEN,
|
|
789
|
-
code: ROLES_ERROR_CODE.MISSING_TOKEN,
|
|
790
|
-
context: {}
|
|
791
|
-
});
|
|
792
|
-
const payload = this.decodeJwtPayload(token);
|
|
787
|
+
const required = meta.roles;
|
|
793
788
|
const availableRoles = /* @__PURE__ */ new Set();
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
789
|
+
const kongRolesHeader = ((_a = req.headers) == null ? void 0 : _a["x-user-roles"]) || ((_b = req.headers) == null ? void 0 : _b["X-User-Roles"]);
|
|
790
|
+
if (kongRolesHeader) {
|
|
791
|
+
kongRolesHeader.split(",").map((r) => r.trim()).filter(Boolean).forEach((r) => availableRoles.add(r));
|
|
792
|
+
} else {
|
|
793
|
+
const authHeader = ((_c = req.headers) == null ? void 0 : _c.authorization) || ((_d = req.headers) == null ? void 0 : _d.Authorization);
|
|
794
|
+
const token = authHeader ? String(authHeader).split(" ")[1] : (_e = req.query) == null ? void 0 : _e.token;
|
|
795
|
+
if (!token) {
|
|
796
|
+
throw new import_shared2.BaseAppError({
|
|
797
|
+
message: "Authorization token not provided",
|
|
798
|
+
status: HTTP_STATUS.FORBIDDEN,
|
|
799
|
+
code: ROLES_ERROR_CODE.MISSING_TOKEN,
|
|
800
|
+
context: {}
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
const payload = this.decodeJwtPayload(token);
|
|
804
|
+
if (((_f = payload == null ? void 0 : payload.realm_access) == null ? void 0 : _f.roles) && Array.isArray(payload.realm_access.roles)) {
|
|
805
|
+
payload.realm_access.roles.forEach((r) => availableRoles.add(r));
|
|
806
|
+
}
|
|
807
|
+
const clientId = (_h = (_g = this.config) == null ? void 0 : _g.credentials) == null ? void 0 : _h.clientId;
|
|
808
|
+
if (clientId && ((_j = (_i = payload == null ? void 0 : payload.resource_access) == null ? void 0 : _i[clientId]) == null ? void 0 : _j.roles)) {
|
|
809
|
+
payload.resource_access[clientId].roles.forEach(
|
|
810
|
+
(r) => availableRoles.add(r)
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
if (meta.type === "both" && (payload == null ? void 0 : payload.resource_access)) {
|
|
814
|
+
Object.values(payload.resource_access).forEach((entry) => {
|
|
815
|
+
if ((entry == null ? void 0 : entry.roles) && Array.isArray(entry.roles)) {
|
|
816
|
+
entry.roles.forEach((r) => availableRoles.add(r));
|
|
817
|
+
}
|
|
818
|
+
});
|
|
819
|
+
}
|
|
811
820
|
}
|
|
812
|
-
const required = meta.roles || [];
|
|
813
821
|
const hasMatch = required.map((r) => availableRoles.has(r));
|
|
814
822
|
const result = meta.mode === "all" ? hasMatch.every(Boolean) : hasMatch.some(Boolean);
|
|
815
|
-
if (!result)
|
|
823
|
+
if (!result) {
|
|
816
824
|
throw new import_shared2.BaseAppError({
|
|
817
825
|
message: "Insufficient roles",
|
|
818
826
|
status: HTTP_STATUS.FORBIDDEN,
|
|
819
827
|
code: ROLES_ERROR_CODE.INSUFFICIENT_ROLES,
|
|
820
828
|
context: { required }
|
|
821
829
|
});
|
|
830
|
+
}
|
|
822
831
|
return true;
|
|
823
832
|
}
|
|
824
833
|
decodeJwtPayload(token) {
|
|
@@ -830,7 +839,7 @@ var RolesGuard = class {
|
|
|
830
839
|
if (!BufferCtor) return {};
|
|
831
840
|
const decoded = BufferCtor.from(payload, "base64").toString("utf8");
|
|
832
841
|
return JSON.parse(decoded);
|
|
833
|
-
} catch
|
|
842
|
+
} catch {
|
|
834
843
|
return {};
|
|
835
844
|
}
|
|
836
845
|
}
|
|
@@ -900,8 +909,89 @@ var KeycloakModule = class {
|
|
|
900
909
|
KeycloakModule = __decorateClass([
|
|
901
910
|
(0, import_common6.Module)({})
|
|
902
911
|
], KeycloakModule);
|
|
912
|
+
|
|
913
|
+
// src/b2b.guard.ts
|
|
914
|
+
var import_common7 = require("@nestjs/common");
|
|
915
|
+
var B2BGuard = class {
|
|
916
|
+
constructor(bearerTokenGuard) {
|
|
917
|
+
this.bearerTokenGuard = bearerTokenGuard;
|
|
918
|
+
}
|
|
919
|
+
canActivate(context) {
|
|
920
|
+
return Promise.resolve(this.bearerTokenGuard.canActivate(context));
|
|
921
|
+
}
|
|
922
|
+
};
|
|
923
|
+
B2BGuard = __decorateClass([
|
|
924
|
+
(0, import_common7.Injectable)()
|
|
925
|
+
], B2BGuard);
|
|
926
|
+
|
|
927
|
+
// src/b2c.guard.ts
|
|
928
|
+
var import_common8 = require("@nestjs/common");
|
|
929
|
+
var import_shared3 = __toESM(require_dist());
|
|
930
|
+
var B2CGuard = class {
|
|
931
|
+
canActivate(context) {
|
|
932
|
+
var _a, _b;
|
|
933
|
+
const request = context.switchToHttp().getRequest();
|
|
934
|
+
const userId = ((_a = request.headers) == null ? void 0 : _a["x-user-id"]) ?? ((_b = request.headers) == null ? void 0 : _b["X-User-Id"]);
|
|
935
|
+
if (userId) return true;
|
|
936
|
+
throw new import_shared3.BaseAppError({
|
|
937
|
+
message: "Missing Kong identity headers (X-User-Id). Route requires user authentication via Kong.",
|
|
938
|
+
status: HTTP_STATUS.UNAUTHORIZED,
|
|
939
|
+
code: BEARER_ERROR_CODE.MISSING_TOKEN,
|
|
940
|
+
context: {}
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
};
|
|
944
|
+
B2CGuard = __decorateClass([
|
|
945
|
+
(0, import_common8.Injectable)()
|
|
946
|
+
], B2CGuard);
|
|
947
|
+
|
|
948
|
+
// src/api-auth.guard.ts
|
|
949
|
+
var import_common9 = require("@nestjs/common");
|
|
950
|
+
var import_shared4 = __toESM(require_dist());
|
|
951
|
+
var ApiAuthGuard = class {
|
|
952
|
+
constructor(b2bGuard, b2cGuard) {
|
|
953
|
+
this.b2bGuard = b2bGuard;
|
|
954
|
+
this.b2cGuard = b2cGuard;
|
|
955
|
+
}
|
|
956
|
+
async canActivate(context) {
|
|
957
|
+
var _a, _b, _c, _d;
|
|
958
|
+
const request = context.switchToHttp().getRequest();
|
|
959
|
+
const authHeader = ((_a = request.headers) == null ? void 0 : _a.authorization) ?? ((_b = request.headers) == null ? void 0 : _b.Authorization);
|
|
960
|
+
if (authHeader == null ? void 0 : authHeader.toLowerCase().startsWith("bearer ")) {
|
|
961
|
+
return this.b2bGuard.canActivate(context);
|
|
962
|
+
}
|
|
963
|
+
const userId = ((_c = request.headers) == null ? void 0 : _c["x-user-id"]) ?? ((_d = request.headers) == null ? void 0 : _d["X-User-Id"]);
|
|
964
|
+
if (userId) {
|
|
965
|
+
return this.b2cGuard.canActivate(context);
|
|
966
|
+
}
|
|
967
|
+
throw new import_shared4.BaseAppError({
|
|
968
|
+
message: "Unauthorized: missing Authorization header (B2B) or Kong identity headers (B2C)",
|
|
969
|
+
status: HTTP_STATUS.UNAUTHORIZED,
|
|
970
|
+
code: BEARER_ERROR_CODE.MISSING_TOKEN,
|
|
971
|
+
context: {}
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
};
|
|
975
|
+
ApiAuthGuard = __decorateClass([
|
|
976
|
+
(0, import_common9.Injectable)()
|
|
977
|
+
], ApiAuthGuard);
|
|
978
|
+
|
|
979
|
+
// src/auth-user.decorator.ts
|
|
980
|
+
var import_common10 = require("@nestjs/common");
|
|
981
|
+
var AuthUser = (0, import_common10.createParamDecorator)(
|
|
982
|
+
(_data, ctx) => {
|
|
983
|
+
var _a, _b;
|
|
984
|
+
const request = ctx.switchToHttp().getRequest();
|
|
985
|
+
const raw = ((_a = request.headers) == null ? void 0 : _a["x-user-id"]) ?? ((_b = request.headers) == null ? void 0 : _b["X-User-Id"]);
|
|
986
|
+
return Array.isArray(raw) ? raw[0] : raw ?? "";
|
|
987
|
+
}
|
|
988
|
+
);
|
|
903
989
|
// Annotate the CommonJS export names for ESM import in node:
|
|
904
990
|
0 && (module.exports = {
|
|
991
|
+
ApiAuthGuard,
|
|
992
|
+
AuthUser,
|
|
993
|
+
B2BGuard,
|
|
994
|
+
B2CGuard,
|
|
905
995
|
BearerTokenGuard,
|
|
906
996
|
KEYCLOAK_CLIENT,
|
|
907
997
|
KEYCLOAK_CONFIG,
|