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,268 @@
|
|
|
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 KeycloakAdminService_1;
|
|
47
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
+
exports.KeycloakAdminService = void 0;
|
|
49
|
+
const crypto = __importStar(require("crypto"));
|
|
50
|
+
const jwks_cache_service_1 = require("./jwks-cache.service");
|
|
51
|
+
const keycloak_token_1 = require("../token/keycloak-token");
|
|
52
|
+
const common_1 = require("@nestjs/common");
|
|
53
|
+
const backchannel_logout_service_1 = require("./backchannel-logout.service");
|
|
54
|
+
const token_validation_service_1 = require("./token-validation.service");
|
|
55
|
+
const errors_1 = require("../errors");
|
|
56
|
+
const keycloak_multitenant_service_1 = require("./keycloak-multitenant.service");
|
|
57
|
+
const constants_1 = require("../constants");
|
|
58
|
+
/**
|
|
59
|
+
* Encapsulates all business logic for Keycloak admin callbacks:
|
|
60
|
+
* push-not-before policy and OIDC back-channel logout.
|
|
61
|
+
*/
|
|
62
|
+
let KeycloakAdminService = KeycloakAdminService_1 = class KeycloakAdminService {
|
|
63
|
+
constructor(tenantConfig, keycloakOpts, multiTenant, tokenValidation, jwksCache, backchannelLogoutService) {
|
|
64
|
+
this.tenantConfig = tenantConfig;
|
|
65
|
+
this.keycloakOpts = keycloakOpts;
|
|
66
|
+
this.multiTenant = multiTenant;
|
|
67
|
+
this.tokenValidation = tokenValidation;
|
|
68
|
+
this.jwksCache = jwksCache;
|
|
69
|
+
this.backchannelLogoutService = backchannelLogoutService;
|
|
70
|
+
this.logger = new common_1.Logger(KeycloakAdminService_1.name);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Process a push-not-before admin callback.
|
|
74
|
+
* Verifies the token signature and updates the not-before policy.
|
|
75
|
+
*/
|
|
76
|
+
processPushNotBefore(body, request) {
|
|
77
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
78
|
+
const payload = this.extractAdminPayload(body, request);
|
|
79
|
+
if (!payload) {
|
|
80
|
+
throw new errors_1.KeycloakConfigError('invalid token');
|
|
81
|
+
}
|
|
82
|
+
const token = new keycloak_token_1.KeycloakToken(payload);
|
|
83
|
+
if (!token.signed) {
|
|
84
|
+
throw new errors_1.KeycloakConfigError('invalid token');
|
|
85
|
+
}
|
|
86
|
+
const tenantConfig = yield this.resolveTenantConfig(request, token);
|
|
87
|
+
yield this.verifySignature(token, tenantConfig.realmUrl);
|
|
88
|
+
if (token.isExpired()) {
|
|
89
|
+
throw new errors_1.KeycloakAdminError('admin request failed: token expired');
|
|
90
|
+
}
|
|
91
|
+
if (token.content.action !== 'PUSH_NOT_BEFORE') {
|
|
92
|
+
throw new errors_1.KeycloakConfigError('unsupported action');
|
|
93
|
+
}
|
|
94
|
+
if (typeof token.content.notBefore !== 'number') {
|
|
95
|
+
throw new errors_1.KeycloakConfigError('invalid token');
|
|
96
|
+
}
|
|
97
|
+
this.tokenValidation.setNotBefore(token.content.notBefore, tenantConfig.realmUrl);
|
|
98
|
+
this.logger.log(`Push not-before (${tenantConfig.realm}): notBefore set to ${token.content.notBefore}`);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Process an OIDC back-channel logout token.
|
|
103
|
+
* Verifies signature, validates claims, and revokes the session/user.
|
|
104
|
+
*/
|
|
105
|
+
processBackchannelLogout(body, request) {
|
|
106
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
107
|
+
var _a, _b;
|
|
108
|
+
const logoutTokenRaw = this.extractLogoutToken(body);
|
|
109
|
+
if (!logoutTokenRaw) {
|
|
110
|
+
throw new errors_1.KeycloakConfigError('invalid logout token');
|
|
111
|
+
}
|
|
112
|
+
const token = new keycloak_token_1.KeycloakToken(logoutTokenRaw);
|
|
113
|
+
if (!token.signed) {
|
|
114
|
+
throw new errors_1.KeycloakConfigError('invalid logout token');
|
|
115
|
+
}
|
|
116
|
+
const tenantConfig = yield this.resolveTenantConfig(request, token);
|
|
117
|
+
yield this.verifySignature(token, tenantConfig.realmUrl);
|
|
118
|
+
if (token.isExpired()) {
|
|
119
|
+
throw new errors_1.KeycloakAdminError('admin request failed: logout token expired');
|
|
120
|
+
}
|
|
121
|
+
// Validate typ is "logout+jwt" or "JWT" (per OIDC Back-Channel Logout spec)
|
|
122
|
+
const typ = (_a = token.header) === null || _a === void 0 ? void 0 : _a.typ;
|
|
123
|
+
if (typ !== 'logout+jwt' && typ !== 'JWT') {
|
|
124
|
+
throw new errors_1.KeycloakConfigError('invalid logout token type');
|
|
125
|
+
}
|
|
126
|
+
// Validate events claim contains back-channel logout event
|
|
127
|
+
const events = (_b = token.content) === null || _b === void 0 ? void 0 : _b.events;
|
|
128
|
+
if (!events ||
|
|
129
|
+
typeof events !== 'object' ||
|
|
130
|
+
!('http://schemas.openid.net/event/backchannel-logout' in events)) {
|
|
131
|
+
throw new errors_1.KeycloakConfigError('missing backchannel-logout event');
|
|
132
|
+
}
|
|
133
|
+
const sid = typeof token.content.sid === 'string'
|
|
134
|
+
? token.content.sid
|
|
135
|
+
: undefined;
|
|
136
|
+
const sub = typeof token.content.sub === 'string'
|
|
137
|
+
? token.content.sub
|
|
138
|
+
: undefined;
|
|
139
|
+
if (!sid && !sub) {
|
|
140
|
+
throw new errors_1.KeycloakConfigError('logout token must contain sid or sub');
|
|
141
|
+
}
|
|
142
|
+
this.backchannelLogoutService.revoke(sid, sub);
|
|
143
|
+
this.logger.log(`Back-channel logout processed: sid=${sid || 'n/a'}, sub=${sub || 'n/a'}`);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
extractAdminPayload(body, request) {
|
|
147
|
+
if (typeof body === 'string' && body.trim() !== '') {
|
|
148
|
+
return body.trim();
|
|
149
|
+
}
|
|
150
|
+
if (Buffer.isBuffer(body) && body.length > 0) {
|
|
151
|
+
return body.toString('utf8').trim();
|
|
152
|
+
}
|
|
153
|
+
if (typeof request.rawBody === 'string' && request.rawBody.trim() !== '') {
|
|
154
|
+
return request.rawBody.trim();
|
|
155
|
+
}
|
|
156
|
+
if (Buffer.isBuffer(request.rawBody) && request.rawBody.length > 0) {
|
|
157
|
+
return request.rawBody.toString('utf8').trim();
|
|
158
|
+
}
|
|
159
|
+
if (body && typeof body === 'object') {
|
|
160
|
+
const map = body;
|
|
161
|
+
if (typeof map.token === 'string' && map.token.trim() !== '') {
|
|
162
|
+
return map.token.trim();
|
|
163
|
+
}
|
|
164
|
+
// URL-encoded bodies can be parsed as { "<jwt>": "" }.
|
|
165
|
+
const keys = Object.keys(map);
|
|
166
|
+
if (keys.length === 1 && map[keys[0]] === '') {
|
|
167
|
+
return keys[0];
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
extractLogoutToken(body) {
|
|
173
|
+
if (body && typeof body === 'object') {
|
|
174
|
+
const map = body;
|
|
175
|
+
if (typeof map.logout_token === 'string' &&
|
|
176
|
+
map.logout_token.trim() !== '') {
|
|
177
|
+
return map.logout_token.trim();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
verifySignature(token, realmUrl) {
|
|
183
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
184
|
+
const kid = token.header && typeof token.header.kid === 'string'
|
|
185
|
+
? token.header.kid
|
|
186
|
+
: undefined;
|
|
187
|
+
if (!kid) {
|
|
188
|
+
throw new errors_1.KeycloakAdminError('admin request failed: missing token kid');
|
|
189
|
+
}
|
|
190
|
+
let key;
|
|
191
|
+
try {
|
|
192
|
+
key = yield this.jwksCache.getKey(realmUrl, kid);
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
throw new errors_1.KeycloakAdminError(`failed to load public key to verify token. Reason: ${err}`);
|
|
196
|
+
}
|
|
197
|
+
const alg = token.header && typeof token.header.alg === 'string'
|
|
198
|
+
? token.header.alg
|
|
199
|
+
: 'RS256';
|
|
200
|
+
if (!KeycloakAdminService_1.ALLOWED_ALGS.has(alg)) {
|
|
201
|
+
throw new errors_1.KeycloakAdminError(`admin request failed: unsupported token algorithm: ${alg}`);
|
|
202
|
+
}
|
|
203
|
+
if (!this.verifyTokenSignature(token.signed, token.signature, key, alg)) {
|
|
204
|
+
throw new errors_1.KeycloakAdminError('admin request failed: invalid token (signature)');
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
verifyTokenSignature(signed, signature, key, jwtAlg) {
|
|
209
|
+
const hashMap = {
|
|
210
|
+
RS256: 'SHA256', RS384: 'SHA384', RS512: 'SHA512',
|
|
211
|
+
ES256: 'SHA256', ES384: 'SHA384', ES512: 'SHA512',
|
|
212
|
+
PS256: 'SHA256', PS384: 'SHA384', PS512: 'SHA512',
|
|
213
|
+
};
|
|
214
|
+
const hash = hashMap[jwtAlg] || 'SHA256';
|
|
215
|
+
if (jwtAlg.startsWith('PS')) {
|
|
216
|
+
return crypto.verify(hash, Buffer.from(signed), { key, padding: crypto.constants.RSA_PKCS1_PSS_PADDING }, signature);
|
|
217
|
+
}
|
|
218
|
+
// crypto.verify() auto-detects key type (RSA vs EC) from the KeyObject
|
|
219
|
+
return crypto.verify(hash, Buffer.from(signed), key, signature);
|
|
220
|
+
}
|
|
221
|
+
resolveTenantConfig(request, token) {
|
|
222
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
223
|
+
var _a;
|
|
224
|
+
if ((_a = this.keycloakOpts.multiTenant) === null || _a === void 0 ? void 0 : _a.realmResolver) {
|
|
225
|
+
try {
|
|
226
|
+
const resolvedRealm = this.keycloakOpts.multiTenant.realmResolver(request);
|
|
227
|
+
const realm = resolvedRealm instanceof Promise
|
|
228
|
+
? yield resolvedRealm
|
|
229
|
+
: resolvedRealm;
|
|
230
|
+
if (!realm) {
|
|
231
|
+
throw new errors_1.KeycloakConfigError('admin request failed: realm resolver returned an empty realm');
|
|
232
|
+
}
|
|
233
|
+
return yield this.multiTenant.get(realm, request);
|
|
234
|
+
}
|
|
235
|
+
catch (err) {
|
|
236
|
+
if (err instanceof errors_1.KeycloakConfigError) {
|
|
237
|
+
throw err;
|
|
238
|
+
}
|
|
239
|
+
throw new errors_1.KeycloakConfigError(`admin request failed: cannot resolve tenant config. Reason: ${err}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
// Single-tenant configured realm: use static tenant config.
|
|
243
|
+
if (this.keycloakOpts.realm) {
|
|
244
|
+
return this.tenantConfig;
|
|
245
|
+
}
|
|
246
|
+
// Do NOT fall back to the token's iss claim — an attacker from a
|
|
247
|
+
// different realm on the same Keycloak server could forge admin
|
|
248
|
+
// callbacks by setting iss to their own realm.
|
|
249
|
+
throw new errors_1.KeycloakConfigError('admin request failed: cannot resolve realm — configure realm or multiTenant.realmResolver for admin callbacks');
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
exports.KeycloakAdminService = KeycloakAdminService;
|
|
254
|
+
KeycloakAdminService.ALLOWED_ALGS = new Set([
|
|
255
|
+
'RS256', 'RS384', 'RS512',
|
|
256
|
+
'ES256', 'ES384', 'ES512',
|
|
257
|
+
'PS256', 'PS384', 'PS512',
|
|
258
|
+
]);
|
|
259
|
+
exports.KeycloakAdminService = KeycloakAdminService = KeycloakAdminService_1 = __decorate([
|
|
260
|
+
(0, common_1.Injectable)(),
|
|
261
|
+
__param(0, (0, common_1.Inject)(constants_1.KEYCLOAK_INSTANCE)),
|
|
262
|
+
__param(1, (0, common_1.Inject)(constants_1.KEYCLOAK_AUTH_OPTIONS)),
|
|
263
|
+
__param(2, (0, common_1.Inject)(constants_1.KEYCLOAK_MULTITENANT_SERVICE)),
|
|
264
|
+
__metadata("design:paramtypes", [Object, Object, keycloak_multitenant_service_1.KeycloakMultiTenantService,
|
|
265
|
+
token_validation_service_1.TokenValidationService,
|
|
266
|
+
jwks_cache_service_1.JwksCacheService,
|
|
267
|
+
backchannel_logout_service_1.BackchannelLogoutService])
|
|
268
|
+
], KeycloakAdminService);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { KeycloakGrant } from '../token/keycloak-grant';
|
|
2
|
+
import { TokenValidationService } from './token-validation.service';
|
|
3
|
+
import { KeycloakAuthConfig } from '../interface/keycloak-auth-options.interface';
|
|
4
|
+
/**
|
|
5
|
+
* Service for creating and validating Keycloak grants.
|
|
6
|
+
* Bearer-only: no refresh token or ID token handling.
|
|
7
|
+
*/
|
|
8
|
+
export declare class KeycloakGrantService {
|
|
9
|
+
private readonly keycloakOpts;
|
|
10
|
+
private readonly tokenValidation;
|
|
11
|
+
private readonly logger;
|
|
12
|
+
private readonly clientId;
|
|
13
|
+
constructor(keycloakOpts: KeycloakAuthConfig, tokenValidation: TokenValidationService);
|
|
14
|
+
/**
|
|
15
|
+
* Create a Grant object from raw JSON data (string or object).
|
|
16
|
+
* Parses the access token and validates.
|
|
17
|
+
*/
|
|
18
|
+
createGrant(rawData: string | Record<string, unknown>, realmUrl: string, clientId?: string, secret?: string): Promise<KeycloakGrant>;
|
|
19
|
+
/**
|
|
20
|
+
* Validate the access token in a grant.
|
|
21
|
+
*/
|
|
22
|
+
validateGrant(grant: KeycloakGrant, realmUrl: string, clientId?: string): Promise<KeycloakGrant>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
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 KeycloakGrantService_1;
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.KeycloakGrantService = void 0;
|
|
26
|
+
const errors_1 = require("../errors");
|
|
27
|
+
const constants_1 = require("../constants");
|
|
28
|
+
const keycloak_grant_1 = require("../token/keycloak-grant");
|
|
29
|
+
const keycloak_token_1 = require("../token/keycloak-token");
|
|
30
|
+
const common_1 = require("@nestjs/common");
|
|
31
|
+
const token_validation_service_1 = require("./token-validation.service");
|
|
32
|
+
/**
|
|
33
|
+
* Service for creating and validating Keycloak grants.
|
|
34
|
+
* Bearer-only: no refresh token or ID token handling.
|
|
35
|
+
*/
|
|
36
|
+
let KeycloakGrantService = KeycloakGrantService_1 = class KeycloakGrantService {
|
|
37
|
+
constructor(keycloakOpts, tokenValidation) {
|
|
38
|
+
this.keycloakOpts = keycloakOpts;
|
|
39
|
+
this.tokenValidation = tokenValidation;
|
|
40
|
+
this.logger = new common_1.Logger(KeycloakGrantService_1.name);
|
|
41
|
+
this.clientId =
|
|
42
|
+
this.keycloakOpts.clientId ||
|
|
43
|
+
this.keycloakOpts['client-id'] ||
|
|
44
|
+
this.keycloakOpts.resource ||
|
|
45
|
+
'';
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Create a Grant object from raw JSON data (string or object).
|
|
49
|
+
* Parses the access token and validates.
|
|
50
|
+
*/
|
|
51
|
+
createGrant(rawData, realmUrl, clientId, secret) {
|
|
52
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
53
|
+
const grantData = (typeof rawData === 'string' ? JSON.parse(rawData) : rawData);
|
|
54
|
+
const cId = clientId || this.clientId;
|
|
55
|
+
const grant = new keycloak_grant_1.KeycloakGrant({
|
|
56
|
+
access_token: grantData.access_token
|
|
57
|
+
? new keycloak_token_1.KeycloakToken(grantData.access_token, cId)
|
|
58
|
+
: undefined,
|
|
59
|
+
expires_in: grantData.expires_in,
|
|
60
|
+
token_type: grantData.token_type,
|
|
61
|
+
__raw: typeof rawData === 'string' ? rawData : JSON.stringify(rawData),
|
|
62
|
+
});
|
|
63
|
+
yield this.validateGrant(grant, realmUrl, cId);
|
|
64
|
+
return grant;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Validate the access token in a grant.
|
|
69
|
+
*/
|
|
70
|
+
validateGrant(grant, realmUrl, clientId) {
|
|
71
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
72
|
+
const cId = clientId || this.clientId;
|
|
73
|
+
if (grant.access_token) {
|
|
74
|
+
const isValid = yield this.tokenValidation.validateOffline(grant.access_token.token, realmUrl, cId, 'Bearer');
|
|
75
|
+
if (!isValid) {
|
|
76
|
+
throw new errors_1.KeycloakTokenError('Grant validation failed. Reason: invalid access_token');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return grant;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
exports.KeycloakGrantService = KeycloakGrantService;
|
|
84
|
+
exports.KeycloakGrantService = KeycloakGrantService = KeycloakGrantService_1 = __decorate([
|
|
85
|
+
(0, common_1.Injectable)(),
|
|
86
|
+
__param(0, (0, common_1.Inject)(constants_1.KEYCLOAK_AUTH_OPTIONS)),
|
|
87
|
+
__metadata("design:paramtypes", [Object, token_validation_service_1.TokenValidationService])
|
|
88
|
+
], KeycloakGrantService);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { HttpService } from '@nestjs/axios';
|
|
2
|
+
import { OidcDiscoveryService } from './oidc-discovery.service';
|
|
3
|
+
import { JwksResponse } from '../interface/jwks.interface';
|
|
4
|
+
import { KeycloakGrantResponse, KeycloakPermission, KeycloakUserInfoResponse, PermissionCheckOptions } from '../interface/keycloak-grant.interface';
|
|
5
|
+
export declare class KeycloakHttpService {
|
|
6
|
+
private readonly httpService;
|
|
7
|
+
private readonly oidcDiscovery;
|
|
8
|
+
constructor(httpService: HttpService, oidcDiscovery: OidcDiscoveryService);
|
|
9
|
+
/**
|
|
10
|
+
* Build authorization headers for token endpoint requests.
|
|
11
|
+
* Public clients send only client_id in the body; confidential clients
|
|
12
|
+
* use HTTP Basic auth.
|
|
13
|
+
*/
|
|
14
|
+
private buildAuthHeaders;
|
|
15
|
+
/**
|
|
16
|
+
* Fetch the JWKS (JSON Web Key Set) from the discovered jwks_uri.
|
|
17
|
+
*/
|
|
18
|
+
fetchJwks(realmUrl: string): Promise<JwksResponse>;
|
|
19
|
+
/**
|
|
20
|
+
* Introspect a token using the discovered introspection_endpoint.
|
|
21
|
+
* Returns the introspection response (with `active` boolean).
|
|
22
|
+
*/
|
|
23
|
+
introspectToken(realmUrl: string, clientId: string, secret: string, token: string): Promise<{
|
|
24
|
+
active: boolean;
|
|
25
|
+
[key: string]: unknown;
|
|
26
|
+
}>;
|
|
27
|
+
/**
|
|
28
|
+
* Obtain a token using the client credentials grant (service account).
|
|
29
|
+
*/
|
|
30
|
+
obtainClientCredentialsGrant(realmUrl: string, clientId: string, secret: string, scope?: string, isPublic?: boolean): Promise<KeycloakGrantResponse>;
|
|
31
|
+
/**
|
|
32
|
+
* Fetch user info from the discovered userinfo_endpoint.
|
|
33
|
+
*/
|
|
34
|
+
getUserInfo(realmUrl: string, accessToken: string): Promise<KeycloakUserInfoResponse>;
|
|
35
|
+
/**
|
|
36
|
+
* Check permissions using the discovered token_endpoint (UMA grant).
|
|
37
|
+
* Supports response_mode 'decision' (returns boolean), 'permissions'
|
|
38
|
+
* (returns permission list), and 'token' (returns grant response).
|
|
39
|
+
*/
|
|
40
|
+
checkPermission(realmUrl: string, clientId: string, secret: string, accessToken: string, permissions: string[], options?: PermissionCheckOptions): Promise<boolean | KeycloakPermission[] | KeycloakGrantResponse>;
|
|
41
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
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 __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
12
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
13
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
14
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
15
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
16
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
17
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.KeycloakHttpService = void 0;
|
|
22
|
+
const rxjs_1 = require("rxjs");
|
|
23
|
+
const axios_1 = require("@nestjs/axios");
|
|
24
|
+
const common_1 = require("@nestjs/common");
|
|
25
|
+
const errors_1 = require("../errors");
|
|
26
|
+
const oidc_discovery_service_1 = require("./oidc-discovery.service");
|
|
27
|
+
const X_CLIENT = 'nestjs-keycloak-auth';
|
|
28
|
+
let KeycloakHttpService = class KeycloakHttpService {
|
|
29
|
+
constructor(httpService, oidcDiscovery) {
|
|
30
|
+
this.httpService = httpService;
|
|
31
|
+
this.oidcDiscovery = oidcDiscovery;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Build authorization headers for token endpoint requests.
|
|
35
|
+
* Public clients send only client_id in the body; confidential clients
|
|
36
|
+
* use HTTP Basic auth.
|
|
37
|
+
*/
|
|
38
|
+
buildAuthHeaders(clientId, secret, isPublic) {
|
|
39
|
+
const headers = {
|
|
40
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
41
|
+
'X-Client': X_CLIENT,
|
|
42
|
+
};
|
|
43
|
+
if (!isPublic) {
|
|
44
|
+
headers.Authorization = `Basic ${Buffer.from(`${clientId}:${secret}`).toString('base64')}`;
|
|
45
|
+
}
|
|
46
|
+
return headers;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Fetch the JWKS (JSON Web Key Set) from the discovered jwks_uri.
|
|
50
|
+
*/
|
|
51
|
+
fetchJwks(realmUrl) {
|
|
52
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
53
|
+
const endpoints = yield this.oidcDiscovery.getEndpoints(realmUrl);
|
|
54
|
+
const { data } = yield (0, rxjs_1.firstValueFrom)(this.httpService.request({
|
|
55
|
+
url: endpoints.jwks_uri,
|
|
56
|
+
method: 'GET',
|
|
57
|
+
headers: {
|
|
58
|
+
'Content-Type': 'application/json',
|
|
59
|
+
'X-Client': X_CLIENT,
|
|
60
|
+
},
|
|
61
|
+
timeout: 10000,
|
|
62
|
+
}));
|
|
63
|
+
return data;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Introspect a token using the discovered introspection_endpoint.
|
|
68
|
+
* Returns the introspection response (with `active` boolean).
|
|
69
|
+
*/
|
|
70
|
+
introspectToken(realmUrl, clientId, secret, token) {
|
|
71
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
72
|
+
const endpoints = yield this.oidcDiscovery.getEndpoints(realmUrl);
|
|
73
|
+
const body = new URLSearchParams({
|
|
74
|
+
client_id: clientId,
|
|
75
|
+
client_secret: secret,
|
|
76
|
+
token,
|
|
77
|
+
}).toString();
|
|
78
|
+
const { data } = yield (0, rxjs_1.firstValueFrom)(this.httpService.request({
|
|
79
|
+
url: endpoints.introspection_endpoint,
|
|
80
|
+
method: 'POST',
|
|
81
|
+
data: body,
|
|
82
|
+
headers: {
|
|
83
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
84
|
+
'X-Client': X_CLIENT,
|
|
85
|
+
},
|
|
86
|
+
timeout: 10000,
|
|
87
|
+
}));
|
|
88
|
+
return data;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Obtain a token using the client credentials grant (service account).
|
|
93
|
+
*/
|
|
94
|
+
obtainClientCredentialsGrant(realmUrl_1, clientId_1, secret_1, scope_1) {
|
|
95
|
+
return __awaiter(this, arguments, void 0, function* (realmUrl, clientId, secret, scope, isPublic = false) {
|
|
96
|
+
const endpoints = yield this.oidcDiscovery.getEndpoints(realmUrl);
|
|
97
|
+
const params = new URLSearchParams({
|
|
98
|
+
grant_type: 'client_credentials',
|
|
99
|
+
scope: scope || 'openid',
|
|
100
|
+
client_id: clientId,
|
|
101
|
+
});
|
|
102
|
+
const { data } = yield (0, rxjs_1.firstValueFrom)(this.httpService.request({
|
|
103
|
+
url: endpoints.token_endpoint,
|
|
104
|
+
method: 'POST',
|
|
105
|
+
data: params.toString(),
|
|
106
|
+
headers: this.buildAuthHeaders(clientId, secret, isPublic),
|
|
107
|
+
timeout: 10000,
|
|
108
|
+
}));
|
|
109
|
+
return data;
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Fetch user info from the discovered userinfo_endpoint.
|
|
114
|
+
*/
|
|
115
|
+
getUserInfo(realmUrl, accessToken) {
|
|
116
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
117
|
+
const endpoints = yield this.oidcDiscovery.getEndpoints(realmUrl);
|
|
118
|
+
const { data } = yield (0, rxjs_1.firstValueFrom)(this.httpService.request({
|
|
119
|
+
url: endpoints.userinfo_endpoint,
|
|
120
|
+
method: 'GET',
|
|
121
|
+
headers: {
|
|
122
|
+
Authorization: `Bearer ${accessToken}`,
|
|
123
|
+
Accept: 'application/json',
|
|
124
|
+
'X-Client': X_CLIENT,
|
|
125
|
+
},
|
|
126
|
+
timeout: 10000,
|
|
127
|
+
}));
|
|
128
|
+
return data;
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Check permissions using the discovered token_endpoint (UMA grant).
|
|
133
|
+
* Supports response_mode 'decision' (returns boolean), 'permissions'
|
|
134
|
+
* (returns permission list), and 'token' (returns grant response).
|
|
135
|
+
*/
|
|
136
|
+
checkPermission(realmUrl, clientId, secret, accessToken, permissions, options) {
|
|
137
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
138
|
+
var _a;
|
|
139
|
+
const endpoints = yield this.oidcDiscovery.getEndpoints(realmUrl);
|
|
140
|
+
const responseMode = (options === null || options === void 0 ? void 0 : options.response_mode) || 'decision';
|
|
141
|
+
const audience = (options === null || options === void 0 ? void 0 : options.audience) || clientId;
|
|
142
|
+
const isPublic = (_a = options === null || options === void 0 ? void 0 : options.isPublic) !== null && _a !== void 0 ? _a : false;
|
|
143
|
+
const params = new URLSearchParams({
|
|
144
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:uma-ticket',
|
|
145
|
+
audience,
|
|
146
|
+
});
|
|
147
|
+
// Only set response_mode for 'decision' and 'permissions';
|
|
148
|
+
// 'token' mode omits it to get a grant response (matching original behavior)
|
|
149
|
+
if (responseMode === 'decision' || responseMode === 'permissions') {
|
|
150
|
+
params.set('response_mode', responseMode);
|
|
151
|
+
}
|
|
152
|
+
for (const permission of permissions) {
|
|
153
|
+
params.append('permission', permission);
|
|
154
|
+
}
|
|
155
|
+
if (options === null || options === void 0 ? void 0 : options.claims) {
|
|
156
|
+
// Keycloak expects every claim value to be a Collection (array).
|
|
157
|
+
// Normalize scalar values to single-element arrays so consumers
|
|
158
|
+
// can return plain strings from their claims() functions.
|
|
159
|
+
const normalized = {};
|
|
160
|
+
for (const [key, value] of Object.entries(options.claims)) {
|
|
161
|
+
normalized[key] = Array.isArray(value) ? value : [value];
|
|
162
|
+
}
|
|
163
|
+
params.set('claim_token', Buffer.from(JSON.stringify(normalized)).toString('base64'));
|
|
164
|
+
params.set('claim_token_format', 'urn:ietf:params:oauth:token-type:jwt');
|
|
165
|
+
}
|
|
166
|
+
if (options === null || options === void 0 ? void 0 : options.subject_token) {
|
|
167
|
+
params.set('subject_token', options.subject_token);
|
|
168
|
+
}
|
|
169
|
+
else if (!isPublic) {
|
|
170
|
+
// Confidential clients send the user bearer as subject_token.
|
|
171
|
+
params.set('subject_token', accessToken);
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
const headers = isPublic
|
|
175
|
+
? {
|
|
176
|
+
Authorization: `Bearer ${accessToken}`,
|
|
177
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
178
|
+
'X-Client': X_CLIENT,
|
|
179
|
+
}
|
|
180
|
+
: this.buildAuthHeaders(clientId, secret, false);
|
|
181
|
+
const { data: result } = yield (0, rxjs_1.firstValueFrom)(this.httpService.request({
|
|
182
|
+
url: endpoints.token_endpoint,
|
|
183
|
+
method: 'POST',
|
|
184
|
+
data: params.toString(),
|
|
185
|
+
headers,
|
|
186
|
+
timeout: 10000,
|
|
187
|
+
}));
|
|
188
|
+
if (responseMode === 'decision') {
|
|
189
|
+
return (result === null || result === void 0 ? void 0 : result.result) === true;
|
|
190
|
+
}
|
|
191
|
+
// 'permissions' and 'token' modes return the raw response
|
|
192
|
+
if (responseMode === 'permissions') {
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
197
|
+
catch (_b) {
|
|
198
|
+
if (responseMode === 'decision') {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
throw new errors_1.KeycloakPermissionError('Permission check failed');
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
exports.KeycloakHttpService = KeycloakHttpService;
|
|
207
|
+
exports.KeycloakHttpService = KeycloakHttpService = __decorate([
|
|
208
|
+
(0, common_1.Injectable)(),
|
|
209
|
+
__metadata("design:paramtypes", [axios_1.HttpService,
|
|
210
|
+
oidc_discovery_service_1.OidcDiscoveryService])
|
|
211
|
+
], KeycloakHttpService);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ResolvedTenantConfig } from '../interface/tenant-config.interface';
|
|
2
|
+
import { KeycloakAuthOptions } from '../interface/keycloak-auth-options.interface';
|
|
3
|
+
/**
|
|
4
|
+
* Stores resolved tenant configurations for multi-tenant scenarios.
|
|
5
|
+
*/
|
|
6
|
+
export declare class KeycloakMultiTenantService {
|
|
7
|
+
private keycloakOpts;
|
|
8
|
+
private configs;
|
|
9
|
+
constructor(keycloakOpts: KeycloakAuthOptions);
|
|
10
|
+
/**
|
|
11
|
+
* Clears the cached tenant configurations.
|
|
12
|
+
*/
|
|
13
|
+
clear(): void;
|
|
14
|
+
/**
|
|
15
|
+
* Retrieves a resolved tenant config based on the realm provided.
|
|
16
|
+
* @param realm the realm to retrieve from
|
|
17
|
+
* @param request the request instance, defaults to undefined
|
|
18
|
+
* @returns the resolved tenant configuration
|
|
19
|
+
*/
|
|
20
|
+
get(realm: string, request?: unknown): Promise<ResolvedTenantConfig>;
|
|
21
|
+
resolveAuthServerUrl(realm: string, request?: unknown): Promise<string>;
|
|
22
|
+
resolveClientId(realm: string, request?: unknown): Promise<string>;
|
|
23
|
+
resolveSecret(realm: string, request?: unknown): Promise<string>;
|
|
24
|
+
}
|