@vivero/stoma 0.1.0-rc.10
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/CHANGELOG.md +196 -0
- package/LICENSE +21 -0
- package/README.md +325 -0
- package/dist/adapters/bun.d.ts +9 -0
- package/dist/adapters/bun.js +8 -0
- package/dist/adapters/bun.js.map +1 -0
- package/dist/adapters/cloudflare.d.ts +49 -0
- package/dist/adapters/cloudflare.js +85 -0
- package/dist/adapters/cloudflare.js.map +1 -0
- package/dist/adapters/deno.d.ts +9 -0
- package/dist/adapters/deno.js +8 -0
- package/dist/adapters/deno.js.map +1 -0
- package/dist/adapters/durable-object.d.ts +63 -0
- package/dist/adapters/durable-object.js +46 -0
- package/dist/adapters/durable-object.js.map +1 -0
- package/dist/adapters/index.d.ts +13 -0
- package/dist/adapters/index.js +53 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/memory.d.ts +9 -0
- package/dist/adapters/memory.js +14 -0
- package/dist/adapters/memory.js.map +1 -0
- package/dist/adapters/node.d.ts +9 -0
- package/dist/adapters/node.js +8 -0
- package/dist/adapters/node.js.map +1 -0
- package/dist/adapters/postgres.d.ts +109 -0
- package/dist/adapters/postgres.js +242 -0
- package/dist/adapters/postgres.js.map +1 -0
- package/dist/adapters/redis.d.ts +116 -0
- package/dist/adapters/redis.js +194 -0
- package/dist/adapters/redis.js.map +1 -0
- package/dist/adapters/testing.d.ts +32 -0
- package/dist/adapters/testing.js +33 -0
- package/dist/adapters/testing.js.map +1 -0
- package/dist/adapters/types.d.ts +4 -0
- package/dist/adapters/types.js +1 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/config/index.d.ts +11 -0
- package/dist/config/index.js +21 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/merge.d.ts +48 -0
- package/dist/config/merge.js +83 -0
- package/dist/config/merge.js.map +1 -0
- package/dist/config/schema.d.ts +254 -0
- package/dist/config/schema.js +109 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/core/errors.d.ts +66 -0
- package/dist/core/errors.js +47 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/gateway.d.ts +44 -0
- package/dist/core/gateway.js +400 -0
- package/dist/core/gateway.js.map +1 -0
- package/dist/core/health.d.ts +78 -0
- package/dist/core/health.js +65 -0
- package/dist/core/health.js.map +1 -0
- package/dist/core/pipeline.d.ts +62 -0
- package/dist/core/pipeline.js +214 -0
- package/dist/core/pipeline.js.map +1 -0
- package/dist/core/protocol.d.ts +4 -0
- package/dist/core/protocol.js +1 -0
- package/dist/core/protocol.js.map +1 -0
- package/dist/core/scope.d.ts +67 -0
- package/dist/core/scope.js +44 -0
- package/dist/core/scope.js.map +1 -0
- package/dist/core/types.d.ts +252 -0
- package/dist/core/types.js +1 -0
- package/dist/core/types.js.map +1 -0
- package/dist/index.d.ts +57 -0
- package/dist/index.js +158 -0
- package/dist/index.js.map +1 -0
- package/dist/observability/admin.d.ts +32 -0
- package/dist/observability/admin.js +85 -0
- package/dist/observability/admin.js.map +1 -0
- package/dist/observability/metrics.d.ts +78 -0
- package/dist/observability/metrics.js +107 -0
- package/dist/observability/metrics.js.map +1 -0
- package/dist/observability/tracing.d.ts +149 -0
- package/dist/observability/tracing.js +191 -0
- package/dist/observability/tracing.js.map +1 -0
- package/dist/policies/auth/api-key-auth.d.ts +64 -0
- package/dist/policies/auth/api-key-auth.js +93 -0
- package/dist/policies/auth/api-key-auth.js.map +1 -0
- package/dist/policies/auth/basic-auth.d.ts +33 -0
- package/dist/policies/auth/basic-auth.js +96 -0
- package/dist/policies/auth/basic-auth.js.map +1 -0
- package/dist/policies/auth/crypto.d.ts +29 -0
- package/dist/policies/auth/crypto.js +100 -0
- package/dist/policies/auth/crypto.js.map +1 -0
- package/dist/policies/auth/generate-http-signature.d.ts +30 -0
- package/dist/policies/auth/generate-http-signature.js +79 -0
- package/dist/policies/auth/generate-http-signature.js.map +1 -0
- package/dist/policies/auth/generate-jwt.d.ts +44 -0
- package/dist/policies/auth/generate-jwt.js +99 -0
- package/dist/policies/auth/generate-jwt.js.map +1 -0
- package/dist/policies/auth/http-signature-base.d.ts +55 -0
- package/dist/policies/auth/http-signature-base.js +140 -0
- package/dist/policies/auth/http-signature-base.js.map +1 -0
- package/dist/policies/auth/jws.d.ts +46 -0
- package/dist/policies/auth/jws.js +317 -0
- package/dist/policies/auth/jws.js.map +1 -0
- package/dist/policies/auth/jwt-auth.d.ts +64 -0
- package/dist/policies/auth/jwt-auth.js +266 -0
- package/dist/policies/auth/jwt-auth.js.map +1 -0
- package/dist/policies/auth/oauth2.d.ts +38 -0
- package/dist/policies/auth/oauth2.js +254 -0
- package/dist/policies/auth/oauth2.js.map +1 -0
- package/dist/policies/auth/rbac.d.ts +30 -0
- package/dist/policies/auth/rbac.js +115 -0
- package/dist/policies/auth/rbac.js.map +1 -0
- package/dist/policies/auth/verify-http-signature.d.ts +30 -0
- package/dist/policies/auth/verify-http-signature.js +147 -0
- package/dist/policies/auth/verify-http-signature.js.map +1 -0
- package/dist/policies/index.d.ts +51 -0
- package/dist/policies/index.js +109 -0
- package/dist/policies/index.js.map +1 -0
- package/dist/policies/mock.d.ts +60 -0
- package/dist/policies/mock.js +29 -0
- package/dist/policies/mock.js.map +1 -0
- package/dist/policies/observability/assign-metrics.d.ts +37 -0
- package/dist/policies/observability/assign-metrics.js +29 -0
- package/dist/policies/observability/assign-metrics.js.map +1 -0
- package/dist/policies/observability/metrics-reporter.d.ts +25 -0
- package/dist/policies/observability/metrics-reporter.js +62 -0
- package/dist/policies/observability/metrics-reporter.js.map +1 -0
- package/dist/policies/observability/request-log.d.ts +135 -0
- package/dist/policies/observability/request-log.js +134 -0
- package/dist/policies/observability/request-log.js.map +1 -0
- package/dist/policies/observability/server-timing.d.ts +35 -0
- package/dist/policies/observability/server-timing.js +89 -0
- package/dist/policies/observability/server-timing.js.map +1 -0
- package/dist/policies/proxy.d.ts +59 -0
- package/dist/policies/proxy.js +47 -0
- package/dist/policies/proxy.js.map +1 -0
- package/dist/policies/resilience/circuit-breaker.d.ts +4 -0
- package/dist/policies/resilience/circuit-breaker.js +280 -0
- package/dist/policies/resilience/circuit-breaker.js.map +1 -0
- package/dist/policies/resilience/latency-injection.d.ts +35 -0
- package/dist/policies/resilience/latency-injection.js +26 -0
- package/dist/policies/resilience/latency-injection.js.map +1 -0
- package/dist/policies/resilience/retry.d.ts +71 -0
- package/dist/policies/resilience/retry.js +79 -0
- package/dist/policies/resilience/retry.js.map +1 -0
- package/dist/policies/resilience/timeout.d.ts +32 -0
- package/dist/policies/resilience/timeout.js +46 -0
- package/dist/policies/resilience/timeout.js.map +1 -0
- package/dist/policies/sdk/define-policy.d.ts +176 -0
- package/dist/policies/sdk/define-policy.js +42 -0
- package/dist/policies/sdk/define-policy.js.map +1 -0
- package/dist/policies/sdk/helpers.d.ts +132 -0
- package/dist/policies/sdk/helpers.js +87 -0
- package/dist/policies/sdk/helpers.js.map +1 -0
- package/dist/policies/sdk/index.d.ts +10 -0
- package/dist/policies/sdk/index.js +35 -0
- package/dist/policies/sdk/index.js.map +1 -0
- package/dist/policies/sdk/priority.d.ts +44 -0
- package/dist/policies/sdk/priority.js +36 -0
- package/dist/policies/sdk/priority.js.map +1 -0
- package/dist/policies/sdk/testing.d.ts +53 -0
- package/dist/policies/sdk/testing.js +41 -0
- package/dist/policies/sdk/testing.js.map +1 -0
- package/dist/policies/sdk/trace.d.ts +73 -0
- package/dist/policies/sdk/trace.js +25 -0
- package/dist/policies/sdk/trace.js.map +1 -0
- package/dist/policies/traffic/cache.d.ts +4 -0
- package/dist/policies/traffic/cache.js +224 -0
- package/dist/policies/traffic/cache.js.map +1 -0
- package/dist/policies/traffic/dynamic-routing.d.ts +54 -0
- package/dist/policies/traffic/dynamic-routing.js +36 -0
- package/dist/policies/traffic/dynamic-routing.js.map +1 -0
- package/dist/policies/traffic/geo-ip-filter.d.ts +37 -0
- package/dist/policies/traffic/geo-ip-filter.js +74 -0
- package/dist/policies/traffic/geo-ip-filter.js.map +1 -0
- package/dist/policies/traffic/http-callout.d.ts +59 -0
- package/dist/policies/traffic/http-callout.js +69 -0
- package/dist/policies/traffic/http-callout.js.map +1 -0
- package/dist/policies/traffic/interrupt.d.ts +46 -0
- package/dist/policies/traffic/interrupt.js +38 -0
- package/dist/policies/traffic/interrupt.js.map +1 -0
- package/dist/policies/traffic/ip-filter.d.ts +47 -0
- package/dist/policies/traffic/ip-filter.js +57 -0
- package/dist/policies/traffic/ip-filter.js.map +1 -0
- package/dist/policies/traffic/json-threat-protection.d.ts +51 -0
- package/dist/policies/traffic/json-threat-protection.js +173 -0
- package/dist/policies/traffic/json-threat-protection.js.map +1 -0
- package/dist/policies/traffic/rate-limit.d.ts +4 -0
- package/dist/policies/traffic/rate-limit.js +145 -0
- package/dist/policies/traffic/rate-limit.js.map +1 -0
- package/dist/policies/traffic/regex-threat-protection.d.ts +54 -0
- package/dist/policies/traffic/regex-threat-protection.js +109 -0
- package/dist/policies/traffic/regex-threat-protection.js.map +1 -0
- package/dist/policies/traffic/request-limit.d.ts +27 -0
- package/dist/policies/traffic/request-limit.js +41 -0
- package/dist/policies/traffic/request-limit.js.map +1 -0
- package/dist/policies/traffic/resource-filter.d.ts +38 -0
- package/dist/policies/traffic/resource-filter.js +184 -0
- package/dist/policies/traffic/resource-filter.js.map +1 -0
- package/dist/policies/traffic/ssl-enforce.d.ts +27 -0
- package/dist/policies/traffic/ssl-enforce.js +38 -0
- package/dist/policies/traffic/ssl-enforce.js.map +1 -0
- package/dist/policies/traffic/traffic-shadow.d.ts +40 -0
- package/dist/policies/traffic/traffic-shadow.js +87 -0
- package/dist/policies/traffic/traffic-shadow.js.map +1 -0
- package/dist/policies/transform/assign-attributes.d.ts +33 -0
- package/dist/policies/transform/assign-attributes.js +38 -0
- package/dist/policies/transform/assign-attributes.js.map +1 -0
- package/dist/policies/transform/assign-content.d.ts +40 -0
- package/dist/policies/transform/assign-content.js +185 -0
- package/dist/policies/transform/assign-content.js.map +1 -0
- package/dist/policies/transform/cors.d.ts +57 -0
- package/dist/policies/transform/cors.js +23 -0
- package/dist/policies/transform/cors.js.map +1 -0
- package/dist/policies/transform/json-validation.d.ts +50 -0
- package/dist/policies/transform/json-validation.js +125 -0
- package/dist/policies/transform/json-validation.js.map +1 -0
- package/dist/policies/transform/override-method.d.ts +33 -0
- package/dist/policies/transform/override-method.js +48 -0
- package/dist/policies/transform/override-method.js.map +1 -0
- package/dist/policies/transform/request-validation.d.ts +59 -0
- package/dist/policies/transform/request-validation.js +121 -0
- package/dist/policies/transform/request-validation.js.map +1 -0
- package/dist/policies/transform/transform.d.ts +75 -0
- package/dist/policies/transform/transform.js +116 -0
- package/dist/policies/transform/transform.js.map +1 -0
- package/dist/policies/types.d.ts +4 -0
- package/dist/policies/types.js +1 -0
- package/dist/policies/types.js.map +1 -0
- package/dist/protocol-2fD3DJrL.d.ts +725 -0
- package/dist/utils/cidr.d.ts +58 -0
- package/dist/utils/cidr.js +107 -0
- package/dist/utils/cidr.js.map +1 -0
- package/dist/utils/debug.d.ts +1 -0
- package/dist/utils/debug.js +13 -0
- package/dist/utils/debug.js.map +1 -0
- package/dist/utils/headers.d.ts +68 -0
- package/dist/utils/headers.js +25 -0
- package/dist/utils/headers.js.map +1 -0
- package/dist/utils/ip.d.ts +64 -0
- package/dist/utils/ip.js +29 -0
- package/dist/utils/ip.js.map +1 -0
- package/dist/utils/redact.d.ts +30 -0
- package/dist/utils/redact.js +52 -0
- package/dist/utils/redact.js.map +1 -0
- package/dist/utils/request-id.d.ts +11 -0
- package/dist/utils/request-id.js +7 -0
- package/dist/utils/request-id.js.map +1 -0
- package/dist/utils/timing-safe.d.ts +31 -0
- package/dist/utils/timing-safe.js +17 -0
- package/dist/utils/timing-safe.js.map +1 -0
- package/dist/utils/timing.d.ts +27 -0
- package/dist/utils/timing.js +12 -0
- package/dist/utils/timing.js.map +1 -0
- package/dist/utils/trace-context.d.ts +51 -0
- package/dist/utils/trace-context.js +37 -0
- package/dist/utils/trace-context.js.map +1 -0
- package/package.json +213 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/policies/auth/jwt-auth.ts"],"sourcesContent":["/**\n * JWT authentication policy - HMAC and JWKS verification.\n *\n * @module jwt-auth\n */\nimport { GatewayError } from \"../../core/errors\";\nimport type { DebugLogger } from \"../../utils/debug\";\nimport { sanitizeHeaderValue, withModifiedHeaders } from \"../../utils/headers\";\nimport { definePolicy, Priority } from \"../sdk\";\nimport type { PolicyConfig } from \"../types\";\nimport {\n base64UrlDecode,\n base64UrlToBuffer,\n fetchJwks,\n hmacAlgorithm,\n rsaAlgorithm,\n} from \"./crypto\";\n\nexport interface JwtAuthConfig extends PolicyConfig {\n /** JWT secret for HMAC verification */\n secret?: string;\n /** JWKS endpoint URL (e.g. Supabase, Auth0) */\n jwksUrl?: string;\n /** Expected JWT issuer */\n issuer?: string;\n /** Expected JWT audience */\n audience?: string;\n /** Header to read the token from. Default: \"Authorization\" */\n headerName?: string;\n /** Token prefix. Default: \"Bearer\" */\n tokenPrefix?: string;\n /** Claims to inject into request headers for upstream consumption */\n forwardClaims?: Record<string, string>;\n /** JWKS cache TTL in milliseconds. Default: 300000 (5 minutes). */\n jwksCacheTtlMs?: number;\n /** JWKS fetch timeout in milliseconds. Default: 10000 (10 seconds). */\n jwksTimeoutMs?: number;\n /** Clock skew tolerance in seconds for expiry checks. Default: 0. */\n clockSkewSeconds?: number;\n /** Require the `exp` claim to be present. Default: false. */\n requireExp?: boolean;\n}\n\ninterface JwtHeader {\n alg: string;\n typ?: string;\n kid?: string;\n}\n\ninterface JwtPayload {\n [key: string]: unknown;\n iss?: string;\n aud?: string | string[];\n exp?: number;\n iat?: number;\n sub?: string;\n}\n\n// ─── Shared JWT Validation ────────────────────────────────────────────\n\n/** Result of JWT token extraction, decoding, verification, and claim validation. */\ntype JwtValidationResult =\n | { ok: true; payload: JwtPayload }\n | { ok: false; status: number; code: string; message: string };\n\n/**\n * Core JWT validation shared between the HTTP handler and protocol-agnostic\n * evaluator.\n *\n * Extracts the token, decodes the JWT, verifies the signature (HMAC or\n * JWKS), and validates standard claims (exp, iss, aud). Returns a\n * discriminated result so callers can map to their runtime's error model.\n *\n * Signature verification errors from {@link verifyHmac}/{@link verifyJwks}\n * propagate as thrown `GatewayError` — both runtimes handle these at a\n * higher level.\n */\nasync function validateJwt(\n authHeader: string | null | undefined,\n config: JwtAuthConfig,\n debug: DebugLogger\n): Promise<JwtValidationResult> {\n if (!authHeader) {\n return {\n ok: false,\n status: 401,\n code: \"unauthorized\",\n message: \"Missing authentication token\",\n };\n }\n\n // Extract token\n let token: string;\n if (config.tokenPrefix) {\n if (!authHeader.startsWith(`${config.tokenPrefix} `)) {\n return {\n ok: false,\n status: 401,\n code: \"unauthorized\",\n message: `Expected ${config.tokenPrefix} token`,\n };\n }\n token = authHeader.slice(config.tokenPrefix.length + 1);\n } else {\n token = authHeader;\n }\n\n if (!token || !token.trim()) {\n return {\n ok: false,\n status: 401,\n code: \"unauthorized\",\n message: \"Empty authentication token\",\n };\n }\n\n // Decode (without verifying yet)\n const parts = token.split(\".\");\n if (parts.length !== 3) {\n return {\n ok: false,\n status: 401,\n code: \"unauthorized\",\n message: \"Malformed JWT: expected 3 parts\",\n };\n }\n\n let header: JwtHeader;\n let payload: JwtPayload;\n try {\n header = JSON.parse(base64UrlDecode(parts[0]));\n payload = JSON.parse(base64UrlDecode(parts[1]));\n } catch {\n return {\n ok: false,\n status: 401,\n code: \"unauthorized\",\n message: \"Malformed JWT: invalid base64 encoding\",\n };\n }\n\n // Block \"none\" algorithm (case-insensitive to prevent bypass)\n if (header.alg.toLowerCase() === \"none\") {\n return {\n ok: false,\n status: 401,\n code: \"unauthorized\",\n message: \"JWT algorithm 'none' is not allowed\",\n };\n }\n\n // Verify signature\n if (config.secret) {\n debug(`HMAC verification (alg=${header.alg})`);\n await verifyHmac(config.secret, parts[0], parts[1], parts[2], header.alg);\n } else if (config.jwksUrl) {\n debug(`JWKS verification (alg=${header.alg}, kid=${header.kid ?? \"none\"})`);\n await verifyJwks(\n config.jwksUrl,\n parts[0],\n parts[1],\n parts[2],\n header,\n config.jwksCacheTtlMs,\n config.jwksTimeoutMs\n );\n }\n\n // Validate claims\n const now = Math.floor(Date.now() / 1000);\n\n if (config.requireExp && payload.exp === undefined) {\n return {\n ok: false,\n status: 401,\n code: \"unauthorized\",\n message: \"JWT must contain an 'exp' claim\",\n };\n }\n\n if (\n payload.exp !== undefined &&\n payload.exp < now - config.clockSkewSeconds!\n ) {\n return {\n ok: false,\n status: 401,\n code: \"unauthorized\",\n message: \"JWT has expired\",\n };\n }\n\n if (config.issuer && payload.iss !== config.issuer) {\n return {\n ok: false,\n status: 401,\n code: \"unauthorized\",\n message: \"JWT issuer mismatch\",\n };\n }\n\n if (config.audience) {\n const aud = Array.isArray(payload.aud) ? payload.aud : [payload.aud];\n if (!aud.includes(config.audience)) {\n return {\n ok: false,\n status: 401,\n code: \"unauthorized\",\n message: \"JWT audience mismatch\",\n };\n }\n }\n\n debug(`verified (sub=${payload.sub ?? \"none\"})`);\n return { ok: true, payload };\n}\n\n// ─── Policy Definition ────────────────────────────────────────────────\n\n/**\n * Validate JWT tokens and optionally forward claims as upstream headers.\n *\n * Supports both HMAC (shared secret) and RSA (JWKS endpoint) verification.\n * JWKS responses are cached for 5 minutes. The `none` algorithm is always\n * rejected to prevent signature bypass attacks.\n *\n * @param config - JWT authentication settings. Requires either `secret` (HMAC) or `jwksUrl` (RSA).\n * @returns A {@link Policy} at priority 10 (runs early, before rate limiting).\n *\n * @example\n * ```ts\n * // HMAC verification with a shared secret\n * createGateway({\n * routes: [{\n * path: \"/api/*\",\n * pipeline: {\n * policies: [jwtAuth({ secret: env.JWT_SECRET })],\n * upstream: { type: \"url\", target: \"https://backend.internal\" },\n * },\n * }],\n * });\n *\n * // JWKS verification (e.g. Supabase, Auth0) with claim forwarding\n * jwtAuth({\n * jwksUrl: \"https://your-project.supabase.co/auth/v1/.well-known/jwks.json\",\n * issuer: \"https://your-project.supabase.co/auth/v1\",\n * forwardClaims: { sub: \"x-user-id\", email: \"x-user-email\" },\n * });\n * ```\n */\nexport const jwtAuth = /*#__PURE__*/ definePolicy<JwtAuthConfig>({\n name: \"jwt-auth\",\n priority: Priority.AUTH,\n phases: [\"request-headers\"],\n defaults: {\n headerName: \"authorization\",\n tokenPrefix: \"Bearer\",\n clockSkewSeconds: 0,\n requireExp: false,\n },\n validate: (config) => {\n if (!config.secret && !config.jwksUrl) {\n throw new GatewayError(\n 500,\n \"config_error\",\n \"jwtAuth requires either 'secret' or 'jwksUrl'\"\n );\n }\n },\n handler: async (c, next, { config, debug }) => {\n const result = await validateJwt(\n c.req.header(config.headerName!),\n config,\n debug\n );\n\n if (!result.ok) {\n throw new GatewayError(result.status, result.code, result.message);\n }\n\n // Forward claims as headers (sanitize to prevent header injection)\n if (config.forwardClaims) {\n const { payload } = result;\n const forwardClaims = config.forwardClaims;\n withModifiedHeaders(c, (headers) => {\n for (const [claim, headerKey] of Object.entries(forwardClaims)) {\n const value = payload[claim];\n if (value !== undefined && value !== null) {\n headers.set(headerKey, sanitizeHeaderValue(String(value)));\n }\n }\n });\n }\n\n await next();\n },\n evaluate: {\n onRequest: async (input, { config, debug }) => {\n const result = await validateJwt(\n input.headers.get(config.headerName!),\n config,\n debug\n );\n\n if (!result.ok) {\n return {\n action: \"reject\",\n status: result.status,\n code: result.code,\n message: result.message,\n };\n }\n\n // Forward claims as header mutations\n if (config.forwardClaims) {\n const { payload } = result;\n const forwardClaims = config.forwardClaims;\n const mutations = [];\n for (const [claim, headerKey] of Object.entries(forwardClaims)) {\n const value = payload[claim];\n if (value !== undefined && value !== null) {\n mutations.push({\n type: \"header\" as const,\n op: \"set\" as const,\n name: headerKey,\n value: sanitizeHeaderValue(String(value)),\n });\n }\n }\n if (mutations.length > 0) {\n return { action: \"continue\", mutations };\n }\n }\n\n return { action: \"continue\" };\n },\n },\n});\n\nasync function verifyHmac(\n secret: string,\n headerB64: string,\n payloadB64: string,\n signatureB64: string,\n alg: string\n): Promise<void> {\n const algorithm = hmacAlgorithm(alg);\n if (!algorithm) {\n throw new GatewayError(\n 401,\n \"unauthorized\",\n `Unsupported JWT algorithm: ${alg}`\n );\n }\n\n const encoder = new TextEncoder();\n const key = await crypto.subtle.importKey(\n \"raw\",\n encoder.encode(secret),\n { name: \"HMAC\", hash: algorithm },\n false,\n [\"verify\"]\n );\n\n const data = encoder.encode(`${headerB64}.${payloadB64}`);\n const signature = base64UrlToBuffer(signatureB64);\n\n const valid = await crypto.subtle.verify(\"HMAC\", key, signature, data);\n if (!valid) {\n throw new GatewayError(401, \"unauthorized\", \"Invalid JWT signature\");\n }\n}\n\nasync function verifyJwks(\n jwksUrl: string,\n headerB64: string,\n payloadB64: string,\n signatureB64: string,\n header: JwtHeader,\n cacheTtlMs?: number,\n timeoutMs?: number\n): Promise<void> {\n const keys = await fetchJwks(jwksUrl, cacheTtlMs, timeoutMs);\n const matchingKey = header.kid\n ? keys.find(\n (k) => (k as unknown as Record<string, unknown>).kid === header.kid\n )\n : keys[0];\n\n if (!matchingKey) {\n throw new GatewayError(401, \"unauthorized\", \"No matching JWKS key found\");\n }\n\n const algorithm = rsaAlgorithm(header.alg);\n if (!algorithm) {\n throw new GatewayError(\n 401,\n \"unauthorized\",\n `Unsupported JWT algorithm: ${header.alg}`\n );\n }\n\n const key = await crypto.subtle.importKey(\n \"jwk\",\n matchingKey,\n algorithm,\n false,\n [\"verify\"]\n );\n\n const encoder = new TextEncoder();\n const data = encoder.encode(`${headerB64}.${payloadB64}`);\n const signature = base64UrlToBuffer(signatureB64);\n\n const valid = await crypto.subtle.verify(algorithm, key, signature, data);\n\n if (!valid) {\n throw new GatewayError(401, \"unauthorized\", \"Invalid JWT signature\");\n }\n}\n\n/**\n * @deprecated Use `clearJwksCache` from `./crypto` instead. This re-export\n * exists for backwards compatibility with existing tests.\n */\nexport { clearJwksCache } from \"./crypto\";\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { g as PolicyConfig, P as Policy } from '../../protocol-2fD3DJrL.js';
|
|
2
|
+
import 'hono';
|
|
3
|
+
import '../sdk/trace.js';
|
|
4
|
+
import '@vivero/stoma-core';
|
|
5
|
+
|
|
6
|
+
interface OAuth2Config extends PolicyConfig {
|
|
7
|
+
/** OAuth2 token introspection endpoint (RFC 7662). */
|
|
8
|
+
introspectionUrl?: string;
|
|
9
|
+
/** Client ID for authenticating with the introspection endpoint. */
|
|
10
|
+
clientId?: string;
|
|
11
|
+
/** Client secret for authenticating with the introspection endpoint. */
|
|
12
|
+
clientSecret?: string;
|
|
13
|
+
/** Local validation function as alternative to introspection. Takes precedence if both provided. */
|
|
14
|
+
localValidate?: (token: string) => boolean | Promise<boolean>;
|
|
15
|
+
/** Where to look for the token. Default: "header". */
|
|
16
|
+
tokenLocation?: "header" | "query";
|
|
17
|
+
/** Header name when tokenLocation is "header". Default: "authorization". */
|
|
18
|
+
headerName?: string;
|
|
19
|
+
/** Prefix to strip from header value. Default: "Bearer". */
|
|
20
|
+
headerPrefix?: string;
|
|
21
|
+
/** Query param name when tokenLocation is "query". Default: "access_token". */
|
|
22
|
+
queryParam?: string;
|
|
23
|
+
/** Map introspection response fields to request headers. Only applies with introspection. */
|
|
24
|
+
forwardTokenInfo?: Record<string, string>;
|
|
25
|
+
/** Cache introspection results for this many seconds. Default: 0 (no cache). */
|
|
26
|
+
cacheTtlSeconds?: number;
|
|
27
|
+
/** Maximum number of tokens to cache. Default: 100. */
|
|
28
|
+
cacheMaxEntries?: number;
|
|
29
|
+
/** Required scopes - token must have ALL of these (space-separated scope string). */
|
|
30
|
+
requiredScopes?: string[];
|
|
31
|
+
/** Introspection endpoint fetch timeout in milliseconds. Default: 5000. */
|
|
32
|
+
introspectionTimeoutMs?: number;
|
|
33
|
+
}
|
|
34
|
+
/** Clear the introspection cache. Exported for testing. */
|
|
35
|
+
declare function clearOAuth2Cache(): void;
|
|
36
|
+
declare const oauth2: (config?: OAuth2Config | undefined) => Policy;
|
|
37
|
+
|
|
38
|
+
export { type OAuth2Config, clearOAuth2Cache, oauth2 };
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { GatewayError } from "../../core/errors";
|
|
2
|
+
import { sanitizeHeaderValue, withModifiedHeaders } from "../../utils/headers";
|
|
3
|
+
import { definePolicy, Priority } from "../sdk";
|
|
4
|
+
const DEFAULT_MAX_CACHE_ENTRIES = 100;
|
|
5
|
+
const introspectionCache = /* @__PURE__ */ new Map();
|
|
6
|
+
function evictIfNeeded(maxSize) {
|
|
7
|
+
if (introspectionCache.size >= maxSize) {
|
|
8
|
+
const oldestKey = introspectionCache.keys().next().value;
|
|
9
|
+
if (oldestKey) {
|
|
10
|
+
introspectionCache.delete(oldestKey);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function clearOAuth2Cache() {
|
|
15
|
+
introspectionCache.clear();
|
|
16
|
+
}
|
|
17
|
+
const oauth2 = /* @__PURE__ */ definePolicy({
|
|
18
|
+
name: "oauth2",
|
|
19
|
+
priority: Priority.AUTH,
|
|
20
|
+
phases: ["request-headers"],
|
|
21
|
+
defaults: {
|
|
22
|
+
tokenLocation: "header",
|
|
23
|
+
headerName: "authorization",
|
|
24
|
+
headerPrefix: "Bearer",
|
|
25
|
+
queryParam: "access_token",
|
|
26
|
+
cacheTtlSeconds: 0
|
|
27
|
+
},
|
|
28
|
+
validate: (config) => {
|
|
29
|
+
if (!config.introspectionUrl && !config.localValidate) {
|
|
30
|
+
throw new GatewayError(
|
|
31
|
+
500,
|
|
32
|
+
"config_error",
|
|
33
|
+
"oauth2 requires either introspectionUrl or localValidate"
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
handler: async (c, next, { config, debug }) => {
|
|
38
|
+
let token;
|
|
39
|
+
if (config.tokenLocation === "query") {
|
|
40
|
+
token = c.req.query(config.queryParam) ?? void 0;
|
|
41
|
+
} else {
|
|
42
|
+
const headerValue = c.req.header(config.headerName);
|
|
43
|
+
if (headerValue && config.headerPrefix) {
|
|
44
|
+
const prefix = `${config.headerPrefix} `;
|
|
45
|
+
if (headerValue.startsWith(prefix)) {
|
|
46
|
+
token = headerValue.slice(prefix.length);
|
|
47
|
+
} else {
|
|
48
|
+
token = void 0;
|
|
49
|
+
}
|
|
50
|
+
} else {
|
|
51
|
+
token = headerValue ?? void 0;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (!token || !token.trim()) {
|
|
55
|
+
throw new GatewayError(401, "unauthorized", "Missing access token");
|
|
56
|
+
}
|
|
57
|
+
if (config.localValidate) {
|
|
58
|
+
debug("local validation");
|
|
59
|
+
const valid = await config.localValidate(token);
|
|
60
|
+
if (!valid) {
|
|
61
|
+
throw new GatewayError(401, "unauthorized", "Token validation failed");
|
|
62
|
+
}
|
|
63
|
+
} else if (config.introspectionUrl) {
|
|
64
|
+
debug("introspection validation");
|
|
65
|
+
const introspectionResult = await introspect(
|
|
66
|
+
token,
|
|
67
|
+
config.introspectionUrl,
|
|
68
|
+
config.clientId,
|
|
69
|
+
config.clientSecret,
|
|
70
|
+
config.cacheTtlSeconds ?? 0,
|
|
71
|
+
config.introspectionTimeoutMs,
|
|
72
|
+
config.cacheMaxEntries
|
|
73
|
+
);
|
|
74
|
+
if (!introspectionResult.active) {
|
|
75
|
+
throw new GatewayError(401, "unauthorized", "Token is not active");
|
|
76
|
+
}
|
|
77
|
+
if (config.requiredScopes && config.requiredScopes.length > 0) {
|
|
78
|
+
const tokenScopes = introspectionResult.scope ? introspectionResult.scope.split(" ") : [];
|
|
79
|
+
const missing = config.requiredScopes.filter(
|
|
80
|
+
(s) => !tokenScopes.includes(s)
|
|
81
|
+
);
|
|
82
|
+
if (missing.length > 0) {
|
|
83
|
+
throw new GatewayError(403, "forbidden", "Insufficient scope");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (config.forwardTokenInfo) {
|
|
87
|
+
const fwd = config.forwardTokenInfo;
|
|
88
|
+
withModifiedHeaders(c, (headers) => {
|
|
89
|
+
for (const [field, headerKey] of Object.entries(fwd)) {
|
|
90
|
+
const value = introspectionResult[field];
|
|
91
|
+
if (value !== void 0 && value !== null) {
|
|
92
|
+
headers.set(headerKey, sanitizeHeaderValue(String(value)));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
await next();
|
|
99
|
+
},
|
|
100
|
+
evaluate: {
|
|
101
|
+
onRequest: async (input, { config, debug }) => {
|
|
102
|
+
let token;
|
|
103
|
+
if (config.tokenLocation === "query") {
|
|
104
|
+
const url = new URL(input.path, "http://localhost");
|
|
105
|
+
token = url.searchParams.get(config.queryParam) ?? void 0;
|
|
106
|
+
} else {
|
|
107
|
+
const headerValue = input.headers.get(config.headerName) ?? void 0;
|
|
108
|
+
if (headerValue && config.headerPrefix) {
|
|
109
|
+
const prefix = `${config.headerPrefix} `;
|
|
110
|
+
if (headerValue.startsWith(prefix)) {
|
|
111
|
+
token = headerValue.slice(prefix.length);
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
token = headerValue;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (!token || !token.trim()) {
|
|
118
|
+
return {
|
|
119
|
+
action: "reject",
|
|
120
|
+
status: 401,
|
|
121
|
+
code: "unauthorized",
|
|
122
|
+
message: "Missing access token"
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
if (config.localValidate) {
|
|
126
|
+
debug("local validation");
|
|
127
|
+
const valid = await config.localValidate(token);
|
|
128
|
+
if (!valid) {
|
|
129
|
+
return {
|
|
130
|
+
action: "reject",
|
|
131
|
+
status: 401,
|
|
132
|
+
code: "unauthorized",
|
|
133
|
+
message: "Token validation failed"
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
return { action: "continue" };
|
|
137
|
+
} else if (config.introspectionUrl) {
|
|
138
|
+
debug("introspection validation");
|
|
139
|
+
const introspectionResult = await introspect(
|
|
140
|
+
token,
|
|
141
|
+
config.introspectionUrl,
|
|
142
|
+
config.clientId,
|
|
143
|
+
config.clientSecret,
|
|
144
|
+
config.cacheTtlSeconds ?? 0,
|
|
145
|
+
config.introspectionTimeoutMs,
|
|
146
|
+
config.cacheMaxEntries
|
|
147
|
+
);
|
|
148
|
+
if (!introspectionResult.active) {
|
|
149
|
+
return {
|
|
150
|
+
action: "reject",
|
|
151
|
+
status: 401,
|
|
152
|
+
code: "unauthorized",
|
|
153
|
+
message: "Token is not active"
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
if (config.requiredScopes && config.requiredScopes.length > 0) {
|
|
157
|
+
const tokenScopes = introspectionResult.scope ? introspectionResult.scope.split(" ") : [];
|
|
158
|
+
const missing = config.requiredScopes.filter(
|
|
159
|
+
(s) => !tokenScopes.includes(s)
|
|
160
|
+
);
|
|
161
|
+
if (missing.length > 0) {
|
|
162
|
+
return {
|
|
163
|
+
action: "reject",
|
|
164
|
+
status: 403,
|
|
165
|
+
code: "forbidden",
|
|
166
|
+
message: "Insufficient scope"
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (config.forwardTokenInfo) {
|
|
171
|
+
const fwd = config.forwardTokenInfo;
|
|
172
|
+
const mutations = [];
|
|
173
|
+
for (const [field, headerKey] of Object.entries(fwd)) {
|
|
174
|
+
const value = introspectionResult[field];
|
|
175
|
+
if (value !== void 0 && value !== null) {
|
|
176
|
+
mutations.push({
|
|
177
|
+
type: "header",
|
|
178
|
+
op: "set",
|
|
179
|
+
name: headerKey,
|
|
180
|
+
value: sanitizeHeaderValue(String(value))
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (mutations.length > 0) {
|
|
185
|
+
return { action: "continue", mutations };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return { action: "continue" };
|
|
189
|
+
}
|
|
190
|
+
return { action: "continue" };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
const DEFAULT_INTROSPECTION_TIMEOUT_MS = 5e3;
|
|
195
|
+
async function introspect(token, url, clientId, clientSecret, cacheTtlSeconds = 0, timeoutMs, cacheMaxEntries = DEFAULT_MAX_CACHE_ENTRIES) {
|
|
196
|
+
if (cacheTtlSeconds > 0) {
|
|
197
|
+
const cached = introspectionCache.get(token);
|
|
198
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
199
|
+
introspectionCache.delete(token);
|
|
200
|
+
introspectionCache.set(token, cached);
|
|
201
|
+
return cached.result;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const headers = {
|
|
205
|
+
"content-type": "application/x-www-form-urlencoded"
|
|
206
|
+
};
|
|
207
|
+
if (clientId && clientSecret) {
|
|
208
|
+
headers.authorization = `Basic ${btoa(`${clientId}:${clientSecret}`)}`;
|
|
209
|
+
}
|
|
210
|
+
const timeout = timeoutMs ?? DEFAULT_INTROSPECTION_TIMEOUT_MS;
|
|
211
|
+
let response;
|
|
212
|
+
try {
|
|
213
|
+
response = await fetch(url, {
|
|
214
|
+
method: "POST",
|
|
215
|
+
headers,
|
|
216
|
+
body: `token=${encodeURIComponent(token)}`,
|
|
217
|
+
signal: AbortSignal.timeout(timeout)
|
|
218
|
+
});
|
|
219
|
+
} catch (err) {
|
|
220
|
+
if (err instanceof DOMException && err.name === "TimeoutError") {
|
|
221
|
+
throw new GatewayError(
|
|
222
|
+
502,
|
|
223
|
+
"introspection_error",
|
|
224
|
+
`Introspection endpoint timed out after ${timeout}ms`
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
throw new GatewayError(
|
|
228
|
+
502,
|
|
229
|
+
"introspection_error",
|
|
230
|
+
`Introspection endpoint error: ${err instanceof Error ? err.message : String(err)}`
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
if (!response.ok) {
|
|
234
|
+
throw new GatewayError(
|
|
235
|
+
502,
|
|
236
|
+
"introspection_error",
|
|
237
|
+
`Introspection endpoint returned ${response.status}`
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
const result = await response.json();
|
|
241
|
+
if (cacheTtlSeconds > 0) {
|
|
242
|
+
evictIfNeeded(cacheMaxEntries);
|
|
243
|
+
introspectionCache.set(token, {
|
|
244
|
+
result,
|
|
245
|
+
expiresAt: Date.now() + cacheTtlSeconds * 1e3
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
return result;
|
|
249
|
+
}
|
|
250
|
+
export {
|
|
251
|
+
clearOAuth2Cache,
|
|
252
|
+
oauth2
|
|
253
|
+
};
|
|
254
|
+
//# sourceMappingURL=oauth2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/policies/auth/oauth2.ts"],"sourcesContent":["/**\n * OAuth2 token validation policy via introspection or local validation.\n *\n * Validates bearer tokens issued by an external authorization server.\n * This is NOT an OAuth2 server - it only validates tokens.\n *\n * @module oauth2\n */\n\nimport { GatewayError } from \"../../core/errors\";\nimport type { Mutation } from \"../../core/protocol\";\nimport { sanitizeHeaderValue, withModifiedHeaders } from \"../../utils/headers\";\nimport { definePolicy, Priority } from \"../sdk\";\nimport type { PolicyConfig } from \"../types\";\n\nexport interface OAuth2Config extends PolicyConfig {\n /** OAuth2 token introspection endpoint (RFC 7662). */\n introspectionUrl?: string;\n /** Client ID for authenticating with the introspection endpoint. */\n clientId?: string;\n /** Client secret for authenticating with the introspection endpoint. */\n clientSecret?: string;\n /** Local validation function as alternative to introspection. Takes precedence if both provided. */\n localValidate?: (token: string) => boolean | Promise<boolean>;\n /** Where to look for the token. Default: \"header\". */\n tokenLocation?: \"header\" | \"query\";\n /** Header name when tokenLocation is \"header\". Default: \"authorization\". */\n headerName?: string;\n /** Prefix to strip from header value. Default: \"Bearer\". */\n headerPrefix?: string;\n /** Query param name when tokenLocation is \"query\". Default: \"access_token\". */\n queryParam?: string;\n /** Map introspection response fields to request headers. Only applies with introspection. */\n forwardTokenInfo?: Record<string, string>;\n /** Cache introspection results for this many seconds. Default: 0 (no cache). */\n cacheTtlSeconds?: number;\n /** Maximum number of tokens to cache. Default: 100. */\n cacheMaxEntries?: number;\n /** Required scopes - token must have ALL of these (space-separated scope string). */\n requiredScopes?: string[];\n /** Introspection endpoint fetch timeout in milliseconds. Default: 5000. */\n introspectionTimeoutMs?: number;\n}\n\ninterface IntrospectionResult {\n active: boolean;\n scope?: string;\n [key: string]: unknown;\n}\n\ninterface CacheEntry {\n result: IntrospectionResult;\n expiresAt: number;\n}\n\n/** Maximum number of entries in the introspection cache. */\nconst DEFAULT_MAX_CACHE_ENTRIES = 100;\n\n/** Module-level introspection cache with LRU eviction. */\nconst introspectionCache = new Map<string, CacheEntry>();\n\n/** Evict oldest entries when cache exceeds max size. */\nfunction evictIfNeeded(maxSize: number): void {\n if (introspectionCache.size >= maxSize) {\n const oldestKey = introspectionCache.keys().next().value;\n if (oldestKey) {\n introspectionCache.delete(oldestKey);\n }\n }\n}\n\n/** Clear the introspection cache. Exported for testing. */\nexport function clearOAuth2Cache(): void {\n introspectionCache.clear();\n}\n\nexport const oauth2 = /*#__PURE__*/ definePolicy<OAuth2Config>({\n name: \"oauth2\",\n priority: Priority.AUTH,\n phases: [\"request-headers\"],\n defaults: {\n tokenLocation: \"header\",\n headerName: \"authorization\",\n headerPrefix: \"Bearer\",\n queryParam: \"access_token\",\n cacheTtlSeconds: 0,\n },\n validate: (config) => {\n if (!config.introspectionUrl && !config.localValidate) {\n throw new GatewayError(\n 500,\n \"config_error\",\n \"oauth2 requires either introspectionUrl or localValidate\"\n );\n }\n },\n handler: async (c, next, { config, debug }) => {\n // 1. Extract token\n let token: string | undefined;\n\n if (config.tokenLocation === \"query\") {\n token = c.req.query(config.queryParam!) ?? undefined;\n } else {\n const headerValue = c.req.header(config.headerName!);\n if (headerValue && config.headerPrefix) {\n const prefix = `${config.headerPrefix} `;\n if (headerValue.startsWith(prefix)) {\n token = headerValue.slice(prefix.length);\n } else {\n token = undefined;\n }\n } else {\n token = headerValue ?? undefined;\n }\n }\n\n if (!token || !token.trim()) {\n throw new GatewayError(401, \"unauthorized\", \"Missing access token\");\n }\n\n // 2. Validate token\n if (config.localValidate) {\n debug(\"local validation\");\n const valid = await config.localValidate(token);\n if (!valid) {\n throw new GatewayError(401, \"unauthorized\", \"Token validation failed\");\n }\n } else if (config.introspectionUrl) {\n debug(\"introspection validation\");\n const introspectionResult = await introspect(\n token,\n config.introspectionUrl,\n config.clientId,\n config.clientSecret,\n config.cacheTtlSeconds ?? 0,\n config.introspectionTimeoutMs,\n config.cacheMaxEntries\n );\n\n if (!introspectionResult.active) {\n throw new GatewayError(401, \"unauthorized\", \"Token is not active\");\n }\n\n // Check required scopes\n if (config.requiredScopes && config.requiredScopes.length > 0) {\n const tokenScopes = introspectionResult.scope\n ? introspectionResult.scope.split(\" \")\n : [];\n const missing = config.requiredScopes.filter(\n (s) => !tokenScopes.includes(s)\n );\n if (missing.length > 0) {\n throw new GatewayError(403, \"forbidden\", \"Insufficient scope\");\n }\n }\n\n // Forward token info as headers\n if (config.forwardTokenInfo) {\n const fwd = config.forwardTokenInfo;\n withModifiedHeaders(c, (headers) => {\n for (const [field, headerKey] of Object.entries(fwd)) {\n const value = introspectionResult[field];\n if (value !== undefined && value !== null) {\n headers.set(headerKey, sanitizeHeaderValue(String(value)));\n }\n }\n });\n }\n }\n\n await next();\n },\n evaluate: {\n onRequest: async (input, { config, debug }) => {\n // 1. Extract token\n let token: string | undefined;\n\n if (config.tokenLocation === \"query\") {\n const url = new URL(input.path, \"http://localhost\");\n token = url.searchParams.get(config.queryParam!) ?? undefined;\n } else {\n const headerValue = input.headers.get(config.headerName!) ?? undefined;\n if (headerValue && config.headerPrefix) {\n const prefix = `${config.headerPrefix} `;\n if (headerValue.startsWith(prefix)) {\n token = headerValue.slice(prefix.length);\n }\n } else {\n token = headerValue;\n }\n }\n\n if (!token || !token.trim()) {\n return {\n action: \"reject\",\n status: 401,\n code: \"unauthorized\",\n message: \"Missing access token\",\n };\n }\n\n // 2. Validate token\n if (config.localValidate) {\n debug(\"local validation\");\n const valid = await config.localValidate(token);\n if (!valid) {\n return {\n action: \"reject\",\n status: 401,\n code: \"unauthorized\",\n message: \"Token validation failed\",\n };\n }\n return { action: \"continue\" };\n } else if (config.introspectionUrl) {\n debug(\"introspection validation\");\n const introspectionResult = await introspect(\n token,\n config.introspectionUrl,\n config.clientId,\n config.clientSecret,\n config.cacheTtlSeconds ?? 0,\n config.introspectionTimeoutMs,\n config.cacheMaxEntries\n );\n\n if (!introspectionResult.active) {\n return {\n action: \"reject\",\n status: 401,\n code: \"unauthorized\",\n message: \"Token is not active\",\n };\n }\n\n // Check required scopes\n if (config.requiredScopes && config.requiredScopes.length > 0) {\n const tokenScopes = introspectionResult.scope\n ? introspectionResult.scope.split(\" \")\n : [];\n const missing = config.requiredScopes.filter(\n (s) => !tokenScopes.includes(s)\n );\n if (missing.length > 0) {\n return {\n action: \"reject\",\n status: 403,\n code: \"forbidden\",\n message: \"Insufficient scope\",\n };\n }\n }\n\n // Forward token info as headers via mutations\n if (config.forwardTokenInfo) {\n const fwd = config.forwardTokenInfo;\n const mutations: Mutation[] = [];\n for (const [field, headerKey] of Object.entries(fwd)) {\n const value = introspectionResult[field];\n if (value !== undefined && value !== null) {\n mutations.push({\n type: \"header\" as const,\n op: \"set\" as const,\n name: headerKey,\n value: sanitizeHeaderValue(String(value)),\n });\n }\n }\n if (mutations.length > 0) {\n return { action: \"continue\", mutations };\n }\n }\n\n return { action: \"continue\" };\n }\n\n return { action: \"continue\" };\n },\n },\n});\n\nconst DEFAULT_INTROSPECTION_TIMEOUT_MS = 5_000;\n\nasync function introspect(\n token: string,\n url: string,\n clientId?: string,\n clientSecret?: string,\n cacheTtlSeconds = 0,\n timeoutMs?: number,\n cacheMaxEntries = DEFAULT_MAX_CACHE_ENTRIES\n): Promise<IntrospectionResult> {\n // Check cache (with LRU: move to end when accessed)\n if (cacheTtlSeconds > 0) {\n const cached = introspectionCache.get(token);\n if (cached && cached.expiresAt > Date.now()) {\n // LRU: re-insert to move to end\n introspectionCache.delete(token);\n introspectionCache.set(token, cached);\n return cached.result;\n }\n }\n\n const headers: Record<string, string> = {\n \"content-type\": \"application/x-www-form-urlencoded\",\n };\n\n if (clientId && clientSecret) {\n headers.authorization = `Basic ${btoa(`${clientId}:${clientSecret}`)}`;\n }\n\n const timeout = timeoutMs ?? DEFAULT_INTROSPECTION_TIMEOUT_MS;\n let response: Response;\n try {\n response = await fetch(url, {\n method: \"POST\",\n headers,\n body: `token=${encodeURIComponent(token)}`,\n signal: AbortSignal.timeout(timeout),\n });\n } catch (err) {\n if (err instanceof DOMException && err.name === \"TimeoutError\") {\n throw new GatewayError(\n 502,\n \"introspection_error\",\n `Introspection endpoint timed out after ${timeout}ms`\n );\n }\n throw new GatewayError(\n 502,\n \"introspection_error\",\n `Introspection endpoint error: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n\n if (!response.ok) {\n throw new GatewayError(\n 502,\n \"introspection_error\",\n `Introspection endpoint returned ${response.status}`\n );\n }\n\n const result = (await response.json()) as IntrospectionResult;\n\n // Cache result\n if (cacheTtlSeconds > 0) {\n evictIfNeeded(cacheMaxEntries);\n introspectionCache.set(token, {\n result,\n expiresAt: Date.now() + cacheTtlSeconds * 1000,\n });\n }\n\n return result;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { g as PolicyConfig, P as Policy } from '../../protocol-2fD3DJrL.js';
|
|
2
|
+
import 'hono';
|
|
3
|
+
import '../sdk/trace.js';
|
|
4
|
+
import '@vivero/stoma-core';
|
|
5
|
+
|
|
6
|
+
interface RbacConfig extends PolicyConfig {
|
|
7
|
+
/** Header name containing the user's role(s). Default: "x-user-role". */
|
|
8
|
+
roleHeader?: string;
|
|
9
|
+
/** Allowed roles - pass if user has ANY of these. */
|
|
10
|
+
roles?: string[];
|
|
11
|
+
/** Required permissions - pass if user has ALL of these. */
|
|
12
|
+
permissions?: string[];
|
|
13
|
+
/** Header containing permissions. Default: "x-user-permissions". */
|
|
14
|
+
permissionHeader?: string;
|
|
15
|
+
/** Delimiter for permission string. Default: ",". */
|
|
16
|
+
permissionDelimiter?: string;
|
|
17
|
+
/** Delimiter for role string. Default: ",". */
|
|
18
|
+
roleDelimiter?: string;
|
|
19
|
+
/** Custom deny message. Default: "Access denied: insufficient permissions". */
|
|
20
|
+
denyMessage?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Strip role/permission headers from incoming requests for security.
|
|
23
|
+
* These headers should only be set by trusted upstream auth policies,
|
|
24
|
+
* not by external clients. Default: true.
|
|
25
|
+
*/
|
|
26
|
+
stripHeaders?: boolean;
|
|
27
|
+
}
|
|
28
|
+
declare const rbac: (config?: RbacConfig | undefined) => Policy;
|
|
29
|
+
|
|
30
|
+
export { type RbacConfig, rbac };
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { GatewayError } from "../../core/errors";
|
|
2
|
+
import { withModifiedHeaders } from "../../utils/headers";
|
|
3
|
+
import { definePolicy, Priority } from "../sdk";
|
|
4
|
+
const rbac = /* @__PURE__ */ definePolicy({
|
|
5
|
+
name: "rbac",
|
|
6
|
+
priority: Priority.AUTH,
|
|
7
|
+
defaults: {
|
|
8
|
+
roleHeader: "x-user-role",
|
|
9
|
+
permissionHeader: "x-user-permissions",
|
|
10
|
+
permissionDelimiter: ",",
|
|
11
|
+
roleDelimiter: ",",
|
|
12
|
+
denyMessage: "Access denied: insufficient permissions",
|
|
13
|
+
stripHeaders: true
|
|
14
|
+
},
|
|
15
|
+
phases: ["request-headers"],
|
|
16
|
+
handler: async (c, next, { config, debug }) => {
|
|
17
|
+
if (config.stripHeaders) {
|
|
18
|
+
withModifiedHeaders(c, (headers) => {
|
|
19
|
+
if (config.roleHeader && headers.has(config.roleHeader)) {
|
|
20
|
+
headers.delete(config.roleHeader);
|
|
21
|
+
debug("stripped role header from incoming request");
|
|
22
|
+
}
|
|
23
|
+
if (config.permissionHeader && headers.has(config.permissionHeader)) {
|
|
24
|
+
headers.delete(config.permissionHeader);
|
|
25
|
+
debug("stripped permission header from incoming request");
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
const hasRoleCheck = config.roles && config.roles.length > 0;
|
|
30
|
+
const hasPermCheck = config.permissions && config.permissions.length > 0;
|
|
31
|
+
if (!hasRoleCheck && !hasPermCheck) {
|
|
32
|
+
debug("no roles or permissions configured, passing through");
|
|
33
|
+
await next();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (hasRoleCheck) {
|
|
37
|
+
const roleHeaderValue = c.req.header(config.roleHeader) ?? "";
|
|
38
|
+
const userRoles = roleHeaderValue ? roleHeaderValue.split(config.roleDelimiter).map((r) => r.trim()) : [];
|
|
39
|
+
debug(
|
|
40
|
+
`checking roles: user=${userRoles.join(",")} required=${config.roles.join(",")}`
|
|
41
|
+
);
|
|
42
|
+
const hasMatchingRole = config.roles.some(
|
|
43
|
+
(role) => userRoles.includes(role)
|
|
44
|
+
);
|
|
45
|
+
if (!hasMatchingRole) {
|
|
46
|
+
throw new GatewayError(403, "forbidden", config.denyMessage);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (hasPermCheck) {
|
|
50
|
+
const permHeaderValue = c.req.header(config.permissionHeader) ?? "";
|
|
51
|
+
const userPermissions = permHeaderValue ? permHeaderValue.split(config.permissionDelimiter).map((p) => p.trim()) : [];
|
|
52
|
+
debug(
|
|
53
|
+
`checking permissions: user=${userPermissions.join(",")} required=${config.permissions.join(",")}`
|
|
54
|
+
);
|
|
55
|
+
const hasAllPermissions = config.permissions.every(
|
|
56
|
+
(perm) => userPermissions.includes(perm)
|
|
57
|
+
);
|
|
58
|
+
if (!hasAllPermissions) {
|
|
59
|
+
throw new GatewayError(403, "forbidden", config.denyMessage);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
await next();
|
|
63
|
+
},
|
|
64
|
+
evaluate: {
|
|
65
|
+
onRequest: async (input, { config, debug }) => {
|
|
66
|
+
const hasRoleCheck = config.roles && config.roles.length > 0;
|
|
67
|
+
const hasPermCheck = config.permissions && config.permissions.length > 0;
|
|
68
|
+
if (!hasRoleCheck && !hasPermCheck) {
|
|
69
|
+
debug("no roles or permissions configured, passing through");
|
|
70
|
+
return { action: "continue" };
|
|
71
|
+
}
|
|
72
|
+
if (hasRoleCheck) {
|
|
73
|
+
const roleHeaderValue = input.headers.get(config.roleHeader) ?? "";
|
|
74
|
+
const userRoles = roleHeaderValue ? roleHeaderValue.split(config.roleDelimiter).map((r) => r.trim()) : [];
|
|
75
|
+
debug(
|
|
76
|
+
`checking roles: user=${userRoles.join(",")} required=${config.roles.join(",")}`
|
|
77
|
+
);
|
|
78
|
+
const hasMatchingRole = config.roles.some(
|
|
79
|
+
(role) => userRoles.includes(role)
|
|
80
|
+
);
|
|
81
|
+
if (!hasMatchingRole) {
|
|
82
|
+
return {
|
|
83
|
+
action: "reject",
|
|
84
|
+
status: 403,
|
|
85
|
+
code: "forbidden",
|
|
86
|
+
message: config.denyMessage
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (hasPermCheck) {
|
|
91
|
+
const permHeaderValue = input.headers.get(config.permissionHeader) ?? "";
|
|
92
|
+
const userPermissions = permHeaderValue ? permHeaderValue.split(config.permissionDelimiter).map((p) => p.trim()) : [];
|
|
93
|
+
debug(
|
|
94
|
+
`checking permissions: user=${userPermissions.join(",")} required=${config.permissions.join(",")}`
|
|
95
|
+
);
|
|
96
|
+
const hasAllPermissions = config.permissions.every(
|
|
97
|
+
(perm) => userPermissions.includes(perm)
|
|
98
|
+
);
|
|
99
|
+
if (!hasAllPermissions) {
|
|
100
|
+
return {
|
|
101
|
+
action: "reject",
|
|
102
|
+
status: 403,
|
|
103
|
+
code: "forbidden",
|
|
104
|
+
message: config.denyMessage
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return { action: "continue" };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
export {
|
|
113
|
+
rbac
|
|
114
|
+
};
|
|
115
|
+
//# sourceMappingURL=rbac.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/policies/auth/rbac.ts"],"sourcesContent":["/**\n * Role-based access control policy.\n *\n * Uses claims forwarded as request headers by upstream auth policies\n * (jwt-auth's `forwardClaims` or oauth2's `forwardTokenInfo`).\n *\n * @module rbac\n */\n\nimport { GatewayError } from \"../../core/errors\";\nimport { withModifiedHeaders } from \"../../utils/headers\";\nimport { definePolicy, Priority } from \"../sdk\";\nimport type { PolicyConfig } from \"../types\";\n\nexport interface RbacConfig extends PolicyConfig {\n /** Header name containing the user's role(s). Default: \"x-user-role\". */\n roleHeader?: string;\n /** Allowed roles - pass if user has ANY of these. */\n roles?: string[];\n /** Required permissions - pass if user has ALL of these. */\n permissions?: string[];\n /** Header containing permissions. Default: \"x-user-permissions\". */\n permissionHeader?: string;\n /** Delimiter for permission string. Default: \",\". */\n permissionDelimiter?: string;\n /** Delimiter for role string. Default: \",\". */\n roleDelimiter?: string;\n /** Custom deny message. Default: \"Access denied: insufficient permissions\". */\n denyMessage?: string;\n /**\n * Strip role/permission headers from incoming requests for security.\n * These headers should only be set by trusted upstream auth policies,\n * not by external clients. Default: true.\n */\n stripHeaders?: boolean;\n}\n\nexport const rbac = /*#__PURE__*/ definePolicy<RbacConfig>({\n name: \"rbac\",\n priority: Priority.AUTH,\n defaults: {\n roleHeader: \"x-user-role\",\n permissionHeader: \"x-user-permissions\",\n permissionDelimiter: \",\",\n roleDelimiter: \",\",\n denyMessage: \"Access denied: insufficient permissions\",\n stripHeaders: true,\n },\n phases: [\"request-headers\"],\n handler: async (c, next, { config, debug }) => {\n if (config.stripHeaders) {\n withModifiedHeaders(c, (headers) => {\n if (config.roleHeader && headers.has(config.roleHeader)) {\n headers.delete(config.roleHeader);\n debug(\"stripped role header from incoming request\");\n }\n if (config.permissionHeader && headers.has(config.permissionHeader)) {\n headers.delete(config.permissionHeader);\n debug(\"stripped permission header from incoming request\");\n }\n });\n }\n\n const hasRoleCheck = config.roles && config.roles.length > 0;\n const hasPermCheck = config.permissions && config.permissions.length > 0;\n\n // If neither is configured, pass through\n if (!hasRoleCheck && !hasPermCheck) {\n debug(\"no roles or permissions configured, passing through\");\n await next();\n return;\n }\n\n // Read roles from header\n if (hasRoleCheck) {\n const roleHeaderValue = c.req.header(config.roleHeader!) ?? \"\";\n const userRoles = roleHeaderValue\n ? roleHeaderValue.split(config.roleDelimiter!).map((r) => r.trim())\n : [];\n\n debug(\n `checking roles: user=${userRoles.join(\",\")} required=${config.roles!.join(\",\")}`\n );\n\n const hasMatchingRole = config.roles!.some((role) =>\n userRoles.includes(role)\n );\n if (!hasMatchingRole) {\n throw new GatewayError(403, \"forbidden\", config.denyMessage!);\n }\n }\n\n // Read permissions from header\n if (hasPermCheck) {\n const permHeaderValue = c.req.header(config.permissionHeader!) ?? \"\";\n const userPermissions = permHeaderValue\n ? permHeaderValue\n .split(config.permissionDelimiter!)\n .map((p) => p.trim())\n : [];\n\n debug(\n `checking permissions: user=${userPermissions.join(\",\")} required=${config.permissions!.join(\",\")}`\n );\n\n const hasAllPermissions = config.permissions!.every((perm) =>\n userPermissions.includes(perm)\n );\n if (!hasAllPermissions) {\n throw new GatewayError(403, \"forbidden\", config.denyMessage!);\n }\n }\n\n await next();\n },\n evaluate: {\n onRequest: async (input, { config, debug }) => {\n const hasRoleCheck = config.roles && config.roles.length > 0;\n const hasPermCheck = config.permissions && config.permissions.length > 0;\n\n if (!hasRoleCheck && !hasPermCheck) {\n debug(\"no roles or permissions configured, passing through\");\n return { action: \"continue\" };\n }\n\n if (hasRoleCheck) {\n const roleHeaderValue = input.headers.get(config.roleHeader!) ?? \"\";\n const userRoles = roleHeaderValue\n ? roleHeaderValue.split(config.roleDelimiter!).map((r) => r.trim())\n : [];\n\n debug(\n `checking roles: user=${userRoles.join(\",\")} required=${config.roles!.join(\",\")}`\n );\n\n const hasMatchingRole = config.roles!.some((role) =>\n userRoles.includes(role)\n );\n if (!hasMatchingRole) {\n return {\n action: \"reject\",\n status: 403,\n code: \"forbidden\",\n message: config.denyMessage!,\n };\n }\n }\n\n if (hasPermCheck) {\n const permHeaderValue =\n input.headers.get(config.permissionHeader!) ?? \"\";\n const userPermissions = permHeaderValue\n ? permHeaderValue\n .split(config.permissionDelimiter!)\n .map((p) => p.trim())\n : [];\n\n debug(\n `checking permissions: user=${userPermissions.join(\",\")} required=${config.permissions!.join(\",\")}`\n );\n\n const hasAllPermissions = config.permissions!.every((perm) =>\n userPermissions.includes(perm)\n );\n if (!hasAllPermissions) {\n return {\n action: \"reject\",\n status: 403,\n code: \"forbidden\",\n message: config.denyMessage!,\n };\n }\n }\n\n return { action: \"continue\" };\n },\n },\n});\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { g as PolicyConfig, P as Policy } from '../../protocol-2fD3DJrL.js';
|
|
2
|
+
import 'hono';
|
|
3
|
+
import '../sdk/trace.js';
|
|
4
|
+
import '@vivero/stoma-core';
|
|
5
|
+
|
|
6
|
+
interface HttpSignatureKey {
|
|
7
|
+
/** HMAC secret. */
|
|
8
|
+
secret?: string;
|
|
9
|
+
/** RSA public key as JWK. */
|
|
10
|
+
publicKey?: JsonWebKey;
|
|
11
|
+
/** Algorithm identifier. */
|
|
12
|
+
algorithm: string;
|
|
13
|
+
}
|
|
14
|
+
interface VerifyHttpSignatureConfig extends PolicyConfig {
|
|
15
|
+
/** Map of keyId to key material. */
|
|
16
|
+
keys: Record<string, HttpSignatureKey>;
|
|
17
|
+
/** Components that MUST be in the signature. Default: ["@method"]. */
|
|
18
|
+
requiredComponents?: string[];
|
|
19
|
+
/** Max signature age in seconds. Default: 300 (5 min). */
|
|
20
|
+
maxAge?: number;
|
|
21
|
+
/** Signature header name. Default: "Signature". */
|
|
22
|
+
signatureHeaderName?: string;
|
|
23
|
+
/** Signature-Input header name. Default: "Signature-Input". */
|
|
24
|
+
signatureInputHeaderName?: string;
|
|
25
|
+
/** Expected signature label. Default: "sig1". */
|
|
26
|
+
label?: string;
|
|
27
|
+
}
|
|
28
|
+
declare const verifyHttpSignature: (config: VerifyHttpSignatureConfig) => Policy;
|
|
29
|
+
|
|
30
|
+
export { type HttpSignatureKey, type VerifyHttpSignatureConfig, verifyHttpSignature };
|