@clemax/nest-keycloak-connect 2.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 +298 -0
- package/dist/constants.d.ts +69 -0
- package/dist/constants.js +76 -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 +7 -0
- package/dist/decorators/public.decorator.js +12 -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 +28 -0
- package/dist/guards/auth.guard.d.ts +19 -0
- package/dist/guards/auth.guard.js +172 -0
- package/dist/guards/resource.guard.d.ts +18 -0
- package/dist/guards/resource.guard.js +175 -0
- package/dist/guards/role.guard.d.ts +17 -0
- package/dist/guards/role.guard.js +139 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +18 -0
- package/dist/interface/keycloak-connect-module-async-options.interface.d.ts +9 -0
- package/dist/interface/keycloak-connect-module-async-options.interface.js +2 -0
- package/dist/interface/keycloak-connect-options-factory.interface.d.ts +4 -0
- package/dist/interface/keycloak-connect-options-factory.interface.js +2 -0
- package/dist/interface/keycloak-connect-options.interface.d.ts +166 -0
- package/dist/interface/keycloak-connect-options.interface.js +3 -0
- package/dist/internal.util.d.ts +8 -0
- package/dist/internal.util.js +69 -0
- package/dist/keycloak-connect.module.d.ts +32 -0
- package/dist/keycloak-connect.module.js +126 -0
- package/dist/keycloak-connect.providers.d.ts +7 -0
- package/dist/keycloak-connect.providers.js +81 -0
- package/dist/services/keycloak-multitenant.service.d.ts +24 -0
- package/dist/services/keycloak-multitenant.service.js +174 -0
- package/dist/util.d.ts +1 -0
- package/dist/util.js +8 -0
- package/package.json +100 -0
|
@@ -0,0 +1,172 @@
|
|
|
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 __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
19
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
20
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
21
|
+
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;
|
|
22
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
23
|
+
};
|
|
24
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
25
|
+
if (mod && mod.__esModule) return mod;
|
|
26
|
+
var result = {};
|
|
27
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
28
|
+
__setModuleDefault(result, mod);
|
|
29
|
+
return result;
|
|
30
|
+
};
|
|
31
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
32
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
33
|
+
};
|
|
34
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
35
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
36
|
+
};
|
|
37
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
38
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
39
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
40
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
41
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
42
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
43
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
var AuthGuard_1;
|
|
47
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
+
exports.AuthGuard = void 0;
|
|
49
|
+
const common_1 = require("@nestjs/common");
|
|
50
|
+
const core_1 = require("@nestjs/core");
|
|
51
|
+
const KeycloakConnect = __importStar(require("keycloak-connect"));
|
|
52
|
+
const constants_1 = require("../constants");
|
|
53
|
+
const public_decorator_1 = require("../decorators/public.decorator");
|
|
54
|
+
const internal_util_1 = require("../internal.util");
|
|
55
|
+
const keycloak_multitenant_service_1 = require("../services/keycloak-multitenant.service");
|
|
56
|
+
const util_1 = require("../util");
|
|
57
|
+
/**
|
|
58
|
+
* An authentication guard. Will return a 401 unauthorized when it is unable to
|
|
59
|
+
* verify the JWT token or Bearer header is missing.
|
|
60
|
+
*/
|
|
61
|
+
let AuthGuard = AuthGuard_1 = class AuthGuard {
|
|
62
|
+
constructor(singleTenant, keycloakOpts, multiTenant) {
|
|
63
|
+
this.singleTenant = singleTenant;
|
|
64
|
+
this.keycloakOpts = keycloakOpts;
|
|
65
|
+
this.multiTenant = multiTenant;
|
|
66
|
+
this.logger = new common_1.Logger(AuthGuard_1.name);
|
|
67
|
+
this.reflector = new core_1.Reflector();
|
|
68
|
+
}
|
|
69
|
+
canActivate(context) {
|
|
70
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
71
|
+
const isPublic = this.reflector.getAllAndOverride(public_decorator_1.META_PUBLIC, [
|
|
72
|
+
context.getClass(),
|
|
73
|
+
context.getHandler(),
|
|
74
|
+
]);
|
|
75
|
+
// Extract request/response
|
|
76
|
+
const cookieKey = this.keycloakOpts.cookieKey || constants_1.KEYCLOAK_COOKIE_DEFAULT;
|
|
77
|
+
const [request] = (0, internal_util_1.extractRequestAndAttachCookie)(context, cookieKey);
|
|
78
|
+
// if is not an HTTP request ignore this guard
|
|
79
|
+
if (!request) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
const jwt = this.extractJwt(request.headers);
|
|
83
|
+
const isJwtEmpty = jwt === null || jwt === undefined;
|
|
84
|
+
// Not a public route, require jwt
|
|
85
|
+
if (!isPublic && isJwtEmpty) {
|
|
86
|
+
this.logger.verbose('Empty jwt, unauthorized');
|
|
87
|
+
throw new common_1.UnauthorizedException();
|
|
88
|
+
}
|
|
89
|
+
// Public route, no jwt sent
|
|
90
|
+
if (isPublic && isJwtEmpty) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
this.logger.verbose(`Validating jwt`, { jwt });
|
|
94
|
+
const keycloak = yield (0, internal_util_1.useKeycloak)(request, jwt, this.singleTenant, this.multiTenant, this.keycloakOpts);
|
|
95
|
+
const isValidToken = yield this.validateToken(keycloak, jwt);
|
|
96
|
+
if (isValidToken) {
|
|
97
|
+
// Attach user info object
|
|
98
|
+
request.user = (0, util_1.parseToken)(jwt);
|
|
99
|
+
// Attach raw access token JWT extracted from bearer/cookie
|
|
100
|
+
request.accessToken = jwt;
|
|
101
|
+
this.logger.verbose(`User authenticated`, { user: request.user });
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
// Valid token should return, this time we warn
|
|
105
|
+
if (isPublic) {
|
|
106
|
+
this.logger.warn(`A jwt token was retrieved but failed validation.`, {
|
|
107
|
+
jwt,
|
|
108
|
+
});
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
throw new common_1.UnauthorizedException();
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
validateToken(keycloak, jwt) {
|
|
115
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
116
|
+
const tokenValidation = this.keycloakOpts.tokenValidation || constants_1.TokenValidation.ONLINE;
|
|
117
|
+
const gm = keycloak.grantManager;
|
|
118
|
+
let grant;
|
|
119
|
+
try {
|
|
120
|
+
grant = yield gm.createGrant({ access_token: jwt });
|
|
121
|
+
}
|
|
122
|
+
catch (ex) {
|
|
123
|
+
this.logger.warn(`Cannot validate access token: ${ex}`);
|
|
124
|
+
// It will fail to create grants on invalid access token (i.e expired or wrong domain)
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
const token = grant.access_token;
|
|
128
|
+
this.logger.verbose(`Using token validation method: ${tokenValidation.toUpperCase()}`);
|
|
129
|
+
try {
|
|
130
|
+
let result;
|
|
131
|
+
switch (tokenValidation) {
|
|
132
|
+
case constants_1.TokenValidation.ONLINE:
|
|
133
|
+
result = yield gm.validateAccessToken(token);
|
|
134
|
+
return result === token;
|
|
135
|
+
case constants_1.TokenValidation.OFFLINE:
|
|
136
|
+
result = yield gm.validateToken(token, 'Bearer');
|
|
137
|
+
return result === token;
|
|
138
|
+
case constants_1.TokenValidation.NONE:
|
|
139
|
+
return true;
|
|
140
|
+
default:
|
|
141
|
+
this.logger.warn(`Unknown validation method: ${tokenValidation}`);
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch (ex) {
|
|
146
|
+
this.logger.warn(`Cannot validate access token: ${ex}`);
|
|
147
|
+
}
|
|
148
|
+
return false;
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
extractJwt(headers) {
|
|
152
|
+
if (headers && !headers.authorization) {
|
|
153
|
+
this.logger.verbose(`No authorization header`);
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
const auth = headers.authorization.split(' ');
|
|
157
|
+
// We only allow bearer
|
|
158
|
+
if (auth[0].toLowerCase() !== 'bearer') {
|
|
159
|
+
this.logger.verbose(`No bearer header`);
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
return auth[1];
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
exports.AuthGuard = AuthGuard;
|
|
166
|
+
exports.AuthGuard = AuthGuard = AuthGuard_1 = __decorate([
|
|
167
|
+
(0, common_1.Injectable)(),
|
|
168
|
+
__param(0, (0, common_1.Inject)(constants_1.KEYCLOAK_INSTANCE)),
|
|
169
|
+
__param(1, (0, common_1.Inject)(constants_1.KEYCLOAK_CONNECT_OPTIONS)),
|
|
170
|
+
__param(2, (0, common_1.Inject)(constants_1.KEYCLOAK_MULTITENANT_SERVICE)),
|
|
171
|
+
__metadata("design:paramtypes", [Object, Object, keycloak_multitenant_service_1.KeycloakMultiTenantService])
|
|
172
|
+
], AuthGuard);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { CanActivate, ExecutionContext } from '@nestjs/common';
|
|
2
|
+
import * as KeycloakConnect from 'keycloak-connect';
|
|
3
|
+
import { KeycloakConnectConfig } from '../interface/keycloak-connect-options.interface';
|
|
4
|
+
import { KeycloakMultiTenantService } from '../services/keycloak-multitenant.service';
|
|
5
|
+
/**
|
|
6
|
+
* This adds a resource guard, which is policy enforcement by default is permissive.
|
|
7
|
+
* Only controllers annotated with `@Resource` and methods with `@Scopes`
|
|
8
|
+
* are handled by this guard.
|
|
9
|
+
*/
|
|
10
|
+
export declare class ResourceGuard implements CanActivate {
|
|
11
|
+
private singleTenant;
|
|
12
|
+
private keycloakOpts;
|
|
13
|
+
private multiTenant;
|
|
14
|
+
private readonly logger;
|
|
15
|
+
private readonly reflector;
|
|
16
|
+
constructor(singleTenant: KeycloakConnect.Keycloak, keycloakOpts: KeycloakConnectConfig, multiTenant: KeycloakMultiTenantService);
|
|
17
|
+
canActivate(context: ExecutionContext): Promise<boolean>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
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 __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
19
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
20
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
21
|
+
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;
|
|
22
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
23
|
+
};
|
|
24
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
25
|
+
if (mod && mod.__esModule) return mod;
|
|
26
|
+
var result = {};
|
|
27
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
28
|
+
__setModuleDefault(result, mod);
|
|
29
|
+
return result;
|
|
30
|
+
};
|
|
31
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
32
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
33
|
+
};
|
|
34
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
35
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
36
|
+
};
|
|
37
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
38
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
39
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
40
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
41
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
42
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
43
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
var ResourceGuard_1;
|
|
47
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
+
exports.ResourceGuard = void 0;
|
|
49
|
+
const common_1 = require("@nestjs/common");
|
|
50
|
+
const core_1 = require("@nestjs/core");
|
|
51
|
+
const KeycloakConnect = __importStar(require("keycloak-connect"));
|
|
52
|
+
const constants_1 = require("../constants");
|
|
53
|
+
const enforcer_options_decorator_1 = require("../decorators/enforcer-options.decorator");
|
|
54
|
+
const public_decorator_1 = require("../decorators/public.decorator");
|
|
55
|
+
const resource_decorator_1 = require("../decorators/resource.decorator");
|
|
56
|
+
const scopes_decorator_1 = require("../decorators/scopes.decorator");
|
|
57
|
+
const internal_util_1 = require("../internal.util");
|
|
58
|
+
const keycloak_multitenant_service_1 = require("../services/keycloak-multitenant.service");
|
|
59
|
+
/**
|
|
60
|
+
* This adds a resource guard, which is policy enforcement by default is permissive.
|
|
61
|
+
* Only controllers annotated with `@Resource` and methods with `@Scopes`
|
|
62
|
+
* are handled by this guard.
|
|
63
|
+
*/
|
|
64
|
+
let ResourceGuard = ResourceGuard_1 = class ResourceGuard {
|
|
65
|
+
constructor(singleTenant, keycloakOpts, multiTenant) {
|
|
66
|
+
this.singleTenant = singleTenant;
|
|
67
|
+
this.keycloakOpts = keycloakOpts;
|
|
68
|
+
this.multiTenant = multiTenant;
|
|
69
|
+
this.logger = new common_1.Logger(ResourceGuard_1.name);
|
|
70
|
+
this.reflector = new core_1.Reflector();
|
|
71
|
+
}
|
|
72
|
+
canActivate(context) {
|
|
73
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
74
|
+
var _a, _b, _c, _d;
|
|
75
|
+
const defaultEnforcerOpts = {
|
|
76
|
+
claims: (request) => {
|
|
77
|
+
const httpUri = request.url;
|
|
78
|
+
const userAgent = request.headers['user-agent'];
|
|
79
|
+
this.logger.verbose(`Enforcing claims, http.uri: ${httpUri}, user.agent: ${userAgent}`);
|
|
80
|
+
return {
|
|
81
|
+
'http.uri': [httpUri],
|
|
82
|
+
'user.agent': [userAgent],
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
const resource = this.reflector.get(resource_decorator_1.META_RESOURCE, context.getClass());
|
|
87
|
+
const explicitScopes = (_a = this.reflector.get(scopes_decorator_1.META_SCOPES, context.getHandler())) !== null && _a !== void 0 ? _a : [];
|
|
88
|
+
const conditionalScopes = this.reflector.get(scopes_decorator_1.META_CONDITIONAL_SCOPES, context.getHandler());
|
|
89
|
+
const isPublic = this.reflector.getAllAndOverride(public_decorator_1.META_PUBLIC, [
|
|
90
|
+
context.getClass(),
|
|
91
|
+
context.getHandler(),
|
|
92
|
+
]);
|
|
93
|
+
const enforcerOpts = (_b = this.reflector.getAllAndOverride(enforcer_options_decorator_1.META_ENFORCER_OPTIONS, [context.getClass(), context.getHandler()])) !== null && _b !== void 0 ? _b : defaultEnforcerOpts;
|
|
94
|
+
// Default to permissive
|
|
95
|
+
const policyEnforcementMode = this.keycloakOpts.policyEnforcement || constants_1.PolicyEnforcementMode.PERMISSIVE;
|
|
96
|
+
const shouldAllow = policyEnforcementMode === constants_1.PolicyEnforcementMode.PERMISSIVE;
|
|
97
|
+
// Extract request/response
|
|
98
|
+
const cookieKey = this.keycloakOpts.cookieKey || constants_1.KEYCLOAK_COOKIE_DEFAULT;
|
|
99
|
+
const [request, response] = (0, internal_util_1.extractRequestAndAttachCookie)(context, cookieKey);
|
|
100
|
+
// if is not an HTTP request ignore this guard
|
|
101
|
+
if (!request) {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
if (!request.user && isPublic) {
|
|
105
|
+
this.logger.verbose(`Route has no user, and is public, allowed`);
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
const keycloak = yield (0, internal_util_1.useKeycloak)(request, request.accessToken, this.singleTenant, this.multiTenant, this.keycloakOpts);
|
|
109
|
+
const grant = yield keycloak.grantManager.createGrant({
|
|
110
|
+
access_token: request.accessToken,
|
|
111
|
+
});
|
|
112
|
+
// No resource given, check policy enforcement mode
|
|
113
|
+
if (!resource) {
|
|
114
|
+
if (shouldAllow) {
|
|
115
|
+
this.logger.verbose(`Controller has no @Resource defined, request allowed due to policy enforcement`);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
this.logger.verbose(`Controller has no @Resource defined, request denied due to policy enforcement`);
|
|
119
|
+
}
|
|
120
|
+
return shouldAllow;
|
|
121
|
+
}
|
|
122
|
+
// Build the required scopes
|
|
123
|
+
const conditionalScopesResult = conditionalScopes != null || conditionalScopes != undefined
|
|
124
|
+
? conditionalScopes(request, grant.access_token)
|
|
125
|
+
: [];
|
|
126
|
+
const scopes = [...explicitScopes, ...conditionalScopesResult];
|
|
127
|
+
// Attach resolved scopes
|
|
128
|
+
request.scopes = scopes;
|
|
129
|
+
// No scopes given, check policy enforcement mode
|
|
130
|
+
if (!scopes || scopes.length === 0) {
|
|
131
|
+
if (shouldAllow) {
|
|
132
|
+
this.logger.verbose(`Route has no @Scope/@ConditionalScopes defined, request allowed due to policy enforcement`);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
this.logger.verbose(`Route has no @Scope/@ConditionalScopes defined, request denied due to policy enforcement`);
|
|
136
|
+
}
|
|
137
|
+
return shouldAllow;
|
|
138
|
+
}
|
|
139
|
+
this.logger.verbose(`Protecting resource [ ${resource} ] with scopes: [ ${scopes} ]`);
|
|
140
|
+
const user = (_d = (_c = request.user) === null || _c === void 0 ? void 0 : _c.preferred_username) !== null && _d !== void 0 ? _d : 'user';
|
|
141
|
+
const enforcerFn = createEnforcerContext(request, response, enforcerOpts);
|
|
142
|
+
// Build permissions
|
|
143
|
+
const permissions = scopes.map((scope) => `${resource}:${scope}`);
|
|
144
|
+
const isAllowed = yield enforcerFn(keycloak, permissions);
|
|
145
|
+
// If statement for verbose logging only
|
|
146
|
+
if (!isAllowed) {
|
|
147
|
+
this.logger.verbose(`Resource [ ${resource} ] denied to [ ${user} ]`);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
this.logger.verbose(`Resource [ ${resource} ] granted to [ ${user} ]`);
|
|
151
|
+
}
|
|
152
|
+
return isAllowed;
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
exports.ResourceGuard = ResourceGuard;
|
|
157
|
+
exports.ResourceGuard = ResourceGuard = ResourceGuard_1 = __decorate([
|
|
158
|
+
(0, common_1.Injectable)(),
|
|
159
|
+
__param(0, (0, common_1.Inject)(constants_1.KEYCLOAK_INSTANCE)),
|
|
160
|
+
__param(1, (0, common_1.Inject)(constants_1.KEYCLOAK_CONNECT_OPTIONS)),
|
|
161
|
+
__param(2, (0, common_1.Inject)(constants_1.KEYCLOAK_MULTITENANT_SERVICE)),
|
|
162
|
+
__metadata("design:paramtypes", [Object, Object, keycloak_multitenant_service_1.KeycloakMultiTenantService])
|
|
163
|
+
], ResourceGuard);
|
|
164
|
+
const createEnforcerContext = (request, response, options) => (keycloak, permissions) =>
|
|
165
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
166
|
+
new Promise((resolve, _) =>
|
|
167
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
168
|
+
keycloak.enforcer(permissions, options)(request, response, (_) => {
|
|
169
|
+
if (request.resourceDenied) {
|
|
170
|
+
resolve(false);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
resolve(true);
|
|
174
|
+
}
|
|
175
|
+
}));
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { CanActivate, ExecutionContext } from '@nestjs/common';
|
|
2
|
+
import * as KeycloakConnect from 'keycloak-connect';
|
|
3
|
+
import { KeycloakConnectConfig } from '../interface/keycloak-connect-options.interface';
|
|
4
|
+
import { KeycloakMultiTenantService } from '../services/keycloak-multitenant.service';
|
|
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: KeycloakConnect.Keycloak, keycloakOpts: KeycloakConnectConfig, multiTenant: KeycloakMultiTenantService);
|
|
16
|
+
canActivate(context: ExecutionContext): Promise<boolean>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
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 __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
19
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
20
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
21
|
+
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;
|
|
22
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
23
|
+
};
|
|
24
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
25
|
+
if (mod && mod.__esModule) return mod;
|
|
26
|
+
var result = {};
|
|
27
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
28
|
+
__setModuleDefault(result, mod);
|
|
29
|
+
return result;
|
|
30
|
+
};
|
|
31
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
32
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
33
|
+
};
|
|
34
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
35
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
36
|
+
};
|
|
37
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
38
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
39
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
40
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
41
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
42
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
43
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
var RoleGuard_1;
|
|
47
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
+
exports.RoleGuard = void 0;
|
|
49
|
+
const common_1 = require("@nestjs/common");
|
|
50
|
+
const core_1 = require("@nestjs/core");
|
|
51
|
+
const KeycloakConnect = __importStar(require("keycloak-connect"));
|
|
52
|
+
const constants_1 = require("../constants");
|
|
53
|
+
const roles_decorator_1 = require("../decorators/roles.decorator");
|
|
54
|
+
const internal_util_1 = require("../internal.util");
|
|
55
|
+
const keycloak_multitenant_service_1 = require("../services/keycloak-multitenant.service");
|
|
56
|
+
/**
|
|
57
|
+
* A permissive type of role guard. Roles are set via `@Roles` decorator.
|
|
58
|
+
* @since 1.1.0
|
|
59
|
+
*/
|
|
60
|
+
let RoleGuard = RoleGuard_1 = class RoleGuard {
|
|
61
|
+
constructor(singleTenant, keycloakOpts, multiTenant) {
|
|
62
|
+
this.singleTenant = singleTenant;
|
|
63
|
+
this.keycloakOpts = keycloakOpts;
|
|
64
|
+
this.multiTenant = multiTenant;
|
|
65
|
+
this.logger = new common_1.Logger(RoleGuard_1.name);
|
|
66
|
+
this.reflector = new core_1.Reflector();
|
|
67
|
+
}
|
|
68
|
+
canActivate(context) {
|
|
69
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
70
|
+
const roleMerge = this.keycloakOpts.roleMerge
|
|
71
|
+
? this.keycloakOpts.roleMerge
|
|
72
|
+
: constants_1.RoleMerge.OVERRIDE;
|
|
73
|
+
const roles = [];
|
|
74
|
+
const matchingMode = this.reflector.getAllAndOverride(roles_decorator_1.META_ROLE_MATCHING_MODE, [context.getClass(), context.getHandler()]);
|
|
75
|
+
if (roleMerge == constants_1.RoleMerge.ALL) {
|
|
76
|
+
const mergedRoles = this.reflector.getAllAndMerge(roles_decorator_1.META_ROLES, [
|
|
77
|
+
context.getClass(),
|
|
78
|
+
context.getHandler(),
|
|
79
|
+
]);
|
|
80
|
+
if (mergedRoles) {
|
|
81
|
+
roles.push(...mergedRoles);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else if (roleMerge == constants_1.RoleMerge.OVERRIDE) {
|
|
85
|
+
const resultRoles = this.reflector.getAllAndOverride(roles_decorator_1.META_ROLES, [context.getClass(), context.getHandler()]);
|
|
86
|
+
if (resultRoles) {
|
|
87
|
+
roles.push(...resultRoles);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
throw Error(`Unknown role merge: ${roleMerge}`);
|
|
92
|
+
}
|
|
93
|
+
if (roles.length === 0) {
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
const roleMatchingMode = matchingMode !== null && matchingMode !== void 0 ? matchingMode : constants_1.RoleMatch.ANY;
|
|
97
|
+
this.logger.verbose(`Using matching mode: ${roleMatchingMode}`, { roles });
|
|
98
|
+
// Extract request
|
|
99
|
+
const cookieKey = this.keycloakOpts.cookieKey || constants_1.KEYCLOAK_COOKIE_DEFAULT;
|
|
100
|
+
const [request] = (0, internal_util_1.extractRequestAndAttachCookie)(context, cookieKey);
|
|
101
|
+
const { accessToken } = request;
|
|
102
|
+
// if is not an HTTP request ignore this guard
|
|
103
|
+
if (!request) {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
if (!accessToken) {
|
|
107
|
+
// No access token attached, auth guard should have attached the necessary token
|
|
108
|
+
this.logger.warn('No access token found in request, are you sure AuthGuard is first in the chain?');
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
// Create grant
|
|
112
|
+
const keycloak = yield (0, internal_util_1.useKeycloak)(request, request.accessToken, this.singleTenant, this.multiTenant, this.keycloakOpts);
|
|
113
|
+
const grant = yield keycloak.grantManager.createGrant({
|
|
114
|
+
access_token: accessToken,
|
|
115
|
+
});
|
|
116
|
+
// Grab access token from grant
|
|
117
|
+
const grantAccessToken = grant.access_token;
|
|
118
|
+
// For verbose logging, we store it instead of returning it immediately
|
|
119
|
+
const granted = roleMatchingMode === constants_1.RoleMatch.ANY
|
|
120
|
+
? roles.some((r) => grantAccessToken.hasRole(r))
|
|
121
|
+
: roles.every((r) => grantAccessToken.hasRole(r));
|
|
122
|
+
if (granted) {
|
|
123
|
+
this.logger.verbose(`Resource granted due to role(s)`);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
this.logger.verbose(`Resource denied due to mismatched role(s)`);
|
|
127
|
+
}
|
|
128
|
+
return granted;
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
exports.RoleGuard = RoleGuard;
|
|
133
|
+
exports.RoleGuard = RoleGuard = RoleGuard_1 = __decorate([
|
|
134
|
+
(0, common_1.Injectable)(),
|
|
135
|
+
__param(0, (0, common_1.Inject)(constants_1.KEYCLOAK_INSTANCE)),
|
|
136
|
+
__param(1, (0, common_1.Inject)(constants_1.KEYCLOAK_CONNECT_OPTIONS)),
|
|
137
|
+
__param(2, (0, common_1.Inject)(constants_1.KEYCLOAK_MULTITENANT_SERVICE)),
|
|
138
|
+
__metadata("design:paramtypes", [Object, Object, keycloak_multitenant_service_1.KeycloakMultiTenantService])
|
|
139
|
+
], RoleGuard);
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './keycloak-connect.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-connect.module"), exports);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ModuleMetadata, Type } from '@nestjs/common/interfaces';
|
|
2
|
+
import { KeycloakConnectOptionsFactory } from './keycloak-connect-options-factory.interface';
|
|
3
|
+
import { KeycloakConnectOptions } from './keycloak-connect-options.interface';
|
|
4
|
+
export interface KeycloakConnectModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
|
|
5
|
+
inject?: any[];
|
|
6
|
+
useExisting?: Type<KeycloakConnectOptionsFactory>;
|
|
7
|
+
useClass?: Type<KeycloakConnectOptionsFactory>;
|
|
8
|
+
useFactory?: (...args: any[]) => Promise<KeycloakConnectOptions> | KeycloakConnectOptions;
|
|
9
|
+
}
|