@pawells/nestjs-auth 1.0.0-dev.3052c75
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 +602 -0
- package/build/LICENSE +21 -0
- package/build/README.md +602 -0
- package/build/admin/client/client.d.ts +82 -0
- package/build/admin/client/client.d.ts.map +1 -0
- package/build/admin/client/client.js +157 -0
- package/build/admin/client/client.js.map +1 -0
- package/build/admin/client/errors/base-error.d.ts +58 -0
- package/build/admin/client/errors/base-error.d.ts.map +1 -0
- package/build/admin/client/errors/base-error.js +100 -0
- package/build/admin/client/errors/base-error.js.map +1 -0
- package/build/admin/client/errors/index.d.ts +2 -0
- package/build/admin/client/errors/index.d.ts.map +1 -0
- package/build/admin/client/errors/index.js +2 -0
- package/build/admin/client/errors/index.js.map +1 -0
- package/build/admin/client/index.d.ts +6 -0
- package/build/admin/client/index.d.ts.map +1 -0
- package/build/admin/client/index.js +11 -0
- package/build/admin/client/index.js.map +1 -0
- package/build/admin/client/services/authentication.service.d.ts +54 -0
- package/build/admin/client/services/authentication.service.d.ts.map +1 -0
- package/build/admin/client/services/authentication.service.js +99 -0
- package/build/admin/client/services/authentication.service.js.map +1 -0
- package/build/admin/client/services/base-service.d.ts +39 -0
- package/build/admin/client/services/base-service.d.ts.map +1 -0
- package/build/admin/client/services/base-service.js +107 -0
- package/build/admin/client/services/base-service.js.map +1 -0
- package/build/admin/client/services/client.service.d.ts +86 -0
- package/build/admin/client/services/client.service.d.ts.map +1 -0
- package/build/admin/client/services/client.service.js +193 -0
- package/build/admin/client/services/client.service.js.map +1 -0
- package/build/admin/client/services/event.service.d.ts +84 -0
- package/build/admin/client/services/event.service.d.ts.map +1 -0
- package/build/admin/client/services/event.service.js +155 -0
- package/build/admin/client/services/event.service.js.map +1 -0
- package/build/admin/client/services/federated-identity.service.d.ts +89 -0
- package/build/admin/client/services/federated-identity.service.d.ts.map +1 -0
- package/build/admin/client/services/federated-identity.service.js +120 -0
- package/build/admin/client/services/federated-identity.service.js.map +1 -0
- package/build/admin/client/services/group.service.d.ts +52 -0
- package/build/admin/client/services/group.service.d.ts.map +1 -0
- package/build/admin/client/services/group.service.js +105 -0
- package/build/admin/client/services/group.service.js.map +1 -0
- package/build/admin/client/services/identity-provider.service.d.ts +47 -0
- package/build/admin/client/services/identity-provider.service.d.ts.map +1 -0
- package/build/admin/client/services/identity-provider.service.js +86 -0
- package/build/admin/client/services/identity-provider.service.js.map +1 -0
- package/build/admin/client/services/index.d.ts +11 -0
- package/build/admin/client/services/index.d.ts.map +1 -0
- package/build/admin/client/services/index.js +11 -0
- package/build/admin/client/services/index.js.map +1 -0
- package/build/admin/client/services/realm.service.d.ts +41 -0
- package/build/admin/client/services/realm.service.d.ts.map +1 -0
- package/build/admin/client/services/realm.service.js +80 -0
- package/build/admin/client/services/realm.service.js.map +1 -0
- package/build/admin/client/services/role.service.d.ts +45 -0
- package/build/admin/client/services/role.service.d.ts.map +1 -0
- package/build/admin/client/services/role.service.js +92 -0
- package/build/admin/client/services/role.service.js.map +1 -0
- package/build/admin/client/services/user.service.d.ts +84 -0
- package/build/admin/client/services/user.service.d.ts.map +1 -0
- package/build/admin/client/services/user.service.js +216 -0
- package/build/admin/client/services/user.service.js.map +1 -0
- package/build/admin/client/types/config.types.d.ts +59 -0
- package/build/admin/client/types/config.types.d.ts.map +1 -0
- package/build/admin/client/types/config.types.js +13 -0
- package/build/admin/client/types/config.types.js.map +1 -0
- package/build/admin/client/types/event.types.d.ts +176 -0
- package/build/admin/client/types/event.types.d.ts.map +1 -0
- package/build/admin/client/types/event.types.js +2 -0
- package/build/admin/client/types/event.types.js.map +1 -0
- package/build/admin/client/types/index.d.ts +4 -0
- package/build/admin/client/types/index.d.ts.map +1 -0
- package/build/admin/client/types/index.js +4 -0
- package/build/admin/client/types/index.js.map +1 -0
- package/build/admin/client/types/keycloak.types.d.ts +169 -0
- package/build/admin/client/types/keycloak.types.d.ts.map +1 -0
- package/build/admin/client/types/keycloak.types.js +2 -0
- package/build/admin/client/types/keycloak.types.js.map +1 -0
- package/build/admin/client/utils/index.d.ts +2 -0
- package/build/admin/client/utils/index.d.ts.map +1 -0
- package/build/admin/client/utils/index.js +2 -0
- package/build/admin/client/utils/index.js.map +1 -0
- package/build/admin/client/utils/retry.d.ts +40 -0
- package/build/admin/client/utils/retry.d.ts.map +1 -0
- package/build/admin/client/utils/retry.js +72 -0
- package/build/admin/client/utils/retry.js.map +1 -0
- package/build/admin/config/keycloak.config.d.ts +33 -0
- package/build/admin/config/keycloak.config.d.ts.map +1 -0
- package/build/admin/config/keycloak.config.js +2 -0
- package/build/admin/config/keycloak.config.js.map +1 -0
- package/build/admin/config/keycloak.defaults.d.ts +11 -0
- package/build/admin/config/keycloak.defaults.d.ts.map +1 -0
- package/build/admin/config/keycloak.defaults.js +60 -0
- package/build/admin/config/keycloak.defaults.js.map +1 -0
- package/build/admin/health/keycloak.health.d.ts +13 -0
- package/build/admin/health/keycloak.health.d.ts.map +1 -0
- package/build/admin/health/keycloak.health.js +54 -0
- package/build/admin/health/keycloak.health.js.map +1 -0
- package/build/admin/index.d.ts +10 -0
- package/build/admin/index.d.ts.map +1 -0
- package/build/admin/index.js +9 -0
- package/build/admin/index.js.map +1 -0
- package/build/admin/keycloak-admin.interfaces.d.ts +45 -0
- package/build/admin/keycloak-admin.interfaces.d.ts.map +1 -0
- package/build/admin/keycloak-admin.interfaces.js +2 -0
- package/build/admin/keycloak-admin.interfaces.js.map +1 -0
- package/build/admin/keycloak-admin.module.d.ts +23 -0
- package/build/admin/keycloak-admin.module.d.ts.map +1 -0
- package/build/admin/keycloak-admin.module.js +101 -0
- package/build/admin/keycloak-admin.module.js.map +1 -0
- package/build/admin/keycloak.constants.d.ts +16 -0
- package/build/admin/keycloak.constants.d.ts.map +1 -0
- package/build/admin/keycloak.constants.js +16 -0
- package/build/admin/keycloak.constants.js.map +1 -0
- package/build/admin/permissions/index.d.ts +2 -0
- package/build/admin/permissions/index.d.ts.map +1 -0
- package/build/admin/permissions/index.js +2 -0
- package/build/admin/permissions/index.js.map +1 -0
- package/build/admin/permissions/keycloak-admin.permissions.d.ts +45 -0
- package/build/admin/permissions/keycloak-admin.permissions.d.ts.map +1 -0
- package/build/admin/permissions/keycloak-admin.permissions.js +68 -0
- package/build/admin/permissions/keycloak-admin.permissions.js.map +1 -0
- package/build/admin/services/keycloak-admin.service.d.ts +64 -0
- package/build/admin/services/keycloak-admin.service.d.ts.map +1 -0
- package/build/admin/services/keycloak-admin.service.js +152 -0
- package/build/admin/services/keycloak-admin.service.js.map +1 -0
- package/build/decorators/auth-decorators.d.ts +217 -0
- package/build/decorators/auth-decorators.d.ts.map +1 -0
- package/build/decorators/auth-decorators.js +251 -0
- package/build/decorators/auth-decorators.js.map +1 -0
- package/build/decorators/context-utils.d.ts +101 -0
- package/build/decorators/context-utils.d.ts.map +1 -0
- package/build/decorators/context-utils.js +178 -0
- package/build/decorators/context-utils.js.map +1 -0
- package/build/decorators/graphql-auth-decorators.d.ts +144 -0
- package/build/decorators/graphql-auth-decorators.d.ts.map +1 -0
- package/build/decorators/graphql-auth-decorators.js +152 -0
- package/build/decorators/graphql-auth-decorators.js.map +1 -0
- package/build/decorators/index.d.ts +5 -0
- package/build/decorators/index.d.ts.map +1 -0
- package/build/decorators/index.js +4 -0
- package/build/decorators/index.js.map +1 -0
- package/build/guards/index.d.ts +4 -0
- package/build/guards/index.d.ts.map +1 -0
- package/build/guards/index.js +4 -0
- package/build/guards/index.js.map +1 -0
- package/build/guards/jwt-auth.guard.d.ts +52 -0
- package/build/guards/jwt-auth.guard.d.ts.map +1 -0
- package/build/guards/jwt-auth.guard.js +97 -0
- package/build/guards/jwt-auth.guard.js.map +1 -0
- package/build/guards/permission.guard.d.ts +37 -0
- package/build/guards/permission.guard.d.ts.map +1 -0
- package/build/guards/permission.guard.js +73 -0
- package/build/guards/permission.guard.js.map +1 -0
- package/build/guards/role.guard.d.ts +33 -0
- package/build/guards/role.guard.d.ts.map +1 -0
- package/build/guards/role.guard.js +69 -0
- package/build/guards/role.guard.js.map +1 -0
- package/build/index.d.ts +92 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +98 -0
- package/build/index.js.map +1 -0
- package/build/keycloak/index.d.ts +7 -0
- package/build/keycloak/index.d.ts.map +1 -0
- package/build/keycloak/index.js +5 -0
- package/build/keycloak/index.js.map +1 -0
- package/build/keycloak/keycloak.constants.d.ts +2 -0
- package/build/keycloak/keycloak.constants.d.ts.map +1 -0
- package/build/keycloak/keycloak.constants.js +2 -0
- package/build/keycloak/keycloak.constants.js.map +1 -0
- package/build/keycloak/keycloak.interfaces.d.ts +12 -0
- package/build/keycloak/keycloak.interfaces.d.ts.map +1 -0
- package/build/keycloak/keycloak.interfaces.js +2 -0
- package/build/keycloak/keycloak.interfaces.js.map +1 -0
- package/build/keycloak/keycloak.module.d.ts +56 -0
- package/build/keycloak/keycloak.module.d.ts.map +1 -0
- package/build/keycloak/keycloak.module.js +104 -0
- package/build/keycloak/keycloak.module.js.map +1 -0
- package/build/keycloak/keycloak.types.d.ts +60 -0
- package/build/keycloak/keycloak.types.d.ts.map +1 -0
- package/build/keycloak/keycloak.types.js +2 -0
- package/build/keycloak/keycloak.types.js.map +1 -0
- package/build/keycloak/services/jwks-cache.service.d.ts +64 -0
- package/build/keycloak/services/jwks-cache.service.d.ts.map +1 -0
- package/build/keycloak/services/jwks-cache.service.js +176 -0
- package/build/keycloak/services/jwks-cache.service.js.map +1 -0
- package/build/keycloak/services/keycloak-token-validation.service.d.ts +88 -0
- package/build/keycloak/services/keycloak-token-validation.service.d.ts.map +1 -0
- package/build/keycloak/services/keycloak-token-validation.service.js +243 -0
- package/build/keycloak/services/keycloak-token-validation.service.js.map +1 -0
- package/build/package.json +72 -0
- package/package.json +93 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aaron Wells
|
|
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,602 @@
|
|
|
1
|
+
# NestJS Authentication Module
|
|
2
|
+
|
|
3
|
+
[](https://github.com/PhillipAWells/nestjs-common/releases)
|
|
4
|
+
[](https://github.com/PhillipAWells/nestjs-common/actions/workflows/ci.yml)
|
|
5
|
+
[](https://www.npmjs.com/package/@pawells/nestjs-auth)
|
|
6
|
+
[](https://nodejs.org)
|
|
7
|
+
[](./LICENSE)
|
|
8
|
+
[](https://github.com/sponsors/PhillipAWells)
|
|
9
|
+
|
|
10
|
+
Keycloak integration library for NestJS resource servers. Validates Keycloak-issued access tokens (online introspection by default; offline JWKS opt-in), enforces role and permission guards on HTTP and GraphQL routes, and provides a typed Admin REST API client for user management, federated identity, and event polling.
|
|
11
|
+
|
|
12
|
+
This package does **not** issue tokens, manage passwords, or run login flows — those are Keycloak's responsibility.
|
|
13
|
+
|
|
14
|
+
## Table of Contents
|
|
15
|
+
|
|
16
|
+
- [Installation](#installation)
|
|
17
|
+
- [Quick Start](#quick-start)
|
|
18
|
+
- [KeycloakModule](#keycloakmodule)
|
|
19
|
+
- [Token Validation Modes](#token-validation-modes)
|
|
20
|
+
- [Guards](#guards)
|
|
21
|
+
- [Decorators](#decorators)
|
|
22
|
+
- [KeycloakAdminModule](#keycloakadminmodule)
|
|
23
|
+
- [Federated Identity](#federated-identity)
|
|
24
|
+
- [Event Polling](#event-polling)
|
|
25
|
+
- [Keycloak Client Configuration](#keycloak-client-configuration)
|
|
26
|
+
- [Security Notes](#security-notes)
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
yarn add @pawells/nestjs-auth
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Peer Dependencies
|
|
35
|
+
|
|
36
|
+
| Package | Version | Required |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| `@nestjs/common` | `>=10.0.0` | Yes |
|
|
39
|
+
| `@nestjs/core` | `>=10.0.0` | Yes |
|
|
40
|
+
| `@nestjs/jwt` | `>=10.0.0` | Yes |
|
|
41
|
+
| `@nestjs/terminus` | `>=10.0.0` | Yes |
|
|
42
|
+
| `joi` | `>=17.0.0` | Yes |
|
|
43
|
+
| `@nestjs/graphql` | `>=12.0.0` | Yes — required to use GraphQL decorators |
|
|
44
|
+
| `jwks-rsa` | `>=3.0.0` | No — required only for offline (JWKS) validation mode |
|
|
45
|
+
|
|
46
|
+
## Quick Start
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
import { Module } from '@nestjs/common';
|
|
50
|
+
import { APP_GUARD } from '@nestjs/core';
|
|
51
|
+
import { ConfigModule, ConfigService } from '@nestjs/config';
|
|
52
|
+
import {
|
|
53
|
+
KeycloakModule,
|
|
54
|
+
KeycloakAdminModule,
|
|
55
|
+
JwtAuthGuard,
|
|
56
|
+
} from '@pawells/nestjs-auth';
|
|
57
|
+
|
|
58
|
+
@Module({
|
|
59
|
+
imports: [
|
|
60
|
+
KeycloakModule.forRootAsync({
|
|
61
|
+
imports: [ConfigModule],
|
|
62
|
+
inject: [ConfigService],
|
|
63
|
+
useFactory: (config: ConfigService) => ({
|
|
64
|
+
authServerUrl: config.get('KEYCLOAK_AUTH_SERVER_URL'),
|
|
65
|
+
realm: config.get('KEYCLOAK_REALM'),
|
|
66
|
+
clientId: config.get('KEYCLOAK_CLIENT_ID'),
|
|
67
|
+
clientSecret: config.get('KEYCLOAK_CLIENT_SECRET'),
|
|
68
|
+
}),
|
|
69
|
+
}),
|
|
70
|
+
KeycloakAdminModule.forRootAsync({
|
|
71
|
+
imports: [ConfigModule],
|
|
72
|
+
inject: [ConfigService],
|
|
73
|
+
useFactory: (config: ConfigService) => ({
|
|
74
|
+
enabled: config.get('KEYCLOAK_ADMIN_ENABLED') === 'true',
|
|
75
|
+
baseUrl: config.get('KEYCLOAK_BASE_URL'),
|
|
76
|
+
realmName: config.get('KEYCLOAK_REALM'),
|
|
77
|
+
credentials: {
|
|
78
|
+
type: 'clientCredentials',
|
|
79
|
+
clientId: config.get('KEYCLOAK_ADMIN_CLIENT_ID'),
|
|
80
|
+
clientSecret: config.get('KEYCLOAK_ADMIN_CLIENT_SECRET'),
|
|
81
|
+
},
|
|
82
|
+
}),
|
|
83
|
+
}),
|
|
84
|
+
],
|
|
85
|
+
providers: [
|
|
86
|
+
{
|
|
87
|
+
provide: APP_GUARD,
|
|
88
|
+
useClass: JwtAuthGuard,
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
})
|
|
92
|
+
export class AppModule {}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## KeycloakModule
|
|
96
|
+
|
|
97
|
+
`KeycloakModule` configures token validation for the service. It provides `KeycloakTokenValidationService` to all modules via its exports.
|
|
98
|
+
|
|
99
|
+
### Options
|
|
100
|
+
|
|
101
|
+
| Field | Type | Default | Description |
|
|
102
|
+
|---|---|---|---|
|
|
103
|
+
| `authServerUrl` | `string` | — | Keycloak realm base URL, e.g. `https://auth.example.com/realms/myrealm` |
|
|
104
|
+
| `realm` | `string` | — | Keycloak realm name |
|
|
105
|
+
| `clientId` | `string` | — | This service's Keycloak client ID — used for audience validation and client role extraction |
|
|
106
|
+
| `validationMode` | `'online' \| 'offline'` | `'online'` | Token validation strategy — see [Token Validation Modes](#token-validation-modes) |
|
|
107
|
+
| `clientSecret` | `string` | — | Client secret for the introspection endpoint. Required when `validationMode` is `'online'` (the default) |
|
|
108
|
+
| `jwksCacheTtlMs` | `number` | `300000` | JWKS public key cache TTL in milliseconds. Used in offline mode only |
|
|
109
|
+
| `issuer` | `string` | `authServerUrl` | Expected `iss` claim value. Must match exactly. Defaults to `authServerUrl` |
|
|
110
|
+
|
|
111
|
+
### forRoot
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
KeycloakModule.forRoot({
|
|
115
|
+
authServerUrl: 'https://auth.example.com/realms/myrealm',
|
|
116
|
+
realm: 'myrealm',
|
|
117
|
+
clientId: 'my-service',
|
|
118
|
+
clientSecret: process.env.KEYCLOAK_CLIENT_SECRET,
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### forRootAsync
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
KeycloakModule.forRootAsync({
|
|
126
|
+
imports: [ConfigModule],
|
|
127
|
+
inject: [ConfigService],
|
|
128
|
+
useFactory: (config: ConfigService) => ({
|
|
129
|
+
authServerUrl: config.get('KEYCLOAK_AUTH_SERVER_URL'),
|
|
130
|
+
realm: config.get('KEYCLOAK_REALM'),
|
|
131
|
+
clientId: config.get('KEYCLOAK_CLIENT_ID'),
|
|
132
|
+
clientSecret: config.get('KEYCLOAK_CLIENT_SECRET'),
|
|
133
|
+
}),
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Token Validation Modes
|
|
138
|
+
|
|
139
|
+
### Online (default)
|
|
140
|
+
|
|
141
|
+
Validates each token by calling Keycloak's introspection endpoint (`/protocol/openid-connect/token/introspect`). The introspection response is authoritative: it detects revoked tokens and expired sessions immediately.
|
|
142
|
+
|
|
143
|
+
- Requires `clientSecret`
|
|
144
|
+
- Adds a network round-trip per request
|
|
145
|
+
- **Recommended for most deployments**
|
|
146
|
+
|
|
147
|
+
### Offline (opt-in)
|
|
148
|
+
|
|
149
|
+
Validates the JWT signature locally using Keycloak's JWKS endpoint. Public keys are fetched once and cached.
|
|
150
|
+
|
|
151
|
+
- Does not detect revocation — a revoked token remains valid until its `exp` claim passes
|
|
152
|
+
- No network hop after the initial key fetch
|
|
153
|
+
- Validates `exp`, `iss`, and `aud` claims locally
|
|
154
|
+
- Requires the `jwks-rsa` peer dependency
|
|
155
|
+
- Set `validationMode: 'offline'` to enable
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
KeycloakModule.forRoot({
|
|
159
|
+
authServerUrl: 'https://auth.example.com/realms/myrealm',
|
|
160
|
+
realm: 'myrealm',
|
|
161
|
+
clientId: 'my-service',
|
|
162
|
+
validationMode: 'offline',
|
|
163
|
+
jwksCacheTtlMs: 600000, // 10 minutes
|
|
164
|
+
});
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Use offline mode only when request throughput makes per-request introspection impractical and token lifetimes are short enough to bound the revocation window.
|
|
168
|
+
|
|
169
|
+
## Guards
|
|
170
|
+
|
|
171
|
+
### JwtAuthGuard
|
|
172
|
+
|
|
173
|
+
Validates the Keycloak access token on every incoming request. Extracts the `Bearer` token from the `Authorization` header, calls `KeycloakTokenValidationService.validateToken`, and attaches the resolved `KeycloakUser` to `request.user`.
|
|
174
|
+
|
|
175
|
+
Routes decorated with `@Public()` bypass the guard entirely.
|
|
176
|
+
|
|
177
|
+
**Register globally (recommended):**
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
import { APP_GUARD } from '@nestjs/core';
|
|
181
|
+
import { JwtAuthGuard } from '@pawells/nestjs-auth';
|
|
182
|
+
|
|
183
|
+
// In your AppModule providers array:
|
|
184
|
+
{
|
|
185
|
+
provide: APP_GUARD,
|
|
186
|
+
useClass: JwtAuthGuard,
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
**Per-route:**
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
import { UseGuards } from '@nestjs/common';
|
|
194
|
+
import { JwtAuthGuard } from '@pawells/nestjs-auth';
|
|
195
|
+
|
|
196
|
+
@UseGuards(JwtAuthGuard)
|
|
197
|
+
@Controller('profile')
|
|
198
|
+
export class ProfileController {}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### RoleGuard
|
|
202
|
+
|
|
203
|
+
Checks whether the authenticated user holds at least one of the roles listed in `@Roles()`. Roles are matched against the union of `realm_access.roles` and `resource_access[clientId].roles` from the token.
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
import { UseGuards } from '@nestjs/common';
|
|
207
|
+
import { RoleGuard, Roles } from '@pawells/nestjs-auth';
|
|
208
|
+
|
|
209
|
+
@UseGuards(RoleGuard)
|
|
210
|
+
@Controller('admin')
|
|
211
|
+
export class AdminController {
|
|
212
|
+
@Roles('admin', 'moderator')
|
|
213
|
+
@Get('users')
|
|
214
|
+
listUsers() {
|
|
215
|
+
return [];
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### PermissionGuard
|
|
221
|
+
|
|
222
|
+
Checks whether the authenticated user holds at least one of the values listed in `@Permissions()`, resolved against the same role arrays as `RoleGuard`.
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
import { UseGuards } from '@nestjs/common';
|
|
226
|
+
import { PermissionGuard, Permissions } from '@pawells/nestjs-auth';
|
|
227
|
+
|
|
228
|
+
@UseGuards(PermissionGuard)
|
|
229
|
+
@Controller('documents')
|
|
230
|
+
export class DocumentsController {
|
|
231
|
+
@Permissions('document.write')
|
|
232
|
+
@Post()
|
|
233
|
+
createDocument(@Body() dto: CreateDocumentDto) {
|
|
234
|
+
return {};
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Decorators
|
|
240
|
+
|
|
241
|
+
### HTTP Decorators
|
|
242
|
+
|
|
243
|
+
| Decorator | Type | Description |
|
|
244
|
+
|---|---|---|
|
|
245
|
+
| `@Auth()` | Method | Marks the route as requiring authentication (sets `isPublic: false`) |
|
|
246
|
+
| `@Public()` | Method | Marks the route as public — `JwtAuthGuard` skips validation |
|
|
247
|
+
| `@Roles(...roles)` | Method | Specifies role requirements for `RoleGuard` |
|
|
248
|
+
| `@Permissions(...permissions)` | Method | Specifies permission requirements for `PermissionGuard` |
|
|
249
|
+
| `@CurrentUser(property?)` | Parameter | Injects the `KeycloakUser` from `request.user`, or a specific property if `property` is given |
|
|
250
|
+
| `@AuthToken()` | Parameter | Injects the raw Bearer token string from the `Authorization` header |
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
import { Controller, Get } from '@nestjs/common';
|
|
254
|
+
import { Auth, Public, Roles, CurrentUser, AuthToken } from '@pawells/nestjs-auth';
|
|
255
|
+
import type { KeycloakUser } from '@pawells/nestjs-auth';
|
|
256
|
+
|
|
257
|
+
@Controller('me')
|
|
258
|
+
export class ProfileController {
|
|
259
|
+
@Public()
|
|
260
|
+
@Get('ping')
|
|
261
|
+
ping() {
|
|
262
|
+
return 'pong';
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
@Auth()
|
|
266
|
+
@Get()
|
|
267
|
+
getProfile(@CurrentUser() user: KeycloakUser) {
|
|
268
|
+
return user;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
@Roles('admin')
|
|
272
|
+
@Get('token')
|
|
273
|
+
getToken(@AuthToken() token: string) {
|
|
274
|
+
return { token };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
@Get('id')
|
|
278
|
+
getId(@CurrentUser('id') userId: string) {
|
|
279
|
+
return { userId };
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### GraphQL Decorators
|
|
285
|
+
|
|
286
|
+
The GraphQL variants are aliases of the HTTP decorators, pre-configured for the GraphQL execution context.
|
|
287
|
+
|
|
288
|
+
| Decorator | Equivalent to | Notes |
|
|
289
|
+
|---|---|---|
|
|
290
|
+
| `@GraphQLAuth()` | `@Auth()` | Marks GraphQL resolver as requiring authentication |
|
|
291
|
+
| `@GraphQLPublic()` | `@Public()` | Marks GraphQL resolver as public |
|
|
292
|
+
| `@GraphQLRoles(...roles)` | `@Roles(...roles)` | Specifies role requirements |
|
|
293
|
+
| `@GraphQLCurrentUser(property?)` | `@CurrentUser(property?, { contextType: 'graphql' })` | Injects user from GraphQL context |
|
|
294
|
+
| `@GraphQLUser(property?)` | `@GraphQLCurrentUser(property?)` | Alias |
|
|
295
|
+
| `@GraphQLAuthToken()` | `@AuthToken({ contextType: 'graphql' })` | Injects Bearer token from GraphQL context |
|
|
296
|
+
| `@GraphQLContextParam()` | — | Injects the full GraphQL context object |
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
import { Resolver, Query, Mutation } from '@nestjs/graphql';
|
|
300
|
+
import {
|
|
301
|
+
GraphQLAuth,
|
|
302
|
+
GraphQLPublic,
|
|
303
|
+
GraphQLRoles,
|
|
304
|
+
GraphQLCurrentUser,
|
|
305
|
+
GraphQLAuthToken,
|
|
306
|
+
} from '@pawells/nestjs-auth';
|
|
307
|
+
import type { KeycloakUser } from '@pawells/nestjs-auth';
|
|
308
|
+
|
|
309
|
+
@Resolver()
|
|
310
|
+
export class UserResolver {
|
|
311
|
+
@GraphQLPublic()
|
|
312
|
+
@Query(() => String)
|
|
313
|
+
async health(): Promise<string> {
|
|
314
|
+
return 'ok';
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
@GraphQLAuth()
|
|
318
|
+
@Query(() => String)
|
|
319
|
+
async me(@GraphQLCurrentUser() user: KeycloakUser): Promise<string> {
|
|
320
|
+
return user.id;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
@GraphQLRoles('admin')
|
|
324
|
+
@Query(() => [String])
|
|
325
|
+
async listUsers(): Promise<string[]> {
|
|
326
|
+
return [];
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
@Mutation(() => Boolean)
|
|
330
|
+
async validateToken(@GraphQLAuthToken() token: string): Promise<boolean> {
|
|
331
|
+
return !!token;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
## KeycloakAdminModule
|
|
337
|
+
|
|
338
|
+
`KeycloakAdminModule` provides a typed client for the Keycloak Admin REST API. It is registered as a global module — import it once in `AppModule` and inject `KeycloakAdminService` anywhere.
|
|
339
|
+
|
|
340
|
+
### Options (KeycloakAdminConfig)
|
|
341
|
+
|
|
342
|
+
| Field | Type | Default | Description |
|
|
343
|
+
|---|---|---|---|
|
|
344
|
+
| `enabled` | `boolean` | `false` | When `false` the client is not initialized — useful for disabling in test environments |
|
|
345
|
+
| `baseUrl` | `string` | `'http://localhost:8080'` | Keycloak server base URL (not realm-specific) |
|
|
346
|
+
| `realmName` | `string` | `'master'` | Target realm for all Admin API calls |
|
|
347
|
+
| `credentials.type` | `'password' \| 'clientCredentials'` | `'password'` | Authentication method |
|
|
348
|
+
| `credentials.username` | `string` | — | Admin username (password auth only) |
|
|
349
|
+
| `credentials.password` | `string` | — | Admin password (password auth only) |
|
|
350
|
+
| `credentials.clientId` | `string` | — | Service account client ID (clientCredentials auth only) |
|
|
351
|
+
| `credentials.clientSecret` | `string` | — | Service account client secret (clientCredentials auth only) |
|
|
352
|
+
| `timeout` | `number` | `30000` | Request timeout in milliseconds |
|
|
353
|
+
| `retry.maxRetries` | `number` | `3` | Maximum retry attempts on transient failures |
|
|
354
|
+
| `retry.retryDelay` | `number` | `1000` | Delay between retries in milliseconds |
|
|
355
|
+
|
|
356
|
+
### forRoot
|
|
357
|
+
|
|
358
|
+
```typescript
|
|
359
|
+
KeycloakAdminModule.forRoot({
|
|
360
|
+
enabled: process.env.KEYCLOAK_ADMIN_ENABLED === 'true',
|
|
361
|
+
baseUrl: 'https://auth.example.com',
|
|
362
|
+
realmName: 'myrealm',
|
|
363
|
+
credentials: {
|
|
364
|
+
type: 'clientCredentials',
|
|
365
|
+
clientId: process.env.KEYCLOAK_ADMIN_CLIENT_ID,
|
|
366
|
+
clientSecret: process.env.KEYCLOAK_ADMIN_CLIENT_SECRET,
|
|
367
|
+
},
|
|
368
|
+
});
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### forRootAsync
|
|
372
|
+
|
|
373
|
+
```typescript
|
|
374
|
+
KeycloakAdminModule.forRootAsync({
|
|
375
|
+
imports: [ConfigModule],
|
|
376
|
+
inject: [ConfigService],
|
|
377
|
+
useFactory: (config: ConfigService) => ({
|
|
378
|
+
enabled: config.get('KEYCLOAK_ADMIN_ENABLED') === 'true',
|
|
379
|
+
baseUrl: config.get('KEYCLOAK_BASE_URL'),
|
|
380
|
+
realmName: config.get('KEYCLOAK_REALM'),
|
|
381
|
+
credentials: {
|
|
382
|
+
type: 'clientCredentials',
|
|
383
|
+
clientId: config.get('KEYCLOAK_ADMIN_CLIENT_ID'),
|
|
384
|
+
clientSecret: config.get('KEYCLOAK_ADMIN_CLIENT_SECRET'),
|
|
385
|
+
},
|
|
386
|
+
}),
|
|
387
|
+
});
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### KeycloakAdminService
|
|
391
|
+
|
|
392
|
+
Inject `KeycloakAdminService` and access the sub-services via its properties.
|
|
393
|
+
|
|
394
|
+
| Property | Service | Responsibility |
|
|
395
|
+
|---|---|---|
|
|
396
|
+
| `.users` | `UserService` | Create, read, update, delete users; assign roles and groups |
|
|
397
|
+
| `.roles` | `RoleService` | Manage realm and client roles |
|
|
398
|
+
| `.realms` | `RealmService` | Realm-level configuration and queries |
|
|
399
|
+
| `.clients` | `ClientService` | Manage clients and client scopes |
|
|
400
|
+
| `.groups` | `GroupService` | Create and manage groups; add/remove members |
|
|
401
|
+
| `.identityProviders` | `IdentityProviderService` | Manage identity provider configurations |
|
|
402
|
+
| `.authentication` | `AuthenticationService` | Manage authentication flows |
|
|
403
|
+
| `.federatedIdentity` | `FederatedIdentityService` | Link and unlink external provider identities — see [Federated Identity](#federated-identity) |
|
|
404
|
+
| `.events` | `EventService` | Query admin and access events — see [Event Polling](#event-polling) |
|
|
405
|
+
|
|
406
|
+
```typescript
|
|
407
|
+
import { Injectable } from '@nestjs/common';
|
|
408
|
+
import { KeycloakAdminService } from '@pawells/nestjs-auth';
|
|
409
|
+
|
|
410
|
+
@Injectable()
|
|
411
|
+
export class UserManagementService {
|
|
412
|
+
constructor(private readonly keycloak: KeycloakAdminService) {}
|
|
413
|
+
|
|
414
|
+
async createUser(email: string, firstName: string): Promise<void> {
|
|
415
|
+
await this.keycloak.users.create({
|
|
416
|
+
email,
|
|
417
|
+
firstName,
|
|
418
|
+
enabled: true,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
async assignRole(userId: string, roleName: string): Promise<void> {
|
|
423
|
+
await this.keycloak.users.assignRole(userId, roleName);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
Call `keycloakAdminService.isEnabled()` before calling sub-services if the module may be disabled in the current environment.
|
|
429
|
+
|
|
430
|
+
## Federated Identity
|
|
431
|
+
|
|
432
|
+
`KeycloakAdminService.federatedIdentity` manages links between Keycloak user accounts and external identity providers.
|
|
433
|
+
|
|
434
|
+
| Method | Signature | Description |
|
|
435
|
+
|---|---|---|
|
|
436
|
+
| `list` | `(userId: string) => Promise<FederatedIdentityLink[]>` | Returns all provider links for a user |
|
|
437
|
+
| `link` | `(userId: string, provider: string, link: { userId: string; userName: string }) => Promise<void>` | Links an external provider identity to a Keycloak user |
|
|
438
|
+
| `unlink` | `(userId: string, provider: string) => Promise<void>` | Removes a provider link from a Keycloak user |
|
|
439
|
+
|
|
440
|
+
`link` performs a pre-flight `list` check and throws `ConflictError` if a link for the same provider and external user ID already exists. This is a workaround for [Keycloak issue #34608](https://github.com/keycloak/keycloak/issues/34608), which can create duplicate federated identity records.
|
|
441
|
+
|
|
442
|
+
```typescript
|
|
443
|
+
import { Injectable } from '@nestjs/common';
|
|
444
|
+
import { KeycloakAdminService, ConflictError } from '@pawells/nestjs-auth';
|
|
445
|
+
|
|
446
|
+
@Injectable()
|
|
447
|
+
export class IdentityLinkingService {
|
|
448
|
+
constructor(private readonly keycloak: KeycloakAdminService) {}
|
|
449
|
+
|
|
450
|
+
async linkGoogleAccount(
|
|
451
|
+
keycloakUserId: string,
|
|
452
|
+
googleUserId: string,
|
|
453
|
+
googleEmail: string,
|
|
454
|
+
): Promise<void> {
|
|
455
|
+
try {
|
|
456
|
+
await this.keycloak.federatedIdentity.link(keycloakUserId, 'google', {
|
|
457
|
+
userId: googleUserId,
|
|
458
|
+
userName: googleEmail,
|
|
459
|
+
});
|
|
460
|
+
} catch (error) {
|
|
461
|
+
if (error instanceof ConflictError) {
|
|
462
|
+
// Already linked — treat as a no-op or surface to the caller
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
throw error;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
async listLinks(keycloakUserId: string) {
|
|
470
|
+
return this.keycloak.federatedIdentity.list(keycloakUserId);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
## Event Polling
|
|
476
|
+
|
|
477
|
+
`KeycloakAdminService.events` queries Keycloak's event log for both admin (resource mutation) and access (login, logout, token) events.
|
|
478
|
+
|
|
479
|
+
### Methods
|
|
480
|
+
|
|
481
|
+
| Method | Signature | Description |
|
|
482
|
+
|---|---|---|
|
|
483
|
+
| `getAdminEvents` | `(query?: AdminEventQuery) => Promise<KeycloakAdminEvent[]>` | Returns admin events matching the query |
|
|
484
|
+
| `getAccessEvents` | `(query?: AccessEventQuery) => Promise<KeycloakAccessEvent[]>` | Returns access events matching the query |
|
|
485
|
+
|
|
486
|
+
### AdminEventQuery Fields
|
|
487
|
+
|
|
488
|
+
| Field | Type | Description |
|
|
489
|
+
|---|---|---|
|
|
490
|
+
| `operationTypes` | `('CREATE' \| 'UPDATE' \| 'DELETE' \| 'ACTION')[]` | Filter by operation type |
|
|
491
|
+
| `resourceTypes` | `string[]` | Filter by resource type (e.g. `['USER']`) |
|
|
492
|
+
| `resourcePath` | `string` | Filter by resource path prefix |
|
|
493
|
+
| `dateFrom` | `Date` | Earliest event timestamp (inclusive) |
|
|
494
|
+
| `dateTo` | `Date` | Latest event timestamp (inclusive) |
|
|
495
|
+
| `first` | `number` | Pagination offset |
|
|
496
|
+
| `max` | `number` | Maximum results to return |
|
|
497
|
+
|
|
498
|
+
`AccessEventQuery` supports the same date and pagination fields plus `type` (string array), `client`, and `user`.
|
|
499
|
+
|
|
500
|
+
### KeycloakAdminEvent Fields
|
|
501
|
+
|
|
502
|
+
| Field | Type | Notes |
|
|
503
|
+
|---|---|---|
|
|
504
|
+
| `time` | `number` | Unix timestamp in milliseconds |
|
|
505
|
+
| `realmId` | `string` | Realm identifier |
|
|
506
|
+
| `operationType` | `'CREATE' \| 'UPDATE' \| 'DELETE' \| 'ACTION'` | Type of operation |
|
|
507
|
+
| `resourceType` | `string` | Resource category, e.g. `USER`, `GROUP` |
|
|
508
|
+
| `resourcePath` | `string` | Path to the affected resource |
|
|
509
|
+
| `representation` | `string \| undefined` | JSON-encoded resource snapshot. Present on CREATE and UPDATE only. Must be parsed with `JSON.parse()` before use |
|
|
510
|
+
| `authDetails` | `object \| undefined` | Actor details: `realmId`, `clientId`, `userId`, `ipAddress` |
|
|
511
|
+
|
|
512
|
+
### Checkpoint Cursor Pattern
|
|
513
|
+
|
|
514
|
+
Keycloak does not provide a persistent event cursor. To avoid re-processing events or missing events between polls, track the `time` of the most recently processed event and pass it as `dateFrom` on subsequent polls. Use a page size (`max`) that fits within your Keycloak event retention window — events older than the retention period are purged and will be lost if polling falls behind.
|
|
515
|
+
|
|
516
|
+
```typescript
|
|
517
|
+
import { Injectable } from '@nestjs/common';
|
|
518
|
+
import { Cron } from '@nestjs/schedule';
|
|
519
|
+
import { KeycloakAdminService } from '@pawells/nestjs-auth';
|
|
520
|
+
|
|
521
|
+
@Injectable()
|
|
522
|
+
export class EventSyncService {
|
|
523
|
+
private lastProcessedTime: Date = new Date(Date.now() - 60_000);
|
|
524
|
+
|
|
525
|
+
constructor(private readonly keycloak: KeycloakAdminService) {}
|
|
526
|
+
|
|
527
|
+
@Cron('*/30 * * * * *') // every 30 seconds
|
|
528
|
+
async pollAdminEvents(): Promise<void> {
|
|
529
|
+
const events = await this.keycloak.events.getAdminEvents({
|
|
530
|
+
dateFrom: this.lastProcessedTime,
|
|
531
|
+
operationTypes: ['CREATE', 'UPDATE', 'DELETE'],
|
|
532
|
+
resourceTypes: ['USER'],
|
|
533
|
+
max: 100,
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
for (const event of events) {
|
|
537
|
+
await this.processEvent(event);
|
|
538
|
+
const eventTime = new Date(event.time);
|
|
539
|
+
if (eventTime > this.lastProcessedTime) {
|
|
540
|
+
this.lastProcessedTime = eventTime;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
private async processEvent(event: any): Promise<void> {
|
|
546
|
+
// Handle the event
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
## Keycloak Client Configuration
|
|
552
|
+
|
|
553
|
+
Three Keycloak clients are typically required when using this package.
|
|
554
|
+
|
|
555
|
+
### React SPA (public client)
|
|
556
|
+
|
|
557
|
+
- **Client type**: Public
|
|
558
|
+
- **Authentication flow**: Standard flow enabled
|
|
559
|
+
- **PKCE**: Required — set Code Challenge Method to `S256`
|
|
560
|
+
- **Valid redirect URIs**: Your frontend origin(s)
|
|
561
|
+
|
|
562
|
+
### NestJS resource server (confidential client)
|
|
563
|
+
|
|
564
|
+
This is the client whose `clientId` and `clientSecret` you provide to `KeycloakModule`. It is not used to authenticate users — it authenticates the service itself for introspection calls.
|
|
565
|
+
|
|
566
|
+
- **Client type**: Confidential
|
|
567
|
+
- **Service accounts**: Not required
|
|
568
|
+
- **Client authentication**: Enabled
|
|
569
|
+
- Introspection requires a client secret — keep `validationMode: 'online'` unless you have a specific reason to use offline mode
|
|
570
|
+
|
|
571
|
+
If you are using offline (JWKS) validation exclusively, a confidential client is not required for token validation. The JWKS endpoint is public.
|
|
572
|
+
|
|
573
|
+
### Admin API caller (confidential service account)
|
|
574
|
+
|
|
575
|
+
This is the client whose credentials you provide to `KeycloakAdminModule`.
|
|
576
|
+
|
|
577
|
+
- **Client type**: Confidential
|
|
578
|
+
- **Service accounts**: Enabled
|
|
579
|
+
- **Required service account roles** (assigned in the `realm-management` client):
|
|
580
|
+
- `manage-users` — create, update, delete users and assign roles
|
|
581
|
+
- `manage-identity-providers` — link and unlink federated identities
|
|
582
|
+
- `view-events` — read admin and access events
|
|
583
|
+
|
|
584
|
+
## Security Notes
|
|
585
|
+
|
|
586
|
+
**Online introspection is the recommended validation mode.** It is authoritative: a revoked Keycloak session is rejected immediately, regardless of token expiry.
|
|
587
|
+
|
|
588
|
+
**Offline JWKS validation does not detect revocation.** A token that has been revoked in Keycloak (e.g. by logging out or disabling the user) continues to pass offline validation until its `exp` claim expires. Only use offline mode when throughput requirements make per-request introspection impractical, and mitigate the revocation window by setting short token lifetimes in Keycloak (5 minutes or less).
|
|
589
|
+
|
|
590
|
+
**Federated identity deduplication (Keycloak #34608).** Keycloak's Admin API can create duplicate federated identity records if `addToFederatedIdentity` is called concurrently for the same user and provider. The `FederatedIdentityService.link` method guards against this with a pre-flight check, but the check is not atomic. Under high concurrency, implement external coordination (e.g. a distributed lock) if duplicate links are not tolerable.
|
|
591
|
+
|
|
592
|
+
**Event polling and retention windows.** Keycloak purges events based on a configurable retention period. If your poll interval exceeds the retention window — or if polling stops and then resumes — events will be permanently lost. Poll at a frequency significantly shorter than the retention window, and align the retention window with your operational requirements in the Keycloak realm settings (`Admin Console > Realm Settings > Events`).
|
|
593
|
+
|
|
594
|
+
## Related Packages
|
|
595
|
+
|
|
596
|
+
- **[@pawells/nestjs-shared](https://www.npmjs.com/package/@pawells/nestjs-shared)** — Foundation: filters, guards, interceptors, logging, CSRF, error handling
|
|
597
|
+
- **[@pawells/nestjs-graphql](https://www.npmjs.com/package/@pawells/nestjs-graphql)** — GraphQL module with Redis subscriptions, DataLoaders, and WebSocket auth
|
|
598
|
+
- **[@pawells/nestjs-open-telemetry](https://www.npmjs.com/package/@pawells/nestjs-open-telemetry)** — OpenTelemetry tracing and metrics integration
|
|
599
|
+
|
|
600
|
+
## License
|
|
601
|
+
|
|
602
|
+
MIT
|
package/build/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aaron Wells
|
|
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.
|