nestjs-keycloak-auth 1.0.0
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/LICENSE +21 -0
- package/README.md +391 -0
- package/dist/constants.d.ts +65 -0
- package/dist/constants.js +72 -0
- package/dist/controllers/keycloak-admin.controller.d.ts +16 -0
- package/dist/controllers/keycloak-admin.controller.js +94 -0
- package/dist/decorators/access-token.decorator.d.ts +5 -0
- package/dist/decorators/access-token.decorator.js +13 -0
- package/dist/decorators/enforcer-options.decorator.d.ts +8 -0
- package/dist/decorators/enforcer-options.decorator.js +12 -0
- package/dist/decorators/keycloak-user.decorator.d.ts +5 -0
- package/dist/decorators/keycloak-user.decorator.js +13 -0
- package/dist/decorators/public.decorator.d.ts +6 -0
- package/dist/decorators/public.decorator.js +11 -0
- package/dist/decorators/resource.decorator.d.ts +6 -0
- package/dist/decorators/resource.decorator.js +11 -0
- package/dist/decorators/roles.decorator.d.ts +10 -0
- package/dist/decorators/roles.decorator.js +15 -0
- package/dist/decorators/scopes.decorator.d.ts +19 -0
- package/dist/decorators/scopes.decorator.js +27 -0
- package/dist/decorators/token-scopes.decorator.d.ts +19 -0
- package/dist/decorators/token-scopes.decorator.js +24 -0
- package/dist/errors.d.ts +24 -0
- package/dist/errors.js +44 -0
- package/dist/guards/auth.guard.d.ts +25 -0
- package/dist/guards/auth.guard.js +176 -0
- package/dist/guards/resource.guard.d.ts +26 -0
- package/dist/guards/resource.guard.js +245 -0
- package/dist/guards/role.guard.d.ts +17 -0
- package/dist/guards/role.guard.js +113 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +18 -0
- package/dist/interface/enforcer-options.interface.d.ts +8 -0
- package/dist/interface/enforcer-options.interface.js +2 -0
- package/dist/interface/jwks.interface.d.ts +22 -0
- package/dist/interface/jwks.interface.js +2 -0
- package/dist/interface/jwt.interface.d.ts +46 -0
- package/dist/interface/jwt.interface.js +2 -0
- package/dist/interface/keycloak-auth-module-async-options.interface.d.ts +11 -0
- package/dist/interface/keycloak-auth-module-async-options.interface.js +2 -0
- package/dist/interface/keycloak-auth-options-factory.interface.d.ts +4 -0
- package/dist/interface/keycloak-auth-options-factory.interface.js +2 -0
- package/dist/interface/keycloak-auth-options.interface.d.ts +162 -0
- package/dist/interface/keycloak-auth-options.interface.js +3 -0
- package/dist/interface/keycloak-grant.interface.d.ts +33 -0
- package/dist/interface/keycloak-grant.interface.js +2 -0
- package/dist/interface/keycloak-request.interface.d.ts +8 -0
- package/dist/interface/keycloak-request.interface.js +2 -0
- package/dist/interface/oidc.interface.d.ts +14 -0
- package/dist/interface/oidc.interface.js +2 -0
- package/dist/interface/server.interface.d.ts +9 -0
- package/dist/interface/server.interface.js +2 -0
- package/dist/interface/tenant-config.interface.d.ts +13 -0
- package/dist/interface/tenant-config.interface.js +2 -0
- package/dist/internal.util.d.ts +13 -0
- package/dist/internal.util.js +54 -0
- package/dist/keycloak-auth.module.d.ts +54 -0
- package/dist/keycloak-auth.module.js +174 -0
- package/dist/keycloak-auth.providers.d.ts +7 -0
- package/dist/keycloak-auth.providers.js +122 -0
- package/dist/services/backchannel-logout.service.d.ts +30 -0
- package/dist/services/backchannel-logout.service.js +100 -0
- package/dist/services/jwks-cache.service.d.ts +25 -0
- package/dist/services/jwks-cache.service.js +130 -0
- package/dist/services/keycloak-admin.service.d.ts +37 -0
- package/dist/services/keycloak-admin.service.js +268 -0
- package/dist/services/keycloak-grant.service.d.ts +23 -0
- package/dist/services/keycloak-grant.service.js +88 -0
- package/dist/services/keycloak-http.service.d.ts +41 -0
- package/dist/services/keycloak-http.service.js +211 -0
- package/dist/services/keycloak-multitenant.service.d.ts +24 -0
- package/dist/services/keycloak-multitenant.service.js +161 -0
- package/dist/services/keycloak-url.service.d.ts +12 -0
- package/dist/services/keycloak-url.service.js +43 -0
- package/dist/services/oidc-discovery.service.d.ts +24 -0
- package/dist/services/oidc-discovery.service.js +86 -0
- package/dist/services/token-validation.service.d.ts +29 -0
- package/dist/services/token-validation.service.js +215 -0
- package/dist/token/keycloak-grant.d.ts +24 -0
- package/dist/token/keycloak-grant.js +37 -0
- package/dist/token/keycloak-token.d.ts +57 -0
- package/dist/token/keycloak-token.js +105 -0
- package/dist/types/conditional-scope.type.d.ts +2 -0
- package/dist/types/conditional-scope.type.js +2 -0
- package/dist/util.d.ts +3 -0
- package/dist/util.js +17 -0
- package/package.json +130 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
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;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
15
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
16
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
17
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
18
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
19
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
20
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
var ResourceGuard_1;
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.ResourceGuard = void 0;
|
|
26
|
+
const core_1 = require("@nestjs/core");
|
|
27
|
+
const keycloak_token_1 = require("../token/keycloak-token");
|
|
28
|
+
const public_decorator_1 = require("../decorators/public.decorator");
|
|
29
|
+
const resource_decorator_1 = require("../decorators/resource.decorator");
|
|
30
|
+
const internal_util_1 = require("../internal.util");
|
|
31
|
+
const keycloak_http_service_1 = require("../services/keycloak-http.service");
|
|
32
|
+
const enforcer_options_decorator_1 = require("../decorators/enforcer-options.decorator");
|
|
33
|
+
const keycloak_multitenant_service_1 = require("../services/keycloak-multitenant.service");
|
|
34
|
+
const common_1 = require("@nestjs/common");
|
|
35
|
+
const scopes_decorator_1 = require("../decorators/scopes.decorator");
|
|
36
|
+
const constants_1 = require("../constants");
|
|
37
|
+
/**
|
|
38
|
+
* This adds a resource guard, which is policy enforcement by default is permissive.
|
|
39
|
+
* Only controllers annotated with `@Resource` and methods with `@Scopes`
|
|
40
|
+
* are handled by this guard.
|
|
41
|
+
*/
|
|
42
|
+
let ResourceGuard = ResourceGuard_1 = class ResourceGuard {
|
|
43
|
+
constructor(singleTenant, keycloakOpts, multiTenant, keycloakHttp) {
|
|
44
|
+
this.singleTenant = singleTenant;
|
|
45
|
+
this.keycloakOpts = keycloakOpts;
|
|
46
|
+
this.multiTenant = multiTenant;
|
|
47
|
+
this.keycloakHttp = keycloakHttp;
|
|
48
|
+
this.logger = new common_1.Logger(ResourceGuard_1.name);
|
|
49
|
+
this.reflector = new core_1.Reflector();
|
|
50
|
+
}
|
|
51
|
+
canActivate(context) {
|
|
52
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
53
|
+
var _a, _b, _c, _d, _e;
|
|
54
|
+
const resource = this.reflector.get(resource_decorator_1.META_RESOURCE, context.getClass());
|
|
55
|
+
const explicitScopes = (_a = this.reflector.get(scopes_decorator_1.META_SCOPES, context.getHandler())) !== null && _a !== void 0 ? _a : [];
|
|
56
|
+
const conditionalScopes = this.reflector.get(scopes_decorator_1.META_CONDITIONAL_SCOPES, context.getHandler());
|
|
57
|
+
const isPublic = this.reflector.getAllAndOverride(public_decorator_1.META_PUBLIC, [
|
|
58
|
+
context.getClass(),
|
|
59
|
+
context.getHandler(),
|
|
60
|
+
]);
|
|
61
|
+
const enforcerOptions = this.reflector.getAllAndOverride(enforcer_options_decorator_1.META_ENFORCER_OPTIONS, [context.getClass(), context.getHandler()]);
|
|
62
|
+
// Default to permissive
|
|
63
|
+
const policyEnforcementMode = this.keycloakOpts.policyEnforcement || constants_1.PolicyEnforcementMode.PERMISSIVE;
|
|
64
|
+
const shouldAllow = policyEnforcementMode === constants_1.PolicyEnforcementMode.PERMISSIVE;
|
|
65
|
+
// Extract request/response
|
|
66
|
+
const [request] = (0, internal_util_1.extractRequest)(context);
|
|
67
|
+
// if is not an HTTP request ignore this guard
|
|
68
|
+
if (!request) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
if (!request.user && isPublic) {
|
|
72
|
+
this.logger.verbose('Route has no user, and is public, allowed');
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
const tenantConfig = yield (0, internal_util_1.useTenantConfig)(request, request.accessToken, this.singleTenant, this.multiTenant, this.keycloakOpts);
|
|
76
|
+
// No resource given, check policy enforcement mode
|
|
77
|
+
if (!resource) {
|
|
78
|
+
if (shouldAllow) {
|
|
79
|
+
this.logger.verbose('Controller has no @Resource defined, request allowed due to policy enforcement');
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
this.logger.verbose('Controller has no @Resource defined, request denied due to policy enforcement');
|
|
83
|
+
}
|
|
84
|
+
return shouldAllow;
|
|
85
|
+
}
|
|
86
|
+
// Build the required scopes
|
|
87
|
+
let token;
|
|
88
|
+
if (conditionalScopes) {
|
|
89
|
+
token = new keycloak_token_1.KeycloakToken(request.accessToken, tenantConfig.clientId);
|
|
90
|
+
}
|
|
91
|
+
const conditionalScopesResult = conditionalScopes
|
|
92
|
+
? conditionalScopes(request, token)
|
|
93
|
+
: [];
|
|
94
|
+
const scopes = [...explicitScopes, ...conditionalScopesResult];
|
|
95
|
+
// Attach resolved scopes
|
|
96
|
+
request.scopes = scopes;
|
|
97
|
+
// No scopes given, check policy enforcement mode
|
|
98
|
+
if (!scopes || scopes.length === 0) {
|
|
99
|
+
if (shouldAllow) {
|
|
100
|
+
this.logger.verbose('Route has no @Scope/@ConditionalScopes defined, request allowed due to policy enforcement');
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
this.logger.verbose('Route has no @Scope/@ConditionalScopes defined, request denied due to policy enforcement');
|
|
104
|
+
}
|
|
105
|
+
return shouldAllow;
|
|
106
|
+
}
|
|
107
|
+
this.logger.verbose(`Protecting resource [ ${resource} ] with scopes: [ ${scopes} ]`);
|
|
108
|
+
const user = (_c = (_b = request.user) === null || _b === void 0 ? void 0 : _b.preferred_username) !== null && _c !== void 0 ? _c : 'user';
|
|
109
|
+
// Build permissions as "resource#scope" pairs (Keycloak UMA format)
|
|
110
|
+
const permissions = scopes.map((scope) => `${resource}#${scope}`);
|
|
111
|
+
// Local permission check: if the access token already contains the
|
|
112
|
+
// required permissions, allow immediately without a server round-trip
|
|
113
|
+
// (matches keycloak-connect enforcer.js behavior)
|
|
114
|
+
if (request.accessToken) {
|
|
115
|
+
const accessToken = token !== null && token !== void 0 ? token : new keycloak_token_1.KeycloakToken(request.accessToken, tenantConfig.clientId);
|
|
116
|
+
const allPermissionsGranted = scopes.every((scope) => accessToken.hasPermission(resource, scope));
|
|
117
|
+
if (allPermissionsGranted) {
|
|
118
|
+
this.logger.verbose(`Resource [ ${resource} ] granted to [ ${user} ] (local token check)`);
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// Server-side permission check
|
|
123
|
+
const claims = this.resolveClaims(request, enforcerOptions);
|
|
124
|
+
const responseMode = (_d = enforcerOptions === null || enforcerOptions === void 0 ? void 0 : enforcerOptions.response_mode) !== null && _d !== void 0 ? _d : 'permissions';
|
|
125
|
+
const audience = (_e = enforcerOptions === null || enforcerOptions === void 0 ? void 0 : enforcerOptions.resource_server_id) !== null && _e !== void 0 ? _e : tenantConfig.clientId;
|
|
126
|
+
const isPublicClient = tenantConfig.isPublic;
|
|
127
|
+
if (responseMode === 'permissions') {
|
|
128
|
+
// Permissions mode: get permission list from server, validate locally
|
|
129
|
+
try {
|
|
130
|
+
const serverPermissions = (yield this.keycloakHttp.checkPermission(tenantConfig.realmUrl, tenantConfig.clientId, tenantConfig.secret, request.accessToken, permissions, {
|
|
131
|
+
claims: claims || undefined,
|
|
132
|
+
response_mode: 'permissions',
|
|
133
|
+
audience,
|
|
134
|
+
isPublic: isPublicClient,
|
|
135
|
+
}));
|
|
136
|
+
const isAllowed = this.validatePermissionsLocally(serverPermissions, resource, scopes);
|
|
137
|
+
if (isAllowed) {
|
|
138
|
+
// Attach permissions to request (matches original enforcer.js)
|
|
139
|
+
request.permissions = serverPermissions;
|
|
140
|
+
this.logger.verbose(`Resource [ ${resource} ] granted to [ ${user} ]`);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
this.logger.verbose(`Resource [ ${resource} ] denied to [ ${user} ]`);
|
|
144
|
+
}
|
|
145
|
+
return isAllowed;
|
|
146
|
+
}
|
|
147
|
+
catch (_f) {
|
|
148
|
+
this.logger.verbose(`Resource [ ${resource} ] denied to [ ${user} ] (permissions check failed)`);
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (responseMode === 'token') {
|
|
153
|
+
// Token mode: get a new grant with permissions, validate locally
|
|
154
|
+
try {
|
|
155
|
+
const grant = (yield this.keycloakHttp.checkPermission(tenantConfig.realmUrl, tenantConfig.clientId, tenantConfig.secret, request.accessToken, permissions, {
|
|
156
|
+
claims: claims || undefined,
|
|
157
|
+
response_mode: 'token',
|
|
158
|
+
audience,
|
|
159
|
+
isPublic: isPublicClient,
|
|
160
|
+
}));
|
|
161
|
+
const grantToken = new keycloak_token_1.KeycloakToken(grant.access_token, tenantConfig.clientId);
|
|
162
|
+
const isAllowed = scopes.every((scope) => grantToken.hasPermission(resource, scope));
|
|
163
|
+
if (isAllowed) {
|
|
164
|
+
this.logger.verbose(`Resource [ ${resource} ] granted to [ ${user} ]`);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
this.logger.verbose(`Resource [ ${resource} ] denied to [ ${user} ]`);
|
|
168
|
+
}
|
|
169
|
+
return isAllowed;
|
|
170
|
+
}
|
|
171
|
+
catch (_g) {
|
|
172
|
+
this.logger.verbose(`Resource [ ${resource} ] denied to [ ${user} ] (token check failed)`);
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// Default: decision mode
|
|
177
|
+
const isAllowed = (yield this.keycloakHttp.checkPermission(tenantConfig.realmUrl, tenantConfig.clientId, tenantConfig.secret, request.accessToken, permissions, {
|
|
178
|
+
claims: claims || undefined,
|
|
179
|
+
response_mode: 'decision',
|
|
180
|
+
audience,
|
|
181
|
+
isPublic: isPublicClient,
|
|
182
|
+
}));
|
|
183
|
+
// If statement for verbose logging only
|
|
184
|
+
if (!isAllowed) {
|
|
185
|
+
this.logger.verbose(`Resource [ ${resource} ] denied to [ ${user} ]`);
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
this.logger.verbose(`Resource [ ${resource} ] granted to [ ${user} ]`);
|
|
189
|
+
}
|
|
190
|
+
return isAllowed;
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Validate server-returned permissions against expected resource:scope pairs.
|
|
195
|
+
* Matches keycloak-connect enforcer.js handlePermissions logic.
|
|
196
|
+
*/
|
|
197
|
+
validatePermissionsLocally(serverPermissions, resource, scopes) {
|
|
198
|
+
if (!serverPermissions || serverPermissions.length === 0) {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
for (const scope of scopes) {
|
|
202
|
+
let found = false;
|
|
203
|
+
for (const permission of serverPermissions) {
|
|
204
|
+
if (permission.rsid === resource || permission.rsname === resource) {
|
|
205
|
+
if (scope) {
|
|
206
|
+
if (permission.scopes && permission.scopes.length > 0) {
|
|
207
|
+
if (!permission.scopes.includes(scope)) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
found = true;
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
found = true;
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (!found) {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
resolveClaims(request, enforcerOptions) {
|
|
226
|
+
var _a;
|
|
227
|
+
if (!enforcerOptions) {
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
230
|
+
const claims = (_a = enforcerOptions.claims) === null || _a === void 0 ? void 0 : _a.call(enforcerOptions, request);
|
|
231
|
+
if (claims) {
|
|
232
|
+
this.logger.verbose(`Enforcing claims, keys: ${Object.keys(claims).join(', ')}`);
|
|
233
|
+
}
|
|
234
|
+
return claims;
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
exports.ResourceGuard = ResourceGuard;
|
|
238
|
+
exports.ResourceGuard = ResourceGuard = ResourceGuard_1 = __decorate([
|
|
239
|
+
(0, common_1.Injectable)(),
|
|
240
|
+
__param(0, (0, common_1.Inject)(constants_1.KEYCLOAK_INSTANCE)),
|
|
241
|
+
__param(1, (0, common_1.Inject)(constants_1.KEYCLOAK_AUTH_OPTIONS)),
|
|
242
|
+
__param(2, (0, common_1.Inject)(constants_1.KEYCLOAK_MULTITENANT_SERVICE)),
|
|
243
|
+
__metadata("design:paramtypes", [Object, Object, keycloak_multitenant_service_1.KeycloakMultiTenantService,
|
|
244
|
+
keycloak_http_service_1.KeycloakHttpService])
|
|
245
|
+
], ResourceGuard);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ResolvedTenantConfig } from '../interface/tenant-config.interface';
|
|
2
|
+
import { KeycloakAuthConfig } from '../interface/keycloak-auth-options.interface';
|
|
3
|
+
import { KeycloakMultiTenantService } from '../services/keycloak-multitenant.service';
|
|
4
|
+
import { CanActivate, ExecutionContext } from '@nestjs/common';
|
|
5
|
+
/**
|
|
6
|
+
* A permissive type of role guard. Roles are set via `@Roles` decorator.
|
|
7
|
+
* @since 1.1.0
|
|
8
|
+
*/
|
|
9
|
+
export declare class RoleGuard implements CanActivate {
|
|
10
|
+
private singleTenant;
|
|
11
|
+
private keycloakOpts;
|
|
12
|
+
private multiTenant;
|
|
13
|
+
private readonly logger;
|
|
14
|
+
private readonly reflector;
|
|
15
|
+
constructor(singleTenant: ResolvedTenantConfig, keycloakOpts: KeycloakAuthConfig, multiTenant: KeycloakMultiTenantService);
|
|
16
|
+
canActivate(context: ExecutionContext): Promise<boolean>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
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;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
15
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
16
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
17
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
18
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
19
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
20
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
var RoleGuard_1;
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.RoleGuard = void 0;
|
|
26
|
+
const core_1 = require("@nestjs/core");
|
|
27
|
+
const errors_1 = require("../errors");
|
|
28
|
+
const keycloak_token_1 = require("../token/keycloak-token");
|
|
29
|
+
const internal_util_1 = require("../internal.util");
|
|
30
|
+
const roles_decorator_1 = require("../decorators/roles.decorator");
|
|
31
|
+
const keycloak_multitenant_service_1 = require("../services/keycloak-multitenant.service");
|
|
32
|
+
const common_1 = require("@nestjs/common");
|
|
33
|
+
const constants_1 = require("../constants");
|
|
34
|
+
/**
|
|
35
|
+
* A permissive type of role guard. Roles are set via `@Roles` decorator.
|
|
36
|
+
* @since 1.1.0
|
|
37
|
+
*/
|
|
38
|
+
let RoleGuard = RoleGuard_1 = class RoleGuard {
|
|
39
|
+
constructor(singleTenant, keycloakOpts, multiTenant) {
|
|
40
|
+
this.singleTenant = singleTenant;
|
|
41
|
+
this.keycloakOpts = keycloakOpts;
|
|
42
|
+
this.multiTenant = multiTenant;
|
|
43
|
+
this.logger = new common_1.Logger(RoleGuard_1.name);
|
|
44
|
+
this.reflector = new core_1.Reflector();
|
|
45
|
+
}
|
|
46
|
+
canActivate(context) {
|
|
47
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
48
|
+
const roleMerge = this.keycloakOpts.roleMerge
|
|
49
|
+
? this.keycloakOpts.roleMerge
|
|
50
|
+
: constants_1.RoleMerge.OVERRIDE;
|
|
51
|
+
const roles = [];
|
|
52
|
+
const matchingMode = this.reflector.getAllAndOverride(roles_decorator_1.META_ROLE_MATCHING_MODE, [context.getClass(), context.getHandler()]);
|
|
53
|
+
if (roleMerge == constants_1.RoleMerge.ALL) {
|
|
54
|
+
const mergedRoles = this.reflector.getAllAndMerge(roles_decorator_1.META_ROLES, [
|
|
55
|
+
context.getClass(),
|
|
56
|
+
context.getHandler(),
|
|
57
|
+
]);
|
|
58
|
+
if (mergedRoles) {
|
|
59
|
+
roles.push(...mergedRoles);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else if (roleMerge == constants_1.RoleMerge.OVERRIDE) {
|
|
63
|
+
const resultRoles = this.reflector.getAllAndOverride(roles_decorator_1.META_ROLES, [context.getClass(), context.getHandler()]);
|
|
64
|
+
if (resultRoles) {
|
|
65
|
+
roles.push(...resultRoles);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
throw new errors_1.KeycloakConfigError(`Unknown role merge: ${roleMerge}`);
|
|
70
|
+
}
|
|
71
|
+
if (roles.length === 0) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
const roleMatchingMode = matchingMode !== null && matchingMode !== void 0 ? matchingMode : constants_1.RoleMatch.ANY;
|
|
75
|
+
this.logger.verbose(`Using matching mode: ${roleMatchingMode}`, { roles });
|
|
76
|
+
// Extract request
|
|
77
|
+
const [request] = (0, internal_util_1.extractRequest)(context);
|
|
78
|
+
// if is not an HTTP request ignore this guard
|
|
79
|
+
if (!request) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
const { accessToken } = request;
|
|
83
|
+
if (!accessToken) {
|
|
84
|
+
// No access token attached, auth guard should have attached the necessary token
|
|
85
|
+
this.logger.warn('No access token found in request, are you sure AuthGuard is first in the chain?');
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
// Resolve tenant config to get clientId
|
|
89
|
+
const tenantConfig = yield (0, internal_util_1.useTenantConfig)(request, accessToken, this.singleTenant, this.multiTenant, this.keycloakOpts);
|
|
90
|
+
// Use native KeycloakToken for role checking — pure in-memory, no HTTP
|
|
91
|
+
const token = new keycloak_token_1.KeycloakToken(accessToken, tenantConfig.clientId);
|
|
92
|
+
// For verbose logging, we store it instead of returning it immediately
|
|
93
|
+
const granted = roleMatchingMode === constants_1.RoleMatch.ANY
|
|
94
|
+
? roles.some((r) => token.hasRole(r))
|
|
95
|
+
: roles.every((r) => token.hasRole(r));
|
|
96
|
+
if (granted) {
|
|
97
|
+
this.logger.verbose('Resource granted due to role(s)');
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
this.logger.verbose('Resource denied due to mismatched role(s)');
|
|
101
|
+
}
|
|
102
|
+
return granted;
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
exports.RoleGuard = RoleGuard;
|
|
107
|
+
exports.RoleGuard = RoleGuard = RoleGuard_1 = __decorate([
|
|
108
|
+
(0, common_1.Injectable)(),
|
|
109
|
+
__param(0, (0, common_1.Inject)(constants_1.KEYCLOAK_INSTANCE)),
|
|
110
|
+
__param(1, (0, common_1.Inject)(constants_1.KEYCLOAK_AUTH_OPTIONS)),
|
|
111
|
+
__param(2, (0, common_1.Inject)(constants_1.KEYCLOAK_MULTITENANT_SERVICE)),
|
|
112
|
+
__metadata("design:paramtypes", [Object, Object, keycloak_multitenant_service_1.KeycloakMultiTenantService])
|
|
113
|
+
], RoleGuard);
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './keycloak-auth.module';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
// public-api
|
|
18
|
+
__exportStar(require("./keycloak-auth.module"), exports);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface JwksKey {
|
|
2
|
+
kid: string;
|
|
3
|
+
kty: string;
|
|
4
|
+
alg?: string;
|
|
5
|
+
use?: string;
|
|
6
|
+
n?: string;
|
|
7
|
+
e?: string;
|
|
8
|
+
x5c?: string[];
|
|
9
|
+
x5t?: string;
|
|
10
|
+
'x5t#S256'?: string;
|
|
11
|
+
crv?: string;
|
|
12
|
+
x?: string;
|
|
13
|
+
y?: string;
|
|
14
|
+
[key: string]: unknown;
|
|
15
|
+
}
|
|
16
|
+
export interface JwksResponse {
|
|
17
|
+
keys: JwksKey[];
|
|
18
|
+
}
|
|
19
|
+
export interface CachedJwks {
|
|
20
|
+
keys: Map<string, JwksKey>;
|
|
21
|
+
fetchedAt: number;
|
|
22
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export interface JwtHeader {
|
|
2
|
+
alg?: string;
|
|
3
|
+
typ?: string;
|
|
4
|
+
kid?: string;
|
|
5
|
+
[key: string]: unknown;
|
|
6
|
+
}
|
|
7
|
+
export interface JwtPermission {
|
|
8
|
+
rsid?: string;
|
|
9
|
+
rsname?: string;
|
|
10
|
+
scopes?: string[];
|
|
11
|
+
}
|
|
12
|
+
export interface JwtContent {
|
|
13
|
+
exp: number;
|
|
14
|
+
iat?: number;
|
|
15
|
+
typ?: string;
|
|
16
|
+
iss?: string;
|
|
17
|
+
aud?: string | string[];
|
|
18
|
+
azp?: string;
|
|
19
|
+
sub?: string;
|
|
20
|
+
preferred_username?: string;
|
|
21
|
+
realm_access?: {
|
|
22
|
+
roles: string[];
|
|
23
|
+
};
|
|
24
|
+
resource_access?: Record<string, {
|
|
25
|
+
roles: string[];
|
|
26
|
+
}>;
|
|
27
|
+
authorization?: {
|
|
28
|
+
permissions: JwtPermission[];
|
|
29
|
+
};
|
|
30
|
+
scope?: string;
|
|
31
|
+
sid?: string;
|
|
32
|
+
events?: Record<string, unknown>;
|
|
33
|
+
action?: string;
|
|
34
|
+
adapterSessionIds?: string[];
|
|
35
|
+
notBefore?: number;
|
|
36
|
+
[key: string]: unknown;
|
|
37
|
+
}
|
|
38
|
+
export interface JwtPayload {
|
|
39
|
+
iss?: string;
|
|
40
|
+
sub?: string;
|
|
41
|
+
aud?: string | string[];
|
|
42
|
+
exp?: number;
|
|
43
|
+
iat?: number;
|
|
44
|
+
preferred_username?: string;
|
|
45
|
+
[key: string]: unknown;
|
|
46
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ModuleMetadata, Type } from '@nestjs/common/interfaces';
|
|
2
|
+
import { KeycloakAuthOptions } from './keycloak-auth-options.interface';
|
|
3
|
+
import { KeycloakAuthOptionsFactory } from './keycloak-auth-options-factory.interface';
|
|
4
|
+
import { InjectionToken } from '@nestjs/common/interfaces/modules/injection-token.interface';
|
|
5
|
+
import { OptionalFactoryDependency } from '@nestjs/common/interfaces/modules/optional-factory-dependency.interface';
|
|
6
|
+
export interface KeycloakAuthModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
|
|
7
|
+
inject?: Array<InjectionToken | OptionalFactoryDependency>;
|
|
8
|
+
useExisting?: Type<KeycloakAuthOptionsFactory>;
|
|
9
|
+
useClass?: Type<KeycloakAuthOptionsFactory>;
|
|
10
|
+
useFactory?: (...args: unknown[]) => Promise<KeycloakAuthOptions> | KeycloakAuthOptions;
|
|
11
|
+
}
|