@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 +136 -45
- package/dist/index.d.ts +315 -8
- package/dist/index.js +322 -45
- 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,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
|
-
*
|
|
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
|
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
784
|
-
const
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
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
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
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 (
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
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
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
940
|
+
const padded = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
829
941
|
const BufferCtor = globalThis.Buffer;
|
|
830
942
|
if (!BufferCtor) return {};
|
|
831
|
-
|
|
832
|
-
|
|
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
|
});
|