@globaltracking/auth-middleware 2.0.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +421 -372
  2. package/dist/config.d.ts.map +1 -1
  3. package/dist/config.js +7 -1
  4. package/dist/config.js.map +1 -1
  5. package/dist/extract-user.d.ts +6 -1
  6. package/dist/extract-user.d.ts.map +1 -1
  7. package/dist/extract-user.js +39 -2
  8. package/dist/extract-user.js.map +1 -1
  9. package/dist/index.d.ts +1 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +3 -1
  12. package/dist/index.js.map +1 -1
  13. package/dist/middlewares/require-permission.d.ts +2 -2
  14. package/dist/middlewares/require-permission.d.ts.map +1 -1
  15. package/dist/middlewares/require-permission.js +23 -8
  16. package/dist/middlewares/require-permission.js.map +1 -1
  17. package/dist/middlewares/require-role.d.ts +4 -2
  18. package/dist/middlewares/require-role.d.ts.map +1 -1
  19. package/dist/middlewares/require-role.js +12 -4
  20. package/dist/middlewares/require-role.js.map +1 -1
  21. package/dist/middlewares/require-self.d.ts +1 -1
  22. package/dist/middlewares/require-self.d.ts.map +1 -1
  23. package/dist/middlewares/require-self.js +4 -3
  24. package/dist/middlewares/require-self.js.map +1 -1
  25. package/dist/nestjs/gt-auth.module.d.ts +22 -0
  26. package/dist/nestjs/gt-auth.module.d.ts.map +1 -1
  27. package/dist/nestjs/gt-auth.module.js +39 -28
  28. package/dist/nestjs/gt-auth.module.js.map +1 -1
  29. package/dist/nestjs/guards/permissions.guard.d.ts +5 -1
  30. package/dist/nestjs/guards/permissions.guard.d.ts.map +1 -1
  31. package/dist/nestjs/guards/permissions.guard.js +12 -3
  32. package/dist/nestjs/guards/permissions.guard.js.map +1 -1
  33. package/dist/strategies/gateway-header.strategy.d.ts.map +1 -1
  34. package/dist/strategies/gateway-header.strategy.js +8 -1
  35. package/dist/strategies/gateway-header.strategy.js.map +1 -1
  36. package/dist/strategies/jwt.strategy.d.ts.map +1 -1
  37. package/dist/strategies/jwt.strategy.js +8 -1
  38. package/dist/strategies/jwt.strategy.js.map +1 -1
  39. package/dist/strategies/trusted-headers.strategy.d.ts.map +1 -1
  40. package/dist/strategies/trusted-headers.strategy.js +16 -1
  41. package/dist/strategies/trusted-headers.strategy.js.map +1 -1
  42. package/dist/types.d.ts +45 -2
  43. package/dist/types.d.ts.map +1 -1
  44. package/dist/utils/helpers.d.ts +14 -1
  45. package/dist/utils/helpers.d.ts.map +1 -1
  46. package/dist/utils/helpers.js +22 -2
  47. package/dist/utils/helpers.js.map +1 -1
  48. package/dist/utils/jwt.js +8 -1
  49. package/dist/utils/jwt.js.map +1 -1
  50. package/package.json +82 -82
package/README.md CHANGED
@@ -1,372 +1,421 @@
1
- # @globaltracking/auth-middleware
2
-
3
- Unified authentication and authorization middleware for the **Global Tracking** platform. Shared by all backend microservices — supports both **Express** and **NestJS**.
4
-
5
- ## Features
6
-
7
- - **Strategy pattern**: Gateway header, trusted headers, JWT — configurable per service
8
- - **NestJS adapter**: `GtAuthModule`, guards, decorators, interceptors, exception filter
9
- - **Express middleware**: `authenticate`, `requirePermission`, `requireRole`, `requireSelf`, `requireTenant`
10
- - **Hybrid permission resolution**: JWT claims (fast) → custom resolver → RBAC HTTP call → deny (fail-closed)
11
- - **Multi-tenant RLS**: `OrgContextInterceptor` sets `SET LOCAL app.current_org_id` for PostgreSQL Row-Level Security
12
- - **Admin bypass**: Configurable `adminRoles[]` bypass all permission checks
13
- - **TypeScript-first**: Full type definitions with Express `Request` augmentation
14
- - **98%+ test coverage**: 89 tests across strategies, middlewares, and config
15
-
16
- ## Installation
17
-
18
- ```bash
19
- npm install @globaltracking/auth-middleware
20
- ```
21
-
22
- **Peer dependencies:**
23
-
24
- | Package | Required for | Optional? |
25
- |---------|-------------|-----------|
26
- | `express` ^4.18 | Express middleware | Required |
27
- | `@nestjs/common` ^11.0 | NestJS adapter | Optional |
28
- | `@nestjs/core` ^11.0 | NestJS adapter | Optional |
29
- | `typeorm` ^0.3.0 | OrgContextInterceptor (RLS) | Optional |
30
- | `rxjs` ^7.0 | OrgContextInterceptor | Optional |
31
-
32
- ---
33
-
34
- ## NestJS Integration (Recommended)
35
-
36
- This is the primary integration path for Global Tracking microservices.
37
-
38
- ### 1. Import GtAuthModule in your AppModule
39
-
40
- ```typescript
41
- import { Module } from '@nestjs/common';
42
- import { ConfigService } from '@nestjs/config';
43
- import { GtAuthModule } from '@globaltracking/auth-middleware/nestjs';
44
-
45
- @Module({
46
- imports: [
47
- // ... ConfigModule, TypeOrmModule, ThrottlerModule ...
48
-
49
- GtAuthModule.forRootAsync({
50
- inject: [ConfigService],
51
- useFactory: (config: ConfigService) => ({
52
- strategies: ['trusted-headers'],
53
- internalGatewayToken: config.get('INTERNAL_GATEWAY_TOKEN'),
54
- adminRoles: ['system_admin', 'org_admin'],
55
- rbacServiceUrl: config.get('RBAC_SERVICE_URL'),
56
- }),
57
- }),
58
-
59
- // ... domain modules ...
60
- ],
61
- })
62
- export class AppModule {}
63
- ```
64
-
65
- **What `GtAuthModule` provides (globally):**
66
-
67
- | Component | What it does |
68
- |-----------|-------------|
69
- | `GtTrustedHeadersMiddleware` | Extracts `req.user` from configured strategy chain (auto-applied to all routes) |
70
- | `InternalOnlyGuard` | Validates `X-Gateway-Token` header — register as `APP_GUARD` |
71
- | `GtPermissionsGuard` | Checks `@RequirePermissions()` — register as `APP_GUARD` |
72
- | `OrgContextInterceptor` | Sets PostgreSQL RLS context — register as `APP_INTERCEPTOR` |
73
- | `AuthExceptionFilter` | Catches `AuthError` and returns standard error envelope |
74
-
75
- ### 2. Use decorators in controllers
76
-
77
- ```typescript
78
- import {
79
- RequirePermissions,
80
- CurrentUser,
81
- CurrentOrg,
82
- Public,
83
- RequireRoles,
84
- } from '@globaltracking/auth-middleware/nestjs';
85
-
86
- @Controller('vehicles')
87
- export class VehiclesController {
88
- @Post()
89
- @RequirePermissions('vehicles:create')
90
- create(
91
- @CurrentOrg() orgId: string,
92
- @CurrentUser('userId') userId: string,
93
- @Body() dto: CreateVehicleDto,
94
- ) {
95
- // orgId and userId extracted from trusted headers
96
- }
97
-
98
- @Get(':id')
99
- @RequirePermissions('vehicles:read')
100
- findOne(
101
- @CurrentOrg() orgId: string,
102
- @Param('id', ParseUUIDPipe) id: string,
103
- ) { ... }
104
-
105
- @Delete(':id')
106
- @RequireRoles('system_admin')
107
- remove(@Param('id', ParseUUIDPipe) id: string) { ... }
108
- }
109
- ```
110
-
111
- ```typescript
112
- // Health endpoints skip auth
113
- @Controller('health')
114
- export class HealthController {
115
- @Get()
116
- @Public()
117
- liveness() {
118
- return { status: 'ok' };
119
- }
120
- }
121
- ```
122
-
123
- ### 3. Register guards and interceptors
124
-
125
- ```typescript
126
- // app.module.ts providers
127
- import {
128
- InternalOnlyGuard,
129
- GtPermissionsGuard,
130
- OrgContextInterceptor,
131
- } from '@globaltracking/auth-middleware/nestjs';
132
-
133
- providers: [
134
- { provide: APP_GUARD, useClass: InternalOnlyGuard },
135
- { provide: APP_GUARD, useClass: GtPermissionsGuard },
136
- { provide: APP_GUARD, useClass: ThrottlerGuard },
137
- { provide: APP_INTERCEPTOR, useClass: OrgContextInterceptor },
138
- // ... your other interceptors
139
- ],
140
- ```
141
-
142
- ### 4. Add env vars
143
-
144
- ```env
145
- INTERNAL_GATEWAY_TOKEN=your-32-char-min-secret
146
- RBAC_SERVICE_URL=http://gt-rbac-service:3000 # optional, for permission resolution
147
- ```
148
-
149
- ### Special case: gt-rbac-service
150
-
151
- The RBAC service **cannot call itself** for permission resolution. Instead, inject its own `ResolveService` via `permissionResolver`:
152
-
153
- ```typescript
154
- GtAuthModule.forRootAsync({
155
- imports: [ResolveModule],
156
- inject: [ConfigService, ResolveService],
157
- useFactory: (config: ConfigService, resolveService: ResolveService) => ({
158
- strategies: ['trusted-headers'],
159
- internalGatewayToken: config.get('INTERNAL_GATEWAY_TOKEN'),
160
- adminRoles: ['system_admin', 'org_admin'],
161
- permissionResolver: (orgId, userId, resource, action) =>
162
- resolveService.checkPermission(orgId, userId, resource, action),
163
- }),
164
- }),
165
- ```
166
-
167
- ---
168
-
169
- ## Express Integration
170
-
171
- For services that use plain Express (no NestJS):
172
-
173
- ```typescript
174
- import express from 'express';
175
- import {
176
- initAuth,
177
- authenticate,
178
- requirePermission,
179
- requireTenant,
180
- authErrorHandler,
181
- } from '@globaltracking/auth-middleware';
182
-
183
- const app = express();
184
-
185
- // Initialize once at startup
186
- initAuth({
187
- strategies: ['gateway-header', 'jwt'],
188
- adminRoles: ['system_admin', 'org_admin'],
189
- publicKeyPath: './keys/public.pem',
190
- });
191
-
192
- // Protected route
193
- app.get('/v1/vehicles',
194
- authenticate,
195
- requireTenant,
196
- requirePermission('vehicles:read'),
197
- async (req, res) => {
198
- // req.user.userId, req.user.orgId, etc.
199
- // req.tenantId set by requireTenant
200
- }
201
- );
202
-
203
- // Error handler (must be last)
204
- app.use(authErrorHandler);
205
- ```
206
-
207
- ---
208
-
209
- ## Configuration
210
-
211
- ```typescript
212
- initAuth({
213
- // Strategy chain — tried in order until one matches
214
- strategies: ['gateway-header', 'jwt'], // default
215
-
216
- // Gateway header name (GCP API Gateway)
217
- gatewayHeaderName: 'x-apigateway-api-userinfo', // default
218
-
219
- // JWT verification (for local dev / non-gateway)
220
- jwtIssuer: 'globaltracking-auth', // default
221
- publicKey: '-----BEGIN PUBLIC KEY-----\n...', // PEM string
222
- publicKeyPath: './keys/public.pem', // or file path
223
-
224
- // Roles that bypass all permission checks
225
- adminRoles: ['system_admin', 'org_admin'], // default
226
-
227
- // Trusted headers strategy config
228
- internalGatewayToken: 'secret', // also reads env INTERNAL_GATEWAY_TOKEN
229
- trustedHeaderNames: {
230
- userId: 'x-user-id', // default
231
- orgId: 'x-org-id', // default
232
- userRole: 'x-user-role', // default
233
- requestId: 'x-request-id', // default
234
- gatewayToken: 'x-gateway-token', // default
235
- },
236
-
237
- // Permission resolution (NestJS GtPermissionsGuard)
238
- rbacServiceUrl: 'http://gt-rbac-service:3000', // HTTP fallback
239
- permissionResolver: async (orgId, userId, resource, action) => {
240
- // Custom resolver (e.g., direct DB call)
241
- return true;
242
- },
243
- });
244
- ```
245
-
246
- Public key resolution order: `publicKey` → `publicKeyPath` → `AUTH_PUBLIC_KEY` env → `AUTH_PUBLIC_KEY_PATH` env.
247
-
248
- ---
249
-
250
- ## Auth Strategies
251
-
252
- | Strategy | When used | What it reads |
253
- |----------|-----------|---------------|
254
- | `gateway-header` | Production (GCP API Gateway) | Base64-encoded JSON in `X-Apigateway-Api-Userinfo` |
255
- | `trusted-headers` | NestJS services behind API Gateway | `X-User-Id`, `X-Org-Id`, `X-User-Role`, `X-Request-Id`, `X-Gateway-Token` |
256
- | `jwt` | Local dev, non-gateway environments | `Authorization: Bearer <token>` (RS256 verification) |
257
-
258
- The strategy chain iterates in the configured order. The first strategy whose `canHandle(req)` returns true is used.
259
-
260
- ---
261
-
262
- ## AuthUser Shape
263
-
264
- Every authenticated request gets `req.user` populated with:
265
-
266
- ```typescript
267
- interface AuthUser {
268
- userId: string; // from JWT sub or X-User-Id
269
- email: string; // from JWT email (empty in trusted-headers mode)
270
- role: UserRole; // from JWT role or X-User-Role
271
- orgId: string; // from JWT org_id or X-Org-Id
272
- tenantId: string; // = orgId in trusted-headers mode
273
- permissions: string[]; // from JWT claims (empty in trusted-headers mode)
274
- requestId: string; // from X-Request-Id
275
- authSource: AuthStrategy; // 'gateway-header' | 'trusted-headers' | 'jwt'
276
- }
277
- ```
278
-
279
- ---
280
-
281
- ## Permission Resolution (GtPermissionsGuard)
282
-
283
- The NestJS `GtPermissionsGuard` resolves permissions in 3 tiers:
284
-
285
- 1. **JWT claims** If `user.permissions` is non-empty, check in-memory (fast path)
286
- 2. **permissionResolver** If configured, call the function directly (for RBAC service)
287
- 3. **RBAC HTTP call** If `rbacServiceUrl` is configured, POST to `/v1/resolve/check`
288
- 4. **Deny** Fail-closed if no resolution mechanism is available
289
-
290
- Admin roles (`config.adminRoles`) always bypass all checks.
291
-
292
- ---
293
-
294
- ## Express Middlewares
295
-
296
- | Middleware | Purpose |
297
- |-----------|---------|
298
- | `authenticate` | Extracts `req.user` from strategy chain |
299
- | `requireRole(...roles)` | Checks `req.user.role` against allowed roles |
300
- | `requirePermission(...perms)` | Requires ALL listed permissions (admin bypass) |
301
- | `requireAnyPermission(...perms)` | Requires AT LEAST ONE permission (admin bypass) |
302
- | `requireTenant` | Ensures `req.user.tenantId` exists, sets `req.tenantId` |
303
- | `requireSelf(paramName?)` | Compares `req.user.userId` with route param (admin bypass) |
304
- | `authErrorHandler` | Error handler for `AuthError` subclasses |
305
-
306
- ---
307
-
308
- ## Error Handling
309
-
310
- All auth errors extend `AuthError` and produce the standard Global Tracking error envelope:
311
-
312
- ```typescript
313
- import { UnauthorizedError, ForbiddenError } from '@globaltracking/auth-middleware';
314
-
315
- throw new UnauthorizedError('Token expired');
316
- // 401 { success: false, error: { code: 'UNAUTHORIZED', message: 'Token expired', statusCode: 401 } }
317
-
318
- throw new ForbiddenError('Insufficient permissions', { required: ['vehicles:write'] });
319
- // 403 { success: false, error: { code: 'FORBIDDEN', message: 'Insufficient permissions', statusCode: 403 } }
320
- ```
321
-
322
- ---
323
-
324
- ## Utility Functions
325
-
326
- ```typescript
327
- import { extractUser, hasRole, hasPermission, hasAnyPermission } from '@globaltracking/auth-middleware';
328
-
329
- const user = extractUser(req); // throws UnauthorizedError if missing
330
- hasRole(user, 'system_admin'); // boolean
331
- hasPermission(user, 'vehicles:read'); // all must match
332
- hasAnyPermission(user, 'a:read', 'b:read'); // at least one
333
- ```
334
-
335
- ---
336
-
337
- ## Files Replaced Per Service
338
-
339
- When a NestJS service adopts this package, these ~10 files can be deleted:
340
-
341
- ```
342
- src/common/decorators/current-org.decorator.ts → @CurrentOrg from package
343
- src/common/decorators/current-user.decorator.ts → @CurrentUser from package
344
- src/common/decorators/permissions.decorator.ts → @RequirePermissions from package
345
- src/common/decorators/public.decorator.ts → @Public from package
346
- src/common/guards/internal-only.guard.ts InternalOnlyGuard from package
347
- src/common/guards/permissions.guard.ts → GtPermissionsGuard from package
348
- src/common/interceptors/org-context.interceptor.ts → OrgContextInterceptor from package
349
- src/common/middleware/trusted-headers.middleware.ts → GtTrustedHeadersMiddleware from package
350
- src/common/interfaces/jwt-payload.interface.ts → AuthUser from package
351
- src/common/interfaces/request-context.interface.ts → (Express augmentation from package)
352
- ```
353
-
354
- ---
355
-
356
- ## Development
357
-
358
- ```bash
359
- npm test # run tests with coverage (89 tests, 98%+ coverage)
360
- npm run test:watch # watch mode
361
- npm run build # compile to dist/
362
- npm run clean # remove dist/
363
- ```
364
-
365
- ---
366
-
367
- ## Migration from v1.x
368
-
369
- - `superAdminRole` → use `adminRoles: ['system_admin', 'org_admin']` (legacy `superAdminRole` still works)
370
- - `RequestUser` type alias still exported for backward compatibility
371
- - Error handler now returns `{ success: false, error: { code, message, statusCode } }` envelope instead of `{ error: message }`
372
- - `extractUser()` now sets `authSource` and `requestId` on returned user
1
+ # @globaltracking/auth-middleware
2
+
3
+ Unified authentication and authorization middleware for the **Global Tracking** platform. Shared by all backend microservices — supports both **Express** and **NestJS**.
4
+
5
+ ## Features
6
+
7
+ - **Strategy pattern**: Gateway header, trusted headers, JWT — configurable per service
8
+ - **NestJS adapter**: `GtAuthModule`, guards, decorators, interceptors, exception filter
9
+ - **Express middleware**: `authenticate`, `requirePermission`, `requireRole`, `requireSelf`, `requireTenant`
10
+ - **Hybrid permission resolution**: JWT claims (fast) → custom resolver → RBAC HTTP call → deny (fail-closed)
11
+ - **Multi-tenant RLS**: `OrgContextInterceptor` sets `SET LOCAL app.current_org_id` for PostgreSQL Row-Level Security
12
+ - **Admin bypass**: Configurable `adminRoles[]` bypass all permission checks
13
+ - **TypeScript-first**: Full type definitions with Express `Request` augmentation
14
+ - **98%+ test coverage**: 89 tests across strategies, middlewares, and config
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install @globaltracking/auth-middleware
20
+ ```
21
+
22
+ **Peer dependencies:**
23
+
24
+ | Package | Required for | Optional? |
25
+ |---------|-------------|-----------|
26
+ | `express` ^4.18 | Express middleware | Required |
27
+ | `@nestjs/common` ^11.0 | NestJS adapter | Optional |
28
+ | `@nestjs/core` ^11.0 | NestJS adapter | Optional |
29
+ | `typeorm` ^0.3.0 | OrgContextInterceptor (RLS) | Optional |
30
+ | `rxjs` ^7.0 | OrgContextInterceptor | Optional |
31
+
32
+ ---
33
+
34
+ ## NestJS Integration (Recommended)
35
+
36
+ This is the primary integration path for Global Tracking microservices.
37
+
38
+ ### 1. Import GtAuthModule in your AppModule
39
+
40
+ ```typescript
41
+ import { Module } from '@nestjs/common';
42
+ import { ConfigService } from '@nestjs/config';
43
+ import { GtAuthModule } from '@globaltracking/auth-middleware/nestjs';
44
+
45
+ @Module({
46
+ imports: [
47
+ // ... ConfigModule, TypeOrmModule, ThrottlerModule ...
48
+
49
+ GtAuthModule.forRootAsync({
50
+ inject: [ConfigService],
51
+ useFactory: (config: ConfigService) => ({
52
+ strategies: ['trusted-headers'],
53
+ internalGatewayToken: config.get('INTERNAL_GATEWAY_TOKEN'),
54
+ adminRoles: ['system_admin', 'org_admin'],
55
+ rbacServiceUrl: config.get('RBAC_SERVICE_URL'),
56
+ }),
57
+ }),
58
+
59
+ // ... domain modules ...
60
+ ],
61
+ })
62
+ export class AppModule {}
63
+ ```
64
+
65
+ **What `GtAuthModule` provides (globally):**
66
+
67
+ | Component | What it does |
68
+ |-----------|-------------|
69
+ | `GtTrustedHeadersMiddleware` | Extracts `req.user` from configured strategy chain (auto-applied to all routes) |
70
+ | `InternalOnlyGuard` | Validates `X-Gateway-Token` header — attach per-route with `@UseGuards()` or register as `APP_GUARD` |
71
+ | `GtPermissionsGuard` | Checks `@RequirePermissions()` — **auto-registered as `APP_GUARD` since v3** (opt out with `registerGuardGlobally: false`) |
72
+ | `OrgContextInterceptor` | Sets PostgreSQL RLS context — register as `APP_INTERCEPTOR` |
73
+ | `AuthExceptionFilter` | Catches `AuthError` and returns standard error envelope |
74
+
75
+ ### 2. Use decorators in controllers
76
+
77
+ ```typescript
78
+ import {
79
+ RequirePermissions,
80
+ CurrentUser,
81
+ CurrentOrg,
82
+ Public,
83
+ RequireRoles,
84
+ } from '@globaltracking/auth-middleware/nestjs';
85
+
86
+ @Controller('vehicles')
87
+ export class VehiclesController {
88
+ @Post()
89
+ @RequirePermissions('vehicles:create')
90
+ create(
91
+ @CurrentOrg() orgId: string,
92
+ @CurrentUser('userId') userId: string,
93
+ @Body() dto: CreateVehicleDto,
94
+ ) {
95
+ // orgId and userId extracted from trusted headers
96
+ }
97
+
98
+ @Get(':id')
99
+ @RequirePermissions('vehicles:read')
100
+ findOne(
101
+ @CurrentOrg() orgId: string,
102
+ @Param('id', ParseUUIDPipe) id: string,
103
+ ) { ... }
104
+
105
+ @Delete(':id')
106
+ @RequireRoles('system_admin')
107
+ remove(@Param('id', ParseUUIDPipe) id: string) { ... }
108
+ }
109
+ ```
110
+
111
+ ```typescript
112
+ // Health endpoints skip auth
113
+ @Controller('health')
114
+ export class HealthController {
115
+ @Get()
116
+ @Public()
117
+ liveness() {
118
+ return { status: 'ok' };
119
+ }
120
+ }
121
+ ```
122
+
123
+ ### 3. Register guards and interceptors
124
+
125
+ Since v3, `GtPermissionsGuard` is auto-registered as an `APP_GUARD` by
126
+ `GtAuthModule.forRoot()` / `forRootAsync()`. You only need to wire the
127
+ interceptor and any service-specific guards:
128
+
129
+ ```typescript
130
+ // app.module.ts providers
131
+ import {
132
+ OrgContextInterceptor,
133
+ } from '@globaltracking/auth-middleware/nestjs';
134
+
135
+ providers: [
136
+ { provide: APP_GUARD, useClass: ThrottlerGuard },
137
+ { provide: APP_INTERCEPTOR, useClass: OrgContextInterceptor },
138
+ // ... your other interceptors
139
+ ],
140
+ ```
141
+
142
+ **Opt-out:** pass `registerGuardGlobally: false` if your service composes its
143
+ own guard chain and wants to attach `GtPermissionsGuard` manually:
144
+
145
+ ```typescript
146
+ GtAuthModule.forRootAsync({
147
+ registerGuardGlobally: false,
148
+ inject: [ConfigService],
149
+ useFactory: (c: ConfigService) => ({ ... }),
150
+ })
151
+ // then in providers:
152
+ { provide: APP_GUARD, useClass: MyCompositeGuard }, // wraps Gt checks itself
153
+ ```
154
+
155
+ **`InternalOnlyGuard`** is not auto-registered — attach it per-route via
156
+ `@UseGuards(InternalOnlyGuard)` on internal-only endpoints, or register it
157
+ globally if *every* route in the service is internal-only.
158
+
159
+ ### 4. Add env vars
160
+
161
+ ```env
162
+ INTERNAL_GATEWAY_TOKEN=your-32-char-min-secret
163
+ RBAC_SERVICE_URL=http://gt-rbac-service:3000 # optional, for permission resolution
164
+ ```
165
+
166
+ ### Special case: gt-rbac-service
167
+
168
+ The RBAC service **cannot call itself** for permission resolution. Instead, inject its own `ResolveService` via `permissionResolver`:
169
+
170
+ ```typescript
171
+ GtAuthModule.forRootAsync({
172
+ imports: [ResolveModule],
173
+ inject: [ConfigService, ResolveService],
174
+ useFactory: (config: ConfigService, resolveService: ResolveService) => ({
175
+ strategies: ['trusted-headers'],
176
+ internalGatewayToken: config.get('INTERNAL_GATEWAY_TOKEN'),
177
+ adminRoles: ['system_admin', 'org_admin'],
178
+ permissionResolver: (orgId, userId, resource, action) =>
179
+ resolveService.checkPermission(orgId, userId, resource, action),
180
+ }),
181
+ }),
182
+ ```
183
+
184
+ ---
185
+
186
+ ## Express Integration
187
+
188
+ For services that use plain Express (no NestJS):
189
+
190
+ ```typescript
191
+ import express from 'express';
192
+ import {
193
+ initAuth,
194
+ authenticate,
195
+ requirePermission,
196
+ requireTenant,
197
+ authErrorHandler,
198
+ } from '@globaltracking/auth-middleware';
199
+
200
+ const app = express();
201
+
202
+ // Initialize once at startup
203
+ initAuth({
204
+ strategies: ['gateway-header', 'jwt'],
205
+ adminRoles: ['system_admin', 'org_admin'],
206
+ publicKeyPath: './keys/public.pem',
207
+ });
208
+
209
+ // Protected route
210
+ app.get('/v1/vehicles',
211
+ authenticate,
212
+ requireTenant,
213
+ requirePermission('vehicles:read'),
214
+ async (req, res) => {
215
+ // req.user.userId, req.user.orgId, etc.
216
+ // req.tenantId set by requireTenant
217
+ }
218
+ );
219
+
220
+ // Error handler (must be last)
221
+ app.use(authErrorHandler);
222
+ ```
223
+
224
+ ---
225
+
226
+ ## Configuration
227
+
228
+ ```typescript
229
+ initAuth({
230
+ // Strategy chain — tried in order until one matches
231
+ strategies: ['gateway-header', 'jwt'], // default
232
+
233
+ // Gateway header name (GCP API Gateway)
234
+ gatewayHeaderName: 'x-apigateway-api-userinfo', // default
235
+
236
+ // JWT verification (for local dev / non-gateway)
237
+ jwtIssuer: 'globaltracking-auth', // default
238
+ publicKey: '-----BEGIN PUBLIC KEY-----\n...', // PEM string
239
+ publicKeyPath: './keys/public.pem', // or file path
240
+
241
+ // Roles that bypass all permission checks
242
+ adminRoles: ['system_admin', 'org_admin'], // default
243
+
244
+ // Trusted headers strategy config
245
+ internalGatewayToken: 'secret', // also reads env INTERNAL_GATEWAY_TOKEN
246
+ trustedHeaderNames: {
247
+ userId: 'x-user-id', // default
248
+ orgId: 'x-org-id', // default
249
+ userRole: 'x-user-role', // default
250
+ requestId: 'x-request-id', // default
251
+ gatewayToken: 'x-gateway-token', // default
252
+ },
253
+
254
+ // Permission resolution (NestJS GtPermissionsGuard)
255
+ rbacServiceUrl: 'http://gt-rbac-service:3000', // HTTP fallback
256
+ permissionResolver: async (orgId, userId, resource, action) => {
257
+ // Custom resolver (e.g., direct DB call)
258
+ return true;
259
+ },
260
+ });
261
+ ```
262
+
263
+ Public key resolution order: `publicKey` → `publicKeyPath` → `AUTH_PUBLIC_KEY` env → `AUTH_PUBLIC_KEY_PATH` env.
264
+
265
+ ---
266
+
267
+ ## Auth Strategies
268
+
269
+ | Strategy | When used | What it reads |
270
+ |----------|-----------|---------------|
271
+ | `gateway-header` | Production (GCP API Gateway) | Base64-encoded JSON in `X-Apigateway-Api-Userinfo` |
272
+ | `trusted-headers` | NestJS services behind API Gateway | `X-User-Id`, `X-Org-Id`, `X-User-Role`, `X-Request-Id`, `X-Gateway-Token` |
273
+ | `jwt` | Local dev, non-gateway environments | `Authorization: Bearer <token>` (RS256 verification) |
274
+
275
+ The strategy chain iterates in the configured order. The first strategy whose `canHandle(req)` returns true is used.
276
+
277
+ ---
278
+
279
+ ## AuthUser Shape
280
+
281
+ Every authenticated request gets `req.user` populated with:
282
+
283
+ ```typescript
284
+ interface AuthUser {
285
+ userId: string; // from JWT sub or X-User-Id
286
+ email: string; // from JWT email (empty in trusted-headers mode)
287
+ role: UserRole; // from JWT role or X-User-Role
288
+ orgId: string; // from JWT org_id or X-Org-Id
289
+ tenantId: string; // = orgId in trusted-headers mode
290
+ permissions: string[]; // from JWT claims (empty in trusted-headers mode)
291
+ requestId: string; // from X-Request-Id
292
+ authSource: AuthStrategy; // 'gateway-header' | 'trusted-headers' | 'jwt'
293
+ }
294
+ ```
295
+
296
+ ---
297
+
298
+ ## Permission Resolution (GtPermissionsGuard)
299
+
300
+ The NestJS `GtPermissionsGuard` resolves permissions in 3 tiers:
301
+
302
+ 1. **JWT claims** If `user.permissions` is non-empty, check in-memory (fast path)
303
+ 2. **permissionResolver** If configured, call the function directly (for RBAC service)
304
+ 3. **RBAC HTTP call** If `rbacServiceUrl` is configured, POST to `/v1/resolve/check`
305
+ 4. **Deny** — Fail-closed if no resolution mechanism is available
306
+
307
+ Admin roles (`config.adminRoles`) always bypass all checks.
308
+
309
+ ---
310
+
311
+ ## Express Middlewares
312
+
313
+ | Middleware | Purpose |
314
+ |-----------|---------|
315
+ | `authenticate` | Extracts `req.user` from strategy chain |
316
+ | `requireRole(...roles)` | Checks `req.user.role` against allowed roles |
317
+ | `requirePermission(...perms)` | Requires ALL listed permissions (admin bypass) |
318
+ | `requireAnyPermission(...perms)` | Requires AT LEAST ONE permission (admin bypass) |
319
+ | `requireTenant` | Ensures `req.user.tenantId` exists, sets `req.tenantId` |
320
+ | `requireSelf(paramName?)` | Compares `req.user.userId` with route param (admin bypass) |
321
+ | `authErrorHandler` | Error handler for `AuthError` subclasses |
322
+
323
+ ---
324
+
325
+ ## Error Handling
326
+
327
+ All auth errors extend `AuthError` and produce the standard Global Tracking error envelope:
328
+
329
+ ```typescript
330
+ import { UnauthorizedError, ForbiddenError } from '@globaltracking/auth-middleware';
331
+
332
+ throw new UnauthorizedError('Token expired');
333
+ // → 401 { success: false, error: { code: 'UNAUTHORIZED', message: 'Token expired', statusCode: 401 } }
334
+
335
+ throw new ForbiddenError('Insufficient permissions', { required: ['vehicles:write'] });
336
+ // → 403 { success: false, error: { code: 'FORBIDDEN', message: 'Insufficient permissions', statusCode: 403 } }
337
+ ```
338
+
339
+ ---
340
+
341
+ ## Utility Functions
342
+
343
+ ```typescript
344
+ import { extractUser, hasRole, hasPermission, hasAnyPermission } from '@globaltracking/auth-middleware';
345
+
346
+ const user = extractUser(req); // throws UnauthorizedError if missing
347
+ hasRole(user, 'system_admin'); // boolean
348
+ hasPermission(user, 'vehicles:read'); // all must match
349
+ hasAnyPermission(user, 'a:read', 'b:read'); // at least one
350
+ ```
351
+
352
+ ---
353
+
354
+ ## Files Replaced Per Service
355
+
356
+ When a NestJS service adopts this package, these ~10 files can be deleted:
357
+
358
+ ```
359
+ src/common/decorators/current-org.decorator.ts → @CurrentOrg from package
360
+ src/common/decorators/current-user.decorator.ts → @CurrentUser from package
361
+ src/common/decorators/permissions.decorator.ts @RequirePermissions from package
362
+ src/common/decorators/public.decorator.ts → @Public from package
363
+ src/common/guards/internal-only.guard.ts → InternalOnlyGuard from package
364
+ src/common/guards/permissions.guard.ts → GtPermissionsGuard from package
365
+ src/common/interceptors/org-context.interceptor.ts → OrgContextInterceptor from package
366
+ src/common/middleware/trusted-headers.middleware.ts → GtTrustedHeadersMiddleware from package
367
+ src/common/interfaces/jwt-payload.interface.ts → AuthUser from package
368
+ src/common/interfaces/request-context.interface.ts → (Express augmentation from package)
369
+ ```
370
+
371
+ ---
372
+
373
+ ## Development
374
+
375
+ ```bash
376
+ npm test # run tests with coverage (89 tests, 98%+ coverage)
377
+ npm run test:watch # watch mode
378
+ npm run build # compile to dist/
379
+ npm run clean # remove dist/
380
+ ```
381
+
382
+ ---
383
+
384
+ ## Migration from v2.x → v3.0
385
+
386
+ **Breaking:** `GtAuthModule.forRoot()` / `forRootAsync()` now auto-registers
387
+ `GtPermissionsGuard` as an `APP_GUARD`. In v2.x the guard was `provided` but
388
+ never globally registered, which meant every `@RequirePermissions()`
389
+ decorator was silently inert (routes returned 2xx for users who lacked the
390
+ declared permission — a platform-wide security defect).
391
+
392
+ **What you need to do:**
393
+
394
+ 1. **Remove** any existing `{ provide: APP_GUARD, useClass: GtPermissionsGuard }`
395
+ line from your service's `app.module.ts` — the library now registers it
396
+ for you. Leaving it in place only double-runs the guard (still correct,
397
+ just wasteful).
398
+
399
+ 2. **Audit for endpoints that were accidentally working without the correct
400
+ permission**. Any route missing the right `@RequirePermissions(...)`
401
+ decorator will start returning 403 to non-admin users. Grep for missing
402
+ decorators on controllers that use other guards already; add the
403
+ decorator or annotate with `@Public()` as appropriate.
404
+
405
+ 3. **If your service composes its own guard chain** (e.g. gt-alert-engine's
406
+ custom `GatewayAuthGuard`/`OrgIdGuard` stack) and you do not want the
407
+ automatic registration, pass `registerGuardGlobally: false`:
408
+
409
+ ```typescript
410
+ GtAuthModule.forRootAsync({
411
+ registerGuardGlobally: false,
412
+ ...
413
+ })
414
+ ```
415
+
416
+ ## Migration from v1.x
417
+
418
+ - `superAdminRole` → use `adminRoles: ['system_admin', 'org_admin']` (legacy `superAdminRole` still works)
419
+ - `RequestUser` type alias still exported for backward compatibility
420
+ - Error handler now returns `{ success: false, error: { code, message, statusCode } }` envelope instead of `{ error: message }`
421
+ - `extractUser()` now sets `authSource` and `requestId` on returned user