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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 B. Joshua Adedigba
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# NestJS Keycloak Auth
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+
[](https://scorecard.dev/viewer/?uri=github.com/bannaarr01/nestjs-keycloak-auth)
|
|
8
|
+
|
|
9
|
+
A bearer-only Keycloak authentication and authorization module for [NestJS](https://nestjs.com/). Uses standard OIDC discovery and has zero runtime dependency on `keycloak-connect`.
|
|
10
|
+
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- Bearer-token API authentication and authorization for NestJS.
|
|
16
|
+
- OIDC discovery — endpoints resolved from `.well-known/openid-configuration` (with fallback).
|
|
17
|
+
- ONLINE and OFFLINE token validation (introspection + JWKS signature verification).
|
|
18
|
+
- Algorithm allowlist — only RS, ES, and PS family algorithms are accepted during offline validation.
|
|
19
|
+
- Per-realm `notBefore` revocation state for multi-tenant safety.
|
|
20
|
+
- Resource/scope authorization via UMA (`@Resource`, `@Scopes`, `@ConditionalScopes`).
|
|
21
|
+
- Role authorization (`@Roles`) with configurable role merge and match modes.
|
|
22
|
+
- OIDC back-channel logout (`sid`/`sub` revocation).
|
|
23
|
+
- Typed error hierarchy — all library errors extend `KeycloakAuthError` for easy catching.
|
|
24
|
+
- Compatible with [Fastify](https://github.com/fastify/fastify) platform.
|
|
25
|
+
|
|
26
|
+
## Runtime Scope (Important)
|
|
27
|
+
|
|
28
|
+
- This package is designed for bearer-only API/server flows.
|
|
29
|
+
- It does **not** implement browser/session middleware flows such as login redirects, auth-code callback exchange, session/cookie grant stores, or logout endpoints.
|
|
30
|
+
- It implements Keycloak admin callback endpoints:
|
|
31
|
+
- `POST /k_push_not_before` for realm `notBefore` revocation updates (used by OFFLINE token validation).
|
|
32
|
+
- `POST /k_logout` for OIDC back-channel logout token handling (`sid`/`sub` revocation).
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
### Yarn
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
yarn add nestjs-keycloak-auth
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### NPM
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm install nestjs-keycloak-auth --save
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Getting Started
|
|
49
|
+
|
|
50
|
+
### Module registration
|
|
51
|
+
|
|
52
|
+
Registering the module:
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
KeycloakAuthModule.register({
|
|
56
|
+
authServerUrl: 'http://localhost:8080', // might be http://localhost:8080/auth for older keycloak versions
|
|
57
|
+
realm: 'master',
|
|
58
|
+
clientId: 'my-nestjs-app',
|
|
59
|
+
secret: 'secret',
|
|
60
|
+
bearerOnly: true,
|
|
61
|
+
policyEnforcement: PolicyEnforcementMode.PERMISSIVE, // optional
|
|
62
|
+
tokenValidation: TokenValidation.ONLINE, // optional
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Async registration is also available:
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
KeycloakAuthModule.registerAsync({
|
|
70
|
+
useExisting: KeycloakConfigService,
|
|
71
|
+
imports: [ConfigModule],
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
#### KeycloakConfigService
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
import { Injectable } from '@nestjs/common';
|
|
79
|
+
import {
|
|
80
|
+
KeycloakAuthOptions,
|
|
81
|
+
KeycloakAuthOptionsFactory,
|
|
82
|
+
PolicyEnforcementMode,
|
|
83
|
+
TokenValidation,
|
|
84
|
+
} from 'nestjs-keycloak-auth';
|
|
85
|
+
|
|
86
|
+
@Injectable()
|
|
87
|
+
export class KeycloakConfigService implements KeycloakAuthOptionsFactory {
|
|
88
|
+
createKeycloakAuthOptions(): KeycloakAuthOptions {
|
|
89
|
+
return {
|
|
90
|
+
// http://localhost:8080/auth for older keycloak versions
|
|
91
|
+
authServerUrl: 'http://localhost:8080',
|
|
92
|
+
realm: 'master',
|
|
93
|
+
clientId: 'my-nestjs-app',
|
|
94
|
+
secret: 'secret',
|
|
95
|
+
bearerOnly: true,
|
|
96
|
+
policyEnforcement: PolicyEnforcementMode.PERMISSIVE,
|
|
97
|
+
tokenValidation: TokenValidation.ONLINE,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
You can also register by just providing the `keycloak.json` path and an optional module configuration:
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
KeycloakAuthModule.register(`./keycloak.json`, {
|
|
107
|
+
policyEnforcement: PolicyEnforcementMode.PERMISSIVE,
|
|
108
|
+
tokenValidation: TokenValidation.ONLINE,
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Guards
|
|
113
|
+
|
|
114
|
+
Register any of the guards either globally, or scoped in your controller.
|
|
115
|
+
|
|
116
|
+
#### Global registration using APP_GUARD token
|
|
117
|
+
|
|
118
|
+
**_NOTE: These are in order, see https://docs.nestjs.com/guards#binding-guards for more information._**
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
providers: [
|
|
122
|
+
{
|
|
123
|
+
provide: APP_GUARD,
|
|
124
|
+
useClass: AuthGuard,
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
provide: APP_GUARD,
|
|
128
|
+
useClass: ResourceGuard,
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
provide: APP_GUARD,
|
|
132
|
+
useClass: RoleGuard,
|
|
133
|
+
},
|
|
134
|
+
];
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
#### Scoped registration
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
@Controller('cats')
|
|
141
|
+
@UseGuards(AuthGuard, ResourceGuard)
|
|
142
|
+
export class CatsController {}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## What do these guards do?
|
|
146
|
+
|
|
147
|
+
### AuthGuard
|
|
148
|
+
|
|
149
|
+
Adds an authentication guard, you can also have it scoped if you like (using regular `@UseGuards(AuthGuard)` in your controllers). By default, it will throw a 401 unauthorized when it is unable to verify the JWT token or `Bearer` header is missing.
|
|
150
|
+
|
|
151
|
+
### ResourceGuard
|
|
152
|
+
|
|
153
|
+
Adds a resource guard, which is permissive by default (can be configured see [options](#nest-keycloak-options)). Only controllers annotated with `@Resource` and methods with `@Scopes` are handled by this guard.
|
|
154
|
+
|
|
155
|
+
**_NOTE: This guard is not necessary if you are using role-based authorization exclusively. You can use role guard exclusively for that._**
|
|
156
|
+
|
|
157
|
+
### RoleGuard
|
|
158
|
+
|
|
159
|
+
Adds a role guard, **can only be used in conjunction with resource guard when enforcement policy is PERMISSIVE**, unless you only use role guard exclusively.
|
|
160
|
+
Permissive by default. Used by controller methods annotated with `@Roles` (matching can be configured)
|
|
161
|
+
|
|
162
|
+
## Configuring controllers
|
|
163
|
+
|
|
164
|
+
In your controllers, simply do:
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
import {
|
|
168
|
+
Resource,
|
|
169
|
+
Roles,
|
|
170
|
+
Scopes,
|
|
171
|
+
Public,
|
|
172
|
+
RoleMatchingMode,
|
|
173
|
+
} from 'nestjs-keycloak-auth';
|
|
174
|
+
import { Controller, Get, Delete, Put, Post, Param } from '@nestjs/common';
|
|
175
|
+
import { Product } from './product';
|
|
176
|
+
import { ProductService } from './product.service';
|
|
177
|
+
|
|
178
|
+
@Controller()
|
|
179
|
+
@Resource(Product.name)
|
|
180
|
+
export class ProductController {
|
|
181
|
+
constructor(private service: ProductService) {}
|
|
182
|
+
|
|
183
|
+
@Get()
|
|
184
|
+
@Public()
|
|
185
|
+
async findAll() {
|
|
186
|
+
return await this.service.findAll();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
@Get()
|
|
190
|
+
@Roles({ roles: ['admin', 'other'] })
|
|
191
|
+
async findAllBarcodes() {
|
|
192
|
+
return await this.service.findAllBarcodes();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
@Get(':code')
|
|
196
|
+
@Scopes('View')
|
|
197
|
+
async findByCode(@Param('code') code: string) {
|
|
198
|
+
return await this.service.findByCode(code);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
@Post()
|
|
202
|
+
@Scopes('Create')
|
|
203
|
+
@ConditionalScopes((request, token) => {
|
|
204
|
+
if (token.hasRealmRole('sysadmin')) {
|
|
205
|
+
return ['Overwrite'];
|
|
206
|
+
}
|
|
207
|
+
return [];
|
|
208
|
+
})
|
|
209
|
+
async create(@Body() product: Product) {
|
|
210
|
+
return await this.service.create(product);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
@Delete(':code')
|
|
214
|
+
@Scopes('Delete')
|
|
215
|
+
@Roles({ roles: ['admin', 'realm:sysadmin'], mode: RoleMatchingMode.ALL })
|
|
216
|
+
async deleteByCode(@Param('code') code: string) {
|
|
217
|
+
return await this.service.deleteByCode(code);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
@Put(':code')
|
|
221
|
+
@Scopes('Edit')
|
|
222
|
+
async update(@Param('code') code: string, @Body() product: Product) {
|
|
223
|
+
return await this.service.update(code, product);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Decorators
|
|
229
|
+
|
|
230
|
+
Here are the decorators you can use in your controllers.
|
|
231
|
+
|
|
232
|
+
| Decorator | Description |
|
|
233
|
+
| ------------------ | --------------------------------------------------------------------------------------------------------- |
|
|
234
|
+
| @KeycloakUser | Retrieves the current Keycloak logged-in user. (must be per method, unless controller is request scoped.) |
|
|
235
|
+
| @AccessToken | Retrieves the access token used in the request |
|
|
236
|
+
| @ResolvedScopes | Retrieves the resolved scopes (used in @ConditionalScopes) |
|
|
237
|
+
| @EnforcerOptions | Keycloak enforcer options. |
|
|
238
|
+
| @Public | Allow any user to use the route. |
|
|
239
|
+
| @Resource | Keycloak application resource name. |
|
|
240
|
+
| @Scopes | Keycloak application scopes. |
|
|
241
|
+
| @ConditionalScopes | Conditional keycloak application scopes. |
|
|
242
|
+
| @Roles | Keycloak realm/application roles. |
|
|
243
|
+
| @TokenScopes | Required OAuth scopes on the access token. |
|
|
244
|
+
|
|
245
|
+
## Multi tenant configuration
|
|
246
|
+
|
|
247
|
+
Setting up for multi-tenant is configured as an option in your configuration:
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
{
|
|
251
|
+
// Add /auth for older keycloak versions
|
|
252
|
+
authServerUrl: 'http://localhost:8180/', // will be used as fallback
|
|
253
|
+
clientId: 'nest-api', // will be used as fallback
|
|
254
|
+
secret: 'fallback', // will be used as fallback
|
|
255
|
+
multiTenant: {
|
|
256
|
+
resolveAlways: true,
|
|
257
|
+
realmResolver: (request) => {
|
|
258
|
+
return request.get('host').split('.')[0];
|
|
259
|
+
},
|
|
260
|
+
realmSecretResolver: (realm, request) => {
|
|
261
|
+
const secrets = { master: 'secret', slave: 'password' };
|
|
262
|
+
return secrets[realm];
|
|
263
|
+
},
|
|
264
|
+
realmClientIdResolver: (realm, request) => {
|
|
265
|
+
const clientIds = { master: 'angular-app', slave: 'vue-app' };
|
|
266
|
+
return clientIds[realm];
|
|
267
|
+
},
|
|
268
|
+
// note to add /auth for older keycloak versions
|
|
269
|
+
realmAuthServerUrlResolver: (realm, request) => {
|
|
270
|
+
const authServerUrls = { master: 'https://master.local/', slave: 'https://slave.local/' };
|
|
271
|
+
return authServerUrls[realm];
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## Admin callback endpoints
|
|
278
|
+
|
|
279
|
+
This module mounts Keycloak admin callback endpoints:
|
|
280
|
+
|
|
281
|
+
- `POST /k_push_not_before`
|
|
282
|
+
- `POST /k_logout`
|
|
283
|
+
|
|
284
|
+
Purpose:
|
|
285
|
+
|
|
286
|
+
- Accepts signed Keycloak admin callbacks with action `PUSH_NOT_BEFORE`.
|
|
287
|
+
- Updates token revocation cutoff (`notBefore`) used by OFFLINE validation.
|
|
288
|
+
- Stores `notBefore` per realm URL, so one realm update does not affect another realm in multi-tenant setups.
|
|
289
|
+
- Accepts signed OIDC back-channel logout tokens and revokes token usage by `sid` and/or `sub`.
|
|
290
|
+
|
|
291
|
+
Realm resolution for callback verification:
|
|
292
|
+
|
|
293
|
+
1. `multiTenant.realmResolver(request)` when configured
|
|
294
|
+
2. Single-tenant configured realm (`realm`)
|
|
295
|
+
|
|
296
|
+
`k_logout` here is callback-only revocation handling for bearer tokens. It does not add browser/session middleware flows.
|
|
297
|
+
|
|
298
|
+
## Error handling
|
|
299
|
+
|
|
300
|
+
All errors thrown by the library extend `KeycloakAuthError`, so you can catch any library error with a single `instanceof` check:
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
import {
|
|
304
|
+
KeycloakAuthError,
|
|
305
|
+
KeycloakConfigError,
|
|
306
|
+
KeycloakTokenError,
|
|
307
|
+
KeycloakPermissionError,
|
|
308
|
+
KeycloakAdminError,
|
|
309
|
+
} from 'nestjs-keycloak-auth';
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
| Error class | Code | When |
|
|
313
|
+
| ------------------------ | --------------------------- | -------------------------------------------------------- |
|
|
314
|
+
| `KeycloakAuthError` | `KEYCLOAK_AUTH_ERROR` | Base class — catches all library errors via `instanceof` |
|
|
315
|
+
| `KeycloakConfigError` | `KEYCLOAK_CONFIG_ERROR` | Missing config, invalid options, file not found |
|
|
316
|
+
| `KeycloakTokenError` | `KEYCLOAK_TOKEN_ERROR` | Malformed JWT, grant validation failure, JWKS key miss |
|
|
317
|
+
| `KeycloakPermissionError`| `KEYCLOAK_PERMISSION_ERROR` | UMA permission check failure |
|
|
318
|
+
| `KeycloakAdminError` | `KEYCLOAK_ADMIN_ERROR` | Admin callback signature/token verification failure |
|
|
319
|
+
|
|
320
|
+
Guards continue to throw standard NestJS `UnauthorizedException` / `ForbiddenException` at the HTTP boundary.
|
|
321
|
+
|
|
322
|
+
## Example project
|
|
323
|
+
|
|
324
|
+
The [`example/`](example/) folder contains a complete working NestJS application with:
|
|
325
|
+
|
|
326
|
+
- Docker Compose setup (Keycloak + PostgreSQL + NestJS API)
|
|
327
|
+
- Pre-configured Keycloak realm exports (single tenant + two tenants)
|
|
328
|
+
- Postman collection for testing all endpoints
|
|
329
|
+
- Product CRUD controller demonstrating `@Resource`, `@Scopes`, `@ConditionalScopes`, `@Roles`, `@EnforcerOptions`, and UMA response modes
|
|
330
|
+
|
|
331
|
+
See [example/README.md](example/README.md) for setup and usage instructions.
|
|
332
|
+
|
|
333
|
+
## Testing
|
|
334
|
+
|
|
335
|
+
```bash
|
|
336
|
+
npm test
|
|
337
|
+
npm run test:cov
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
Current test setup uses Jest + ts-jest and is configured to enforce 100% global coverage thresholds.
|
|
341
|
+
|
|
342
|
+
## Configuration options
|
|
343
|
+
|
|
344
|
+
### Nest Keycloak Options
|
|
345
|
+
|
|
346
|
+
| Option | Description | Required | Default |
|
|
347
|
+
| ----------------- | -------------------------------------------------------------------------- | -------- | ---------- |
|
|
348
|
+
| policyEnforcement | Sets the policy enforcement mode | no | PERMISSIVE |
|
|
349
|
+
| tokenValidation | Sets the token validation method | no | ONLINE |
|
|
350
|
+
| multiTenant | Sets options for [multi-tenant configuration](#multi-tenant-configuration) | no | - |
|
|
351
|
+
| roleMerge | Sets the merge mode for `@Roles` decorator | no | OVERRIDE |
|
|
352
|
+
|
|
353
|
+
### Common Keycloak Config Fields
|
|
354
|
+
|
|
355
|
+
| Option | Description |
|
|
356
|
+
| ------------------------------ | ------------------------------------------------------ |
|
|
357
|
+
| `realm` | Realm name |
|
|
358
|
+
| `clientId` / `client-id` | Client ID (or `resource`) |
|
|
359
|
+
| `secret` / `credentials.secret` | Client secret (confidential clients) |
|
|
360
|
+
| `authServerUrl` / `serverUrl` | Keycloak base URL |
|
|
361
|
+
| `bearerOnly` / `bearer-only` | Marks bearer-only behavior |
|
|
362
|
+
| `public` / `public-client` | Public client mode |
|
|
363
|
+
| `realmPublicKey` | Static realm public key for OFFLINE validation |
|
|
364
|
+
| `verifyTokenAudience` | Enables strict audience check in OFFLINE validation |
|
|
365
|
+
| `minTimeBetweenJwksRequests` | JWKS retry throttle |
|
|
366
|
+
|
|
367
|
+
### Multi Tenant Options
|
|
368
|
+
|
|
369
|
+
| Option | Description | Required | Default |
|
|
370
|
+
| -------------------------- | --------------------------------------------------------------------------------------------------------- | -------- | ------- |
|
|
371
|
+
| resolveAlways | Always resolve realm config instead of using cached values | no | false |
|
|
372
|
+
| realmResolver | Resolves realm from request | yes | - |
|
|
373
|
+
| realmSecretResolver | Resolves secret by realm (and optional request) | no | - |
|
|
374
|
+
| realmAuthServerUrlResolver | Resolves auth server URL by realm (and optional request) | no | - |
|
|
375
|
+
| realmClientIdResolver | Resolves client ID by realm (and optional request) | yes | - |
|
|
376
|
+
|
|
377
|
+
## Contributing
|
|
378
|
+
|
|
379
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and guidelines.
|
|
380
|
+
|
|
381
|
+
## Security
|
|
382
|
+
|
|
383
|
+
To report a vulnerability, see [SECURITY.md](SECURITY.md). Do not open a public issue.
|
|
384
|
+
|
|
385
|
+
## Acknowledgements
|
|
386
|
+
|
|
387
|
+
Inspired by [nest-keycloak-connect](https://github.com/ferrerojosh/nest-keycloak-connect) by [John Joshua Ferrer](https://github.com/ferrerojosh) and the official [keycloak-nodejs-connect](https://github.com/keycloak/keycloak-nodejs-connect) adapter.
|
|
388
|
+
|
|
389
|
+
## License
|
|
390
|
+
|
|
391
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Used internally, provides keycloak options for the Nest guards.
|
|
3
|
+
*/
|
|
4
|
+
export declare const KEYCLOAK_AUTH_OPTIONS = "KEYCLOAK_AUTH_OPTIONS";
|
|
5
|
+
/**
|
|
6
|
+
* Key for injecting a keycloak instance.
|
|
7
|
+
*/
|
|
8
|
+
export declare const KEYCLOAK_INSTANCE = "KEYCLOAK_INSTANCE";
|
|
9
|
+
/**
|
|
10
|
+
* Key for injecting a keycloak multi tenant service.
|
|
11
|
+
*/
|
|
12
|
+
export declare const KEYCLOAK_MULTITENANT_SERVICE = "KEYCLOAK_MULTITENANT_SERVICE";
|
|
13
|
+
/**
|
|
14
|
+
* Role matching mode.
|
|
15
|
+
*/
|
|
16
|
+
export declare enum RoleMatch {
|
|
17
|
+
/**
|
|
18
|
+
* Match all roles
|
|
19
|
+
*/
|
|
20
|
+
ALL = "all",
|
|
21
|
+
/**
|
|
22
|
+
* Match any roles
|
|
23
|
+
*/
|
|
24
|
+
ANY = "any"
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Policy enforcement mode.
|
|
28
|
+
*/
|
|
29
|
+
export declare enum PolicyEnforcementMode {
|
|
30
|
+
/**
|
|
31
|
+
* Deny all request when there is no matching resource.
|
|
32
|
+
*/
|
|
33
|
+
ENFORCING = "enforcing",
|
|
34
|
+
/**
|
|
35
|
+
* Allow all request even when there's no matching resource.
|
|
36
|
+
*/
|
|
37
|
+
PERMISSIVE = "permissive"
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Token validation methods.
|
|
41
|
+
*/
|
|
42
|
+
export declare enum TokenValidation {
|
|
43
|
+
/**
|
|
44
|
+
* The default validation method, performs live validation via Keycloak servers.
|
|
45
|
+
*/
|
|
46
|
+
ONLINE = "online",
|
|
47
|
+
/**
|
|
48
|
+
* Validate offline against the configured keycloak options.
|
|
49
|
+
*/
|
|
50
|
+
OFFLINE = "offline",
|
|
51
|
+
/**
|
|
52
|
+
* Does not check for any validation. Should only be used for special cases (i.e development, internal networks)
|
|
53
|
+
*/
|
|
54
|
+
NONE = "none"
|
|
55
|
+
}
|
|
56
|
+
export declare enum RoleMerge {
|
|
57
|
+
/**
|
|
58
|
+
* Overrides roles if defined both controller and handlers, with controller taking over.
|
|
59
|
+
*/
|
|
60
|
+
OVERRIDE = 0,
|
|
61
|
+
/**
|
|
62
|
+
* Merges all roles from both controller and handlers.
|
|
63
|
+
*/
|
|
64
|
+
ALL = 1
|
|
65
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RoleMerge = exports.TokenValidation = exports.PolicyEnforcementMode = exports.RoleMatch = exports.KEYCLOAK_MULTITENANT_SERVICE = exports.KEYCLOAK_INSTANCE = exports.KEYCLOAK_AUTH_OPTIONS = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Used internally, provides keycloak options for the Nest guards.
|
|
6
|
+
*/
|
|
7
|
+
exports.KEYCLOAK_AUTH_OPTIONS = 'KEYCLOAK_AUTH_OPTIONS';
|
|
8
|
+
/**
|
|
9
|
+
* Key for injecting a keycloak instance.
|
|
10
|
+
*/
|
|
11
|
+
exports.KEYCLOAK_INSTANCE = 'KEYCLOAK_INSTANCE';
|
|
12
|
+
/**
|
|
13
|
+
* Key for injecting a keycloak multi tenant service.
|
|
14
|
+
*/
|
|
15
|
+
exports.KEYCLOAK_MULTITENANT_SERVICE = 'KEYCLOAK_MULTITENANT_SERVICE';
|
|
16
|
+
/**
|
|
17
|
+
* Role matching mode.
|
|
18
|
+
*/
|
|
19
|
+
var RoleMatch;
|
|
20
|
+
(function (RoleMatch) {
|
|
21
|
+
/**
|
|
22
|
+
* Match all roles
|
|
23
|
+
*/
|
|
24
|
+
RoleMatch["ALL"] = "all";
|
|
25
|
+
/**
|
|
26
|
+
* Match any roles
|
|
27
|
+
*/
|
|
28
|
+
RoleMatch["ANY"] = "any";
|
|
29
|
+
})(RoleMatch || (exports.RoleMatch = RoleMatch = {}));
|
|
30
|
+
/**
|
|
31
|
+
* Policy enforcement mode.
|
|
32
|
+
*/
|
|
33
|
+
var PolicyEnforcementMode;
|
|
34
|
+
(function (PolicyEnforcementMode) {
|
|
35
|
+
/**
|
|
36
|
+
* Deny all request when there is no matching resource.
|
|
37
|
+
*/
|
|
38
|
+
PolicyEnforcementMode["ENFORCING"] = "enforcing";
|
|
39
|
+
/**
|
|
40
|
+
* Allow all request even when there's no matching resource.
|
|
41
|
+
*/
|
|
42
|
+
PolicyEnforcementMode["PERMISSIVE"] = "permissive";
|
|
43
|
+
})(PolicyEnforcementMode || (exports.PolicyEnforcementMode = PolicyEnforcementMode = {}));
|
|
44
|
+
/**
|
|
45
|
+
* Token validation methods.
|
|
46
|
+
*/
|
|
47
|
+
var TokenValidation;
|
|
48
|
+
(function (TokenValidation) {
|
|
49
|
+
/**
|
|
50
|
+
* The default validation method, performs live validation via Keycloak servers.
|
|
51
|
+
*/
|
|
52
|
+
TokenValidation["ONLINE"] = "online";
|
|
53
|
+
/**
|
|
54
|
+
* Validate offline against the configured keycloak options.
|
|
55
|
+
*/
|
|
56
|
+
TokenValidation["OFFLINE"] = "offline";
|
|
57
|
+
/**
|
|
58
|
+
* Does not check for any validation. Should only be used for special cases (i.e development, internal networks)
|
|
59
|
+
*/
|
|
60
|
+
TokenValidation["NONE"] = "none";
|
|
61
|
+
})(TokenValidation || (exports.TokenValidation = TokenValidation = {}));
|
|
62
|
+
var RoleMerge;
|
|
63
|
+
(function (RoleMerge) {
|
|
64
|
+
/**
|
|
65
|
+
* Overrides roles if defined both controller and handlers, with controller taking over.
|
|
66
|
+
*/
|
|
67
|
+
RoleMerge[RoleMerge["OVERRIDE"] = 0] = "OVERRIDE";
|
|
68
|
+
/**
|
|
69
|
+
* Merges all roles from both controller and handlers.
|
|
70
|
+
*/
|
|
71
|
+
RoleMerge[RoleMerge["ALL"] = 1] = "ALL";
|
|
72
|
+
})(RoleMerge || (exports.RoleMerge = RoleMerge = {}));
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { KeycloakAdminService } from '../services/keycloak-admin.service';
|
|
2
|
+
import { ServerRequest, ServerResponse } from '../interface/server.interface';
|
|
3
|
+
/**
|
|
4
|
+
* Handles Keycloak admin callbacks:
|
|
5
|
+
* - `POST k_push_not_before`: not-before policy updates
|
|
6
|
+
* - `POST k_logout`: OIDC back-channel logout tokens
|
|
7
|
+
*
|
|
8
|
+
* All business logic is delegated to {@link KeycloakAdminService}.
|
|
9
|
+
*/
|
|
10
|
+
export declare class KeycloakAdminController {
|
|
11
|
+
private readonly adminService;
|
|
12
|
+
private readonly logger;
|
|
13
|
+
constructor(adminService: KeycloakAdminService);
|
|
14
|
+
handlePushNotBefore(body: unknown, request: ServerRequest, response: ServerResponse): Promise<void>;
|
|
15
|
+
handleBackchannelLogout(body: unknown, request: ServerRequest, response: ServerResponse): Promise<void>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
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 KeycloakAdminController_1;
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.KeycloakAdminController = void 0;
|
|
26
|
+
const errors_1 = require("../errors");
|
|
27
|
+
const keycloak_admin_service_1 = require("../services/keycloak-admin.service");
|
|
28
|
+
const public_decorator_1 = require("../decorators/public.decorator");
|
|
29
|
+
const common_1 = require("@nestjs/common");
|
|
30
|
+
/**
|
|
31
|
+
* Handles Keycloak admin callbacks:
|
|
32
|
+
* - `POST k_push_not_before`: not-before policy updates
|
|
33
|
+
* - `POST k_logout`: OIDC back-channel logout tokens
|
|
34
|
+
*
|
|
35
|
+
* All business logic is delegated to {@link KeycloakAdminService}.
|
|
36
|
+
*/
|
|
37
|
+
let KeycloakAdminController = KeycloakAdminController_1 = class KeycloakAdminController {
|
|
38
|
+
constructor(adminService) {
|
|
39
|
+
this.adminService = adminService;
|
|
40
|
+
this.logger = new common_1.Logger(KeycloakAdminController_1.name);
|
|
41
|
+
}
|
|
42
|
+
handlePushNotBefore(body, request, response) {
|
|
43
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
44
|
+
try {
|
|
45
|
+
yield this.adminService.processPushNotBefore(body, request);
|
|
46
|
+
response.send('ok');
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
this.logger.warn(`Push not-before failed: ${err}`);
|
|
50
|
+
const status = err instanceof errors_1.KeycloakAdminError ? 401 : 400;
|
|
51
|
+
response.status(status).send(status === 401 ? 'unauthorized' : 'bad request');
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
handleBackchannelLogout(body, request, response) {
|
|
56
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
57
|
+
try {
|
|
58
|
+
yield this.adminService.processBackchannelLogout(body, request);
|
|
59
|
+
response.send('ok');
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
this.logger.warn(`Back-channel logout failed: ${err}`);
|
|
63
|
+
const status = err instanceof errors_1.KeycloakAdminError ? 401 : 400;
|
|
64
|
+
response.status(status).send(status === 401 ? 'unauthorized' : 'bad request');
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
exports.KeycloakAdminController = KeycloakAdminController;
|
|
70
|
+
__decorate([
|
|
71
|
+
(0, common_1.Post)('k_push_not_before'),
|
|
72
|
+
(0, common_1.HttpCode)(200),
|
|
73
|
+
__param(0, (0, common_1.Body)()),
|
|
74
|
+
__param(1, (0, common_1.Req)()),
|
|
75
|
+
__param(2, (0, common_1.Res)()),
|
|
76
|
+
__metadata("design:type", Function),
|
|
77
|
+
__metadata("design:paramtypes", [Object, Object, Object]),
|
|
78
|
+
__metadata("design:returntype", Promise)
|
|
79
|
+
], KeycloakAdminController.prototype, "handlePushNotBefore", null);
|
|
80
|
+
__decorate([
|
|
81
|
+
(0, common_1.Post)('k_logout'),
|
|
82
|
+
(0, common_1.HttpCode)(200),
|
|
83
|
+
__param(0, (0, common_1.Body)()),
|
|
84
|
+
__param(1, (0, common_1.Req)()),
|
|
85
|
+
__param(2, (0, common_1.Res)()),
|
|
86
|
+
__metadata("design:type", Function),
|
|
87
|
+
__metadata("design:paramtypes", [Object, Object, Object]),
|
|
88
|
+
__metadata("design:returntype", Promise)
|
|
89
|
+
], KeycloakAdminController.prototype, "handleBackchannelLogout", null);
|
|
90
|
+
exports.KeycloakAdminController = KeycloakAdminController = KeycloakAdminController_1 = __decorate([
|
|
91
|
+
(0, public_decorator_1.Public)(),
|
|
92
|
+
(0, common_1.Controller)(),
|
|
93
|
+
__metadata("design:paramtypes", [keycloak_admin_service_1.KeycloakAdminService])
|
|
94
|
+
], KeycloakAdminController);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AccessToken = void 0;
|
|
4
|
+
const internal_util_1 = require("../internal.util");
|
|
5
|
+
const common_1 = require("@nestjs/common");
|
|
6
|
+
/**
|
|
7
|
+
* Retrieves the currently used access token
|
|
8
|
+
* @since 2.0.0
|
|
9
|
+
*/
|
|
10
|
+
exports.AccessToken = (0, common_1.createParamDecorator)((data, ctx) => {
|
|
11
|
+
const [req] = (0, internal_util_1.extractRequest)(ctx);
|
|
12
|
+
return req.accessToken;
|
|
13
|
+
});
|