@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,317 @@
|
|
|
1
|
+
import { GatewayError } from "../../core/errors";
|
|
2
|
+
import { sanitizeHeaderValue, withModifiedHeaders } from "../../utils/headers";
|
|
3
|
+
import { definePolicy, Priority } from "../sdk";
|
|
4
|
+
import {
|
|
5
|
+
base64UrlDecode,
|
|
6
|
+
base64UrlEncodeBytes,
|
|
7
|
+
base64UrlToBuffer,
|
|
8
|
+
fetchJwks,
|
|
9
|
+
hmacAlgorithm,
|
|
10
|
+
rsaAlgorithm
|
|
11
|
+
} from "./crypto";
|
|
12
|
+
import { clearJwksCache } from "./crypto";
|
|
13
|
+
const jws = /* @__PURE__ */ definePolicy({
|
|
14
|
+
name: "jws",
|
|
15
|
+
priority: Priority.AUTH,
|
|
16
|
+
defaults: {
|
|
17
|
+
headerName: "X-JWS-Signature",
|
|
18
|
+
payloadSource: "embedded",
|
|
19
|
+
forwardPayload: false,
|
|
20
|
+
forwardHeaderName: "X-JWS-Payload"
|
|
21
|
+
},
|
|
22
|
+
validate: (config) => {
|
|
23
|
+
if (!config.secret && !config.jwksUrl) {
|
|
24
|
+
throw new GatewayError(
|
|
25
|
+
500,
|
|
26
|
+
"config_error",
|
|
27
|
+
"jws requires either 'secret' or 'jwksUrl'"
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
handler: async (c, next, { config, debug }) => {
|
|
32
|
+
const jwsCompact = c.req.header(config.headerName);
|
|
33
|
+
if (!jwsCompact) {
|
|
34
|
+
throw new GatewayError(
|
|
35
|
+
401,
|
|
36
|
+
"jws_missing",
|
|
37
|
+
`Missing JWS header: ${config.headerName}`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
const parts = jwsCompact.split(".");
|
|
41
|
+
if (parts.length !== 3) {
|
|
42
|
+
throw new GatewayError(
|
|
43
|
+
401,
|
|
44
|
+
"jws_invalid",
|
|
45
|
+
"Malformed JWS: expected 3 parts"
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
const [headerB64, payloadB64, signatureB64] = parts;
|
|
49
|
+
let header;
|
|
50
|
+
try {
|
|
51
|
+
header = JSON.parse(base64UrlDecode(headerB64));
|
|
52
|
+
} catch {
|
|
53
|
+
throw new GatewayError(
|
|
54
|
+
401,
|
|
55
|
+
"jws_invalid",
|
|
56
|
+
"Malformed JWS: invalid header encoding"
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
if (header.alg.toLowerCase() === "none") {
|
|
60
|
+
throw new GatewayError(
|
|
61
|
+
401,
|
|
62
|
+
"jws_invalid",
|
|
63
|
+
"JWS algorithm 'none' is not allowed"
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
let verifyPayloadB64;
|
|
67
|
+
if (config.payloadSource === "body") {
|
|
68
|
+
const body = await c.req.raw.clone().text();
|
|
69
|
+
const encoder2 = new TextEncoder();
|
|
70
|
+
verifyPayloadB64 = base64UrlEncodeBytes(encoder2.encode(body));
|
|
71
|
+
} else {
|
|
72
|
+
if (!payloadB64) {
|
|
73
|
+
throw new GatewayError(
|
|
74
|
+
401,
|
|
75
|
+
"jws_invalid",
|
|
76
|
+
"JWS has empty payload but payloadSource is 'embedded'"
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
verifyPayloadB64 = payloadB64;
|
|
80
|
+
}
|
|
81
|
+
const encoder = new TextEncoder();
|
|
82
|
+
const signingInput = encoder.encode(`${headerB64}.${verifyPayloadB64}`);
|
|
83
|
+
const signature = base64UrlToBuffer(signatureB64);
|
|
84
|
+
if (config.secret) {
|
|
85
|
+
const hash = hmacAlgorithm(header.alg);
|
|
86
|
+
if (!hash) {
|
|
87
|
+
throw new GatewayError(
|
|
88
|
+
401,
|
|
89
|
+
"jws_invalid",
|
|
90
|
+
`Unsupported JWS algorithm: ${header.alg}`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
debug(`HMAC verification (alg=${header.alg})`);
|
|
94
|
+
const key = await crypto.subtle.importKey(
|
|
95
|
+
"raw",
|
|
96
|
+
encoder.encode(config.secret),
|
|
97
|
+
{ name: "HMAC", hash },
|
|
98
|
+
false,
|
|
99
|
+
["verify"]
|
|
100
|
+
);
|
|
101
|
+
const valid = await crypto.subtle.verify(
|
|
102
|
+
"HMAC",
|
|
103
|
+
key,
|
|
104
|
+
signature,
|
|
105
|
+
signingInput
|
|
106
|
+
);
|
|
107
|
+
if (!valid) {
|
|
108
|
+
throw new GatewayError(401, "jws_invalid", "Invalid JWS signature");
|
|
109
|
+
}
|
|
110
|
+
} else if (config.jwksUrl) {
|
|
111
|
+
const algorithm = rsaAlgorithm(header.alg);
|
|
112
|
+
if (!algorithm) {
|
|
113
|
+
throw new GatewayError(
|
|
114
|
+
401,
|
|
115
|
+
"jws_invalid",
|
|
116
|
+
`Unsupported JWS algorithm: ${header.alg}`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
const keys = await fetchJwks(
|
|
120
|
+
config.jwksUrl,
|
|
121
|
+
config.jwksCacheTtlMs,
|
|
122
|
+
config.jwksTimeoutMs
|
|
123
|
+
);
|
|
124
|
+
const matchingKey = header.kid ? keys.find(
|
|
125
|
+
(k) => k.kid === header.kid
|
|
126
|
+
) : keys[0];
|
|
127
|
+
if (!matchingKey) {
|
|
128
|
+
throw new GatewayError(
|
|
129
|
+
401,
|
|
130
|
+
"jws_invalid",
|
|
131
|
+
"No matching JWKS key found"
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
debug(
|
|
135
|
+
`JWKS verification (alg=${header.alg}, kid=${header.kid ?? "none"})`
|
|
136
|
+
);
|
|
137
|
+
const key = await crypto.subtle.importKey(
|
|
138
|
+
"jwk",
|
|
139
|
+
matchingKey,
|
|
140
|
+
algorithm,
|
|
141
|
+
false,
|
|
142
|
+
["verify"]
|
|
143
|
+
);
|
|
144
|
+
const valid = await crypto.subtle.verify(
|
|
145
|
+
algorithm,
|
|
146
|
+
key,
|
|
147
|
+
signature,
|
|
148
|
+
signingInput
|
|
149
|
+
);
|
|
150
|
+
if (!valid) {
|
|
151
|
+
throw new GatewayError(401, "jws_invalid", "Invalid JWS signature");
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (config.forwardPayload) {
|
|
155
|
+
try {
|
|
156
|
+
const decodedPayload = base64UrlDecode(verifyPayloadB64);
|
|
157
|
+
withModifiedHeaders(c, (headers) => {
|
|
158
|
+
headers.set(
|
|
159
|
+
config.forwardHeaderName,
|
|
160
|
+
sanitizeHeaderValue(decodedPayload)
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
} catch {
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
debug("JWS verified");
|
|
167
|
+
await next();
|
|
168
|
+
},
|
|
169
|
+
evaluate: {
|
|
170
|
+
onRequest: async (input, { config, debug }) => {
|
|
171
|
+
const jwsCompact = input.headers.get(config.headerName);
|
|
172
|
+
if (!jwsCompact) {
|
|
173
|
+
return {
|
|
174
|
+
action: "reject",
|
|
175
|
+
status: 401,
|
|
176
|
+
code: "jws_missing",
|
|
177
|
+
message: `Missing JWS header: ${config.headerName}`
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
const parts = jwsCompact.split(".");
|
|
181
|
+
if (parts.length !== 3) {
|
|
182
|
+
return {
|
|
183
|
+
action: "reject",
|
|
184
|
+
status: 401,
|
|
185
|
+
code: "jws_invalid",
|
|
186
|
+
message: "Malformed JWS: expected 3 parts"
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
const [headerB64, payloadB64, signatureB64] = parts;
|
|
190
|
+
let header;
|
|
191
|
+
try {
|
|
192
|
+
header = JSON.parse(base64UrlDecode(headerB64));
|
|
193
|
+
} catch {
|
|
194
|
+
return {
|
|
195
|
+
action: "reject",
|
|
196
|
+
status: 401,
|
|
197
|
+
code: "jws_invalid",
|
|
198
|
+
message: "Malformed JWS header: invalid base64"
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
if (header.alg.toLowerCase() === "none") {
|
|
202
|
+
return {
|
|
203
|
+
action: "reject",
|
|
204
|
+
status: 401,
|
|
205
|
+
code: "jws_invalid",
|
|
206
|
+
message: "JWS algorithm 'none' is not allowed"
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
const verifyPayloadB64 = config.payloadSource === "body" && input.body ? typeof input.body === "string" ? base64UrlEncodeBytes(new TextEncoder().encode(input.body)) : base64UrlEncodeBytes(new Uint8Array(input.body)) : payloadB64;
|
|
210
|
+
const signingInput = `${headerB64}.${verifyPayloadB64}`;
|
|
211
|
+
const signature = base64UrlToBuffer(signatureB64);
|
|
212
|
+
if (config.secret) {
|
|
213
|
+
const algorithm = hmacAlgorithm(header.alg);
|
|
214
|
+
if (!algorithm) {
|
|
215
|
+
return {
|
|
216
|
+
action: "reject",
|
|
217
|
+
status: 401,
|
|
218
|
+
code: "jws_invalid",
|
|
219
|
+
message: `Unsupported JWS algorithm: ${header.alg}`
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
const key = await crypto.subtle.importKey(
|
|
223
|
+
"raw",
|
|
224
|
+
new TextEncoder().encode(config.secret),
|
|
225
|
+
{ name: "HMAC", hash: algorithm },
|
|
226
|
+
false,
|
|
227
|
+
["verify"]
|
|
228
|
+
);
|
|
229
|
+
const valid = await crypto.subtle.verify(
|
|
230
|
+
algorithm,
|
|
231
|
+
key,
|
|
232
|
+
signature,
|
|
233
|
+
new TextEncoder().encode(signingInput)
|
|
234
|
+
);
|
|
235
|
+
if (!valid) {
|
|
236
|
+
return {
|
|
237
|
+
action: "reject",
|
|
238
|
+
status: 401,
|
|
239
|
+
code: "jws_invalid",
|
|
240
|
+
message: "Invalid JWS signature"
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
} else if (config.jwksUrl) {
|
|
244
|
+
const algorithm = rsaAlgorithm(header.alg);
|
|
245
|
+
if (!algorithm) {
|
|
246
|
+
return {
|
|
247
|
+
action: "reject",
|
|
248
|
+
status: 401,
|
|
249
|
+
code: "jws_invalid",
|
|
250
|
+
message: `Unsupported JWS algorithm: ${header.alg}`
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
const keys = await fetchJwks(
|
|
254
|
+
config.jwksUrl,
|
|
255
|
+
config.jwksCacheTtlMs,
|
|
256
|
+
config.jwksTimeoutMs
|
|
257
|
+
);
|
|
258
|
+
const matchingKey = header.kid ? keys.find(
|
|
259
|
+
(k) => k.kid === header.kid
|
|
260
|
+
) : keys[0];
|
|
261
|
+
if (!matchingKey) {
|
|
262
|
+
return {
|
|
263
|
+
action: "reject",
|
|
264
|
+
status: 401,
|
|
265
|
+
code: "jws_invalid",
|
|
266
|
+
message: "No matching JWKS key found"
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
const key = await crypto.subtle.importKey(
|
|
270
|
+
"jwk",
|
|
271
|
+
matchingKey,
|
|
272
|
+
algorithm,
|
|
273
|
+
false,
|
|
274
|
+
["verify"]
|
|
275
|
+
);
|
|
276
|
+
const valid = await crypto.subtle.verify(
|
|
277
|
+
algorithm,
|
|
278
|
+
key,
|
|
279
|
+
signature,
|
|
280
|
+
new TextEncoder().encode(signingInput)
|
|
281
|
+
);
|
|
282
|
+
if (!valid) {
|
|
283
|
+
return {
|
|
284
|
+
action: "reject",
|
|
285
|
+
status: 401,
|
|
286
|
+
code: "jws_invalid",
|
|
287
|
+
message: "Invalid JWS signature"
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (config.forwardPayload) {
|
|
292
|
+
try {
|
|
293
|
+
const decodedPayload = base64UrlDecode(verifyPayloadB64);
|
|
294
|
+
return {
|
|
295
|
+
action: "continue",
|
|
296
|
+
mutations: [
|
|
297
|
+
{
|
|
298
|
+
type: "header",
|
|
299
|
+
op: "set",
|
|
300
|
+
name: config.forwardHeaderName,
|
|
301
|
+
value: sanitizeHeaderValue(decodedPayload)
|
|
302
|
+
}
|
|
303
|
+
]
|
|
304
|
+
};
|
|
305
|
+
} catch {
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
debug("JWS verified");
|
|
309
|
+
return { action: "continue" };
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
export {
|
|
314
|
+
clearJwksCache as clearJwsJwksCache,
|
|
315
|
+
jws
|
|
316
|
+
};
|
|
317
|
+
//# sourceMappingURL=jws.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/policies/auth/jws.ts"],"sourcesContent":["/**\n * JWS verification policy - verifies JSON Web Signature compact serialization.\n *\n * Supports both embedded and detached payloads, HMAC (HS256/384/512) and\n * RSA (RS256/384/512) via JWKS endpoints.\n *\n * @module jws\n */\n\nimport { GatewayError } from \"../../core/errors\";\nimport { sanitizeHeaderValue, withModifiedHeaders } from \"../../utils/headers\";\nimport { definePolicy, Priority } from \"../sdk\";\nimport type { PolicyConfig } from \"../types\";\nimport {\n base64UrlDecode,\n base64UrlEncodeBytes,\n base64UrlToBuffer,\n fetchJwks,\n hmacAlgorithm,\n rsaAlgorithm,\n} from \"./crypto\";\n\nexport interface JwsConfig extends PolicyConfig {\n /** HMAC secret for verification */\n secret?: string;\n /** JWKS endpoint for RSA verification */\n jwksUrl?: string;\n /** Header containing the JWS. Default: \"X-JWS-Signature\" */\n headerName?: string;\n /** Where the payload comes from for detached JWS. Default: \"embedded\" */\n payloadSource?: \"embedded\" | \"body\";\n /** Whether to forward the verified payload as a header. Default: false */\n forwardPayload?: boolean;\n /** Header name for forwarded payload. Default: \"X-JWS-Payload\" */\n forwardHeaderName?: string;\n /** JWKS cache TTL in ms. Default: 300000 */\n jwksCacheTtlMs?: number;\n /** JWKS fetch timeout in milliseconds. Default: 10000 (10 seconds). */\n jwksTimeoutMs?: number;\n}\n\ninterface JwsHeader {\n alg: string;\n typ?: string;\n kid?: string;\n}\n\n/**\n * @deprecated Use `clearJwksCache` from `./crypto` instead. This re-export\n * exists for backwards compatibility with existing tests.\n */\nexport { clearJwksCache as clearJwsJwksCache } from \"./crypto\";\n\n/**\n * Verify JWS compact serialization signatures on requests.\n *\n * The `none` algorithm is always rejected to prevent signature bypass attacks.\n * Config validation (`secret` or `jwksUrl` required) is performed at construction\n * time - a missing config throws immediately, not on first request.\n *\n * @example\n * ```ts\n * import { jws } from \"@vivero/stoma\";\n *\n * // HMAC verification with embedded payload\n * jws({ secret: env.JWS_SECRET });\n *\n * // Detached JWS - payload comes from the request body\n * jws({ secret: env.JWS_SECRET, payloadSource: \"body\" });\n * ```\n */\nexport const jws = /*#__PURE__*/ definePolicy<JwsConfig>({\n name: \"jws\",\n priority: Priority.AUTH,\n defaults: {\n headerName: \"X-JWS-Signature\",\n payloadSource: \"embedded\",\n forwardPayload: false,\n forwardHeaderName: \"X-JWS-Payload\",\n },\n validate: (config) => {\n if (!config.secret && !config.jwksUrl) {\n throw new GatewayError(\n 500,\n \"config_error\",\n \"jws requires either 'secret' or 'jwksUrl'\"\n );\n }\n },\n handler: async (c, next, { config, debug }) => {\n // Extract JWS from header\n const jwsCompact = c.req.header(config.headerName!);\n if (!jwsCompact) {\n throw new GatewayError(\n 401,\n \"jws_missing\",\n `Missing JWS header: ${config.headerName}`\n );\n }\n\n // Parse JWS compact serialization: header.payload.signature\n const parts = jwsCompact.split(\".\");\n if (parts.length !== 3) {\n throw new GatewayError(\n 401,\n \"jws_invalid\",\n \"Malformed JWS: expected 3 parts\"\n );\n }\n\n const [headerB64, payloadB64, signatureB64] = parts;\n\n // Decode header\n let header: JwsHeader;\n try {\n header = JSON.parse(base64UrlDecode(headerB64));\n } catch {\n throw new GatewayError(\n 401,\n \"jws_invalid\",\n \"Malformed JWS: invalid header encoding\"\n );\n }\n\n // Block \"none\" algorithm (case-insensitive to prevent bypass)\n if (header.alg.toLowerCase() === \"none\") {\n throw new GatewayError(\n 401,\n \"jws_invalid\",\n \"JWS algorithm 'none' is not allowed\"\n );\n }\n\n // Resolve the payload for verification\n let verifyPayloadB64: string;\n if (config.payloadSource === \"body\") {\n // Detached JWS: payload section should be empty, actual payload is request body\n const body = await c.req.raw.clone().text();\n const encoder = new TextEncoder();\n verifyPayloadB64 = base64UrlEncodeBytes(encoder.encode(body));\n } else {\n // Embedded: payload is in the JWS itself\n if (!payloadB64) {\n throw new GatewayError(\n 401,\n \"jws_invalid\",\n \"JWS has empty payload but payloadSource is 'embedded'\"\n );\n }\n verifyPayloadB64 = payloadB64;\n }\n\n // Verify signature\n const encoder = new TextEncoder();\n const signingInput = encoder.encode(`${headerB64}.${verifyPayloadB64}`);\n const signature = base64UrlToBuffer(signatureB64);\n\n if (config.secret) {\n const hash = hmacAlgorithm(header.alg);\n if (!hash) {\n throw new GatewayError(\n 401,\n \"jws_invalid\",\n `Unsupported JWS algorithm: ${header.alg}`\n );\n }\n\n debug(`HMAC verification (alg=${header.alg})`);\n const key = await crypto.subtle.importKey(\n \"raw\",\n encoder.encode(config.secret),\n { name: \"HMAC\", hash },\n false,\n [\"verify\"]\n );\n\n const valid = await crypto.subtle.verify(\n \"HMAC\",\n key,\n signature,\n signingInput\n );\n if (!valid) {\n throw new GatewayError(401, \"jws_invalid\", \"Invalid JWS signature\");\n }\n } else if (config.jwksUrl) {\n const algorithm = rsaAlgorithm(header.alg);\n if (!algorithm) {\n throw new GatewayError(\n 401,\n \"jws_invalid\",\n `Unsupported JWS algorithm: ${header.alg}`\n );\n }\n\n const keys = await fetchJwks(\n config.jwksUrl,\n config.jwksCacheTtlMs,\n config.jwksTimeoutMs\n );\n const matchingKey = (header as JwsHeader).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(\n 401,\n \"jws_invalid\",\n \"No matching JWKS key found\"\n );\n }\n\n debug(\n `JWKS verification (alg=${header.alg}, kid=${header.kid ?? \"none\"})`\n );\n const key = await crypto.subtle.importKey(\n \"jwk\",\n matchingKey,\n algorithm,\n false,\n [\"verify\"]\n );\n\n const valid = await crypto.subtle.verify(\n algorithm,\n key,\n signature,\n signingInput\n );\n if (!valid) {\n throw new GatewayError(401, \"jws_invalid\", \"Invalid JWS signature\");\n }\n }\n\n // Forward verified payload if requested\n if (config.forwardPayload) {\n try {\n const decodedPayload = base64UrlDecode(verifyPayloadB64);\n withModifiedHeaders(c, (headers) => {\n headers.set(\n config.forwardHeaderName!,\n sanitizeHeaderValue(decodedPayload)\n );\n });\n } catch {\n // If payload isn't decodable, skip forwarding\n }\n }\n\n debug(\"JWS verified\");\n await next();\n },\n evaluate: {\n onRequest: async (input, { config, debug }) => {\n // Extract JWS from header\n const jwsCompact = input.headers.get(config.headerName!);\n if (!jwsCompact) {\n return {\n action: \"reject\",\n status: 401,\n code: \"jws_missing\",\n message: `Missing JWS header: ${config.headerName}`,\n };\n }\n\n // Parse JWS compact serialization: header.payload.signature\n const parts = jwsCompact.split(\".\");\n if (parts.length !== 3) {\n return {\n action: \"reject\",\n status: 401,\n code: \"jws_invalid\",\n message: \"Malformed JWS: expected 3 parts\",\n };\n }\n\n const [headerB64, payloadB64, signatureB64] = parts;\n\n // Decode header\n let header: JwsHeader;\n try {\n header = JSON.parse(base64UrlDecode(headerB64));\n } catch {\n return {\n action: \"reject\",\n status: 401,\n code: \"jws_invalid\",\n message: \"Malformed JWS header: invalid base64\",\n };\n }\n\n // Block \"none\" algorithm\n if (header.alg.toLowerCase() === \"none\") {\n return {\n action: \"reject\",\n status: 401,\n code: \"jws_invalid\",\n message: \"JWS algorithm 'none' is not allowed\",\n };\n }\n\n // Get payload (embedded or from body)\n const verifyPayloadB64 =\n config.payloadSource === \"body\" && input.body\n ? typeof input.body === \"string\"\n ? base64UrlEncodeBytes(new TextEncoder().encode(input.body))\n : base64UrlEncodeBytes(new Uint8Array(input.body))\n : payloadB64;\n\n const signingInput = `${headerB64}.${verifyPayloadB64}`;\n\n // Verify signature\n const signature = base64UrlToBuffer(signatureB64);\n\n if (config.secret) {\n const algorithm = hmacAlgorithm(header.alg);\n if (!algorithm) {\n return {\n action: \"reject\",\n status: 401,\n code: \"jws_invalid\",\n message: `Unsupported JWS algorithm: ${header.alg}`,\n };\n }\n\n const key = await crypto.subtle.importKey(\n \"raw\",\n new TextEncoder().encode(config.secret),\n { name: \"HMAC\", hash: algorithm },\n false,\n [\"verify\"]\n );\n\n const valid = await crypto.subtle.verify(\n algorithm,\n key,\n signature,\n new TextEncoder().encode(signingInput)\n );\n if (!valid) {\n return {\n action: \"reject\",\n status: 401,\n code: \"jws_invalid\",\n message: \"Invalid JWS signature\",\n };\n }\n } else if (config.jwksUrl) {\n const algorithm = rsaAlgorithm(header.alg);\n if (!algorithm) {\n return {\n action: \"reject\",\n status: 401,\n code: \"jws_invalid\",\n message: `Unsupported JWS algorithm: ${header.alg}`,\n };\n }\n\n const keys = await fetchJwks(\n config.jwksUrl,\n config.jwksCacheTtlMs,\n config.jwksTimeoutMs\n );\n const matchingKey = header.kid\n ? keys.find(\n (k) =>\n (k as unknown as Record<string, unknown>).kid === header.kid\n )\n : keys[0];\n\n if (!matchingKey) {\n return {\n action: \"reject\",\n status: 401,\n code: \"jws_invalid\",\n message: \"No matching JWKS key found\",\n };\n }\n\n const key = await crypto.subtle.importKey(\n \"jwk\",\n matchingKey,\n algorithm,\n false,\n [\"verify\"]\n );\n\n const valid = await crypto.subtle.verify(\n algorithm,\n key,\n signature,\n new TextEncoder().encode(signingInput)\n );\n if (!valid) {\n return {\n action: \"reject\",\n status: 401,\n code: \"jws_invalid\",\n message: \"Invalid JWS signature\",\n };\n }\n }\n\n // Forward verified payload if requested\n if (config.forwardPayload) {\n try {\n const decodedPayload = base64UrlDecode(verifyPayloadB64);\n return {\n action: \"continue\",\n mutations: [\n {\n type: \"header\" as const,\n op: \"set\" as const,\n name: config.forwardHeaderName!,\n value: sanitizeHeaderValue(decodedPayload),\n },\n ],\n };\n } catch {\n // If payload isn't decodable, continue without forwarding\n }\n }\n\n debug(\"JWS verified\");\n return { action: \"continue\" };\n },\n },\n});\n"],"mappings":"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","names":["encoder"]}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { g as PolicyConfig, P as Policy } from '../../protocol-2fD3DJrL.js';
|
|
2
|
+
export { clearJwksCache } from './crypto.js';
|
|
3
|
+
import 'hono';
|
|
4
|
+
import '../sdk/trace.js';
|
|
5
|
+
import '@vivero/stoma-core';
|
|
6
|
+
|
|
7
|
+
interface JwtAuthConfig extends PolicyConfig {
|
|
8
|
+
/** JWT secret for HMAC verification */
|
|
9
|
+
secret?: string;
|
|
10
|
+
/** JWKS endpoint URL (e.g. Supabase, Auth0) */
|
|
11
|
+
jwksUrl?: string;
|
|
12
|
+
/** Expected JWT issuer */
|
|
13
|
+
issuer?: string;
|
|
14
|
+
/** Expected JWT audience */
|
|
15
|
+
audience?: string;
|
|
16
|
+
/** Header to read the token from. Default: "Authorization" */
|
|
17
|
+
headerName?: string;
|
|
18
|
+
/** Token prefix. Default: "Bearer" */
|
|
19
|
+
tokenPrefix?: string;
|
|
20
|
+
/** Claims to inject into request headers for upstream consumption */
|
|
21
|
+
forwardClaims?: Record<string, string>;
|
|
22
|
+
/** JWKS cache TTL in milliseconds. Default: 300000 (5 minutes). */
|
|
23
|
+
jwksCacheTtlMs?: number;
|
|
24
|
+
/** JWKS fetch timeout in milliseconds. Default: 10000 (10 seconds). */
|
|
25
|
+
jwksTimeoutMs?: number;
|
|
26
|
+
/** Clock skew tolerance in seconds for expiry checks. Default: 0. */
|
|
27
|
+
clockSkewSeconds?: number;
|
|
28
|
+
/** Require the `exp` claim to be present. Default: false. */
|
|
29
|
+
requireExp?: boolean;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Validate JWT tokens and optionally forward claims as upstream headers.
|
|
33
|
+
*
|
|
34
|
+
* Supports both HMAC (shared secret) and RSA (JWKS endpoint) verification.
|
|
35
|
+
* JWKS responses are cached for 5 minutes. The `none` algorithm is always
|
|
36
|
+
* rejected to prevent signature bypass attacks.
|
|
37
|
+
*
|
|
38
|
+
* @param config - JWT authentication settings. Requires either `secret` (HMAC) or `jwksUrl` (RSA).
|
|
39
|
+
* @returns A {@link Policy} at priority 10 (runs early, before rate limiting).
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* // HMAC verification with a shared secret
|
|
44
|
+
* createGateway({
|
|
45
|
+
* routes: [{
|
|
46
|
+
* path: "/api/*",
|
|
47
|
+
* pipeline: {
|
|
48
|
+
* policies: [jwtAuth({ secret: env.JWT_SECRET })],
|
|
49
|
+
* upstream: { type: "url", target: "https://backend.internal" },
|
|
50
|
+
* },
|
|
51
|
+
* }],
|
|
52
|
+
* });
|
|
53
|
+
*
|
|
54
|
+
* // JWKS verification (e.g. Supabase, Auth0) with claim forwarding
|
|
55
|
+
* jwtAuth({
|
|
56
|
+
* jwksUrl: "https://your-project.supabase.co/auth/v1/.well-known/jwks.json",
|
|
57
|
+
* issuer: "https://your-project.supabase.co/auth/v1",
|
|
58
|
+
* forwardClaims: { sub: "x-user-id", email: "x-user-email" },
|
|
59
|
+
* });
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
declare const jwtAuth: (config?: JwtAuthConfig | undefined) => Policy;
|
|
63
|
+
|
|
64
|
+
export { type JwtAuthConfig, jwtAuth };
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { GatewayError } from "../../core/errors";
|
|
2
|
+
import { sanitizeHeaderValue, withModifiedHeaders } from "../../utils/headers";
|
|
3
|
+
import { definePolicy, Priority } from "../sdk";
|
|
4
|
+
import {
|
|
5
|
+
base64UrlDecode,
|
|
6
|
+
base64UrlToBuffer,
|
|
7
|
+
fetchJwks,
|
|
8
|
+
hmacAlgorithm,
|
|
9
|
+
rsaAlgorithm
|
|
10
|
+
} from "./crypto";
|
|
11
|
+
async function validateJwt(authHeader, config, debug) {
|
|
12
|
+
if (!authHeader) {
|
|
13
|
+
return {
|
|
14
|
+
ok: false,
|
|
15
|
+
status: 401,
|
|
16
|
+
code: "unauthorized",
|
|
17
|
+
message: "Missing authentication token"
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
let token;
|
|
21
|
+
if (config.tokenPrefix) {
|
|
22
|
+
if (!authHeader.startsWith(`${config.tokenPrefix} `)) {
|
|
23
|
+
return {
|
|
24
|
+
ok: false,
|
|
25
|
+
status: 401,
|
|
26
|
+
code: "unauthorized",
|
|
27
|
+
message: `Expected ${config.tokenPrefix} token`
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
token = authHeader.slice(config.tokenPrefix.length + 1);
|
|
31
|
+
} else {
|
|
32
|
+
token = authHeader;
|
|
33
|
+
}
|
|
34
|
+
if (!token || !token.trim()) {
|
|
35
|
+
return {
|
|
36
|
+
ok: false,
|
|
37
|
+
status: 401,
|
|
38
|
+
code: "unauthorized",
|
|
39
|
+
message: "Empty authentication token"
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const parts = token.split(".");
|
|
43
|
+
if (parts.length !== 3) {
|
|
44
|
+
return {
|
|
45
|
+
ok: false,
|
|
46
|
+
status: 401,
|
|
47
|
+
code: "unauthorized",
|
|
48
|
+
message: "Malformed JWT: expected 3 parts"
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
let header;
|
|
52
|
+
let payload;
|
|
53
|
+
try {
|
|
54
|
+
header = JSON.parse(base64UrlDecode(parts[0]));
|
|
55
|
+
payload = JSON.parse(base64UrlDecode(parts[1]));
|
|
56
|
+
} catch {
|
|
57
|
+
return {
|
|
58
|
+
ok: false,
|
|
59
|
+
status: 401,
|
|
60
|
+
code: "unauthorized",
|
|
61
|
+
message: "Malformed JWT: invalid base64 encoding"
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (header.alg.toLowerCase() === "none") {
|
|
65
|
+
return {
|
|
66
|
+
ok: false,
|
|
67
|
+
status: 401,
|
|
68
|
+
code: "unauthorized",
|
|
69
|
+
message: "JWT algorithm 'none' is not allowed"
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (config.secret) {
|
|
73
|
+
debug(`HMAC verification (alg=${header.alg})`);
|
|
74
|
+
await verifyHmac(config.secret, parts[0], parts[1], parts[2], header.alg);
|
|
75
|
+
} else if (config.jwksUrl) {
|
|
76
|
+
debug(`JWKS verification (alg=${header.alg}, kid=${header.kid ?? "none"})`);
|
|
77
|
+
await verifyJwks(
|
|
78
|
+
config.jwksUrl,
|
|
79
|
+
parts[0],
|
|
80
|
+
parts[1],
|
|
81
|
+
parts[2],
|
|
82
|
+
header,
|
|
83
|
+
config.jwksCacheTtlMs,
|
|
84
|
+
config.jwksTimeoutMs
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
88
|
+
if (config.requireExp && payload.exp === void 0) {
|
|
89
|
+
return {
|
|
90
|
+
ok: false,
|
|
91
|
+
status: 401,
|
|
92
|
+
code: "unauthorized",
|
|
93
|
+
message: "JWT must contain an 'exp' claim"
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
if (payload.exp !== void 0 && payload.exp < now - config.clockSkewSeconds) {
|
|
97
|
+
return {
|
|
98
|
+
ok: false,
|
|
99
|
+
status: 401,
|
|
100
|
+
code: "unauthorized",
|
|
101
|
+
message: "JWT has expired"
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (config.issuer && payload.iss !== config.issuer) {
|
|
105
|
+
return {
|
|
106
|
+
ok: false,
|
|
107
|
+
status: 401,
|
|
108
|
+
code: "unauthorized",
|
|
109
|
+
message: "JWT issuer mismatch"
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
if (config.audience) {
|
|
113
|
+
const aud = Array.isArray(payload.aud) ? payload.aud : [payload.aud];
|
|
114
|
+
if (!aud.includes(config.audience)) {
|
|
115
|
+
return {
|
|
116
|
+
ok: false,
|
|
117
|
+
status: 401,
|
|
118
|
+
code: "unauthorized",
|
|
119
|
+
message: "JWT audience mismatch"
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
debug(`verified (sub=${payload.sub ?? "none"})`);
|
|
124
|
+
return { ok: true, payload };
|
|
125
|
+
}
|
|
126
|
+
const jwtAuth = /* @__PURE__ */ definePolicy({
|
|
127
|
+
name: "jwt-auth",
|
|
128
|
+
priority: Priority.AUTH,
|
|
129
|
+
phases: ["request-headers"],
|
|
130
|
+
defaults: {
|
|
131
|
+
headerName: "authorization",
|
|
132
|
+
tokenPrefix: "Bearer",
|
|
133
|
+
clockSkewSeconds: 0,
|
|
134
|
+
requireExp: false
|
|
135
|
+
},
|
|
136
|
+
validate: (config) => {
|
|
137
|
+
if (!config.secret && !config.jwksUrl) {
|
|
138
|
+
throw new GatewayError(
|
|
139
|
+
500,
|
|
140
|
+
"config_error",
|
|
141
|
+
"jwtAuth requires either 'secret' or 'jwksUrl'"
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
handler: async (c, next, { config, debug }) => {
|
|
146
|
+
const result = await validateJwt(
|
|
147
|
+
c.req.header(config.headerName),
|
|
148
|
+
config,
|
|
149
|
+
debug
|
|
150
|
+
);
|
|
151
|
+
if (!result.ok) {
|
|
152
|
+
throw new GatewayError(result.status, result.code, result.message);
|
|
153
|
+
}
|
|
154
|
+
if (config.forwardClaims) {
|
|
155
|
+
const { payload } = result;
|
|
156
|
+
const forwardClaims = config.forwardClaims;
|
|
157
|
+
withModifiedHeaders(c, (headers) => {
|
|
158
|
+
for (const [claim, headerKey] of Object.entries(forwardClaims)) {
|
|
159
|
+
const value = payload[claim];
|
|
160
|
+
if (value !== void 0 && value !== null) {
|
|
161
|
+
headers.set(headerKey, sanitizeHeaderValue(String(value)));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
await next();
|
|
167
|
+
},
|
|
168
|
+
evaluate: {
|
|
169
|
+
onRequest: async (input, { config, debug }) => {
|
|
170
|
+
const result = await validateJwt(
|
|
171
|
+
input.headers.get(config.headerName),
|
|
172
|
+
config,
|
|
173
|
+
debug
|
|
174
|
+
);
|
|
175
|
+
if (!result.ok) {
|
|
176
|
+
return {
|
|
177
|
+
action: "reject",
|
|
178
|
+
status: result.status,
|
|
179
|
+
code: result.code,
|
|
180
|
+
message: result.message
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
if (config.forwardClaims) {
|
|
184
|
+
const { payload } = result;
|
|
185
|
+
const forwardClaims = config.forwardClaims;
|
|
186
|
+
const mutations = [];
|
|
187
|
+
for (const [claim, headerKey] of Object.entries(forwardClaims)) {
|
|
188
|
+
const value = payload[claim];
|
|
189
|
+
if (value !== void 0 && value !== null) {
|
|
190
|
+
mutations.push({
|
|
191
|
+
type: "header",
|
|
192
|
+
op: "set",
|
|
193
|
+
name: headerKey,
|
|
194
|
+
value: sanitizeHeaderValue(String(value))
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (mutations.length > 0) {
|
|
199
|
+
return { action: "continue", mutations };
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return { action: "continue" };
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
async function verifyHmac(secret, headerB64, payloadB64, signatureB64, alg) {
|
|
207
|
+
const algorithm = hmacAlgorithm(alg);
|
|
208
|
+
if (!algorithm) {
|
|
209
|
+
throw new GatewayError(
|
|
210
|
+
401,
|
|
211
|
+
"unauthorized",
|
|
212
|
+
`Unsupported JWT algorithm: ${alg}`
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
const encoder = new TextEncoder();
|
|
216
|
+
const key = await crypto.subtle.importKey(
|
|
217
|
+
"raw",
|
|
218
|
+
encoder.encode(secret),
|
|
219
|
+
{ name: "HMAC", hash: algorithm },
|
|
220
|
+
false,
|
|
221
|
+
["verify"]
|
|
222
|
+
);
|
|
223
|
+
const data = encoder.encode(`${headerB64}.${payloadB64}`);
|
|
224
|
+
const signature = base64UrlToBuffer(signatureB64);
|
|
225
|
+
const valid = await crypto.subtle.verify("HMAC", key, signature, data);
|
|
226
|
+
if (!valid) {
|
|
227
|
+
throw new GatewayError(401, "unauthorized", "Invalid JWT signature");
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
async function verifyJwks(jwksUrl, headerB64, payloadB64, signatureB64, header, cacheTtlMs, timeoutMs) {
|
|
231
|
+
const keys = await fetchJwks(jwksUrl, cacheTtlMs, timeoutMs);
|
|
232
|
+
const matchingKey = header.kid ? keys.find(
|
|
233
|
+
(k) => k.kid === header.kid
|
|
234
|
+
) : keys[0];
|
|
235
|
+
if (!matchingKey) {
|
|
236
|
+
throw new GatewayError(401, "unauthorized", "No matching JWKS key found");
|
|
237
|
+
}
|
|
238
|
+
const algorithm = rsaAlgorithm(header.alg);
|
|
239
|
+
if (!algorithm) {
|
|
240
|
+
throw new GatewayError(
|
|
241
|
+
401,
|
|
242
|
+
"unauthorized",
|
|
243
|
+
`Unsupported JWT algorithm: ${header.alg}`
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
const key = await crypto.subtle.importKey(
|
|
247
|
+
"jwk",
|
|
248
|
+
matchingKey,
|
|
249
|
+
algorithm,
|
|
250
|
+
false,
|
|
251
|
+
["verify"]
|
|
252
|
+
);
|
|
253
|
+
const encoder = new TextEncoder();
|
|
254
|
+
const data = encoder.encode(`${headerB64}.${payloadB64}`);
|
|
255
|
+
const signature = base64UrlToBuffer(signatureB64);
|
|
256
|
+
const valid = await crypto.subtle.verify(algorithm, key, signature, data);
|
|
257
|
+
if (!valid) {
|
|
258
|
+
throw new GatewayError(401, "unauthorized", "Invalid JWT signature");
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
import { clearJwksCache } from "./crypto";
|
|
262
|
+
export {
|
|
263
|
+
clearJwksCache,
|
|
264
|
+
jwtAuth
|
|
265
|
+
};
|
|
266
|
+
//# sourceMappingURL=jwt-auth.js.map
|