@adatechnology/auth-keycloak 0.1.1 → 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 +136 -45
- package/dist/index.d.ts +227 -35
- package/dist/index.js +248 -61
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -129,66 +129,153 @@ Resultado no log:
|
|
|
129
129
|
[MyController.getToken][InMemoryCacheProvider.set] → token cached
|
|
130
130
|
```
|
|
131
131
|
|
|
132
|
-
###
|
|
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
|
-
|
|
135
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
@
|
|
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
|
-
|
|
169
|
+
#### `@CallerToken(param?)`
|
|
170
|
+
Extrai claim do token em `Authorization`. Padrão: claim configurado em `callerId` (default `azp`).
|
|
151
171
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
158
|
-
|
|
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
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
176
|
-
@UseGuards(RolesGuard)
|
|
177
|
-
@Roles("admin")
|
|
178
|
-
adminOnly() {
|
|
179
|
-
return { ok: true };
|
|
180
|
-
}
|
|
196
|
+
---
|
|
181
197
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
198
|
+
### Exemplos de uso
|
|
199
|
+
|
|
200
|
+
**B2C — usuá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
|
-
|
|
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,44 +157,101 @@ 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
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
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
|
|
|
140
232
|
/**
|
|
141
|
-
* Guard that
|
|
233
|
+
* Guard that enforces role requirements declared by @Roles, @B2CRoles, and @B2BRoles.
|
|
142
234
|
*
|
|
143
|
-
*
|
|
235
|
+
* Three decorator modes:
|
|
144
236
|
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
* - `X-User-Roles` — comma-separated realm roles
|
|
149
|
-
* The guard reads roles from `X-User-Roles` header.
|
|
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)
|
|
150
240
|
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
* The guard decodes the JWT locally and reads `realm_access.roles`.
|
|
241
|
+
* @B2CRoles('x') — always check the user JWT in X-Access-Token
|
|
242
|
+
* @B2BRoles('x') — always check the service JWT in Authorization
|
|
154
243
|
*
|
|
155
|
-
*
|
|
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".
|
|
156
246
|
*/
|
|
157
247
|
declare class RolesGuard implements CanActivate {
|
|
158
248
|
private readonly reflector;
|
|
159
249
|
private readonly config?;
|
|
160
250
|
constructor(reflector: Reflector, config?: KeycloakConfig);
|
|
161
|
-
canActivate(context: ExecutionContext): boolean
|
|
251
|
+
canActivate(context: ExecutionContext): boolean;
|
|
252
|
+
private getMeta;
|
|
253
|
+
private extractRoles;
|
|
254
|
+
private assertRoles;
|
|
162
255
|
private decodeJwtPayload;
|
|
163
256
|
}
|
|
164
257
|
|
|
@@ -189,13 +282,18 @@ declare class B2BGuard implements CanActivate {
|
|
|
189
282
|
}
|
|
190
283
|
|
|
191
284
|
/**
|
|
192
|
-
* B2C Guard — user
|
|
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)
|
|
193
292
|
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
* headers are present — it does NOT re-validate any token.
|
|
293
|
+
* This guard asserts the user context headers are present.
|
|
294
|
+
* It does NOT re-validate the user token — that is Kong's responsibility.
|
|
197
295
|
*
|
|
198
|
-
*
|
|
296
|
+
* Pair with B2BGuard (or ApiAuthGuard) + RolesGuard for full auth.
|
|
199
297
|
*
|
|
200
298
|
* @example
|
|
201
299
|
* ```ts
|
|
@@ -241,24 +339,79 @@ declare class ApiAuthGuard implements CanActivate {
|
|
|
241
339
|
canActivate(context: ExecutionContext): Promise<boolean>;
|
|
242
340
|
}
|
|
243
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
|
+
}
|
|
244
366
|
/**
|
|
245
|
-
*
|
|
246
|
-
*
|
|
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.
|
|
247
370
|
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
371
|
+
* @param param - Claim name, array of claim names (first non-empty wins),
|
|
372
|
+
* or `{ header, claim }` to fully customize both.
|
|
250
373
|
*
|
|
251
374
|
* @example
|
|
252
375
|
* ```ts
|
|
253
|
-
* @
|
|
254
|
-
* @
|
|
255
|
-
* @
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
|
|
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
|
|
259
412
|
* ```
|
|
260
413
|
*/
|
|
261
|
-
declare const
|
|
414
|
+
declare const AccessToken: (...dataOrPipes: (string | _nestjs_common.PipeTransform<any, any> | _nestjs_common.Type<_nestjs_common.PipeTransform<any, any>>)[]) => ParameterDecorator;
|
|
262
415
|
|
|
263
416
|
declare class KeycloakError extends Error {
|
|
264
417
|
readonly statusCode?: number;
|
|
@@ -271,4 +424,43 @@ declare class KeycloakError extends Error {
|
|
|
271
424
|
});
|
|
272
425
|
}
|
|
273
426
|
|
|
274
|
-
|
|
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
|
@@ -268,11 +268,15 @@ var require_dist = __commonJS({
|
|
|
268
268
|
// src/index.ts
|
|
269
269
|
var index_exports = {};
|
|
270
270
|
__export(index_exports, {
|
|
271
|
+
AccessToken: () => AccessToken,
|
|
271
272
|
ApiAuthGuard: () => ApiAuthGuard,
|
|
272
273
|
AuthUser: () => AuthUser,
|
|
273
274
|
B2BGuard: () => B2BGuard,
|
|
275
|
+
B2BRoles: () => B2BRoles,
|
|
274
276
|
B2CGuard: () => B2CGuard,
|
|
277
|
+
B2CRoles: () => B2CRoles,
|
|
275
278
|
BearerTokenGuard: () => BearerTokenGuard,
|
|
279
|
+
CallerToken: () => CallerToken,
|
|
276
280
|
KEYCLOAK_CLIENT: () => KEYCLOAK_CLIENT,
|
|
277
281
|
KEYCLOAK_CONFIG: () => KEYCLOAK_CONFIG,
|
|
278
282
|
KEYCLOAK_HTTP_INTERCEPTOR: () => KEYCLOAK_HTTP_INTERCEPTOR,
|
|
@@ -280,7 +284,8 @@ __export(index_exports, {
|
|
|
280
284
|
KeycloakError: () => KeycloakError,
|
|
281
285
|
KeycloakModule: () => KeycloakModule,
|
|
282
286
|
Roles: () => Roles,
|
|
283
|
-
RolesGuard: () => RolesGuard
|
|
287
|
+
RolesGuard: () => RolesGuard,
|
|
288
|
+
TokenRoles: () => TokenRoles
|
|
284
289
|
});
|
|
285
290
|
module.exports = __toCommonJS(index_exports);
|
|
286
291
|
|
|
@@ -306,7 +311,7 @@ var KEYCLOAK_PROVIDER = "KEYCLOAK_PROVIDER";
|
|
|
306
311
|
// package.json
|
|
307
312
|
var package_default = {
|
|
308
313
|
name: "@adatechnology/auth-keycloak",
|
|
309
|
-
version: "0.1.
|
|
314
|
+
version: "0.1.2",
|
|
310
315
|
publishConfig: {
|
|
311
316
|
access: "public"
|
|
312
317
|
},
|
|
@@ -757,7 +762,19 @@ var import_core = require("@nestjs/core");
|
|
|
757
762
|
// src/roles.decorator.ts
|
|
758
763
|
var import_common4 = require("@nestjs/common");
|
|
759
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";
|
|
760
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) {
|
|
761
778
|
let payload;
|
|
762
779
|
if (args.length === 1 && typeof args[0] === "object" && !Array.isArray(args[0])) {
|
|
763
780
|
payload = args[0];
|
|
@@ -769,76 +786,161 @@ function Roles(...args) {
|
|
|
769
786
|
}
|
|
770
787
|
payload.mode = payload.mode ?? "any";
|
|
771
788
|
payload.type = payload.type ?? "both";
|
|
772
|
-
return
|
|
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]);
|
|
773
800
|
}
|
|
774
801
|
|
|
775
802
|
// src/roles.guard.ts
|
|
776
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
|
|
777
850
|
var RolesGuard = class {
|
|
778
851
|
constructor(reflector, config) {
|
|
779
852
|
this.reflector = reflector;
|
|
780
853
|
this.config = config;
|
|
781
854
|
}
|
|
782
855
|
canActivate(context) {
|
|
783
|
-
var _a, _b, _c, _d, _e, _f
|
|
784
|
-
const meta = this.reflector.get(ROLES_META_KEY, context.getHandler()) || this.reflector.get(ROLES_META_KEY, context.getClass());
|
|
785
|
-
if (!meta || !meta.roles || meta.roles.length === 0) return true;
|
|
856
|
+
var _a, _b, _c, _d, _e, _f;
|
|
786
857
|
const req = context.switchToHttp().getRequest();
|
|
787
|
-
const
|
|
788
|
-
const
|
|
789
|
-
const
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
const
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
const
|
|
808
|
-
if (
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
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)");
|
|
870
|
+
}
|
|
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)");
|
|
876
|
+
}
|
|
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
|
+
});
|
|
892
|
+
}
|
|
893
|
+
const roles = this.extractRoles(token, "b2b");
|
|
894
|
+
this.assertRoles(roles, genericMeta, "B2B (service)");
|
|
812
895
|
}
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
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}`);
|
|
902
|
+
}
|
|
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));
|
|
819
920
|
}
|
|
820
921
|
}
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
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) {
|
|
824
928
|
throw new import_shared2.BaseAppError({
|
|
825
|
-
message:
|
|
929
|
+
message: `Insufficient roles for ${label} token`,
|
|
826
930
|
status: HTTP_STATUS.FORBIDDEN,
|
|
827
931
|
code: ROLES_ERROR_CODE.INSUFFICIENT_ROLES,
|
|
828
|
-
context: { required }
|
|
932
|
+
context: { required: meta.roles, source: label }
|
|
829
933
|
});
|
|
830
934
|
}
|
|
831
|
-
return true;
|
|
832
935
|
}
|
|
833
936
|
decodeJwtPayload(token) {
|
|
834
937
|
try {
|
|
835
938
|
const parts = token.split(".");
|
|
836
939
|
if (parts.length < 2) return {};
|
|
837
|
-
const
|
|
940
|
+
const padded = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
838
941
|
const BufferCtor = globalThis.Buffer;
|
|
839
942
|
if (!BufferCtor) return {};
|
|
840
|
-
|
|
841
|
-
return JSON.parse(decoded);
|
|
943
|
+
return JSON.parse(BufferCtor.from(padded, "base64").toString("utf8"));
|
|
842
944
|
} catch {
|
|
843
945
|
return {};
|
|
844
946
|
}
|
|
@@ -854,6 +956,8 @@ RolesGuard = __decorateClass([
|
|
|
854
956
|
// src/keycloak.module.ts
|
|
855
957
|
var KeycloakModule = class {
|
|
856
958
|
static forRoot(config, httpConfig) {
|
|
959
|
+
if (config.headers) configureTokenHeaders(config.headers);
|
|
960
|
+
if (config.claims) configureTokenClaims(config.claims);
|
|
857
961
|
return {
|
|
858
962
|
module: KeycloakModule,
|
|
859
963
|
global: true,
|
|
@@ -929,12 +1033,12 @@ var import_common8 = require("@nestjs/common");
|
|
|
929
1033
|
var import_shared3 = __toESM(require_dist());
|
|
930
1034
|
var B2CGuard = class {
|
|
931
1035
|
canActivate(context) {
|
|
932
|
-
var _a
|
|
1036
|
+
var _a;
|
|
933
1037
|
const request = context.switchToHttp().getRequest();
|
|
934
|
-
const
|
|
935
|
-
if (
|
|
1038
|
+
const accessToken = (_a = request.headers) == null ? void 0 : _a[getB2CTokenHeader()];
|
|
1039
|
+
if (accessToken) return true;
|
|
936
1040
|
throw new import_shared3.BaseAppError({
|
|
937
|
-
message: "Missing
|
|
1041
|
+
message: "Missing X-Access-Token header. Route requires Kong-forwarded user authentication.",
|
|
938
1042
|
status: HTTP_STATUS.UNAUTHORIZED,
|
|
939
1043
|
code: BEARER_ERROR_CODE.MISSING_TOKEN,
|
|
940
1044
|
context: {}
|
|
@@ -954,18 +1058,18 @@ var ApiAuthGuard = class {
|
|
|
954
1058
|
this.b2cGuard = b2cGuard;
|
|
955
1059
|
}
|
|
956
1060
|
async canActivate(context) {
|
|
957
|
-
var _a, _b
|
|
1061
|
+
var _a, _b;
|
|
958
1062
|
const request = context.switchToHttp().getRequest();
|
|
959
|
-
const
|
|
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()];
|
|
960
1068
|
if (authHeader == null ? void 0 : authHeader.toLowerCase().startsWith("bearer ")) {
|
|
961
1069
|
return this.b2bGuard.canActivate(context);
|
|
962
1070
|
}
|
|
963
|
-
const userId = ((_c = request.headers) == null ? void 0 : _c["x-user-id"]) ?? ((_d = request.headers) == null ? void 0 : _d["X-User-Id"]);
|
|
964
|
-
if (userId) {
|
|
965
|
-
return this.b2cGuard.canActivate(context);
|
|
966
|
-
}
|
|
967
1071
|
throw new import_shared4.BaseAppError({
|
|
968
|
-
message: "Unauthorized: missing
|
|
1072
|
+
message: "Unauthorized: missing X-Access-Token (Kong/B2C) or Authorization header (B2B)",
|
|
969
1073
|
status: HTTP_STATUS.UNAUTHORIZED,
|
|
970
1074
|
code: BEARER_ERROR_CODE.MISSING_TOKEN,
|
|
971
1075
|
context: {}
|
|
@@ -979,20 +1083,102 @@ ApiAuthGuard = __decorateClass([
|
|
|
979
1083
|
// src/auth-user.decorator.ts
|
|
980
1084
|
var import_common10 = require("@nestjs/common");
|
|
981
1085
|
var AuthUser = (0, import_common10.createParamDecorator)(
|
|
982
|
-
(
|
|
983
|
-
var _a
|
|
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;
|
|
984
1110
|
const request = ctx.switchToHttp().getRequest();
|
|
985
|
-
const
|
|
1111
|
+
const h = (header == null ? void 0 : header.toLowerCase()) ?? getB2CTokenHeader();
|
|
1112
|
+
const raw = (_a = request.headers) == null ? void 0 : _a[h];
|
|
986
1113
|
return Array.isArray(raw) ? raw[0] : raw ?? "";
|
|
987
1114
|
}
|
|
988
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
|
+
}
|
|
989
1171
|
// Annotate the CommonJS export names for ESM import in node:
|
|
990
1172
|
0 && (module.exports = {
|
|
1173
|
+
AccessToken,
|
|
991
1174
|
ApiAuthGuard,
|
|
992
1175
|
AuthUser,
|
|
993
1176
|
B2BGuard,
|
|
1177
|
+
B2BRoles,
|
|
994
1178
|
B2CGuard,
|
|
1179
|
+
B2CRoles,
|
|
995
1180
|
BearerTokenGuard,
|
|
1181
|
+
CallerToken,
|
|
996
1182
|
KEYCLOAK_CLIENT,
|
|
997
1183
|
KEYCLOAK_CONFIG,
|
|
998
1184
|
KEYCLOAK_HTTP_INTERCEPTOR,
|
|
@@ -1000,5 +1186,6 @@ var AuthUser = (0, import_common10.createParamDecorator)(
|
|
|
1000
1186
|
KeycloakError,
|
|
1001
1187
|
KeycloakModule,
|
|
1002
1188
|
Roles,
|
|
1003
|
-
RolesGuard
|
|
1189
|
+
RolesGuard,
|
|
1190
|
+
TokenRoles
|
|
1004
1191
|
});
|