@derian-cordoba/api-gateway 1.0.0 → 1.1.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.
- package/README.md +213 -62
- package/dist/src/apps/api-gateway/config/app-env.d.ts +2 -0
- package/dist/src/apps/api-gateway/config/app-env.js +2 -0
- package/dist/src/apps/api-gateway/config/auth/config.d.ts +5 -0
- package/dist/src/apps/api-gateway/config/auth/config.js +8 -0
- package/dist/src/apps/api-gateway/middleware/auth/ApiKeyAuthStrategy.d.ts +9 -0
- package/dist/src/apps/api-gateway/middleware/auth/ApiKeyAuthStrategy.js +20 -0
- package/dist/src/apps/api-gateway/middleware/auth/AuthStrategy.d.ts +4 -0
- package/dist/src/apps/api-gateway/middleware/auth/AuthStrategy.js +2 -0
- package/dist/src/apps/api-gateway/middleware/auth/JwtAuthStrategy.d.ts +11 -0
- package/dist/src/apps/api-gateway/middleware/auth/JwtAuthStrategy.js +55 -0
- package/dist/src/apps/api-gateway/middleware/authMiddleware.d.ts +3 -0
- package/dist/src/apps/api-gateway/middleware/authMiddleware.js +16 -0
- package/dist/src/apps/api-gateway/routes/ProxyManager.js +5 -0
- package/dist/src/apps/api-gateway/routes/RouteValidator.d.ts +12 -0
- package/dist/src/apps/api-gateway/routes/RouteValidator.js +15 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -12,6 +12,9 @@ A generic, configuration-driven HTTP API gateway. Routes incoming requests to up
|
|
|
12
12
|
- [Configuration](#configuration)
|
|
13
13
|
- [Environment Variables](#environment-variables)
|
|
14
14
|
- [Route Configuration](#route-configuration)
|
|
15
|
+
- [Authentication](#authentication)
|
|
16
|
+
- [JWT](#jwt)
|
|
17
|
+
- [API Key](#api-key)
|
|
15
18
|
- [Running the Gateway](#running-the-gateway)
|
|
16
19
|
- [Health Check](#health-check)
|
|
17
20
|
- [Project Structure](#project-structure)
|
|
@@ -23,6 +26,7 @@ A generic, configuration-driven HTTP API gateway. Routes incoming requests to up
|
|
|
23
26
|
## Features
|
|
24
27
|
|
|
25
28
|
- **Configuration-driven routing** — define proxy routes in a JSON file, an environment variable, or both; changes take effect on restart with zero code changes
|
|
29
|
+
- **Per-route authentication** — protect any route with a JWT Bearer token (HMAC or RSA/EC) or an API key; set `enabled: false` to bypass with zero overhead
|
|
26
30
|
- **Per-route rate limiting** — each route can declare its own `max` requests / `windowMs` window, enforced by `express-rate-limit`
|
|
27
31
|
- **Startup validation** — route config is validated with Zod at boot time; the process exits with a descriptive error rather than silently misbehaving
|
|
28
32
|
- **Structured logging** — `pino` + `pino-http` emit newline-delimited JSON in production and human-readable output (via `pino-pretty`) in development
|
|
@@ -98,6 +102,13 @@ Copy `.env.example` to `.env` and edit as needed.
|
|
|
98
102
|
| `ROUTES_FILE_PATH` | `routes.json` | Path to the JSON route config file, relative to `process.cwd()`. |
|
|
99
103
|
| `ROUTES` | _(none)_ | Inline route definitions as a JSON array. Merged with `ROUTES_FILE_PATH`. Useful for containerised deployments where injecting a file is inconvenient. |
|
|
100
104
|
|
|
105
|
+
#### Authentication
|
|
106
|
+
|
|
107
|
+
| Variable | Default | Description |
|
|
108
|
+
|---|---|---|
|
|
109
|
+
| `JWT_SECRET` | _(none)_ | Fallback HMAC signing secret used when a JWT route has no inline `secret` field. |
|
|
110
|
+
| `JWT_PUBLIC_KEY` | _(none)_ | Fallback PEM public key used when a JWT route has no inline `publicKey` field. Takes precedence over `JWT_SECRET`. |
|
|
111
|
+
|
|
101
112
|
---
|
|
102
113
|
|
|
103
114
|
### Route Configuration
|
|
@@ -135,6 +146,35 @@ Routes are defined as a JSON array. Each entry is a **Gateway** object:
|
|
|
135
146
|
|
|
136
147
|
Responses include standard `RateLimit-*` headers (RFC draft-8).
|
|
137
148
|
|
|
149
|
+
#### `Auth`
|
|
150
|
+
|
|
151
|
+
Adds authentication middleware to a route. When `enabled` is `false` the middleware is a no-op passthrough — no overhead, no token check.
|
|
152
|
+
|
|
153
|
+
Two strategies are supported, selected with the `strategy` field.
|
|
154
|
+
|
|
155
|
+
**`"jwt"` — Bearer token validation**
|
|
156
|
+
|
|
157
|
+
| Field | Type | Required | Description |
|
|
158
|
+
|---|---|---|---|
|
|
159
|
+
| `enabled` | `boolean` | ✅ | `true` to enforce, `false` to bypass. |
|
|
160
|
+
| `strategy` | `"jwt"` | ✅ | — |
|
|
161
|
+
| `secret` | `string` | — | Shared secret for HMAC algorithms (HS256, HS384, HS512). Falls back to `JWT_SECRET` env var. |
|
|
162
|
+
| `publicKey` | `string` | — | PEM-encoded public key or X.509 certificate for asymmetric algorithms (RS256, RS384, RS512, ES256 …). Falls back to `JWT_PUBLIC_KEY` env var. Takes precedence over `secret` when both are present. |
|
|
163
|
+
| `algorithms` | `string[]` | — | Explicit algorithm allowlist. Defaults to `["RS256"]` when `publicKey` is used, `["HS256"]` otherwise. Recommended to prevent algorithm-confusion attacks. |
|
|
164
|
+
|
|
165
|
+
The gateway expects the token in the `Authorization: Bearer <token>` header and returns `401` on a missing, malformed, or invalid token.
|
|
166
|
+
|
|
167
|
+
**`"apiKey"` — Header-based API key**
|
|
168
|
+
|
|
169
|
+
| Field | Type | Required | Description |
|
|
170
|
+
|---|---|---|---|
|
|
171
|
+
| `enabled` | `boolean` | ✅ | `true` to enforce, `false` to bypass. |
|
|
172
|
+
| `strategy` | `"apiKey"` | ✅ | — |
|
|
173
|
+
| `keys` | `string[]` | ✅ | List of valid API keys. At least one entry required. |
|
|
174
|
+
| `header` | `string` | — | Header name to read the key from (default: `x-api-key`). |
|
|
175
|
+
|
|
176
|
+
Returns `401` when the header is absent or the value is not in `keys`.
|
|
177
|
+
|
|
138
178
|
#### Example `routes.json`
|
|
139
179
|
|
|
140
180
|
```json
|
|
@@ -158,11 +198,24 @@ Responses include standard `RateLimit-*` headers (RFC draft-8).
|
|
|
158
198
|
"proxy": {
|
|
159
199
|
"target": "http://orders-service:3002",
|
|
160
200
|
"changeOrigin": true,
|
|
161
|
-
"pathRewrite": { "^/orders": "" }
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
"
|
|
201
|
+
"pathRewrite": { "^/orders": "" }
|
|
202
|
+
},
|
|
203
|
+
"auth": {
|
|
204
|
+
"enabled": true,
|
|
205
|
+
"strategy": "jwt"
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
"baseURL": "/reports",
|
|
210
|
+
"proxy": {
|
|
211
|
+
"target": "http://reports-service:3003",
|
|
212
|
+
"changeOrigin": true,
|
|
213
|
+
"pathRewrite": { "^/reports": "" }
|
|
214
|
+
},
|
|
215
|
+
"auth": {
|
|
216
|
+
"enabled": true,
|
|
217
|
+
"strategy": "apiKey",
|
|
218
|
+
"keys": ["key-service-alpha-123", "key-service-beta-456"]
|
|
166
219
|
}
|
|
167
220
|
}
|
|
168
221
|
]
|
|
@@ -177,6 +230,93 @@ Routes are loaded from two sources and **merged**:
|
|
|
177
230
|
|
|
178
231
|
---
|
|
179
232
|
|
|
233
|
+
## Authentication
|
|
234
|
+
|
|
235
|
+
Authentication is optional and configured per route via the `auth` field. The middleware is applied before rate limiting and proxying. When `enabled: false` the handler is a single no-op function — zero overhead on unprotected routes.
|
|
236
|
+
|
|
237
|
+
### JWT
|
|
238
|
+
|
|
239
|
+
Protect a route with a Bearer token. The gateway validates the token signature; your upstream receives the request only if verification passes.
|
|
240
|
+
|
|
241
|
+
```json
|
|
242
|
+
{
|
|
243
|
+
"baseURL": "/orders",
|
|
244
|
+
"proxy": { "target": "http://orders-service:3002", "changeOrigin": true },
|
|
245
|
+
"auth": {
|
|
246
|
+
"enabled": true,
|
|
247
|
+
"strategy": "jwt"
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
The signing key is resolved in this order:
|
|
253
|
+
|
|
254
|
+
1. `publicKey` field in the route config (PEM — use for RS256 / ES256)
|
|
255
|
+
2. `JWT_PUBLIC_KEY` environment variable
|
|
256
|
+
3. `secret` field in the route config (string — use for HS256)
|
|
257
|
+
4. `JWT_SECRET` environment variable
|
|
258
|
+
|
|
259
|
+
`publicKey` always takes precedence over `secret`. If neither is present the gateway returns `401`.
|
|
260
|
+
|
|
261
|
+
**HMAC (HS256) — shared secret:**
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
# .env
|
|
265
|
+
JWT_SECRET=super-secret-key-change-in-production
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
TOKEN=$(curl -s -X POST http://localhost:3000/auth/login \
|
|
270
|
+
-H "Content-Type: application/json" \
|
|
271
|
+
-d '{"username":"alice","password":"password123"}' | jq -r '.token')
|
|
272
|
+
|
|
273
|
+
curl http://localhost:3000/orders \
|
|
274
|
+
-H "Authorization: Bearer $TOKEN"
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
**RSA (RS256) — public/private key pair:**
|
|
278
|
+
|
|
279
|
+
```json
|
|
280
|
+
{
|
|
281
|
+
"auth": {
|
|
282
|
+
"enabled": true,
|
|
283
|
+
"strategy": "jwt",
|
|
284
|
+
"publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjAN...\n-----END PUBLIC KEY-----",
|
|
285
|
+
"algorithms": ["RS256"]
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### API Key
|
|
291
|
+
|
|
292
|
+
Protect a route with a pre-shared key delivered in a request header.
|
|
293
|
+
|
|
294
|
+
```json
|
|
295
|
+
{
|
|
296
|
+
"baseURL": "/reports",
|
|
297
|
+
"proxy": { "target": "http://reports-service:3003", "changeOrigin": true },
|
|
298
|
+
"auth": {
|
|
299
|
+
"enabled": true,
|
|
300
|
+
"strategy": "apiKey",
|
|
301
|
+
"header": "x-api-key",
|
|
302
|
+
"keys": ["key-service-alpha-123", "key-service-beta-456"]
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
```bash
|
|
308
|
+
# Valid key → 200
|
|
309
|
+
curl http://localhost:3000/reports \
|
|
310
|
+
-H "x-api-key: key-service-alpha-123"
|
|
311
|
+
|
|
312
|
+
# Missing or wrong key → 401
|
|
313
|
+
curl http://localhost:3000/reports
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Multiple keys in `keys` let you rotate credentials without downtime — add the new key, deploy, then remove the old one.
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
180
320
|
## Running the Gateway
|
|
181
321
|
|
|
182
322
|
### Development (hot-reload)
|
|
@@ -245,88 +385,98 @@ src/apps/api-gateway/
|
|
|
245
385
|
│ ├── env/config.ts # NODE_ENV → isDev flag
|
|
246
386
|
│ ├── gateway/config.ts # GATEWAY_PORT, GATEWAY_PREFIX
|
|
247
387
|
│ ├── cors/config.ts # CORS_ORIGINS, CORS_METHODS, CORS_HEADERS
|
|
248
|
-
│
|
|
388
|
+
│ ├── routes/config.ts # ROUTES_FILE_PATH
|
|
389
|
+
│ └── auth/config.ts # JWT_SECRET, JWT_PUBLIC_KEY
|
|
390
|
+
│
|
|
391
|
+
├── middleware/
|
|
392
|
+
│ ├── authMiddleware.ts # Factory — returns the right strategy or a no-op
|
|
393
|
+
│ └── auth/
|
|
394
|
+
│ ├── AuthStrategy.ts # Interface (Strategy pattern)
|
|
395
|
+
│ ├── JwtAuthStrategy.ts # JWT Bearer token validation (HMAC + RSA/EC)
|
|
396
|
+
│ └── ApiKeyAuthStrategy.ts # Header-based API key validation
|
|
249
397
|
│
|
|
250
398
|
├── routes/
|
|
251
399
|
│ ├── Router.ts # Middleware pipeline (logging → security → CORS → body → proxy → errors)
|
|
252
400
|
│ ├── ProxyManager.ts # Reads, validates, and registers proxy routes
|
|
253
|
-
│ ├── RouteValidator.ts # Zod schemas for Gateway / Proxy / RateLimit types
|
|
401
|
+
│ ├── RouteValidator.ts # Zod schemas for Gateway / Proxy / RateLimit / Auth types
|
|
254
402
|
│ └── HealthRouter.ts # GET /health handler
|
|
255
403
|
│
|
|
256
404
|
└── types/
|
|
257
405
|
├── gateway.d.ts # Gateway type
|
|
258
406
|
├── proxy.d.ts # Proxy type
|
|
259
|
-
|
|
407
|
+
├── rate-limit.d.ts # RateLimit type
|
|
408
|
+
└── auth.d.ts # Auth type (JwtAuth | ApiKeyAuth)
|
|
260
409
|
```
|
|
261
410
|
|
|
262
411
|
---
|
|
263
412
|
|
|
264
413
|
## Example Project
|
|
265
414
|
|
|
266
|
-
`examples
|
|
267
|
-
|
|
268
|
-
### What's included
|
|
269
|
-
|
|
270
|
-
| File | Description |
|
|
271
|
-
|---|---|
|
|
272
|
-
| `routes.json` | Gateway config with two routes (`/users`, `/products`) each with rate limiting and path rewriting |
|
|
273
|
-
| `.env` | Gateway environment for the example |
|
|
274
|
-
| `upstream-users.js` | Mock Users service on port `4001` |
|
|
275
|
-
| `upstream-products.js` | Mock Products service on port `4002` |
|
|
276
|
-
| `run.sh` | Starts all three processes and stops them together on Ctrl+C |
|
|
277
|
-
|
|
278
|
-
### Running the example
|
|
415
|
+
`examples/` contains a self-contained demo that starts five upstream services and one gateway covering all features: rate limiting, JWT auth, and API key auth.
|
|
279
416
|
|
|
280
417
|
```bash
|
|
281
418
|
pnpm example
|
|
282
419
|
```
|
|
283
420
|
|
|
284
|
-
This copies `examples
|
|
285
|
-
|
|
286
|
-
| Endpoint | Description |
|
|
287
|
-
|---|---|
|
|
288
|
-
| `http://localhost:3000/health` | Gateway health check |
|
|
289
|
-
| `http://localhost:3000/users` | Proxied to Users service |
|
|
290
|
-
| `http://localhost:3000/products` | Proxied to Products service |
|
|
291
|
-
|
|
292
|
-
### Users service endpoints (`/users`)
|
|
293
|
-
|
|
294
|
-
| Method | Path | Description |
|
|
295
|
-
|---|---|---|
|
|
296
|
-
| `GET` | `/users` | List all users |
|
|
297
|
-
| `GET` | `/users/:id` | Get a user by ID |
|
|
298
|
-
| `POST` | `/users` | Create a user (body: `{ name, email }`) |
|
|
299
|
-
| `DELETE` | `/users/:id` | Delete a user by ID |
|
|
300
|
-
|
|
301
|
-
### Products service endpoints (`/products`)
|
|
421
|
+
This copies `examples/.env` to the project root and starts everything. Once running:
|
|
302
422
|
|
|
303
|
-
|
|
|
423
|
+
| Endpoint | Auth | Description |
|
|
304
424
|
|---|---|---|
|
|
305
|
-
| `
|
|
306
|
-
| `
|
|
307
|
-
| `
|
|
308
|
-
| `
|
|
425
|
+
| `http://localhost:3000/health` | — | Gateway health check |
|
|
426
|
+
| `http://localhost:3000/users` | — | Users service (rate limited) |
|
|
427
|
+
| `http://localhost:3000/products` | — | Products service (rate limited) |
|
|
428
|
+
| `http://localhost:3000/auth/login` | — | Issues JWT tokens |
|
|
429
|
+
| `http://localhost:3000/orders` | JWT Bearer | Orders service |
|
|
430
|
+
| `http://localhost:3000/reports` | API key | Reports service |
|
|
309
431
|
|
|
310
|
-
###
|
|
432
|
+
### Public routes
|
|
311
433
|
|
|
312
434
|
```bash
|
|
313
|
-
# List users
|
|
314
435
|
curl http://localhost:3000/users
|
|
436
|
+
curl http://localhost:3000/products/1
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
### JWT-protected route (`/orders`)
|
|
315
440
|
|
|
316
|
-
|
|
317
|
-
|
|
441
|
+
```bash
|
|
442
|
+
# 1. Log in to get a token (users: alice/password123, bob/password456)
|
|
443
|
+
TOKEN=$(curl -s -X POST http://localhost:3000/auth/login \
|
|
318
444
|
-H "Content-Type: application/json" \
|
|
319
|
-
-d '{"
|
|
445
|
+
-d '{"username":"alice","password":"password123"}' | jq -r '.token')
|
|
320
446
|
|
|
321
|
-
#
|
|
322
|
-
curl http://localhost:3000/
|
|
447
|
+
# 2. Access the protected route
|
|
448
|
+
curl http://localhost:3000/orders \
|
|
449
|
+
-H "Authorization: Bearer $TOKEN"
|
|
323
450
|
|
|
324
|
-
#
|
|
325
|
-
curl -X
|
|
451
|
+
# 3. Create an order
|
|
452
|
+
curl -X POST http://localhost:3000/orders \
|
|
453
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
326
454
|
-H "Content-Type: application/json" \
|
|
327
|
-
-d '{"
|
|
455
|
+
-d '{"userId":1,"items":[{"productId":2,"qty":1}],"total":24.99}'
|
|
328
456
|
```
|
|
329
457
|
|
|
458
|
+
### API-key-protected route (`/reports`)
|
|
459
|
+
|
|
460
|
+
```bash
|
|
461
|
+
# Valid keys: key-service-alpha-123 · key-service-beta-456
|
|
462
|
+
|
|
463
|
+
curl http://localhost:3000/reports \
|
|
464
|
+
-H "x-api-key: key-service-alpha-123"
|
|
465
|
+
|
|
466
|
+
curl http://localhost:3000/reports/sales \
|
|
467
|
+
-H "x-api-key: key-service-beta-456"
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
### Individual example directories
|
|
471
|
+
|
|
472
|
+
Each sub-directory is also usable as a standalone reference:
|
|
473
|
+
|
|
474
|
+
| Directory | Description |
|
|
475
|
+
|---|---|
|
|
476
|
+
| `examples/basic/` | Rate limiting only — Users + Products services |
|
|
477
|
+
| `examples/jwt-auth/` | JWT auth — Auth service + Orders service |
|
|
478
|
+
| `examples/api-key-auth/` | API key auth — Reports service |
|
|
479
|
+
|
|
330
480
|
---
|
|
331
481
|
|
|
332
482
|
## Architecture
|
|
@@ -339,18 +489,19 @@ Client
|
|
|
339
489
|
▼
|
|
340
490
|
Express app
|
|
341
491
|
│
|
|
342
|
-
├─ pino-http
|
|
343
|
-
├─ helmet
|
|
344
|
-
├─ cors
|
|
345
|
-
├─ express.json
|
|
346
|
-
├─ compression
|
|
492
|
+
├─ pino-http structured request/response logging
|
|
493
|
+
├─ helmet security headers (CSP, HSTS, X-Frame-Options, …)
|
|
494
|
+
├─ cors configurable origin / method / header policy
|
|
495
|
+
├─ express.json body parsing
|
|
496
|
+
├─ compression gzip response compression
|
|
347
497
|
│
|
|
348
|
-
├─ GET /health
|
|
498
|
+
├─ GET /health health check — short-circuits here
|
|
349
499
|
│
|
|
350
|
-
├─
|
|
500
|
+
├─ authMiddleware per-route — JWT or API key check → 401 on failure
|
|
501
|
+
├─ express-rate-limit per-route request throttling → 429 on exceeded
|
|
351
502
|
├─ http-proxy-middleware proxies request to upstream, forwards body
|
|
352
503
|
│
|
|
353
|
-
└─ error handler
|
|
504
|
+
└─ error handler catches unhandled errors → 500 JSON response
|
|
354
505
|
```
|
|
355
506
|
|
|
356
507
|
### Startup sequence
|
|
@@ -2,10 +2,12 @@ import { type GatewayConfig } from "./gateway/config";
|
|
|
2
2
|
import { type CorsConfig } from "./cors/config";
|
|
3
3
|
import { type RoutesConfig } from "./routes/config";
|
|
4
4
|
import { type EnvConfig } from "./env/config";
|
|
5
|
+
import { type AuthConfig } from "./auth/config";
|
|
5
6
|
export type AppEnv = {
|
|
6
7
|
env: EnvConfig;
|
|
7
8
|
gateway: GatewayConfig;
|
|
8
9
|
cors: CorsConfig;
|
|
9
10
|
routes: RoutesConfig;
|
|
11
|
+
auth: AuthConfig;
|
|
10
12
|
};
|
|
11
13
|
export declare const appEnv: AppEnv;
|
|
@@ -5,9 +5,11 @@ const config_1 = require("./gateway/config");
|
|
|
5
5
|
const config_2 = require("./cors/config");
|
|
6
6
|
const config_3 = require("./routes/config");
|
|
7
7
|
const config_4 = require("./env/config");
|
|
8
|
+
const config_5 = require("./auth/config");
|
|
8
9
|
exports.appEnv = {
|
|
9
10
|
env: config_4.envConfig,
|
|
10
11
|
gateway: config_1.gatewayConfig,
|
|
11
12
|
cors: config_2.corsConfig,
|
|
12
13
|
routes: config_3.routesConfig,
|
|
14
|
+
auth: config_5.authConfig,
|
|
13
15
|
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Request, Response, NextFunction } from "express";
|
|
2
|
+
import type { ApiKeyAuth } from "../../types/auth";
|
|
3
|
+
import type { AuthStrategy } from "./AuthStrategy";
|
|
4
|
+
export declare class ApiKeyAuthStrategy implements AuthStrategy {
|
|
5
|
+
private readonly auth;
|
|
6
|
+
private readonly headerName;
|
|
7
|
+
constructor(auth: ApiKeyAuth);
|
|
8
|
+
handle(req: Request, res: Response, next: NextFunction): void;
|
|
9
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ApiKeyAuthStrategy = void 0;
|
|
4
|
+
const http_status_codes_1 = require("http-status-codes");
|
|
5
|
+
class ApiKeyAuthStrategy {
|
|
6
|
+
constructor(auth) {
|
|
7
|
+
var _a;
|
|
8
|
+
this.auth = auth;
|
|
9
|
+
this.headerName = ((_a = auth.header) !== null && _a !== void 0 ? _a : "x-api-key").toLowerCase();
|
|
10
|
+
}
|
|
11
|
+
handle(req, res, next) {
|
|
12
|
+
const provided = req.headers[this.headerName];
|
|
13
|
+
if (typeof provided !== "string" || !this.auth.keys.includes(provided)) {
|
|
14
|
+
res.status(http_status_codes_1.StatusCodes.UNAUTHORIZED).json({ error: "Invalid or missing API key" });
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
next();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
exports.ApiKeyAuthStrategy = ApiKeyAuthStrategy;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Request, Response, NextFunction } from "express";
|
|
2
|
+
import type { JwtAuth } from "../../types/auth";
|
|
3
|
+
import type { AuthStrategy } from "./AuthStrategy";
|
|
4
|
+
export declare class JwtAuthStrategy implements AuthStrategy {
|
|
5
|
+
private readonly auth;
|
|
6
|
+
constructor(auth: JwtAuth);
|
|
7
|
+
handle(req: Request, res: Response, next: NextFunction): void;
|
|
8
|
+
private extractToken;
|
|
9
|
+
private resolveKey;
|
|
10
|
+
private resolveAlgorithms;
|
|
11
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.JwtAuthStrategy = void 0;
|
|
7
|
+
const http_status_codes_1 = require("http-status-codes");
|
|
8
|
+
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
9
|
+
const app_env_1 = require("../../config/app-env");
|
|
10
|
+
class JwtAuthStrategy {
|
|
11
|
+
constructor(auth) {
|
|
12
|
+
this.auth = auth;
|
|
13
|
+
}
|
|
14
|
+
handle(req, res, next) {
|
|
15
|
+
const token = this.extractToken(req);
|
|
16
|
+
if (!token) {
|
|
17
|
+
res.status(http_status_codes_1.StatusCodes.UNAUTHORIZED).json({ error: "Missing or malformed Authorization header" });
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const resolved = this.resolveKey();
|
|
21
|
+
if (!resolved) {
|
|
22
|
+
res.status(http_status_codes_1.StatusCodes.UNAUTHORIZED).json({ error: "JWT key not configured" });
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
jsonwebtoken_1.default.verify(token, resolved.key, { algorithms: this.resolveAlgorithms(resolved.isAsymmetric) });
|
|
27
|
+
next();
|
|
28
|
+
}
|
|
29
|
+
catch (_a) {
|
|
30
|
+
res.status(http_status_codes_1.StatusCodes.UNAUTHORIZED).json({ error: "Invalid or expired token" });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
extractToken(req) {
|
|
34
|
+
const { authorization } = req.headers;
|
|
35
|
+
if (!(authorization === null || authorization === void 0 ? void 0 : authorization.startsWith("Bearer ")))
|
|
36
|
+
return null;
|
|
37
|
+
return authorization.slice(7);
|
|
38
|
+
}
|
|
39
|
+
resolveKey() {
|
|
40
|
+
var _a, _b;
|
|
41
|
+
const publicKey = (_a = this.auth.publicKey) !== null && _a !== void 0 ? _a : app_env_1.appEnv.auth.jwtPublicKey;
|
|
42
|
+
if (publicKey)
|
|
43
|
+
return { key: publicKey, isAsymmetric: true };
|
|
44
|
+
const secret = (_b = this.auth.secret) !== null && _b !== void 0 ? _b : app_env_1.appEnv.auth.jwtSecret;
|
|
45
|
+
if (secret)
|
|
46
|
+
return { key: secret, isAsymmetric: false };
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
resolveAlgorithms(isAsymmetric) {
|
|
50
|
+
if (this.auth.algorithms)
|
|
51
|
+
return this.auth.algorithms;
|
|
52
|
+
return isAsymmetric ? ["RS256"] : ["HS256"];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
exports.JwtAuthStrategy = JwtAuthStrategy;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createAuthMiddleware = createAuthMiddleware;
|
|
4
|
+
const JwtAuthStrategy_1 = require("./auth/JwtAuthStrategy");
|
|
5
|
+
const ApiKeyAuthStrategy_1 = require("./auth/ApiKeyAuthStrategy");
|
|
6
|
+
const strategyFactories = {
|
|
7
|
+
jwt: (auth) => new JwtAuthStrategy_1.JwtAuthStrategy(auth),
|
|
8
|
+
apiKey: (auth) => new ApiKeyAuthStrategy_1.ApiKeyAuthStrategy(auth),
|
|
9
|
+
};
|
|
10
|
+
function createAuthMiddleware(auth) {
|
|
11
|
+
if (!auth.enabled) {
|
|
12
|
+
return (_req, _res, next) => next();
|
|
13
|
+
}
|
|
14
|
+
const strategy = strategyFactories[auth.strategy](auth);
|
|
15
|
+
return (req, res, next) => strategy.handle(req, res, next);
|
|
16
|
+
}
|
|
@@ -18,6 +18,7 @@ const express_rate_limit_1 = __importDefault(require("express-rate-limit"));
|
|
|
18
18
|
const http_status_codes_1 = require("http-status-codes");
|
|
19
19
|
const promises_1 = require("node:fs/promises");
|
|
20
20
|
const RouteValidator_1 = require("./RouteValidator");
|
|
21
|
+
const authMiddleware_1 = require("../middleware/authMiddleware");
|
|
21
22
|
const logger_1 = require("../logger");
|
|
22
23
|
const app_env_1 = require("../config/app-env");
|
|
23
24
|
class ProxyManager {
|
|
@@ -37,6 +38,10 @@ class ProxyManager {
|
|
|
37
38
|
}
|
|
38
39
|
routes.forEach((route) => {
|
|
39
40
|
var _a, _b;
|
|
41
|
+
// Apply per-route authentication when configured
|
|
42
|
+
if (route.auth) {
|
|
43
|
+
this.router.use(route.baseURL, (0, authMiddleware_1.createAuthMiddleware)(route.auth));
|
|
44
|
+
}
|
|
40
45
|
// Apply per-route rate limiting when configured
|
|
41
46
|
if (route.rateLimit) {
|
|
42
47
|
this.router.use(route.baseURL, (0, express_rate_limit_1.default)({
|
|
@@ -17,5 +17,17 @@ export declare const GatewaysSchema: z.ZodArray<z.ZodObject<{
|
|
|
17
17
|
statusCode: z.ZodOptional<z.ZodNumber>;
|
|
18
18
|
message: z.ZodOptional<z.ZodString>;
|
|
19
19
|
}, z.core.$strip>>;
|
|
20
|
+
auth: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
21
|
+
enabled: z.ZodBoolean;
|
|
22
|
+
strategy: z.ZodLiteral<"jwt">;
|
|
23
|
+
secret: z.ZodOptional<z.ZodString>;
|
|
24
|
+
publicKey: z.ZodOptional<z.ZodString>;
|
|
25
|
+
algorithms: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
26
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
27
|
+
enabled: z.ZodBoolean;
|
|
28
|
+
strategy: z.ZodLiteral<"apiKey">;
|
|
29
|
+
header: z.ZodOptional<z.ZodString>;
|
|
30
|
+
keys: z.ZodArray<z.ZodString>;
|
|
31
|
+
}, z.core.$strip>], "strategy">>;
|
|
20
32
|
}, z.core.$strip>>;
|
|
21
33
|
export declare function validateRoutes(data: unknown): Gateway[];
|
|
@@ -18,10 +18,25 @@ const RateLimitSchema = zod_1.z.object({
|
|
|
18
18
|
statusCode: zod_1.z.number().optional(),
|
|
19
19
|
message: zod_1.z.string().optional(),
|
|
20
20
|
});
|
|
21
|
+
const JwtAuthSchema = zod_1.z.object({
|
|
22
|
+
enabled: zod_1.z.boolean(),
|
|
23
|
+
strategy: zod_1.z.literal("jwt"),
|
|
24
|
+
secret: zod_1.z.string().optional(),
|
|
25
|
+
publicKey: zod_1.z.string().optional(),
|
|
26
|
+
algorithms: zod_1.z.array(zod_1.z.string()).optional(),
|
|
27
|
+
});
|
|
28
|
+
const ApiKeyAuthSchema = zod_1.z.object({
|
|
29
|
+
enabled: zod_1.z.boolean(),
|
|
30
|
+
strategy: zod_1.z.literal("apiKey"),
|
|
31
|
+
header: zod_1.z.string().optional(),
|
|
32
|
+
keys: zod_1.z.array(zod_1.z.string()).min(1, "apiKey auth requires at least one key"),
|
|
33
|
+
});
|
|
34
|
+
const AuthSchema = zod_1.z.discriminatedUnion("strategy", [JwtAuthSchema, ApiKeyAuthSchema]);
|
|
21
35
|
const GatewaySchema = zod_1.z.object({
|
|
22
36
|
baseURL: zod_1.z.string().startsWith("/", "baseURL must start with /"),
|
|
23
37
|
proxy: ProxySchema,
|
|
24
38
|
rateLimit: RateLimitSchema.optional(),
|
|
39
|
+
auth: AuthSchema.optional(),
|
|
25
40
|
});
|
|
26
41
|
exports.GatewaysSchema = zod_1.z.array(GatewaySchema);
|
|
27
42
|
function validateRoutes(data) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@derian-cordoba/api-gateway",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "A generic, configuration-driven HTTP API gateway with per-route rate limiting, structured logging, and security headers",
|
|
5
5
|
"main": "./dist/src/apps/api-gateway/index.js",
|
|
6
6
|
"types": "./dist/src/apps/api-gateway/index.d.ts",
|
|
@@ -37,12 +37,13 @@
|
|
|
37
37
|
"prepare": "pnpm build",
|
|
38
38
|
"test": "vitest run",
|
|
39
39
|
"test:watch": "vitest",
|
|
40
|
-
"example": "bash examples/
|
|
40
|
+
"example": "bash examples/run.sh"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/compression": "^1.8.0",
|
|
44
44
|
"@types/cors": "^2.8.18",
|
|
45
45
|
"@types/express": "^5.0.2",
|
|
46
|
+
"@types/jsonwebtoken": "^9.0.10",
|
|
46
47
|
"@types/node": "^22.15.21",
|
|
47
48
|
"@types/supertest": "^7.2.0",
|
|
48
49
|
"pino-pretty": "^13.1.3",
|
|
@@ -60,6 +61,7 @@
|
|
|
60
61
|
"helmet": "^8.1.0",
|
|
61
62
|
"http-proxy-middleware": "^3.0.5",
|
|
62
63
|
"http-status-codes": "^2.3.0",
|
|
64
|
+
"jsonwebtoken": "^9.0.3",
|
|
63
65
|
"pino": "^10.3.1",
|
|
64
66
|
"pino-http": "^11.0.0",
|
|
65
67
|
"zod": "^4.4.3"
|