@adatechnology/auth-keycloak 0.1.0 → 0.1.2

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/README.md CHANGED
@@ -129,66 +129,153 @@ Resultado no log:
129
129
  [MyController.getToken][InMemoryCacheProvider.set] → token cached
130
130
  ```
131
131
 
132
- ### BearerTokenGuard autenticação B2B via introspecção
132
+ ### Contrato de headers (Kong API)
133
+
134
+ ```
135
+ Authorization: Bearer <service_token> → identidade do chamador (B2B)
136
+ X-Access-Token: <user_jwt> → token original do usuário (B2C)
137
+ ```
138
+
139
+ Kong valida o JWT do usuário via JWKS (local, zero chamadas ao Keycloak por request) e injeta os dois headers antes de encaminhar a request ao API.
140
+
141
+ ---
142
+
143
+ ### Guards
133
144
 
134
- Valida que o header `Authorization: Bearer <token>` contém um token ativo chamando
135
- `POST /token/introspect` no Keycloak. Use sempre em conjunto com `RolesGuard` em rotas B2B.
145
+ | Guard | Valida | Quando usar |
146
+ |---|---|---|
147
+ | `B2CGuard` | `X-Access-Token` presente | Rota exclusiva de usuários via Kong |
148
+ | `B2BGuard` | `Authorization` via introspection | Rota exclusiva de serviços internos |
149
+ | `ApiAuthGuard` | Detecta path e delega | Rota acessível pelos dois paths |
150
+ | `RolesGuard` | Roles no JWT correto | Sempre junto a um guard acima |
151
+ | `BearerTokenGuard` | `Authorization` via introspection | Nível baixo; prefira `B2BGuard` |
152
+
153
+ **Guard order matters** — guards de autenticação devem vir antes de `RolesGuard`.
154
+
155
+ ---
156
+
157
+ ### Decorators
158
+
159
+ #### `@AuthUser(param?)`
160
+ Extrai claim do token em `X-Access-Token`. Padrão: claim configurado em `userId` (default `sub`). Decodificação local, sem I/O.
136
161
 
137
162
  ```ts
138
- import { Controller, Headers, Post, UseGuards } from "@nestjs/common";
139
- import { BearerTokenGuard, Roles, RolesGuard } from "@adatechnology/auth-keycloak";
140
-
141
- @Controller("orders")
142
- export class OrdersController {
143
- @Post()
144
- @Roles("manage-requests")
145
- @UseGuards(BearerTokenGuard, RolesGuard)
146
- create(@Headers("x-user-id") keycloakId: string) {}
147
- }
163
+ @AuthUser() // sub (default)
164
+ @AuthUser('email') // claim único
165
+ @AuthUser(['preferred_username', 'email', 'sub']) // primeiro não-vazio
166
+ @AuthUser({ claim: 'email', header: 'x-user-jwt' }) // header customizado por rota
148
167
  ```
149
168
 
150
- **Por que dois guards em sequência?**
169
+ #### `@CallerToken(param?)`
170
+ Extrai claim do token em `Authorization`. Padrão: claim configurado em `callerId` (default `azp`).
151
171
 
152
- | Guard | Mecanismo | HTTP? | Falha |
153
- |---|---|---|---|
154
- | `BearerTokenGuard` | `POST /token/introspect` ao Keycloak | Sim | 401 — token inativo/expirado/forjado |
155
- | `RolesGuard` | Decode local do payload JWT | Não | 403 — permissão insuficiente |
172
+ ```ts
173
+ @CallerToken() // azp (default)
174
+ @CallerToken('sub') // claim único
175
+ @CallerToken(['client_id', 'azp']) // primeiro não-vazio
176
+ @CallerToken({ header: 'x-service-token', claim: 'azp' }) // header customizado
177
+ ```
156
178
 
157
- O `RolesGuard` sozinho **não é seguro** para autenticação: ele apenas decodifica o payload JWT
158
- sem verificar a assinatura, o que significa que um token forjado passaria. O `BearerTokenGuard`
159
- é quem garante autenticidade via introspecção.
179
+ #### `@AccessToken(header?)`
180
+ Retorna o JWT bruto do header B2C. Use quando precisar de claims não-string ou repassar o token.
160
181
 
161
- ### Autorização com @Roles
182
+ ```ts
183
+ @AccessToken() // header B2C padrão
184
+ @AccessToken('x-user-jwt') // header customizado
185
+ ```
186
+
187
+ #### `@Roles(...)`
188
+ Declara roles necessárias. Sempre usado com `RolesGuard`.
162
189
 
163
190
  ```ts
164
- import { Controller, Get, UseGuards } from "@nestjs/common";
165
- import { Roles, RolesGuard } from "@adatechnology/auth-keycloak";
166
-
167
- @Controller("secure")
168
- export class SecureController {
169
- @Get("public")
170
- @UseGuards(RolesGuard)
171
- public() {
172
- return { ok: true };
173
- }
191
+ @Roles('user-manager')
192
+ @Roles({ roles: ['admin', 'user-manager'], mode: 'any' }) // qualquer (default)
193
+ @Roles({ roles: ['admin', 'user-manager'], mode: 'all' }) // todas
194
+ ```
174
195
 
175
- @Get("admin")
176
- @UseGuards(RolesGuard)
177
- @Roles("admin")
178
- adminOnly() {
179
- return { ok: true };
180
- }
196
+ ---
181
197
 
182
- @Get("team")
183
- @UseGuards(RolesGuard)
184
- @Roles({ roles: ["manager", "lead"], mode: "all" }) // AND requer ambas as roles
185
- teamOnly() {
186
- return { ok: true };
187
- }
198
+ ### Exemplos de uso
199
+
200
+ **B2Cusuário via Kong:**
201
+ ```ts
202
+ @Get('me')
203
+ @Roles('user-manager')
204
+ @UseGuards(B2CGuard, RolesGuard)
205
+ async getMe(
206
+ @AuthUser() id: string,
207
+ @AuthUser('email') email: string,
208
+ @AuthUser(['preferred_username', 'email']) name: string,
209
+ @AccessToken() rawToken: string,
210
+ ) {
211
+ return { id, email, name };
188
212
  }
189
213
  ```
190
214
 
191
- O `RolesGuard` extrai roles de `realm_access.roles` e `resource_access[clientId].roles` do JWT.
215
+ **B2B serviço interno:**
216
+ ```ts
217
+ @Post('internal/notify')
218
+ @Roles('send-notifications')
219
+ @UseGuards(B2BGuard, RolesGuard)
220
+ async notify(
221
+ @CallerToken() caller: string, // 'domestic-backend-bff'
222
+ ) {
223
+ return { caller };
224
+ }
225
+ ```
226
+
227
+ **Ambos os paths:**
228
+ ```ts
229
+ @Get(':id')
230
+ @Roles('user-manager')
231
+ @UseGuards(ApiAuthGuard, RolesGuard)
232
+ async findById(
233
+ @Param('id') id: string,
234
+ @AuthUser() userId: string, // vazio no B2B-only path
235
+ @CallerToken() caller: string,
236
+ ) { ... }
237
+ ```
238
+
239
+ ---
240
+
241
+ ### Configuração de headers e claims
242
+
243
+ Configurável via env ou `forRoot()`. Prioridade: `forRoot()` > `process.env` > default.
244
+
245
+ **Env vars:**
246
+ ```env
247
+ KEYCLOAK_B2C_TOKEN_HEADER=x-access-token # header do user JWT
248
+ KEYCLOAK_B2B_TOKEN_HEADER=authorization # header do service token
249
+ KEYCLOAK_USER_ID_CLAIM=sub # claim(s) para user ID (comma-separated)
250
+ KEYCLOAK_CALLER_ID_CLAIM=azp # claim(s) para caller ID (comma-separated)
251
+ ```
252
+
253
+ **`forRoot()` (sobrescreve env):**
254
+ ```ts
255
+ KeycloakModule.forRoot({
256
+ ...
257
+ headers: { b2cToken: 'x-access-token', b2bToken: 'authorization' },
258
+ claims: {
259
+ userId: ['preferred_username', 'email', 'sub'], // string ou string[]
260
+ callerId: ['client_id', 'azp'],
261
+ },
262
+ })
263
+ ```
264
+
265
+ ---
266
+
267
+ ### BearerTokenGuard — autenticação B2B via introspecção
268
+
269
+ Valida `Authorization: Bearer <token>` chamando `POST /token/introspect` no Keycloak.
270
+
271
+ | Guard | Mecanismo | HTTP? | Falha |
272
+ |---|---|---|---|
273
+ | `BearerTokenGuard` | `POST /token/introspect` | Sim | 401 |
274
+ | `RolesGuard` | Decode local do payload | Não | 403 |
275
+
276
+ O `RolesGuard` sozinho **não é seguro** — ele decodifica sem verificar assinatura.
277
+
278
+ ---
192
279
 
193
280
  ### Tratamento de erros
194
281
 
@@ -213,6 +300,10 @@ try {
213
300
  | `KEYCLOAK_REALM` | `BACKEND` |
214
301
  | `KEYCLOAK_CLIENT_ID` | `backend-api` |
215
302
  | `KEYCLOAK_CLIENT_SECRET` | `backend-api-secret` |
303
+ | `KEYCLOAK_B2C_TOKEN_HEADER` | `x-access-token` |
304
+ | `KEYCLOAK_B2B_TOKEN_HEADER` | `authorization` |
305
+ | `KEYCLOAK_USER_ID_CLAIM` | `sub` |
306
+ | `KEYCLOAK_CALLER_ID_CLAIM` | `azp` |
216
307
 
217
308
  ### Notas
218
309
 
package/dist/index.d.ts CHANGED
@@ -44,6 +44,42 @@ interface KeycloakConfig {
44
44
  * determine how long to cache the access token instead of deriving TTL from the token's expires_in.
45
45
  */
46
46
  tokenCacheTtl?: number;
47
+ /**
48
+ * Header names for B2C and B2B token flows.
49
+ * Overrides process.env values when provided.
50
+ *
51
+ * Env equivalents:
52
+ * KEYCLOAK_B2C_TOKEN_HEADER (default: 'x-access-token')
53
+ * KEYCLOAK_B2B_TOKEN_HEADER (default: 'authorization')
54
+ */
55
+ headers?: {
56
+ /** Header carrying the user JWT forwarded by Kong. */
57
+ b2cToken?: string;
58
+ /** Header carrying the service account token. */
59
+ b2bToken?: string;
60
+ };
61
+ /**
62
+ * JWT claim names used to identify users and callers.
63
+ * Overrides process.env values when provided.
64
+ *
65
+ * Env equivalents:
66
+ * KEYCLOAK_USER_ID_CLAIM (default: 'sub')
67
+ * KEYCLOAK_CALLER_ID_CLAIM (default: 'azp')
68
+ */
69
+ claims?: {
70
+ /**
71
+ * Claim name(s) for the user identifier (from the B2C token).
72
+ * When an array, the first non-empty value found in the JWT is used.
73
+ * Env: KEYCLOAK_USER_ID_CLAIM (comma-separated). Default: 'sub'
74
+ */
75
+ userId?: string | string[];
76
+ /**
77
+ * Claim name(s) for the calling client identifier (from the B2B token).
78
+ * When an array, the first non-empty value found in the JWT is used.
79
+ * Env: KEYCLOAK_CALLER_ID_CLAIM (comma-separated). Default: 'azp'
80
+ */
81
+ callerId?: string | string[];
82
+ };
47
83
  }
48
84
  /**
49
85
  * Keycloak client interface
@@ -121,30 +157,262 @@ declare const KEYCLOAK_PROVIDER = "KEYCLOAK_PROVIDER";
121
157
 
122
158
  type RolesMode = "any" | "all";
123
159
  type RolesType = "realm" | "client" | "both";
160
+ interface TokenRolesOptions {
161
+ /** Header name where the JWT lives. E.g. 'x-access-token', 'authorization'. */
162
+ header: string;
163
+ /** Roles required from that token. */
164
+ roles: string[];
165
+ /** Match mode. Default: 'any' (at least one role must match). */
166
+ mode?: RolesMode;
167
+ /**
168
+ * If true, strips the 'Bearer ' prefix before decoding.
169
+ * Auto-detected when omitted: stripped when header is 'authorization'.
170
+ */
171
+ bearer?: boolean;
172
+ }
124
173
  type RolesOptions = {
125
174
  roles: string[];
126
175
  mode?: RolesMode;
127
176
  type?: RolesType;
128
177
  };
129
178
  /**
130
- * Decorator to declare required roles for a route or controller.
131
- * Accepts either a list of strings or a single options object.
132
- * Examples:
179
+ * Declares required roles without specifying the token source.
180
+ * RolesGuard resolves the token automatically:
181
+ * - X-Access-Token present → reads from user JWT (B2C)
182
+ * - Authorization only → reads from service JWT (B2B)
183
+ *
184
+ * @example
133
185
  * @Roles('admin')
134
- * @Roles('admin','editor')
135
- * @Roles(['admin','editor'])
136
- * @Roles({ roles: ['a','b'], mode: 'all', type: 'client' })
186
+ * @Roles('admin', 'editor')
187
+ * @Roles({ roles: ['admin', 'editor'], mode: 'all' })
137
188
  */
138
189
  declare function Roles(...args: Array<string | string[] | RolesOptions>): _nestjs_common.CustomDecorator<string>;
190
+ /**
191
+ * Declares roles that must be present in the **user JWT** (`X-Access-Token`).
192
+ * Checked independently from B2BRoles — both must pass when both are declared.
193
+ *
194
+ * Use when the route requires a specific user role regardless of which service called it.
195
+ *
196
+ * @example
197
+ * @B2CRoles('user-manager')
198
+ * @B2CRoles({ roles: ['admin', 'user-manager'], mode: 'all' })
199
+ */
200
+ declare function B2CRoles(...args: Array<string | string[] | RolesOptions>): _nestjs_common.CustomDecorator<string>;
201
+ /**
202
+ * Declares roles that must be present in the **service token** (`Authorization`).
203
+ * Checked independently from B2CRoles — both must pass when both are declared.
204
+ *
205
+ * Use when the route requires the calling service to have a specific role,
206
+ * regardless of which user triggered the request.
207
+ *
208
+ * @example
209
+ * @B2BRoles('manage-requests')
210
+ * @B2BRoles({ roles: ['manage-requests', 'send-notifications'], mode: 'any' })
211
+ */
212
+ declare function B2BRoles(...args: Array<string | string[] | RolesOptions>): _nestjs_common.CustomDecorator<string>;
213
+ /**
214
+ * Declares role requirements tied to a specific JWT header.
215
+ * Fully dynamic — works with any header that carries a JWT.
216
+ *
217
+ * Multiple uses are accumulated (AND logic): every @TokenRoles block must pass.
218
+ * Uses Reflector.getAllAndMerge internally, so stacking works correctly.
219
+ *
220
+ * @example
221
+ * // Verify user roles from X-Access-Token AND service roles from Authorization:
222
+ * @TokenRoles({ header: 'x-access-token', roles: ['user-manager'] })
223
+ * @TokenRoles({ header: 'authorization', roles: ['manage-requests'] })
224
+ * @UseGuards(B2BGuard, B2CGuard, RolesGuard)
225
+ *
226
+ * // Custom token header with ALL mode:
227
+ * @TokenRoles({ header: 'x-partner-token', roles: ['partner-admin', 'partner-api'], mode: 'all' })
228
+ * @UseGuards(RolesGuard)
229
+ */
230
+ declare function TokenRoles(options: TokenRolesOptions): _nestjs_common.CustomDecorator<string>;
139
231
 
232
+ /**
233
+ * Guard that enforces role requirements declared by @Roles, @B2CRoles, and @B2BRoles.
234
+ *
235
+ * Three decorator modes:
236
+ *
237
+ * @Roles('x') — auto-detect token source:
238
+ * X-Access-Token present → read from user JWT (B2C)
239
+ * Authorization only → read from service JWT (B2B)
240
+ *
241
+ * @B2CRoles('x') — always check the user JWT in X-Access-Token
242
+ * @B2BRoles('x') — always check the service JWT in Authorization
243
+ *
244
+ * When @B2CRoles AND @B2BRoles are both declared, BOTH checks must pass.
245
+ * This allows expressing: "the calling service must have role X AND the user must have role Y".
246
+ */
140
247
  declare class RolesGuard implements CanActivate {
141
248
  private readonly reflector;
142
249
  private readonly config?;
143
250
  constructor(reflector: Reflector, config?: KeycloakConfig);
144
- canActivate(context: ExecutionContext): boolean | Promise<boolean>;
251
+ canActivate(context: ExecutionContext): boolean;
252
+ private getMeta;
253
+ private extractRoles;
254
+ private assertRoles;
145
255
  private decodeJwtPayload;
146
256
  }
147
257
 
258
+ /**
259
+ * B2B Guard — service-to-service routes (e.g. BFF → API, Worker → API).
260
+ *
261
+ * The caller must send a valid service account token (client_credentials)
262
+ * in the Authorization header. Delegates full token validation to
263
+ * BearerTokenGuard (Keycloak introspection).
264
+ *
265
+ * The caller is also expected to forward X-User-Id so the API knows which
266
+ * end-user context the call belongs to.
267
+ *
268
+ * Use for routes that are ONLY called by internal services, not by end users.
269
+ *
270
+ * @example
271
+ * ```ts
272
+ * @Post('internal/notify')
273
+ * @Roles('send-notifications')
274
+ * @UseGuards(B2BGuard, RolesGuard)
275
+ * async notify(@AuthUser() keycloakId: string) { ... }
276
+ * ```
277
+ */
278
+ declare class B2BGuard implements CanActivate {
279
+ private readonly bearerTokenGuard;
280
+ constructor(bearerTokenGuard: BearerTokenGuard);
281
+ canActivate(context: ExecutionContext): Promise<boolean>;
282
+ }
283
+
284
+ /**
285
+ * B2C Guard — validates user context on routes that receive Kong-forwarded requests.
286
+ *
287
+ * Kong validates the user JWT (JWKS local) and injects:
288
+ * - `Authorization: Bearer <service_token>` — the calling service identity (B2B)
289
+ * - `X-Access-Token: <user_token>` — the original user token (B2C context)
290
+ * - `X-User-Id` — keycloak sub (extracted by Kong)
291
+ * - `X-User-Roles` — realm roles CSV (extracted by Kong)
292
+ *
293
+ * This guard asserts the user context headers are present.
294
+ * It does NOT re-validate the user token — that is Kong's responsibility.
295
+ *
296
+ * Pair with B2BGuard (or ApiAuthGuard) + RolesGuard for full auth.
297
+ *
298
+ * @example
299
+ * ```ts
300
+ * @Get('me')
301
+ * @Roles('user-manager')
302
+ * @UseGuards(B2CGuard, RolesGuard)
303
+ * async getMe(@AuthUser() keycloakId: string) { ... }
304
+ * ```
305
+ */
306
+ declare class B2CGuard implements CanActivate {
307
+ canActivate(context: ExecutionContext): boolean;
308
+ }
309
+
310
+ /**
311
+ * ApiAuthGuard — composite guard for routes that accept both paths.
312
+ *
313
+ * Detects which path the request is coming from and delegates accordingly:
314
+ *
315
+ * - **B2B path** (Authorization header present):
316
+ * Service-to-service call (e.g. BFF → API). Delegates to B2BGuard,
317
+ * which validates the service account token via BearerTokenGuard.
318
+ *
319
+ * - **B2C path** (X-User-Id header present, no Authorization):
320
+ * User call routed by Kong. Delegates to B2CGuard,
321
+ * which asserts Kong identity headers are present.
322
+ *
323
+ * Use when the same route must be reachable both from Kong (users) and
324
+ * from internal services (BFF, Worker). If the route is exclusive to one
325
+ * path, prefer B2BGuard or B2CGuard directly for explicit intent.
326
+ *
327
+ * @example
328
+ * ```ts
329
+ * @Get('me')
330
+ * @Roles('user-manager')
331
+ * @UseGuards(ApiAuthGuard, RolesGuard)
332
+ * async getMe(@AuthUser() keycloakId: string) { ... }
333
+ * ```
334
+ */
335
+ declare class ApiAuthGuard implements CanActivate {
336
+ private readonly b2bGuard;
337
+ private readonly b2cGuard;
338
+ constructor(b2bGuard: B2BGuard, b2cGuard: B2CGuard);
339
+ canActivate(context: ExecutionContext): Promise<boolean>;
340
+ }
341
+
342
+ interface AuthUserOptions {
343
+ /**
344
+ * Override the header to read the B2C token from.
345
+ * Defaults to the configured B2C token header (env: KEYCLOAK_B2C_TOKEN_HEADER).
346
+ */
347
+ header?: string;
348
+ /**
349
+ * Claim name(s) to extract from the JWT payload.
350
+ * First non-empty value wins. Defaults to configured userId claim(s).
351
+ */
352
+ claim?: string | string[];
353
+ }
354
+ interface CallerTokenOptions {
355
+ /**
356
+ * Override the header to read the B2B token from.
357
+ * Defaults to the configured B2B token header (env: KEYCLOAK_B2B_TOKEN_HEADER).
358
+ */
359
+ header?: string;
360
+ /**
361
+ * Claim name(s) to extract from the JWT payload.
362
+ * First non-empty value wins. Defaults to configured callerId claim(s).
363
+ */
364
+ claim?: string | string[];
365
+ }
366
+ /**
367
+ * Extracts a claim from the user JWT forwarded by Kong in the B2C token header.
368
+ *
369
+ * Claims are decoded locally — no extra I/O.
370
+ *
371
+ * @param param - Claim name, array of claim names (first non-empty wins),
372
+ * or `{ header, claim }` to fully customize both.
373
+ *
374
+ * @example
375
+ * ```ts
376
+ * @AuthUser() // sub (default)
377
+ * @AuthUser('email') // single claim
378
+ * @AuthUser(['preferred_username', 'email']) // first non-empty
379
+ * @AuthUser({ claim: 'email', header: 'x-user-jwt' }) // custom header
380
+ * ```
381
+ */
382
+ declare const AuthUser: (...dataOrPipes: (string | string[] | AuthUserOptions | _nestjs_common.PipeTransform<any, any> | _nestjs_common.Type<_nestjs_common.PipeTransform<any, any>>)[]) => ParameterDecorator;
383
+ /**
384
+ * Extracts a claim from the service token in the B2B token header.
385
+ *
386
+ * Use this to identify the calling service.
387
+ *
388
+ * @param param - Claim name, array of claim names (first non-empty wins),
389
+ * or `{ header, claim }` to fully customize both.
390
+ *
391
+ * @example
392
+ * ```ts
393
+ * @CallerToken() // azp (default)
394
+ * @CallerToken('sub') // single claim
395
+ * @CallerToken(['client_id', 'azp']) // first non-empty
396
+ * @CallerToken({ header: 'x-service-token', claim: 'azp' }) // custom header
397
+ * ```
398
+ */
399
+ declare const CallerToken: (...dataOrPipes: (string | string[] | CallerTokenOptions | _nestjs_common.PipeTransform<any, any> | _nestjs_common.Type<_nestjs_common.PipeTransform<any, any>>)[]) => ParameterDecorator;
400
+ /**
401
+ * Extracts the raw B2C token header value (full user JWT string).
402
+ *
403
+ * Use this when you need the full token to pass downstream or inspect
404
+ * non-string claims (arrays, objects). For scalar claims prefer `@AuthUser(claim)`.
405
+ *
406
+ * @param header - Override the header name. Defaults to configured B2C token header.
407
+ *
408
+ * @example
409
+ * ```ts
410
+ * @AccessToken() // reads from default B2C header
411
+ * @AccessToken('x-user-jwt') // reads from custom header
412
+ * ```
413
+ */
414
+ declare const AccessToken: (...dataOrPipes: (string | _nestjs_common.PipeTransform<any, any> | _nestjs_common.Type<_nestjs_common.PipeTransform<any, any>>)[]) => ParameterDecorator;
415
+
148
416
  declare class KeycloakError extends Error {
149
417
  readonly statusCode?: number;
150
418
  readonly details?: unknown;
@@ -156,4 +424,43 @@ declare class KeycloakError extends Error {
156
424
  });
157
425
  }
158
426
 
159
- export { BearerTokenGuard, KEYCLOAK_CLIENT, KEYCLOAK_CONFIG, KEYCLOAK_HTTP_INTERCEPTOR, KEYCLOAK_PROVIDER, type KeycloakClientInterface, type KeycloakConfig, KeycloakError, KeycloakModule, type KeycloakProviderInterface, type KeycloakTokenResponse, Roles, RolesGuard };
427
+ /**
428
+ * Runtime-configurable header names and JWT claim names for the B2B/B2C auth flow.
429
+ *
430
+ * Priority (highest → lowest):
431
+ * 1. KeycloakModule.forRoot({ headers, claims }) — programmatic config
432
+ * 2. process.env vars — set in .env files
433
+ * 3. Built-in defaults
434
+ *
435
+ * Environment variables:
436
+ * KEYCLOAK_B2C_TOKEN_HEADER — header for the user JWT (default: x-access-token)
437
+ * KEYCLOAK_B2B_TOKEN_HEADER — header for the service token (default: authorization)
438
+ * KEYCLOAK_USER_ID_CLAIM — claim(s) for user ID, comma-separated (default: sub)
439
+ * KEYCLOAK_CALLER_ID_CLAIM — claim(s) for caller ID, comma-separated (default: azp)
440
+ *
441
+ * Multiple claims example (first non-empty value wins):
442
+ * KEYCLOAK_USER_ID_CLAIM=preferred_username,email,sub
443
+ * KEYCLOAK_CALLER_ID_CLAIM=client_id,azp
444
+ */
445
+ interface TokenHeaderConfig {
446
+ /** Header name for the B2C user JWT forwarded by Kong. Default: 'x-access-token' */
447
+ b2cToken?: string;
448
+ /** Header name for the B2B service account token. Default: 'authorization' */
449
+ b2bToken?: string;
450
+ }
451
+ interface TokenClaimConfig {
452
+ /**
453
+ * Claim name(s) for the user identifier (from the B2C token).
454
+ * When an array, the first non-empty value found in the JWT is returned.
455
+ * Default: ['sub']
456
+ */
457
+ userId?: string | string[];
458
+ /**
459
+ * Claim name(s) for the caller/client identifier (from the B2B token).
460
+ * When an array, the first non-empty value found in the JWT is returned.
461
+ * Default: ['azp']
462
+ */
463
+ callerId?: string | string[];
464
+ }
465
+
466
+ export { AccessToken, ApiAuthGuard, AuthUser, type AuthUserOptions, B2BGuard, B2BRoles, B2CGuard, B2CRoles, BearerTokenGuard, CallerToken, type CallerTokenOptions, KEYCLOAK_CLIENT, KEYCLOAK_CONFIG, KEYCLOAK_HTTP_INTERCEPTOR, KEYCLOAK_PROVIDER, type KeycloakClientInterface, type KeycloakConfig, KeycloakError, KeycloakModule, type KeycloakProviderInterface, type KeycloakTokenResponse, Roles, RolesGuard, type TokenClaimConfig, type TokenHeaderConfig, TokenRoles, type TokenRolesOptions };
package/dist/index.js CHANGED
@@ -68,7 +68,7 @@ var require_base_app_error = __commonJS({
68
68
  "use strict";
69
69
  Object.defineProperty(exports2, "__esModule", { value: true });
70
70
  exports2.BaseAppError = void 0;
71
- var BaseAppError3 = class extends Error {
71
+ var BaseAppError5 = class extends Error {
72
72
  code;
73
73
  status;
74
74
  context;
@@ -83,7 +83,7 @@ var require_base_app_error = __commonJS({
83
83
  (_a = capturable.captureStackTrace) == null ? void 0 : _a.call(capturable, this, this.constructor);
84
84
  }
85
85
  };
86
- exports2.BaseAppError = BaseAppError3;
86
+ exports2.BaseAppError = BaseAppError5;
87
87
  }
88
88
  });
89
89
 
@@ -268,7 +268,15 @@ var require_dist = __commonJS({
268
268
  // src/index.ts
269
269
  var index_exports = {};
270
270
  __export(index_exports, {
271
+ AccessToken: () => AccessToken,
272
+ ApiAuthGuard: () => ApiAuthGuard,
273
+ AuthUser: () => AuthUser,
274
+ B2BGuard: () => B2BGuard,
275
+ B2BRoles: () => B2BRoles,
276
+ B2CGuard: () => B2CGuard,
277
+ B2CRoles: () => B2CRoles,
271
278
  BearerTokenGuard: () => BearerTokenGuard,
279
+ CallerToken: () => CallerToken,
272
280
  KEYCLOAK_CLIENT: () => KEYCLOAK_CLIENT,
273
281
  KEYCLOAK_CONFIG: () => KEYCLOAK_CONFIG,
274
282
  KEYCLOAK_HTTP_INTERCEPTOR: () => KEYCLOAK_HTTP_INTERCEPTOR,
@@ -276,7 +284,8 @@ __export(index_exports, {
276
284
  KeycloakError: () => KeycloakError,
277
285
  KeycloakModule: () => KeycloakModule,
278
286
  Roles: () => Roles,
279
- RolesGuard: () => RolesGuard
287
+ RolesGuard: () => RolesGuard,
288
+ TokenRoles: () => TokenRoles
280
289
  });
281
290
  module.exports = __toCommonJS(index_exports);
282
291
 
@@ -302,7 +311,7 @@ var KEYCLOAK_PROVIDER = "KEYCLOAK_PROVIDER";
302
311
  // package.json
303
312
  var package_default = {
304
313
  name: "@adatechnology/auth-keycloak",
305
- version: "0.1.0",
314
+ version: "0.1.2",
306
315
  publishConfig: {
307
316
  access: "public"
308
317
  },
@@ -753,7 +762,19 @@ var import_core = require("@nestjs/core");
753
762
  // src/roles.decorator.ts
754
763
  var import_common4 = require("@nestjs/common");
755
764
  var ROLES_META_KEY = "roles";
765
+ var B2C_ROLES_META_KEY = "roles:b2c";
766
+ var B2B_ROLES_META_KEY = "roles:b2b";
767
+ var TOKEN_ROLES_META_KEY = "roles:token";
756
768
  function Roles(...args) {
769
+ return (0, import_common4.SetMetadata)(ROLES_META_KEY, normalizeRolesOptions(args));
770
+ }
771
+ function B2CRoles(...args) {
772
+ return (0, import_common4.SetMetadata)(B2C_ROLES_META_KEY, normalizeRolesOptions(args));
773
+ }
774
+ function B2BRoles(...args) {
775
+ return (0, import_common4.SetMetadata)(B2B_ROLES_META_KEY, normalizeRolesOptions(args));
776
+ }
777
+ function normalizeRolesOptions(args) {
757
778
  let payload;
758
779
  if (args.length === 1 && typeof args[0] === "object" && !Array.isArray(args[0])) {
759
780
  payload = args[0];
@@ -765,72 +786,162 @@ function Roles(...args) {
765
786
  }
766
787
  payload.mode = payload.mode ?? "any";
767
788
  payload.type = payload.type ?? "both";
768
- return (0, import_common4.SetMetadata)(ROLES_META_KEY, payload);
789
+ return payload;
790
+ }
791
+ function TokenRoles(options) {
792
+ const normalized = {
793
+ ...options,
794
+ header: options.header.toLowerCase(),
795
+ mode: options.mode ?? "any",
796
+ // auto-detect bearer stripping: true when header is 'authorization'
797
+ bearer: options.bearer ?? options.header.toLowerCase() === "authorization"
798
+ };
799
+ return (0, import_common4.SetMetadata)(TOKEN_ROLES_META_KEY, [normalized]);
769
800
  }
770
801
 
771
802
  // src/roles.guard.ts
772
803
  var import_shared2 = __toESM(require_dist());
804
+
805
+ // src/keycloak.headers.ts
806
+ var state = {
807
+ headers: {
808
+ b2cToken: parseEnvHeader("KEYCLOAK_B2C_TOKEN_HEADER", "x-access-token"),
809
+ b2bToken: parseEnvHeader("KEYCLOAK_B2B_TOKEN_HEADER", "authorization")
810
+ },
811
+ claims: {
812
+ userId: parseEnvClaims("KEYCLOAK_USER_ID_CLAIM", ["sub"]),
813
+ callerId: parseEnvClaims("KEYCLOAK_CALLER_ID_CLAIM", ["azp"])
814
+ }
815
+ };
816
+ function configureTokenHeaders(cfg) {
817
+ if (cfg.b2cToken) state.headers.b2cToken = cfg.b2cToken.toLowerCase();
818
+ if (cfg.b2bToken) state.headers.b2bToken = cfg.b2bToken.toLowerCase();
819
+ }
820
+ function configureTokenClaims(cfg) {
821
+ if (cfg.userId) state.claims.userId = normalizeClaims(cfg.userId);
822
+ if (cfg.callerId) state.claims.callerId = normalizeClaims(cfg.callerId);
823
+ }
824
+ function getB2CTokenHeader() {
825
+ return state.headers.b2cToken;
826
+ }
827
+ function getB2BTokenHeader() {
828
+ return state.headers.b2bToken;
829
+ }
830
+ function getUserIdClaims() {
831
+ return state.claims.userId;
832
+ }
833
+ function getCallerIdClaims() {
834
+ return state.claims.callerId;
835
+ }
836
+ function parseEnvHeader(key, fallback) {
837
+ return (process.env[key] ?? fallback).toLowerCase();
838
+ }
839
+ function parseEnvClaims(key, fallback) {
840
+ const raw = process.env[key];
841
+ if (!raw) return fallback;
842
+ return raw.split(",").map((c) => c.trim()).filter(Boolean);
843
+ }
844
+ function normalizeClaims(value) {
845
+ if (Array.isArray(value)) return value.filter(Boolean);
846
+ return value.split(",").map((c) => c.trim()).filter(Boolean);
847
+ }
848
+
849
+ // src/roles.guard.ts
773
850
  var RolesGuard = class {
774
851
  constructor(reflector, config) {
775
852
  this.reflector = reflector;
776
853
  this.config = config;
777
854
  }
778
855
  canActivate(context) {
779
- var _a, _b, _c, _d, _e, _f, _g, _h;
780
- const meta = this.reflector.get(ROLES_META_KEY, context.getHandler()) || this.reflector.get(ROLES_META_KEY, context.getClass());
781
- if (!meta || !meta.roles || meta.roles.length === 0) return true;
856
+ var _a, _b, _c, _d, _e, _f;
782
857
  const req = context.switchToHttp().getRequest();
783
- const authHeader = ((_a = req.headers) == null ? void 0 : _a.authorization) || ((_b = req.headers) == null ? void 0 : _b.Authorization);
784
- const token = authHeader ? String(authHeader).split(" ")[1] : (_c = req.query) == null ? void 0 : _c.token;
785
- if (!token)
786
- throw new import_shared2.BaseAppError({
787
- message: "Authorization token not provided",
788
- status: HTTP_STATUS.FORBIDDEN,
789
- code: ROLES_ERROR_CODE.MISSING_TOKEN,
790
- context: {}
791
- });
792
- const payload = this.decodeJwtPayload(token);
793
- const availableRoles = /* @__PURE__ */ new Set();
794
- if (((_d = payload == null ? void 0 : payload.realm_access) == null ? void 0 : _d.roles) && Array.isArray(payload.realm_access.roles)) {
795
- payload.realm_access.roles.forEach((r) => availableRoles.add(r));
858
+ const b2cMeta = this.getMeta(B2C_ROLES_META_KEY, context);
859
+ const b2bMeta = this.getMeta(B2B_ROLES_META_KEY, context);
860
+ const genericMeta = this.getMeta(ROLES_META_KEY, context);
861
+ const tokenRules = this.reflector.getAllAndMerge(
862
+ TOKEN_ROLES_META_KEY,
863
+ [context.getHandler(), context.getClass()]
864
+ ) ?? [];
865
+ if (!b2cMeta && !b2bMeta && !genericMeta && tokenRules.length === 0) return true;
866
+ if (b2cMeta) {
867
+ const token = (_a = req.headers) == null ? void 0 : _a[getB2CTokenHeader()];
868
+ const roles = token ? this.extractRoles(token, "b2c") : /* @__PURE__ */ new Set();
869
+ this.assertRoles(roles, b2cMeta, "B2C (user)");
796
870
  }
797
- const clientId = (_f = (_e = this.config) == null ? void 0 : _e.credentials) == null ? void 0 : _f.clientId;
798
- if (clientId && ((_h = (_g = payload == null ? void 0 : payload.resource_access) == null ? void 0 : _g[clientId]) == null ? void 0 : _h.roles)) {
799
- payload.resource_access[clientId].roles.forEach(
800
- (r) => availableRoles.add(r)
801
- );
871
+ if (b2bMeta) {
872
+ const raw = (_b = req.headers) == null ? void 0 : _b[getB2BTokenHeader()];
873
+ const token = raw == null ? void 0 : raw.split(" ")[1];
874
+ const roles = token ? this.extractRoles(token, "b2b") : /* @__PURE__ */ new Set();
875
+ this.assertRoles(roles, b2bMeta, "B2B (service)");
802
876
  }
803
- if (meta.type === "both" && (payload == null ? void 0 : payload.resource_access)) {
804
- Object.values(payload.resource_access).forEach((entry) => {
805
- if ((entry == null ? void 0 : entry.roles) && Array.isArray(entry.roles)) {
806
- entry.roles.forEach(
807
- (r) => availableRoles.add(r)
808
- );
877
+ if (genericMeta) {
878
+ const accessToken = (_c = req.headers) == null ? void 0 : _c[getB2CTokenHeader()];
879
+ if (accessToken) {
880
+ const roles = this.extractRoles(accessToken, "b2c");
881
+ this.assertRoles(roles, genericMeta, "B2C (user)");
882
+ } else {
883
+ const raw = (_d = req.headers) == null ? void 0 : _d[getB2BTokenHeader()];
884
+ const token = (raw == null ? void 0 : raw.split(" ")[1]) ?? ((_e = req.query) == null ? void 0 : _e.token);
885
+ if (!token) {
886
+ throw new import_shared2.BaseAppError({
887
+ message: "Authorization token not provided",
888
+ status: HTTP_STATUS.FORBIDDEN,
889
+ code: ROLES_ERROR_CODE.MISSING_TOKEN,
890
+ context: {}
891
+ });
809
892
  }
810
- });
893
+ const roles = this.extractRoles(token, "b2b");
894
+ this.assertRoles(roles, genericMeta, "B2B (service)");
895
+ }
896
+ }
897
+ for (const rule of tokenRules) {
898
+ const raw = (_f = req.headers) == null ? void 0 : _f[rule.header];
899
+ const token = rule.bearer ? raw == null ? void 0 : raw.split(" ")[1] : raw;
900
+ const roles = token ? this.extractRoles(token, "b2c") : /* @__PURE__ */ new Set();
901
+ this.assertRoles(roles, { roles: rule.roles, mode: rule.mode ?? "any" }, `header:${rule.header}`);
811
902
  }
812
- const required = meta.roles || [];
813
- const hasMatch = required.map((r) => availableRoles.has(r));
814
- const result = meta.mode === "all" ? hasMatch.every(Boolean) : hasMatch.some(Boolean);
815
- if (!result)
903
+ return true;
904
+ }
905
+ // ── Helpers ───────────────────────────────────────────────────────────────
906
+ getMeta(key, ctx) {
907
+ return this.reflector.get(key, ctx.getHandler()) || this.reflector.get(key, ctx.getClass()) || void 0;
908
+ }
909
+ extractRoles(token, source) {
910
+ var _a, _b, _c, _d;
911
+ const payload = this.decodeJwtPayload(token);
912
+ const roles = /* @__PURE__ */ new Set();
913
+ if (((_a = payload == null ? void 0 : payload.realm_access) == null ? void 0 : _a.roles) && Array.isArray(payload.realm_access.roles)) {
914
+ payload.realm_access.roles.forEach((r) => roles.add(r));
915
+ }
916
+ if (source === "b2b" && (payload == null ? void 0 : payload.resource_access)) {
917
+ const clientId = (_c = (_b = this.config) == null ? void 0 : _b.credentials) == null ? void 0 : _c.clientId;
918
+ if (clientId && ((_d = payload.resource_access[clientId]) == null ? void 0 : _d.roles)) {
919
+ payload.resource_access[clientId].roles.forEach((r) => roles.add(r));
920
+ }
921
+ }
922
+ return roles;
923
+ }
924
+ assertRoles(available, meta, label) {
925
+ const hasMatch = meta.roles.map((r) => available.has(r));
926
+ const passed = meta.mode === "all" ? hasMatch.every(Boolean) : hasMatch.some(Boolean);
927
+ if (!passed) {
816
928
  throw new import_shared2.BaseAppError({
817
- message: "Insufficient roles",
929
+ message: `Insufficient roles for ${label} token`,
818
930
  status: HTTP_STATUS.FORBIDDEN,
819
931
  code: ROLES_ERROR_CODE.INSUFFICIENT_ROLES,
820
- context: { required }
932
+ context: { required: meta.roles, source: label }
821
933
  });
822
- return true;
934
+ }
823
935
  }
824
936
  decodeJwtPayload(token) {
825
937
  try {
826
938
  const parts = token.split(".");
827
939
  if (parts.length < 2) return {};
828
- const payload = parts[1];
940
+ const padded = parts[1].replace(/-/g, "+").replace(/_/g, "/");
829
941
  const BufferCtor = globalThis.Buffer;
830
942
  if (!BufferCtor) return {};
831
- const decoded = BufferCtor.from(payload, "base64").toString("utf8");
832
- return JSON.parse(decoded);
833
- } catch (e) {
943
+ return JSON.parse(BufferCtor.from(padded, "base64").toString("utf8"));
944
+ } catch {
834
945
  return {};
835
946
  }
836
947
  }
@@ -845,6 +956,8 @@ RolesGuard = __decorateClass([
845
956
  // src/keycloak.module.ts
846
957
  var KeycloakModule = class {
847
958
  static forRoot(config, httpConfig) {
959
+ if (config.headers) configureTokenHeaders(config.headers);
960
+ if (config.claims) configureTokenClaims(config.claims);
848
961
  return {
849
962
  module: KeycloakModule,
850
963
  global: true,
@@ -900,9 +1013,172 @@ var KeycloakModule = class {
900
1013
  KeycloakModule = __decorateClass([
901
1014
  (0, import_common6.Module)({})
902
1015
  ], KeycloakModule);
1016
+
1017
+ // src/b2b.guard.ts
1018
+ var import_common7 = require("@nestjs/common");
1019
+ var B2BGuard = class {
1020
+ constructor(bearerTokenGuard) {
1021
+ this.bearerTokenGuard = bearerTokenGuard;
1022
+ }
1023
+ canActivate(context) {
1024
+ return Promise.resolve(this.bearerTokenGuard.canActivate(context));
1025
+ }
1026
+ };
1027
+ B2BGuard = __decorateClass([
1028
+ (0, import_common7.Injectable)()
1029
+ ], B2BGuard);
1030
+
1031
+ // src/b2c.guard.ts
1032
+ var import_common8 = require("@nestjs/common");
1033
+ var import_shared3 = __toESM(require_dist());
1034
+ var B2CGuard = class {
1035
+ canActivate(context) {
1036
+ var _a;
1037
+ const request = context.switchToHttp().getRequest();
1038
+ const accessToken = (_a = request.headers) == null ? void 0 : _a[getB2CTokenHeader()];
1039
+ if (accessToken) return true;
1040
+ throw new import_shared3.BaseAppError({
1041
+ message: "Missing X-Access-Token header. Route requires Kong-forwarded user authentication.",
1042
+ status: HTTP_STATUS.UNAUTHORIZED,
1043
+ code: BEARER_ERROR_CODE.MISSING_TOKEN,
1044
+ context: {}
1045
+ });
1046
+ }
1047
+ };
1048
+ B2CGuard = __decorateClass([
1049
+ (0, import_common8.Injectable)()
1050
+ ], B2CGuard);
1051
+
1052
+ // src/api-auth.guard.ts
1053
+ var import_common9 = require("@nestjs/common");
1054
+ var import_shared4 = __toESM(require_dist());
1055
+ var ApiAuthGuard = class {
1056
+ constructor(b2bGuard, b2cGuard) {
1057
+ this.b2bGuard = b2bGuard;
1058
+ this.b2cGuard = b2cGuard;
1059
+ }
1060
+ async canActivate(context) {
1061
+ var _a, _b;
1062
+ const request = context.switchToHttp().getRequest();
1063
+ const accessToken = (_a = request.headers) == null ? void 0 : _a[getB2CTokenHeader()];
1064
+ if (accessToken) {
1065
+ return this.b2cGuard.canActivate(context);
1066
+ }
1067
+ const authHeader = (_b = request.headers) == null ? void 0 : _b[getB2BTokenHeader()];
1068
+ if (authHeader == null ? void 0 : authHeader.toLowerCase().startsWith("bearer ")) {
1069
+ return this.b2bGuard.canActivate(context);
1070
+ }
1071
+ throw new import_shared4.BaseAppError({
1072
+ message: "Unauthorized: missing X-Access-Token (Kong/B2C) or Authorization header (B2B)",
1073
+ status: HTTP_STATUS.UNAUTHORIZED,
1074
+ code: BEARER_ERROR_CODE.MISSING_TOKEN,
1075
+ context: {}
1076
+ });
1077
+ }
1078
+ };
1079
+ ApiAuthGuard = __decorateClass([
1080
+ (0, import_common9.Injectable)()
1081
+ ], ApiAuthGuard);
1082
+
1083
+ // src/auth-user.decorator.ts
1084
+ var import_common10 = require("@nestjs/common");
1085
+ var AuthUser = (0, import_common10.createParamDecorator)(
1086
+ (param, ctx) => {
1087
+ var _a;
1088
+ const request = ctx.switchToHttp().getRequest();
1089
+ const { header, claims } = resolveB2CParam(param);
1090
+ const raw = (_a = request.headers) == null ? void 0 : _a[header];
1091
+ const token = Array.isArray(raw) ? raw[0] : raw;
1092
+ if (!token) return "";
1093
+ return decodeJwtClaims(String(token), claims) ?? "";
1094
+ }
1095
+ );
1096
+ var CallerToken = (0, import_common10.createParamDecorator)(
1097
+ (param, ctx) => {
1098
+ var _a;
1099
+ const request = ctx.switchToHttp().getRequest();
1100
+ const { header, claims } = resolveB2BParam(param);
1101
+ const raw = (_a = request.headers) == null ? void 0 : _a[header];
1102
+ const token = raw == null ? void 0 : raw.split(" ")[1];
1103
+ if (!token) return "";
1104
+ return decodeJwtClaims(token, claims) ?? "";
1105
+ }
1106
+ );
1107
+ var AccessToken = (0, import_common10.createParamDecorator)(
1108
+ (header, ctx) => {
1109
+ var _a;
1110
+ const request = ctx.switchToHttp().getRequest();
1111
+ const h = (header == null ? void 0 : header.toLowerCase()) ?? getB2CTokenHeader();
1112
+ const raw = (_a = request.headers) == null ? void 0 : _a[h];
1113
+ return Array.isArray(raw) ? raw[0] : raw ?? "";
1114
+ }
1115
+ );
1116
+ function resolveB2CParam(param) {
1117
+ var _a;
1118
+ if (!param) {
1119
+ return { header: getB2CTokenHeader(), claims: getUserIdClaims() };
1120
+ }
1121
+ if (typeof param === "string") {
1122
+ return { header: getB2CTokenHeader(), claims: [param] };
1123
+ }
1124
+ if (Array.isArray(param)) {
1125
+ return { header: getB2CTokenHeader(), claims: param };
1126
+ }
1127
+ return {
1128
+ header: ((_a = param.header) == null ? void 0 : _a.toLowerCase()) ?? getB2CTokenHeader(),
1129
+ claims: param.claim ? normalizeClaims2(param.claim) : getUserIdClaims()
1130
+ };
1131
+ }
1132
+ function resolveB2BParam(param) {
1133
+ var _a;
1134
+ if (!param) {
1135
+ return { header: getB2BTokenHeader(), claims: getCallerIdClaims() };
1136
+ }
1137
+ if (typeof param === "string") {
1138
+ return { header: getB2BTokenHeader(), claims: [param] };
1139
+ }
1140
+ if (Array.isArray(param)) {
1141
+ return { header: getB2BTokenHeader(), claims: param };
1142
+ }
1143
+ return {
1144
+ header: ((_a = param.header) == null ? void 0 : _a.toLowerCase()) ?? getB2BTokenHeader(),
1145
+ claims: param.claim ? normalizeClaims2(param.claim) : getCallerIdClaims()
1146
+ };
1147
+ }
1148
+ function normalizeClaims2(value) {
1149
+ if (Array.isArray(value)) return value.filter(Boolean);
1150
+ return value.split(",").map((c) => c.trim()).filter(Boolean);
1151
+ }
1152
+ function decodeJwtClaims(token, claims) {
1153
+ try {
1154
+ const parts = token.split(".");
1155
+ if (parts.length < 2) return void 0;
1156
+ const padded = parts[1].replace(/-/g, "+").replace(/_/g, "/");
1157
+ const BufferCtor = globalThis.Buffer;
1158
+ if (!BufferCtor) return void 0;
1159
+ const payload = JSON.parse(
1160
+ BufferCtor.from(padded, "base64").toString("utf8")
1161
+ );
1162
+ for (const claim of claims) {
1163
+ const value = payload[claim];
1164
+ if (typeof value === "string" && value.length > 0) return value;
1165
+ }
1166
+ return void 0;
1167
+ } catch {
1168
+ return void 0;
1169
+ }
1170
+ }
903
1171
  // Annotate the CommonJS export names for ESM import in node:
904
1172
  0 && (module.exports = {
1173
+ AccessToken,
1174
+ ApiAuthGuard,
1175
+ AuthUser,
1176
+ B2BGuard,
1177
+ B2BRoles,
1178
+ B2CGuard,
1179
+ B2CRoles,
905
1180
  BearerTokenGuard,
1181
+ CallerToken,
906
1182
  KEYCLOAK_CLIENT,
907
1183
  KEYCLOAK_CONFIG,
908
1184
  KEYCLOAK_HTTP_INTERCEPTOR,
@@ -910,5 +1186,6 @@ KeycloakModule = __decorateClass([
910
1186
  KeycloakError,
911
1187
  KeycloakModule,
912
1188
  Roles,
913
- RolesGuard
1189
+ RolesGuard,
1190
+ TokenRoles
914
1191
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adatechnology/auth-keycloak",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },