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.
Files changed (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +391 -0
  3. package/dist/constants.d.ts +65 -0
  4. package/dist/constants.js +72 -0
  5. package/dist/controllers/keycloak-admin.controller.d.ts +16 -0
  6. package/dist/controllers/keycloak-admin.controller.js +94 -0
  7. package/dist/decorators/access-token.decorator.d.ts +5 -0
  8. package/dist/decorators/access-token.decorator.js +13 -0
  9. package/dist/decorators/enforcer-options.decorator.d.ts +8 -0
  10. package/dist/decorators/enforcer-options.decorator.js +12 -0
  11. package/dist/decorators/keycloak-user.decorator.d.ts +5 -0
  12. package/dist/decorators/keycloak-user.decorator.js +13 -0
  13. package/dist/decorators/public.decorator.d.ts +6 -0
  14. package/dist/decorators/public.decorator.js +11 -0
  15. package/dist/decorators/resource.decorator.d.ts +6 -0
  16. package/dist/decorators/resource.decorator.js +11 -0
  17. package/dist/decorators/roles.decorator.d.ts +10 -0
  18. package/dist/decorators/roles.decorator.js +15 -0
  19. package/dist/decorators/scopes.decorator.d.ts +19 -0
  20. package/dist/decorators/scopes.decorator.js +27 -0
  21. package/dist/decorators/token-scopes.decorator.d.ts +19 -0
  22. package/dist/decorators/token-scopes.decorator.js +24 -0
  23. package/dist/errors.d.ts +24 -0
  24. package/dist/errors.js +44 -0
  25. package/dist/guards/auth.guard.d.ts +25 -0
  26. package/dist/guards/auth.guard.js +176 -0
  27. package/dist/guards/resource.guard.d.ts +26 -0
  28. package/dist/guards/resource.guard.js +245 -0
  29. package/dist/guards/role.guard.d.ts +17 -0
  30. package/dist/guards/role.guard.js +113 -0
  31. package/dist/index.d.ts +1 -0
  32. package/dist/index.js +18 -0
  33. package/dist/interface/enforcer-options.interface.d.ts +8 -0
  34. package/dist/interface/enforcer-options.interface.js +2 -0
  35. package/dist/interface/jwks.interface.d.ts +22 -0
  36. package/dist/interface/jwks.interface.js +2 -0
  37. package/dist/interface/jwt.interface.d.ts +46 -0
  38. package/dist/interface/jwt.interface.js +2 -0
  39. package/dist/interface/keycloak-auth-module-async-options.interface.d.ts +11 -0
  40. package/dist/interface/keycloak-auth-module-async-options.interface.js +2 -0
  41. package/dist/interface/keycloak-auth-options-factory.interface.d.ts +4 -0
  42. package/dist/interface/keycloak-auth-options-factory.interface.js +2 -0
  43. package/dist/interface/keycloak-auth-options.interface.d.ts +162 -0
  44. package/dist/interface/keycloak-auth-options.interface.js +3 -0
  45. package/dist/interface/keycloak-grant.interface.d.ts +33 -0
  46. package/dist/interface/keycloak-grant.interface.js +2 -0
  47. package/dist/interface/keycloak-request.interface.d.ts +8 -0
  48. package/dist/interface/keycloak-request.interface.js +2 -0
  49. package/dist/interface/oidc.interface.d.ts +14 -0
  50. package/dist/interface/oidc.interface.js +2 -0
  51. package/dist/interface/server.interface.d.ts +9 -0
  52. package/dist/interface/server.interface.js +2 -0
  53. package/dist/interface/tenant-config.interface.d.ts +13 -0
  54. package/dist/interface/tenant-config.interface.js +2 -0
  55. package/dist/internal.util.d.ts +13 -0
  56. package/dist/internal.util.js +54 -0
  57. package/dist/keycloak-auth.module.d.ts +54 -0
  58. package/dist/keycloak-auth.module.js +174 -0
  59. package/dist/keycloak-auth.providers.d.ts +7 -0
  60. package/dist/keycloak-auth.providers.js +122 -0
  61. package/dist/services/backchannel-logout.service.d.ts +30 -0
  62. package/dist/services/backchannel-logout.service.js +100 -0
  63. package/dist/services/jwks-cache.service.d.ts +25 -0
  64. package/dist/services/jwks-cache.service.js +130 -0
  65. package/dist/services/keycloak-admin.service.d.ts +37 -0
  66. package/dist/services/keycloak-admin.service.js +268 -0
  67. package/dist/services/keycloak-grant.service.d.ts +23 -0
  68. package/dist/services/keycloak-grant.service.js +88 -0
  69. package/dist/services/keycloak-http.service.d.ts +41 -0
  70. package/dist/services/keycloak-http.service.js +211 -0
  71. package/dist/services/keycloak-multitenant.service.d.ts +24 -0
  72. package/dist/services/keycloak-multitenant.service.js +161 -0
  73. package/dist/services/keycloak-url.service.d.ts +12 -0
  74. package/dist/services/keycloak-url.service.js +43 -0
  75. package/dist/services/oidc-discovery.service.d.ts +24 -0
  76. package/dist/services/oidc-discovery.service.js +86 -0
  77. package/dist/services/token-validation.service.d.ts +29 -0
  78. package/dist/services/token-validation.service.js +215 -0
  79. package/dist/token/keycloak-grant.d.ts +24 -0
  80. package/dist/token/keycloak-grant.js +37 -0
  81. package/dist/token/keycloak-token.d.ts +57 -0
  82. package/dist/token/keycloak-token.js +105 -0
  83. package/dist/types/conditional-scope.type.d.ts +2 -0
  84. package/dist/types/conditional-scope.type.js +2 -0
  85. package/dist/util.d.ts +3 -0
  86. package/dist/util.js +17 -0
  87. 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;