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,161 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.KeycloakMultiTenantService = void 0;
|
|
25
|
+
const errors_1 = require("../errors");
|
|
26
|
+
const common_1 = require("@nestjs/common");
|
|
27
|
+
const constants_1 = require("../constants");
|
|
28
|
+
/**
|
|
29
|
+
* Stores resolved tenant configurations for multi-tenant scenarios.
|
|
30
|
+
*/
|
|
31
|
+
let KeycloakMultiTenantService = class KeycloakMultiTenantService {
|
|
32
|
+
constructor(keycloakOpts) {
|
|
33
|
+
this.keycloakOpts = keycloakOpts;
|
|
34
|
+
this.configs = new Map();
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Clears the cached tenant configurations.
|
|
38
|
+
*/
|
|
39
|
+
clear() {
|
|
40
|
+
this.configs.clear();
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Retrieves a resolved tenant config based on the realm provided.
|
|
44
|
+
* @param realm the realm to retrieve from
|
|
45
|
+
* @param request the request instance, defaults to undefined
|
|
46
|
+
* @returns the resolved tenant configuration
|
|
47
|
+
*/
|
|
48
|
+
get(realm_1) {
|
|
49
|
+
return __awaiter(this, arguments, void 0, function* (realm, request = undefined) {
|
|
50
|
+
var _a, _b, _c, _d;
|
|
51
|
+
if (typeof this.keycloakOpts === 'string') {
|
|
52
|
+
throw new errors_1.KeycloakConfigError('Keycloak configuration is a configuration path. This should not happen after module load.');
|
|
53
|
+
}
|
|
54
|
+
if (this.keycloakOpts.multiTenant === null ||
|
|
55
|
+
this.keycloakOpts.multiTenant === undefined) {
|
|
56
|
+
throw new errors_1.KeycloakConfigError('Multi tenant is not defined yet multi tenant service is being called.');
|
|
57
|
+
}
|
|
58
|
+
// Check if existing — return early before resolving
|
|
59
|
+
if (this.configs.has(realm) &&
|
|
60
|
+
!this.keycloakOpts.multiTenant.resolveAlways) {
|
|
61
|
+
return this.configs.get(realm);
|
|
62
|
+
}
|
|
63
|
+
const authServerUrl = yield this.resolveAuthServerUrl(realm, request);
|
|
64
|
+
const secret = yield this.resolveSecret(realm, request);
|
|
65
|
+
const clientId = yield this.resolveClientId(realm, request);
|
|
66
|
+
const realmUrl = `${authServerUrl.replace(/\/$/, '')}/realms/${realm}`;
|
|
67
|
+
const realmAdminUrl = `${authServerUrl.replace(/\/$/, '')}/admin/realms/${realm}`;
|
|
68
|
+
const isPublic = typeof this.keycloakOpts !== 'string' &&
|
|
69
|
+
!!((_b = (_a = this.keycloakOpts['public-client']) !== null && _a !== void 0 ? _a : this.keycloakOpts.public) !== null && _b !== void 0 ? _b : false);
|
|
70
|
+
const bearerOnly = typeof this.keycloakOpts !== 'string' &&
|
|
71
|
+
!!((_d = (_c = this.keycloakOpts['bearer-only']) !== null && _c !== void 0 ? _c : this.keycloakOpts.bearerOnly) !== null && _d !== void 0 ? _d : false);
|
|
72
|
+
const config = {
|
|
73
|
+
authServerUrl,
|
|
74
|
+
realm,
|
|
75
|
+
clientId,
|
|
76
|
+
secret,
|
|
77
|
+
realmUrl,
|
|
78
|
+
realmAdminUrl,
|
|
79
|
+
isPublic,
|
|
80
|
+
bearerOnly,
|
|
81
|
+
};
|
|
82
|
+
this.configs.set(realm, config);
|
|
83
|
+
return config;
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
resolveAuthServerUrl(realm_1) {
|
|
87
|
+
return __awaiter(this, arguments, void 0, function* (realm, request = undefined) {
|
|
88
|
+
if (typeof this.keycloakOpts === 'string') {
|
|
89
|
+
throw new errors_1.KeycloakConfigError('Keycloak configuration is a configuration path. This should not happen after module load.');
|
|
90
|
+
}
|
|
91
|
+
if (this.keycloakOpts.multiTenant === null ||
|
|
92
|
+
this.keycloakOpts.multiTenant === undefined) {
|
|
93
|
+
throw new errors_1.KeycloakConfigError('Multi tenant is not defined yet multi tenant service is being called.');
|
|
94
|
+
}
|
|
95
|
+
// If no realm auth server url resolver is defined, return defaults
|
|
96
|
+
if (!this.keycloakOpts.multiTenant.realmAuthServerUrlResolver) {
|
|
97
|
+
return (this.keycloakOpts.authServerUrl ||
|
|
98
|
+
this.keycloakOpts['auth-server-url'] ||
|
|
99
|
+
this.keycloakOpts.serverUrl ||
|
|
100
|
+
this.keycloakOpts['server-url']);
|
|
101
|
+
}
|
|
102
|
+
// Resolve realm authServerUrl
|
|
103
|
+
const authServerUrl = yield this.keycloakOpts.multiTenant.realmAuthServerUrlResolver(realm, request);
|
|
104
|
+
// Override auth server url
|
|
105
|
+
// Order of priority: resolved realm auth server url > provided auth server url
|
|
106
|
+
return (authServerUrl ||
|
|
107
|
+
this.keycloakOpts.authServerUrl ||
|
|
108
|
+
this.keycloakOpts['auth-server-url'] ||
|
|
109
|
+
this.keycloakOpts.serverUrl ||
|
|
110
|
+
this.keycloakOpts['server-url']);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
resolveClientId(realm_1) {
|
|
114
|
+
return __awaiter(this, arguments, void 0, function* (realm, request = undefined) {
|
|
115
|
+
if (typeof this.keycloakOpts === 'string') {
|
|
116
|
+
throw new errors_1.KeycloakConfigError('Keycloak configuration is a configuration path. This should not happen after module load.');
|
|
117
|
+
}
|
|
118
|
+
if (this.keycloakOpts.multiTenant === null ||
|
|
119
|
+
this.keycloakOpts.multiTenant === undefined) {
|
|
120
|
+
throw new errors_1.KeycloakConfigError('Multi tenant is not defined yet multi tenant service is being called.');
|
|
121
|
+
}
|
|
122
|
+
// If no realm client-id resolver is defined, return defaults
|
|
123
|
+
if (!this.keycloakOpts.multiTenant.realmClientIdResolver) {
|
|
124
|
+
return this.keycloakOpts.clientId || this.keycloakOpts['client-id'];
|
|
125
|
+
}
|
|
126
|
+
// Resolve realm client-id
|
|
127
|
+
const realmClientId = yield this.keycloakOpts.multiTenant.realmClientIdResolver(realm, request);
|
|
128
|
+
// Override client-id
|
|
129
|
+
// Order of priority: resolved realm client-id > default global client-id
|
|
130
|
+
return (realmClientId ||
|
|
131
|
+
this.keycloakOpts.clientId ||
|
|
132
|
+
this.keycloakOpts['client-id']);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
resolveSecret(realm_1) {
|
|
136
|
+
return __awaiter(this, arguments, void 0, function* (realm, request = undefined) {
|
|
137
|
+
if (typeof this.keycloakOpts === 'string') {
|
|
138
|
+
throw new errors_1.KeycloakConfigError('Keycloak configuration is a configuration path. This should not happen after module load.');
|
|
139
|
+
}
|
|
140
|
+
if (this.keycloakOpts.multiTenant === null ||
|
|
141
|
+
this.keycloakOpts.multiTenant === undefined) {
|
|
142
|
+
throw new errors_1.KeycloakConfigError('Multi tenant is not defined yet multi tenant service is being called.');
|
|
143
|
+
}
|
|
144
|
+
// If no realm secret resolver is defined, return defaults
|
|
145
|
+
if (!this.keycloakOpts.multiTenant.realmSecretResolver) {
|
|
146
|
+
return this.keycloakOpts.secret;
|
|
147
|
+
}
|
|
148
|
+
// Resolve realm secret
|
|
149
|
+
const realmSecret = yield this.keycloakOpts.multiTenant.realmSecretResolver(realm, request);
|
|
150
|
+
// Override secret
|
|
151
|
+
// Order of priority: resolved realm secret > default global secret
|
|
152
|
+
return realmSecret || this.keycloakOpts.secret;
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
exports.KeycloakMultiTenantService = KeycloakMultiTenantService;
|
|
157
|
+
exports.KeycloakMultiTenantService = KeycloakMultiTenantService = __decorate([
|
|
158
|
+
(0, common_1.Injectable)(),
|
|
159
|
+
__param(0, (0, common_1.Inject)(constants_1.KEYCLOAK_AUTH_OPTIONS)),
|
|
160
|
+
__metadata("design:paramtypes", [Object])
|
|
161
|
+
], KeycloakMultiTenantService);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { KeycloakAuthConfig } from '../interface/keycloak-auth-options.interface';
|
|
2
|
+
/**
|
|
3
|
+
* Service for generating Keycloak endpoint URLs.
|
|
4
|
+
*/
|
|
5
|
+
export declare class KeycloakUrlService {
|
|
6
|
+
private readonly keycloakOpts;
|
|
7
|
+
constructor(keycloakOpts: KeycloakAuthConfig);
|
|
8
|
+
/**
|
|
9
|
+
* Get the realm admin API base URL.
|
|
10
|
+
*/
|
|
11
|
+
realmAdminUrl(): string;
|
|
12
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.KeycloakUrlService = void 0;
|
|
16
|
+
const constants_1 = require("../constants");
|
|
17
|
+
const common_1 = require("@nestjs/common");
|
|
18
|
+
/**
|
|
19
|
+
* Service for generating Keycloak endpoint URLs.
|
|
20
|
+
*/
|
|
21
|
+
let KeycloakUrlService = class KeycloakUrlService {
|
|
22
|
+
constructor(keycloakOpts) {
|
|
23
|
+
this.keycloakOpts = keycloakOpts;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Get the realm admin API base URL.
|
|
27
|
+
*/
|
|
28
|
+
realmAdminUrl() {
|
|
29
|
+
const authServerUrl = (this.keycloakOpts.authServerUrl ||
|
|
30
|
+
this.keycloakOpts['auth-server-url'] ||
|
|
31
|
+
this.keycloakOpts.serverUrl ||
|
|
32
|
+
this.keycloakOpts['server-url'] ||
|
|
33
|
+
'').replace(/\/+$/, '');
|
|
34
|
+
const realm = this.keycloakOpts.realm || '';
|
|
35
|
+
return `${authServerUrl}/admin/realms/${realm}`;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
exports.KeycloakUrlService = KeycloakUrlService;
|
|
39
|
+
exports.KeycloakUrlService = KeycloakUrlService = __decorate([
|
|
40
|
+
(0, common_1.Injectable)(),
|
|
41
|
+
__param(0, (0, common_1.Inject)(constants_1.KEYCLOAK_AUTH_OPTIONS)),
|
|
42
|
+
__metadata("design:paramtypes", [Object])
|
|
43
|
+
], KeycloakUrlService);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { HttpService } from '@nestjs/axios';
|
|
2
|
+
import { OidcEndpoints } from '../interface/oidc.interface';
|
|
3
|
+
export { OidcEndpoints };
|
|
4
|
+
/**
|
|
5
|
+
* Fetches and caches OIDC discovery metadata per realm.
|
|
6
|
+
* Endpoints are resolved from {realmUrl}/.well-known/openid-configuration
|
|
7
|
+
* instead of being hardcoded.
|
|
8
|
+
*/
|
|
9
|
+
export declare class OidcDiscoveryService {
|
|
10
|
+
private readonly httpService;
|
|
11
|
+
private readonly logger;
|
|
12
|
+
private readonly cache;
|
|
13
|
+
private readonly cacheTtlMs;
|
|
14
|
+
constructor(httpService: HttpService);
|
|
15
|
+
/**
|
|
16
|
+
* Get the discovered OIDC endpoints for a realm URL.
|
|
17
|
+
* Results are cached for 5 minutes per realm.
|
|
18
|
+
*/
|
|
19
|
+
getEndpoints(realmUrl: string): Promise<OidcEndpoints>;
|
|
20
|
+
/**
|
|
21
|
+
* Clear all cached discovery metadata.
|
|
22
|
+
*/
|
|
23
|
+
clearCache(): void;
|
|
24
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
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
|
+
var OidcDiscoveryService_1;
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.OidcDiscoveryService = void 0;
|
|
23
|
+
const rxjs_1 = require("rxjs");
|
|
24
|
+
const axios_1 = require("@nestjs/axios");
|
|
25
|
+
const common_1 = require("@nestjs/common");
|
|
26
|
+
/**
|
|
27
|
+
* Fetches and caches OIDC discovery metadata per realm.
|
|
28
|
+
* Endpoints are resolved from {realmUrl}/.well-known/openid-configuration
|
|
29
|
+
* instead of being hardcoded.
|
|
30
|
+
*/
|
|
31
|
+
let OidcDiscoveryService = OidcDiscoveryService_1 = class OidcDiscoveryService {
|
|
32
|
+
constructor(httpService) {
|
|
33
|
+
this.httpService = httpService;
|
|
34
|
+
this.logger = new common_1.Logger(OidcDiscoveryService_1.name);
|
|
35
|
+
this.cache = new Map();
|
|
36
|
+
this.cacheTtlMs = 300000; // 5 minutes
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Get the discovered OIDC endpoints for a realm URL.
|
|
40
|
+
* Results are cached for 5 minutes per realm.
|
|
41
|
+
*/
|
|
42
|
+
getEndpoints(realmUrl) {
|
|
43
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
44
|
+
const cached = this.cache.get(realmUrl);
|
|
45
|
+
if (cached && Date.now() - cached.fetchedAt < this.cacheTtlMs) {
|
|
46
|
+
return cached.endpoints;
|
|
47
|
+
}
|
|
48
|
+
const discoveryUrl = `${realmUrl}/.well-known/openid-configuration`;
|
|
49
|
+
this.logger.verbose(`Fetching OIDC discovery from ${discoveryUrl}`);
|
|
50
|
+
const { data } = yield (0, rxjs_1.firstValueFrom)(this.httpService.request({
|
|
51
|
+
url: discoveryUrl,
|
|
52
|
+
method: 'GET',
|
|
53
|
+
headers: {
|
|
54
|
+
Accept: 'application/json',
|
|
55
|
+
'X-Client': 'nestjs-keycloak-auth',
|
|
56
|
+
},
|
|
57
|
+
timeout: 10000,
|
|
58
|
+
}));
|
|
59
|
+
const endpoints = {
|
|
60
|
+
jwks_uri: data.jwks_uri ||
|
|
61
|
+
`${realmUrl}/protocol/openid-connect/certs`,
|
|
62
|
+
token_endpoint: data.token_endpoint ||
|
|
63
|
+
`${realmUrl}/protocol/openid-connect/token`,
|
|
64
|
+
introspection_endpoint: data.introspection_endpoint ||
|
|
65
|
+
`${realmUrl}/protocol/openid-connect/token/introspect`,
|
|
66
|
+
userinfo_endpoint: data.userinfo_endpoint ||
|
|
67
|
+
`${realmUrl}/protocol/openid-connect/userinfo`,
|
|
68
|
+
end_session_endpoint: data.end_session_endpoint ||
|
|
69
|
+
`${realmUrl}/protocol/openid-connect/logout`,
|
|
70
|
+
};
|
|
71
|
+
this.cache.set(realmUrl, { endpoints, fetchedAt: Date.now() });
|
|
72
|
+
return endpoints;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Clear all cached discovery metadata.
|
|
77
|
+
*/
|
|
78
|
+
clearCache() {
|
|
79
|
+
this.cache.clear();
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
exports.OidcDiscoveryService = OidcDiscoveryService;
|
|
83
|
+
exports.OidcDiscoveryService = OidcDiscoveryService = OidcDiscoveryService_1 = __decorate([
|
|
84
|
+
(0, common_1.Injectable)(),
|
|
85
|
+
__metadata("design:paramtypes", [axios_1.HttpService])
|
|
86
|
+
], OidcDiscoveryService);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { JwksCacheService } from './jwks-cache.service';
|
|
2
|
+
import { KeycloakHttpService } from './keycloak-http.service';
|
|
3
|
+
import { KeycloakAuthConfig } from '../interface/keycloak-auth-options.interface';
|
|
4
|
+
export declare class TokenValidationService {
|
|
5
|
+
private readonly keycloakOpts;
|
|
6
|
+
private readonly keycloakHttp;
|
|
7
|
+
private readonly jwksCache;
|
|
8
|
+
private readonly logger;
|
|
9
|
+
private readonly publicKey;
|
|
10
|
+
private _notBefore;
|
|
11
|
+
private readonly notBeforeByRealm;
|
|
12
|
+
constructor(keycloakOpts: KeycloakAuthConfig, keycloakHttp: KeycloakHttpService, jwksCache: JwksCacheService);
|
|
13
|
+
get notBefore(): number;
|
|
14
|
+
set notBefore(value: number);
|
|
15
|
+
getNotBefore(realmUrl?: string): number;
|
|
16
|
+
setNotBefore(value: number, realmUrl?: string): void;
|
|
17
|
+
/**
|
|
18
|
+
* Validate a token online via the Keycloak introspection endpoint.
|
|
19
|
+
*/
|
|
20
|
+
validateOnline(jwt: string, realmUrl: string, clientId: string, secret: string): Promise<boolean>;
|
|
21
|
+
private static readonly ALLOWED_ALGS;
|
|
22
|
+
/**
|
|
23
|
+
* Validate a token offline by verifying signature via JWKS and checking
|
|
24
|
+
* expiry, type, notBefore, issuer, audience, azp, and signature.
|
|
25
|
+
* Matches keycloak-connect's GrantManager.validateToken() logic.
|
|
26
|
+
*/
|
|
27
|
+
validateOffline(jwt: string, realmUrl: string, clientId?: string, expectedType?: string): Promise<boolean>;
|
|
28
|
+
private verifySignature;
|
|
29
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
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 TokenValidationService_1;
|
|
47
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
+
exports.TokenValidationService = void 0;
|
|
49
|
+
const crypto = __importStar(require("crypto"));
|
|
50
|
+
const jwks_cache_service_1 = require("./jwks-cache.service");
|
|
51
|
+
const constants_1 = require("../constants");
|
|
52
|
+
const keycloak_token_1 = require("../token/keycloak-token");
|
|
53
|
+
const keycloak_http_service_1 = require("./keycloak-http.service");
|
|
54
|
+
const common_1 = require("@nestjs/common");
|
|
55
|
+
let TokenValidationService = TokenValidationService_1 = class TokenValidationService {
|
|
56
|
+
constructor(keycloakOpts, keycloakHttp, jwksCache) {
|
|
57
|
+
var _a;
|
|
58
|
+
this.keycloakOpts = keycloakOpts;
|
|
59
|
+
this.keycloakHttp = keycloakHttp;
|
|
60
|
+
this.jwksCache = jwksCache;
|
|
61
|
+
this.logger = new common_1.Logger(TokenValidationService_1.name);
|
|
62
|
+
this._notBefore = 0;
|
|
63
|
+
this.notBeforeByRealm = new Map();
|
|
64
|
+
// Format static realm public key if provided (matches keycloak-connect config.js)
|
|
65
|
+
const plainKey = (_a = this.keycloakOpts.realmPublicKey) !== null && _a !== void 0 ? _a : this.keycloakOpts['realm-public-key'];
|
|
66
|
+
if (plainKey) {
|
|
67
|
+
let pem = '-----BEGIN PUBLIC KEY-----\n';
|
|
68
|
+
for (let i = 0; i < plainKey.length; i += 64) {
|
|
69
|
+
pem += plainKey.substring(i, i + 64) + '\n';
|
|
70
|
+
}
|
|
71
|
+
pem += '-----END PUBLIC KEY-----';
|
|
72
|
+
this.publicKey = pem;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
get notBefore() {
|
|
76
|
+
return this._notBefore;
|
|
77
|
+
}
|
|
78
|
+
set notBefore(value) {
|
|
79
|
+
this._notBefore = value;
|
|
80
|
+
}
|
|
81
|
+
getNotBefore(realmUrl) {
|
|
82
|
+
var _a;
|
|
83
|
+
if (!realmUrl) {
|
|
84
|
+
return this._notBefore;
|
|
85
|
+
}
|
|
86
|
+
return (_a = this.notBeforeByRealm.get(realmUrl)) !== null && _a !== void 0 ? _a : this._notBefore;
|
|
87
|
+
}
|
|
88
|
+
setNotBefore(value, realmUrl) {
|
|
89
|
+
if (!realmUrl) {
|
|
90
|
+
this._notBefore = value;
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
this.notBeforeByRealm.set(realmUrl, value);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Validate a token online via the Keycloak introspection endpoint.
|
|
97
|
+
*/
|
|
98
|
+
validateOnline(jwt, realmUrl, clientId, secret) {
|
|
99
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
100
|
+
try {
|
|
101
|
+
const result = yield this.keycloakHttp.introspectToken(realmUrl, clientId, secret, jwt);
|
|
102
|
+
return result.active === true;
|
|
103
|
+
}
|
|
104
|
+
catch (ex) {
|
|
105
|
+
this.logger.warn(`Online token validation failed: ${ex}`);
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Validate a token offline by verifying signature via JWKS and checking
|
|
112
|
+
* expiry, type, notBefore, issuer, audience, azp, and signature.
|
|
113
|
+
* Matches keycloak-connect's GrantManager.validateToken() logic.
|
|
114
|
+
*/
|
|
115
|
+
validateOffline(jwt_1, realmUrl_1, clientId_1) {
|
|
116
|
+
return __awaiter(this, arguments, void 0, function* (jwt, realmUrl, clientId, expectedType = 'Bearer') {
|
|
117
|
+
var _a, _b;
|
|
118
|
+
try {
|
|
119
|
+
const token = new keycloak_token_1.KeycloakToken(jwt);
|
|
120
|
+
// Check token exists and is parseable (signed portion present)
|
|
121
|
+
if (!token.signed) {
|
|
122
|
+
this.logger.verbose('invalid token (not signed)');
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
// Check expiry
|
|
126
|
+
if (token.isExpired()) {
|
|
127
|
+
this.logger.verbose('invalid token (expired)');
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
// Check token type matches expected
|
|
131
|
+
if (token.content.typ !== expectedType) {
|
|
132
|
+
this.logger.verbose(`invalid token (wrong type): expected ${expectedType}, got ${token.content.typ}`);
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
// Check notBefore policy (stale token)
|
|
136
|
+
const realmNotBefore = this.getNotBefore(realmUrl);
|
|
137
|
+
if (token.content.iat < realmNotBefore) {
|
|
138
|
+
this.logger.verbose(`invalid token (stale token): iat ${token.content.iat} < notBefore ${realmNotBefore}`);
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
// Check issuer matches realm URL
|
|
142
|
+
if (token.content.iss !== realmUrl) {
|
|
143
|
+
this.logger.verbose(`invalid token (wrong ISS): ${token.content.iss} !== ${realmUrl}`);
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
// Audience and azp checks (matches keycloak-connect logic)
|
|
147
|
+
const audienceData = Array.isArray(token.content.aud)
|
|
148
|
+
? token.content.aud
|
|
149
|
+
: [token.content.aud];
|
|
150
|
+
// Bearer tokens only check audience if verifyTokenAudience is enabled
|
|
151
|
+
const verifyAudience = (_b = (_a = this.keycloakOpts.verifyTokenAudience) !== null && _a !== void 0 ? _a : this.keycloakOpts['verify-token-audience']) !== null && _b !== void 0 ? _b : false;
|
|
152
|
+
if (verifyAudience && clientId && !audienceData.includes(clientId)) {
|
|
153
|
+
this.logger.verbose(`invalid token (wrong audience): ${JSON.stringify(token.content.aud)} does not include ${clientId}`);
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
// Verify algorithm is on the allowlist (prevent algorithm confusion attacks)
|
|
157
|
+
const alg = token.header.alg || 'RS256';
|
|
158
|
+
if (!TokenValidationService_1.ALLOWED_ALGS.has(alg)) {
|
|
159
|
+
this.logger.verbose(`invalid token (unsupported alg): ${alg}`);
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
if (this.publicKey) {
|
|
163
|
+
// Use static public key if configured
|
|
164
|
+
if (!this.verifySignature(token.signed, token.signature, this.publicKey, alg)) {
|
|
165
|
+
this.logger.verbose('invalid token (signature)');
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
// Otherwise use JWKS rotation
|
|
171
|
+
const kid = token.header.kid;
|
|
172
|
+
if (!kid) {
|
|
173
|
+
this.logger.warn('Token has no kid in header');
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
const key = yield this.jwksCache.getKey(realmUrl, kid);
|
|
177
|
+
if (!this.verifySignature(token.signed, token.signature, key, alg)) {
|
|
178
|
+
this.logger.verbose('invalid token (public key signature)');
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
catch (ex) {
|
|
184
|
+
this.logger.warn(`Offline token validation failed: ${ex}`);
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
verifySignature(signed, signature, key, jwtAlg) {
|
|
190
|
+
const hashMap = {
|
|
191
|
+
RS256: 'SHA256', RS384: 'SHA384', RS512: 'SHA512',
|
|
192
|
+
ES256: 'SHA256', ES384: 'SHA384', ES512: 'SHA512',
|
|
193
|
+
PS256: 'SHA256', PS384: 'SHA384', PS512: 'SHA512',
|
|
194
|
+
};
|
|
195
|
+
const hash = hashMap[jwtAlg] || 'SHA256';
|
|
196
|
+
const keyObj = typeof key === 'string' ? crypto.createPublicKey(key) : key;
|
|
197
|
+
if (jwtAlg.startsWith('PS')) {
|
|
198
|
+
return crypto.verify(hash, Buffer.from(signed), { key: keyObj, padding: crypto.constants.RSA_PKCS1_PSS_PADDING }, signature);
|
|
199
|
+
}
|
|
200
|
+
// crypto.verify() auto-detects key type (RSA vs EC) from the KeyObject
|
|
201
|
+
return crypto.verify(hash, Buffer.from(signed), keyObj, signature);
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
exports.TokenValidationService = TokenValidationService;
|
|
205
|
+
TokenValidationService.ALLOWED_ALGS = new Set([
|
|
206
|
+
'RS256', 'RS384', 'RS512',
|
|
207
|
+
'ES256', 'ES384', 'ES512',
|
|
208
|
+
'PS256', 'PS384', 'PS512',
|
|
209
|
+
]);
|
|
210
|
+
exports.TokenValidationService = TokenValidationService = TokenValidationService_1 = __decorate([
|
|
211
|
+
(0, common_1.Injectable)(),
|
|
212
|
+
__param(0, (0, common_1.Inject)(constants_1.KEYCLOAK_AUTH_OPTIONS)),
|
|
213
|
+
__metadata("design:paramtypes", [Object, keycloak_http_service_1.KeycloakHttpService,
|
|
214
|
+
jwks_cache_service_1.JwksCacheService])
|
|
215
|
+
], TokenValidationService);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { KeycloakToken } from './keycloak-token';
|
|
2
|
+
/**
|
|
3
|
+
* Represents a Keycloak grant containing an access token.
|
|
4
|
+
*/
|
|
5
|
+
export declare class KeycloakGrant {
|
|
6
|
+
access_token: KeycloakToken | undefined;
|
|
7
|
+
token_type: string | undefined;
|
|
8
|
+
expires_in: number | undefined;
|
|
9
|
+
__raw: string | undefined;
|
|
10
|
+
constructor(grant: Partial<KeycloakGrant>);
|
|
11
|
+
/**
|
|
12
|
+
* Update this grant in-place given data in another grant.
|
|
13
|
+
*/
|
|
14
|
+
update(grant: Partial<KeycloakGrant>): void;
|
|
15
|
+
/**
|
|
16
|
+
* Returns the raw String of the grant, if available.
|
|
17
|
+
*/
|
|
18
|
+
toString(): string | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* Determine if this grant is expired/out-of-date.
|
|
21
|
+
* Determination is made based upon the expiration status of the access_token.
|
|
22
|
+
*/
|
|
23
|
+
isExpired(): boolean;
|
|
24
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.KeycloakGrant = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Represents a Keycloak grant containing an access token.
|
|
6
|
+
*/
|
|
7
|
+
class KeycloakGrant {
|
|
8
|
+
constructor(grant) {
|
|
9
|
+
this.update(grant);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Update this grant in-place given data in another grant.
|
|
13
|
+
*/
|
|
14
|
+
update(grant) {
|
|
15
|
+
this.access_token = grant.access_token;
|
|
16
|
+
this.token_type = grant.token_type;
|
|
17
|
+
this.expires_in = grant.expires_in;
|
|
18
|
+
this.__raw = grant.__raw;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Returns the raw String of the grant, if available.
|
|
22
|
+
*/
|
|
23
|
+
toString() {
|
|
24
|
+
return this.__raw;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Determine if this grant is expired/out-of-date.
|
|
28
|
+
* Determination is made based upon the expiration status of the access_token.
|
|
29
|
+
*/
|
|
30
|
+
isExpired() {
|
|
31
|
+
if (!this.access_token) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
return this.access_token.isExpired();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
exports.KeycloakGrant = KeycloakGrant;
|