@zauso-ai/capstan-auth 1.0.0-beta.5 → 1.0.0-beta.6

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/dist/dpop.d.ts ADDED
@@ -0,0 +1,46 @@
1
+ import type { KeyValueStore } from "./store.js";
2
+ /**
3
+ * Result of a successful DPoP proof validation.
4
+ */
5
+ export interface DpopValidationResult {
6
+ /** Base64url-encoded JWK thumbprint (RFC 7638) of the proof key. */
7
+ thumbprint: string;
8
+ }
9
+ /**
10
+ * Replace the default in-memory DPoP replay store with a custom implementation.
11
+ *
12
+ * Call this at application startup before any DPoP proofs are validated.
13
+ */
14
+ export declare function setDpopReplayStore(store: KeyValueStore<true>): void;
15
+ /**
16
+ * Validate a DPoP proof JWT per RFC 9449.
17
+ *
18
+ * The proof is a compact-serialized JWT whose header contains the public key
19
+ * (`jwk`) used to sign the proof. The function:
20
+ *
21
+ * 1. Parses the JOSE header and verifies `typ: "dpop+jwt"` and a supported `alg`.
22
+ * 2. Extracts the embedded `jwk` and imports it via Web Crypto.
23
+ * 3. Verifies the JWT signature using the imported key.
24
+ * 4. Validates required claims:
25
+ * - `htm` — must match the provided HTTP method.
26
+ * - `htu` — must match the provided HTTP URI (scheme + authority + path).
27
+ * - `iat` — must be within the acceptable time window.
28
+ * - `jti` — must be unique (checked against an in-memory replay cache).
29
+ * 5. If an `accessToken` is provided, verifies the `ath` (access token hash)
30
+ * claim matches the SHA-256 hash of the token.
31
+ *
32
+ * Returns the JWK thumbprint on success, or `null` if validation fails.
33
+ *
34
+ * @param proof - The DPoP proof from the `DPoP` request header.
35
+ * @param method - The HTTP method of the request (e.g. "POST").
36
+ * @param url - The full request URL.
37
+ * @param accessToken - Optional access token to verify `ath` binding.
38
+ */
39
+ export declare function validateDpopProof(proof: string, method: string, url: string, accessToken?: string): Promise<DpopValidationResult | null>;
40
+ /**
41
+ * Clear the DPoP JTI replay cache.
42
+ *
43
+ * Useful for tests so state does not leak between test cases.
44
+ */
45
+ export declare function clearDpopReplayCache(): Promise<void>;
46
+ //# sourceMappingURL=dpop.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dpop.d.ts","sourceRoot":"","sources":["../src/dpop.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAOhD;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,oEAAoE;IACpE,UAAU,EAAE,MAAM,CAAC;CACpB;AAoBD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,aAAa,CAAC,IAAI,CAAC,GAAG,IAAI,CAEnE;AAmGD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAsB,iBAAiB,CACrC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,EACX,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC,CA4JtC;AAED;;;;GAIG;AACH,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,IAAI,CAAC,CAE1D"}
package/dist/dpop.js ADDED
@@ -0,0 +1,259 @@
1
+ import { createHash } from "node:crypto";
2
+ import { MemoryStore } from "./store.js";
3
+ /** Maximum age (in seconds) for a DPoP proof's `iat` claim. */
4
+ const MAX_PROOF_AGE_SECONDS = 300; // 5 minutes
5
+ /** Minimum age (in seconds) — reject proofs from the far future. */
6
+ const MAX_CLOCK_SKEW_SECONDS = 60;
7
+ /** TTL for JTI replay cache entries (proof age + clock skew). */
8
+ const JTI_TTL_MS = (MAX_PROOF_AGE_SECONDS + MAX_CLOCK_SKEW_SECONDS) * 1000;
9
+ /**
10
+ * Pluggable store for tracking recently seen `jti` values to prevent replay.
11
+ *
12
+ * Each entry stores `true` as a sentinel value with a TTL equal to the proof
13
+ * window. The `MemoryStore` default lazily expires entries on access; custom
14
+ * implementations (e.g. Redis) can use native TTL support.
15
+ */
16
+ let dpopReplayStore = new MemoryStore();
17
+ /**
18
+ * Replace the default in-memory DPoP replay store with a custom implementation.
19
+ *
20
+ * Call this at application startup before any DPoP proofs are validated.
21
+ */
22
+ export function setDpopReplayStore(store) {
23
+ dpopReplayStore = store;
24
+ }
25
+ // ---------------------------------------------------------------------------
26
+ // Base64url helpers
27
+ // ---------------------------------------------------------------------------
28
+ function base64urlDecode(str) {
29
+ // Convert base64url to standard base64.
30
+ let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
31
+ // Pad with '=' to make length a multiple of 4.
32
+ while (base64.length % 4 !== 0) {
33
+ base64 += "=";
34
+ }
35
+ return Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
36
+ }
37
+ function base64urlEncode(buffer) {
38
+ const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
39
+ let binary = "";
40
+ for (const b of bytes) {
41
+ binary += String.fromCharCode(b);
42
+ }
43
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
44
+ }
45
+ // ---------------------------------------------------------------------------
46
+ // JWK Thumbprint (RFC 7638)
47
+ // ---------------------------------------------------------------------------
48
+ /**
49
+ * Compute the JWK thumbprint per RFC 7638.
50
+ *
51
+ * For RSA keys the required members are: e, kty, n
52
+ * For EC keys: crv, kty, x, y
53
+ */
54
+ function computeJwkThumbprint(jwk) {
55
+ let canonicalMembers;
56
+ if (jwk.kty === "RSA") {
57
+ canonicalMembers = { e: jwk.e, kty: jwk.kty, n: jwk.n };
58
+ }
59
+ else if (jwk.kty === "EC") {
60
+ canonicalMembers = {
61
+ crv: jwk.crv,
62
+ kty: jwk.kty,
63
+ x: jwk.x,
64
+ y: jwk.y,
65
+ };
66
+ }
67
+ else {
68
+ throw new Error(`Unsupported key type: ${jwk.kty}`);
69
+ }
70
+ // Lexicographic ordering of keys (JSON Canonicalization is just sorted keys
71
+ // for the required members).
72
+ const sortedKeys = Object.keys(canonicalMembers).sort();
73
+ const json = "{" +
74
+ sortedKeys
75
+ .map((k) => `${JSON.stringify(k)}:${JSON.stringify(canonicalMembers[k])}`)
76
+ .join(",") +
77
+ "}";
78
+ const hash = createHash("sha256").update(json).digest();
79
+ return base64urlEncode(hash);
80
+ }
81
+ function mapAlgorithm(alg) {
82
+ switch (alg) {
83
+ case "RS256":
84
+ return { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" };
85
+ case "RS384":
86
+ return { name: "RSASSA-PKCS1-v1_5", hash: "SHA-384" };
87
+ case "RS512":
88
+ return { name: "RSASSA-PKCS1-v1_5", hash: "SHA-512" };
89
+ case "ES256":
90
+ return { name: "ECDSA", hash: "SHA-256", namedCurve: "P-256" };
91
+ case "ES384":
92
+ return { name: "ECDSA", hash: "SHA-384", namedCurve: "P-384" };
93
+ case "ES512":
94
+ return { name: "ECDSA", hash: "SHA-512", namedCurve: "P-521" };
95
+ default:
96
+ return null;
97
+ }
98
+ }
99
+ // ---------------------------------------------------------------------------
100
+ // Public API
101
+ // ---------------------------------------------------------------------------
102
+ /**
103
+ * Validate a DPoP proof JWT per RFC 9449.
104
+ *
105
+ * The proof is a compact-serialized JWT whose header contains the public key
106
+ * (`jwk`) used to sign the proof. The function:
107
+ *
108
+ * 1. Parses the JOSE header and verifies `typ: "dpop+jwt"` and a supported `alg`.
109
+ * 2. Extracts the embedded `jwk` and imports it via Web Crypto.
110
+ * 3. Verifies the JWT signature using the imported key.
111
+ * 4. Validates required claims:
112
+ * - `htm` — must match the provided HTTP method.
113
+ * - `htu` — must match the provided HTTP URI (scheme + authority + path).
114
+ * - `iat` — must be within the acceptable time window.
115
+ * - `jti` — must be unique (checked against an in-memory replay cache).
116
+ * 5. If an `accessToken` is provided, verifies the `ath` (access token hash)
117
+ * claim matches the SHA-256 hash of the token.
118
+ *
119
+ * Returns the JWK thumbprint on success, or `null` if validation fails.
120
+ *
121
+ * @param proof - The DPoP proof from the `DPoP` request header.
122
+ * @param method - The HTTP method of the request (e.g. "POST").
123
+ * @param url - The full request URL.
124
+ * @param accessToken - Optional access token to verify `ath` binding.
125
+ */
126
+ export async function validateDpopProof(proof, method, url, accessToken) {
127
+ // ── 1. Split compact serialization ──────────────────────────────
128
+ const parts = proof.split(".");
129
+ if (parts.length !== 3)
130
+ return null;
131
+ const [headerB64, payloadB64, signatureB64] = parts;
132
+ // ── 2. Parse header ─────────────────────────────────────────────
133
+ let header;
134
+ try {
135
+ header = JSON.parse(new TextDecoder().decode(base64urlDecode(headerB64)));
136
+ }
137
+ catch {
138
+ return null;
139
+ }
140
+ // Must be typ: dpop+jwt
141
+ if (header.typ !== "dpop+jwt")
142
+ return null;
143
+ // Must have a supported algorithm
144
+ if (!header.alg)
145
+ return null;
146
+ const algMapping = mapAlgorithm(header.alg);
147
+ if (!algMapping)
148
+ return null;
149
+ // Must have an embedded JWK
150
+ if (!header.jwk || typeof header.jwk !== "object")
151
+ return null;
152
+ // The JWK must not contain a private key
153
+ if ("d" in header.jwk)
154
+ return null;
155
+ // ── 3. Import the public key and verify signature ───────────────
156
+ let importAlg;
157
+ if (algMapping.name === "RSASSA-PKCS1-v1_5") {
158
+ importAlg = { name: algMapping.name, hash: algMapping.hash };
159
+ }
160
+ else {
161
+ importAlg = {
162
+ name: algMapping.name,
163
+ namedCurve: algMapping.namedCurve,
164
+ };
165
+ }
166
+ let publicKey;
167
+ try {
168
+ publicKey = await crypto.subtle.importKey("jwk", header.jwk, importAlg, false, ["verify"]);
169
+ }
170
+ catch {
171
+ return null;
172
+ }
173
+ // Verify the signature over header.payload
174
+ const signingInput = new TextEncoder().encode(`${headerB64}.${payloadB64}`);
175
+ const signature = base64urlDecode(signatureB64);
176
+ let verifyAlg;
177
+ if (algMapping.name === "ECDSA") {
178
+ verifyAlg = { name: "ECDSA", hash: algMapping.hash };
179
+ }
180
+ else {
181
+ verifyAlg = { name: algMapping.name };
182
+ }
183
+ let valid;
184
+ try {
185
+ valid = await crypto.subtle.verify(verifyAlg, publicKey, signature.buffer, signingInput);
186
+ }
187
+ catch {
188
+ return null;
189
+ }
190
+ if (!valid)
191
+ return null;
192
+ // ── 4. Parse and validate payload claims ────────────────────────
193
+ let payload;
194
+ try {
195
+ payload = JSON.parse(new TextDecoder().decode(base64urlDecode(payloadB64)));
196
+ }
197
+ catch {
198
+ return null;
199
+ }
200
+ // htm — HTTP method must match
201
+ if (typeof payload.htm !== "string")
202
+ return null;
203
+ if (payload.htm.toUpperCase() !== method.toUpperCase())
204
+ return null;
205
+ // htu — HTTP URI must match (scheme + authority + path, no query/fragment)
206
+ if (typeof payload.htu !== "string")
207
+ return null;
208
+ try {
209
+ const proofUrl = new URL(payload.htu);
210
+ const requestUrl = new URL(url);
211
+ // Compare origin + pathname (ignoring query string and fragment).
212
+ if (proofUrl.origin !== requestUrl.origin ||
213
+ proofUrl.pathname !== requestUrl.pathname) {
214
+ return null;
215
+ }
216
+ }
217
+ catch {
218
+ return null;
219
+ }
220
+ // iat — issued at must be recent
221
+ if (typeof payload.iat !== "number")
222
+ return null;
223
+ const nowSeconds = Math.floor(Date.now() / 1000);
224
+ if (payload.iat > nowSeconds + MAX_CLOCK_SKEW_SECONDS)
225
+ return null;
226
+ if (nowSeconds - payload.iat > MAX_PROOF_AGE_SECONDS)
227
+ return null;
228
+ // jti — unique identifier, prevent replay
229
+ if (typeof payload.jti !== "string" || payload.jti.length === 0)
230
+ return null;
231
+ if (await dpopReplayStore.has(payload.jti))
232
+ return null;
233
+ // Record the jti with a TTL matching the proof window.
234
+ await dpopReplayStore.set(payload.jti, true, JTI_TTL_MS);
235
+ // ── 5. Access token hash binding (ath) ──────────────────────────
236
+ if (accessToken !== undefined) {
237
+ const expectedAth = base64urlEncode(createHash("sha256").update(accessToken).digest());
238
+ if (payload.ath !== expectedAth)
239
+ return null;
240
+ }
241
+ // ── 6. Compute JWK thumbprint ───────────────────────────────────
242
+ let thumbprint;
243
+ try {
244
+ thumbprint = computeJwkThumbprint(header.jwk);
245
+ }
246
+ catch {
247
+ return null;
248
+ }
249
+ return { thumbprint };
250
+ }
251
+ /**
252
+ * Clear the DPoP JTI replay cache.
253
+ *
254
+ * Useful for tests so state does not leak between test cases.
255
+ */
256
+ export async function clearDpopReplayCache() {
257
+ await dpopReplayStore.clear();
258
+ }
259
+ //# sourceMappingURL=dpop.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dpop.js","sourceRoot":"","sources":["../src/dpop.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -2,5 +2,9 @@ export { signSession, verifySession } from "./session.js";
2
2
  export { generateApiKey, verifyApiKey, extractApiKeyPrefix, } from "./api-key.js";
3
3
  export { createAuthMiddleware } from "./middleware.js";
4
4
  export { checkPermission, derivePermission } from "./permissions.js";
5
+ export { validateDpopProof, clearDpopReplayCache, setDpopReplayStore } from "./dpop.js";
6
+ export { extractWorkloadIdentity, isValidSpiffeId } from "./workload.js";
7
+ export type { DpopValidationResult } from "./dpop.js";
8
+ export type { WorkloadIdentity } from "./workload.js";
5
9
  export type { AuthConfig, SessionPayload, AgentCredential, AuthContext, AuthResolverDeps, } from "./types.js";
6
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC1D,OAAO,EACL,cAAc,EACd,YAAY,EACZ,mBAAmB,GACpB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACrE,YAAY,EACV,UAAU,EACV,cAAc,EACd,eAAe,EACf,WAAW,EACX,gBAAgB,GACjB,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC1D,OAAO,EACL,cAAc,EACd,YAAY,EACZ,mBAAmB,GACpB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACrE,OAAO,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AACxF,OAAO,EAAE,uBAAuB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACzE,YAAY,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC;AACtD,YAAY,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,YAAY,EACV,UAAU,EACV,cAAc,EACd,eAAe,EACf,WAAW,EACX,gBAAgB,GACjB,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -2,4 +2,6 @@ export { signSession, verifySession } from "./session.js";
2
2
  export { generateApiKey, verifyApiKey, extractApiKeyPrefix, } from "./api-key.js";
3
3
  export { createAuthMiddleware } from "./middleware.js";
4
4
  export { checkPermission, derivePermission } from "./permissions.js";
5
+ export { validateDpopProof, clearDpopReplayCache, setDpopReplayStore } from "./dpop.js";
6
+ export { extractWorkloadIdentity, isValidSpiffeId } from "./workload.js";
5
7
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC1D,OAAO,EACL,cAAc,EACd,YAAY,EACZ,mBAAmB,GACpB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC1D,OAAO,EACL,cAAc,EACd,YAAY,EACZ,mBAAmB,GACpB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACrE,OAAO,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AACxF,OAAO,EAAE,uBAAuB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC"}
@@ -4,11 +4,14 @@ import type { AuthConfig, AuthContext, AuthResolverDeps } from "./types.js";
4
4
  * incoming `Request`.
5
5
  *
6
6
  * Resolution order:
7
- * 1. Session cookie (`capstan_session`) — verifies JWT, returns human context.
8
- * 2. `Authorization: Bearer <token>` header — if the token matches the
7
+ * 1. Client certificate / workload identity (`X-Client-Cert` or
8
+ * `X-Forwarded-Client-Cert` header)extracts SPIFFE ID, validates
9
+ * trust domain, returns workload context.
10
+ * 2. Session cookie (`capstan_session`) — verifies JWT, returns human context.
11
+ * 3. `Authorization: Bearer <token>` header — if the token matches the
9
12
  * configured API key prefix, looks up the agent credential and verifies
10
13
  * the key hash.
11
- * 3. Falls back to `{ type: "anonymous", isAuthenticated: false }`.
14
+ * 4. Falls back to `{ type: "anonymous", isAuthenticated: false }`.
12
15
  */
13
16
  export declare function createAuthMiddleware(config: AuthConfig, deps: AuthResolverDeps): (request: Request) => Promise<AuthContext>;
14
17
  //# sourceMappingURL=middleware.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EACV,WAAW,EACX,gBAAgB,EACjB,MAAM,YAAY,CAAC;AA2BpB;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,UAAU,EAClB,IAAI,EAAE,gBAAgB,GACrB,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,WAAW,CAAC,CAuD5C"}
1
+ {"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EACV,WAAW,EACX,gBAAgB,EACjB,MAAM,YAAY,CAAC;AA6BpB;;;;;;;;;;;;;GAaG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,UAAU,EAClB,IAAI,EAAE,gBAAgB,GACrB,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,WAAW,CAAC,CAyG5C"}
@@ -1,5 +1,7 @@
1
1
  import { verifySession } from "./session.js";
2
2
  import { verifyApiKey, extractApiKeyPrefix } from "./api-key.js";
3
+ import { validateDpopProof } from "./dpop.js";
4
+ import { extractWorkloadIdentity } from "./workload.js";
3
5
  const SESSION_COOKIE_NAME = "capstan_session";
4
6
  const DEFAULT_API_KEY_PREFIX = "cap_ak_";
5
7
  const ANONYMOUS_CONTEXT = {
@@ -25,19 +27,41 @@ function parseCookies(header) {
25
27
  * incoming `Request`.
26
28
  *
27
29
  * Resolution order:
28
- * 1. Session cookie (`capstan_session`) — verifies JWT, returns human context.
29
- * 2. `Authorization: Bearer <token>` header — if the token matches the
30
+ * 1. Client certificate / workload identity (`X-Client-Cert` or
31
+ * `X-Forwarded-Client-Cert` header)extracts SPIFFE ID, validates
32
+ * trust domain, returns workload context.
33
+ * 2. Session cookie (`capstan_session`) — verifies JWT, returns human context.
34
+ * 3. `Authorization: Bearer <token>` header — if the token matches the
30
35
  * configured API key prefix, looks up the agent credential and verifies
31
36
  * the key hash.
32
- * 3. Falls back to `{ type: "anonymous", isAuthenticated: false }`.
37
+ * 4. Falls back to `{ type: "anonymous", isAuthenticated: false }`.
33
38
  */
34
39
  export function createAuthMiddleware(config, deps) {
35
40
  const apiKeyPrefix = config.apiKeys?.prefix ?? DEFAULT_API_KEY_PREFIX;
36
41
  const authHeaderName = config.apiKeys?.headerName ?? "Authorization";
42
+ const trustedDomains = config.trustedDomains ?? [];
37
43
  return async (request) => {
38
- // ── 1. Session cookie ────────────────────────────────────────
44
+ let authCtx;
45
+ let accessToken;
46
+ // ── 1. Workload identity (mTLS / SPIFFE) ─────────────────────
47
+ if (trustedDomains.length > 0) {
48
+ const headers = {};
49
+ for (const [key, value] of request.headers.entries()) {
50
+ headers[key] = value;
51
+ }
52
+ const identity = extractWorkloadIdentity(headers, trustedDomains);
53
+ if (identity) {
54
+ authCtx = {
55
+ isAuthenticated: true,
56
+ type: "workload",
57
+ spiffeId: identity.spiffeId,
58
+ certFingerprint: identity.certFingerprint,
59
+ };
60
+ }
61
+ }
62
+ // ── 2. Session cookie ────────────────────────────────────────
39
63
  const cookieHeader = request.headers.get("cookie");
40
- if (cookieHeader) {
64
+ if (!authCtx && cookieHeader) {
41
65
  const cookies = parseCookies(cookieHeader);
42
66
  const sessionToken = cookies.get(SESSION_COOKIE_NAME);
43
67
  if (sessionToken) {
@@ -52,35 +76,55 @@ export function createAuthMiddleware(config, deps) {
52
76
  ctx.role = payload.role;
53
77
  if (payload.email !== undefined)
54
78
  ctx.email = payload.email;
55
- return ctx;
79
+ authCtx = ctx;
80
+ accessToken = sessionToken;
56
81
  }
57
82
  }
58
83
  }
59
- // ── 2. API key via Authorization header ──────────────────────
60
- const authHeader = request.headers.get(authHeaderName);
61
- if (authHeader) {
62
- const token = authHeader.startsWith("Bearer ")
63
- ? authHeader.slice(7)
64
- : null;
65
- if (token && token.startsWith(apiKeyPrefix) && deps.findAgentByKeyPrefix) {
66
- const prefix = extractApiKeyPrefix(token);
67
- const credential = await deps.findAgentByKeyPrefix(prefix);
68
- if (credential && !credential.revokedAt) {
69
- const valid = await verifyApiKey(token, credential.apiKeyHash);
70
- if (valid) {
71
- return {
72
- isAuthenticated: true,
73
- type: "agent",
74
- agentId: credential.id,
75
- agentName: credential.name,
76
- permissions: credential.permissions,
77
- };
84
+ // ── 3. API key via Authorization header ──────────────────────
85
+ if (!authCtx) {
86
+ const authHeader = request.headers.get(authHeaderName);
87
+ if (authHeader) {
88
+ const token = authHeader.startsWith("Bearer ")
89
+ ? authHeader.slice(7)
90
+ : authHeader.startsWith("DPoP ")
91
+ ? authHeader.slice(5)
92
+ : null;
93
+ if (token && token.startsWith(apiKeyPrefix) && deps.findAgentByKeyPrefix) {
94
+ const prefix = extractApiKeyPrefix(token);
95
+ const credential = await deps.findAgentByKeyPrefix(prefix);
96
+ if (credential && !credential.revokedAt) {
97
+ const valid = await verifyApiKey(token, credential.apiKeyHash);
98
+ if (valid) {
99
+ authCtx = {
100
+ isAuthenticated: true,
101
+ type: "agent",
102
+ agentId: credential.id,
103
+ agentName: credential.name,
104
+ permissions: credential.permissions,
105
+ };
106
+ accessToken = token;
107
+ }
78
108
  }
79
109
  }
80
110
  }
81
111
  }
82
- // ── 3. Anonymous ─────────────────────────────────────────────
83
- return ANONYMOUS_CONTEXT;
112
+ // ── 4. DPoP proof validation ─────────────────────────────────
113
+ // If a DPoP header is present, validate the proof and bind the
114
+ // token to the key thumbprint. A missing or invalid proof when
115
+ // the header is present causes the auth context to be rejected.
116
+ const dpopHeader = request.headers.get("dpop");
117
+ if (dpopHeader && authCtx) {
118
+ const result = await validateDpopProof(dpopHeader, request.method, request.url, accessToken);
119
+ if (!result) {
120
+ // DPoP proof failed validation — treat as unauthenticated.
121
+ return ANONYMOUS_CONTEXT;
122
+ }
123
+ // Bind the DPoP thumbprint to the auth context.
124
+ authCtx.dpopThumbprint = result.thumbprint;
125
+ }
126
+ // ── 5. Anonymous ─────────────────────────────────────────────
127
+ return authCtx ?? ANONYMOUS_CONTEXT;
84
128
  };
85
129
  }
86
130
  //# sourceMappingURL=middleware.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"middleware.js","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"middleware.js","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Pluggable key-value store interface.
3
+ *
4
+ * Identical to `@zauso-ai/capstan-core`'s `KeyValueStore` — duplicated here
5
+ * so that `@zauso-ai/capstan-auth` has no hard dependency on the core package.
6
+ */
7
+ export interface KeyValueStore<T> {
8
+ get(key: string): Promise<T | undefined>;
9
+ set(key: string, value: T, ttlMs?: number): Promise<void>;
10
+ delete(key: string): Promise<boolean>;
11
+ has(key: string): Promise<boolean>;
12
+ clear(): Promise<void>;
13
+ }
14
+ /**
15
+ * In-memory implementation of `KeyValueStore` with optional per-entry TTL.
16
+ */
17
+ export declare class MemoryStore<T> implements KeyValueStore<T> {
18
+ private data;
19
+ get(key: string): Promise<T | undefined>;
20
+ set(key: string, value: T, ttlMs?: number): Promise<void>;
21
+ delete(key: string): Promise<boolean>;
22
+ has(key: string): Promise<boolean>;
23
+ clear(): Promise<void>;
24
+ }
25
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,WAAW,aAAa,CAAC,CAAC;IAC9B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;IACzC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACtC,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACnC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED;;GAEG;AACH,qBAAa,WAAW,CAAC,CAAC,CAAE,YAAW,aAAa,CAAC,CAAC,CAAC;IACrD,OAAO,CAAC,IAAI,CAAkE;IAExE,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC;IAUxC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAOzD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAIrC,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAKlC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7B"}
package/dist/store.js ADDED
@@ -0,0 +1,33 @@
1
+ /**
2
+ * In-memory implementation of `KeyValueStore` with optional per-entry TTL.
3
+ */
4
+ export class MemoryStore {
5
+ data = new Map();
6
+ async get(key) {
7
+ const entry = this.data.get(key);
8
+ if (!entry)
9
+ return undefined;
10
+ if (entry.expiresAt !== undefined && Date.now() > entry.expiresAt) {
11
+ this.data.delete(key);
12
+ return undefined;
13
+ }
14
+ return entry.value;
15
+ }
16
+ async set(key, value, ttlMs) {
17
+ this.data.set(key, {
18
+ value,
19
+ expiresAt: ttlMs ? Date.now() + ttlMs : undefined,
20
+ });
21
+ }
22
+ async delete(key) {
23
+ return this.data.delete(key);
24
+ }
25
+ async has(key) {
26
+ const val = await this.get(key); // triggers TTL check
27
+ return val !== undefined;
28
+ }
29
+ async clear() {
30
+ this.data.clear();
31
+ }
32
+ }
33
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAcA;;GAEG;AACH,MAAM,OAAO,WAAW;IACd,IAAI,GAAG,IAAI,GAAG,EAAuD,CAAC;IAE9E,KAAK,CAAC,GAAG,CAAC,GAAW;QACnB,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACjC,IAAI,CAAC,KAAK;YAAE,OAAO,SAAS,CAAC;QAC7B,IAAI,KAAK,CAAC,SAAS,KAAK,SAAS,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,SAAS,EAAE,CAAC;YAClE,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACtB,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,OAAO,KAAK,CAAC,KAAK,CAAC;IACrB,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,GAAW,EAAE,KAAQ,EAAE,KAAc;QAC7C,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE;YACjB,KAAK;YACL,SAAS,EAAE,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,CAAC,CAAC,SAAS;SAClD,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,GAAW;QACtB,OAAO,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC/B,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,GAAW;QACnB,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,qBAAqB;QACtD,OAAO,GAAG,KAAK,SAAS,CAAC;IAC3B,CAAC;IAED,KAAK,CAAC,KAAK;QACT,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;IACpB,CAAC;CACF"}
package/dist/types.d.ts CHANGED
@@ -7,6 +7,10 @@ export interface AuthConfig {
7
7
  prefix?: string;
8
8
  headerName?: string;
9
9
  };
10
+ /** Trusted SPIFFE trust domains for mTLS workload authentication. */
11
+ trustedDomains?: string[];
12
+ /** Whether to require client certificates (mTLS). */
13
+ mtls?: boolean;
10
14
  }
11
15
  export interface SessionPayload {
12
16
  userId: string;
@@ -25,13 +29,19 @@ export interface AgentCredential {
25
29
  }
26
30
  export interface AuthContext {
27
31
  isAuthenticated: boolean;
28
- type: "human" | "agent" | "anonymous";
32
+ type: "human" | "agent" | "anonymous" | "workload";
29
33
  userId?: string;
30
34
  role?: string;
31
35
  email?: string;
32
36
  agentId?: string;
33
37
  agentName?: string;
34
38
  permissions?: string[];
39
+ /** DPoP key thumbprint — present when the request included a valid DPoP proof. */
40
+ dpopThumbprint?: string;
41
+ /** SPIFFE ID from client certificate (e.g. "spiffe://example.org/agent/crawler"). */
42
+ spiffeId?: string;
43
+ /** Client certificate fingerprint (SHA-256 hex digest). */
44
+ certFingerprint?: string;
35
45
  }
36
46
  export interface AuthResolverDeps {
37
47
  /** Look up an agent credential by API key prefix */
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE;QACP,MAAM,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC;IACF,OAAO,CAAC,EAAE;QACR,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,CAAC;CACH;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,eAAe,EAAE,OAAO,CAAC;IACzB,IAAI,EAAE,OAAO,GAAG,OAAO,GAAG,WAAW,CAAC;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,gBAAgB;IAC/B,oDAAoD;IACpD,oBAAoB,CAAC,EAAE,CACrB,MAAM,EAAE,MAAM,KACX,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAAC;CACtC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE;QACP,MAAM,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC;IACF,OAAO,CAAC,EAAE;QACR,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,CAAC;IACF,qEAAqE;IACrE,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,qDAAqD;IACrD,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,eAAe,EAAE,OAAO,CAAC;IACzB,IAAI,EAAE,OAAO,GAAG,OAAO,GAAG,WAAW,GAAG,UAAU,CAAC;IACnD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,kFAAkF;IAClF,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,qFAAqF;IACrF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2DAA2D;IAC3D,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,gBAAgB;IAC/B,oDAAoD;IACpD,oBAAoB,CAAC,EAAE,CACrB,MAAM,EAAE,MAAM,KACX,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAAC;CACtC"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Parsed workload identity extracted from a client certificate.
3
+ */
4
+ export interface WorkloadIdentity {
5
+ /** Full SPIFFE ID URI (e.g. "spiffe://example.org/agent/crawler"). */
6
+ spiffeId: string;
7
+ /** Trust domain portion of the SPIFFE ID (e.g. "example.org"). */
8
+ trustDomain: string;
9
+ /** Workload path portion of the SPIFFE ID (e.g. "/agent/crawler"). */
10
+ workloadPath: string;
11
+ /** SHA-256 hex digest of the PEM-encoded client certificate. */
12
+ certFingerprint: string;
13
+ }
14
+ /**
15
+ * Validate that a string is a well-formed SPIFFE ID per the SPIFFE spec:
16
+ *
17
+ * - Scheme must be `spiffe`
18
+ * - Trust domain must be a non-empty hostname-like string (lower-case
19
+ * alphanumerics, hyphens, dots; no leading/trailing hyphens or dots).
20
+ * - Workload path must be non-empty and start with `/`.
21
+ */
22
+ export declare function isValidSpiffeId(id: string): boolean;
23
+ /**
24
+ * Extract a workload identity from client certificate information provided
25
+ * via request headers.
26
+ *
27
+ * In a typical deployment the mTLS termination happens at a reverse proxy
28
+ * (Envoy, Nginx, Istio ingress, etc.) which forwards the client certificate
29
+ * and its SPIFFE ID through HTTP headers.
30
+ *
31
+ * Supported header combinations:
32
+ *
33
+ * | SPIFFE ID source | Cert source |
34
+ * |---------------------------------------|--------------------------|
35
+ * | `X-Client-Cert-Spiffe-Id` header | `X-Client-Cert` |
36
+ * | `URI=` in `X-Forwarded-Client-Cert` | `X-Forwarded-Client-Cert`|
37
+ *
38
+ * @param certOrHeaders PEM-encoded client certificate string, or a
39
+ * header map (e.g. from `Object.fromEntries(request.headers)`).
40
+ * @param trustedDomains Whitelist of SPIFFE trust domains to accept.
41
+ * @returns The parsed `WorkloadIdentity` if the certificate is present, the
42
+ * SPIFFE ID is valid, and the trust domain is in the whitelist.
43
+ * Returns `null` otherwise.
44
+ */
45
+ export declare function extractWorkloadIdentity(certOrHeaders: string | Record<string, string | undefined>, trustedDomains: string[]): WorkloadIdentity | null;
46
+ //# sourceMappingURL=workload.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workload.d.ts","sourceRoot":"","sources":["../src/workload.ts"],"names":[],"mappings":"AAMA;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,sEAAsE;IACtE,QAAQ,EAAE,MAAM,CAAC;IACjB,kEAAkE;IAClE,WAAW,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,YAAY,EAAE,MAAM,CAAC;IACrB,gEAAgE;IAChE,eAAe,EAAE,MAAM,CAAC;CACzB;AAiBD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CA6BnD;AAkID;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,uBAAuB,CACrC,aAAa,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,EAC1D,cAAc,EAAE,MAAM,EAAE,GACvB,gBAAgB,GAAG,IAAI,CAyCzB"}
@@ -0,0 +1,227 @@
1
+ import { createHash } from "node:crypto";
2
+ /**
3
+ * Headers that reverse proxies commonly use to forward client certificates.
4
+ *
5
+ * - `x-client-cert` — raw PEM or URL-encoded PEM (Envoy, Nginx, etc.)
6
+ * - `x-forwarded-client-cert` — Envoy XFCC header (may contain key=value pairs)
7
+ */
8
+ const CLIENT_CERT_HEADERS = [
9
+ "x-client-cert",
10
+ "x-forwarded-client-cert",
11
+ ];
12
+ // ---------------------------------------------------------------------------
13
+ // SPIFFE ID validation
14
+ // ---------------------------------------------------------------------------
15
+ /**
16
+ * Validate that a string is a well-formed SPIFFE ID per the SPIFFE spec:
17
+ *
18
+ * - Scheme must be `spiffe`
19
+ * - Trust domain must be a non-empty hostname-like string (lower-case
20
+ * alphanumerics, hyphens, dots; no leading/trailing hyphens or dots).
21
+ * - Workload path must be non-empty and start with `/`.
22
+ */
23
+ export function isValidSpiffeId(id) {
24
+ if (typeof id !== "string" || id.length === 0)
25
+ return false;
26
+ // Must start with the spiffe scheme.
27
+ if (!id.startsWith("spiffe://"))
28
+ return false;
29
+ const rest = id.slice("spiffe://".length);
30
+ // Find the first "/" after the trust domain.
31
+ const slashIndex = rest.indexOf("/");
32
+ if (slashIndex <= 0)
33
+ return false; // no trust domain or no path
34
+ const trustDomain = rest.slice(0, slashIndex);
35
+ const workloadPath = rest.slice(slashIndex);
36
+ // Trust domain: valid DNS-like label(s), no leading/trailing dot or hyphen.
37
+ if (!isValidTrustDomain(trustDomain))
38
+ return false;
39
+ // Workload path must be non-empty (beyond the leading slash).
40
+ if (workloadPath.length < 2)
41
+ return false;
42
+ // Path segments must not be empty (no double slashes) and must not
43
+ // contain `.` or `..` traversals.
44
+ const segments = workloadPath.slice(1).split("/");
45
+ for (const seg of segments) {
46
+ if (seg.length === 0 || seg === "." || seg === "..")
47
+ return false;
48
+ }
49
+ return true;
50
+ }
51
+ /**
52
+ * A trust domain is a valid lower-case DNS name: labels separated by dots,
53
+ * each label consisting of alphanumerics and hyphens, not starting/ending
54
+ * with a hyphen.
55
+ */
56
+ function isValidTrustDomain(domain) {
57
+ if (domain.length === 0 || domain.length > 255)
58
+ return false;
59
+ const labels = domain.split(".");
60
+ for (const label of labels) {
61
+ if (label.length === 0 || label.length > 63)
62
+ return false;
63
+ if (/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(label) === false)
64
+ return false;
65
+ }
66
+ return true;
67
+ }
68
+ // ---------------------------------------------------------------------------
69
+ // Certificate parsing helpers
70
+ // ---------------------------------------------------------------------------
71
+ /**
72
+ * Extract the PEM-encoded client certificate from request headers.
73
+ *
74
+ * Supports:
75
+ * - `X-Client-Cert: <PEM or URL-encoded PEM>`
76
+ * - `X-Forwarded-Client-Cert: Cert="<URL-encoded PEM>"` (Envoy XFCC format)
77
+ */
78
+ function extractPemFromHeaders(headers) {
79
+ // Normalise header names to lower case for case-insensitive lookup.
80
+ const normalised = {};
81
+ for (const [k, v] of Object.entries(headers)) {
82
+ if (v !== undefined) {
83
+ normalised[k.toLowerCase()] = v;
84
+ }
85
+ }
86
+ for (const headerName of CLIENT_CERT_HEADERS) {
87
+ const raw = normalised[headerName];
88
+ if (!raw || raw.length === 0)
89
+ continue;
90
+ // XFCC header from Envoy may look like:
91
+ // By=...;Cert="<url-encoded PEM>";Hash=...
92
+ // Extract the Cert value if present.
93
+ const certMatch = raw.match(/Cert="([^"]+)"/i);
94
+ if (certMatch?.[1]) {
95
+ return decodePem(certMatch[1]);
96
+ }
97
+ // Otherwise treat the entire header value as PEM (possibly URL-encoded).
98
+ return decodePem(raw);
99
+ }
100
+ return null;
101
+ }
102
+ /**
103
+ * Decode a PEM string that may be URL-encoded (common in proxy headers).
104
+ */
105
+ function decodePem(value) {
106
+ // If the value contains %2F or %2B it was URL-encoded.
107
+ if (value.includes("%")) {
108
+ try {
109
+ return decodeURIComponent(value);
110
+ }
111
+ catch {
112
+ // Not valid URL encoding — use as-is.
113
+ }
114
+ }
115
+ return value;
116
+ }
117
+ /**
118
+ * Compute the SHA-256 hex fingerprint of a PEM-encoded certificate.
119
+ *
120
+ * The fingerprint is computed over the raw PEM text (including header/footer
121
+ * lines) to keep the implementation dependency-free. This is sufficient for
122
+ * identity-binding purposes as each unique cert will produce a unique hash.
123
+ */
124
+ function computeCertFingerprint(pem) {
125
+ return createHash("sha256").update(pem).digest("hex");
126
+ }
127
+ /**
128
+ * Extract the SPIFFE URI SAN from a PEM-encoded certificate.
129
+ *
130
+ * Real X.509 parsing requires ASN.1 decoding which would need a dependency.
131
+ * Instead we use a pragmatic approach: the SPIFFE ID is looked up from a
132
+ * companion header (`X-Client-Cert-Spiffe-Id`) that mTLS-terminating proxies
133
+ * (Envoy, Istio, Linkerd) can be configured to set, or it can be embedded
134
+ * in the XFCC header as `URI=spiffe://...`.
135
+ *
136
+ * If neither is available we fall back to scanning the PEM base64 payload
137
+ * for the `spiffe://` URI (works for unencrypted certs where the SAN is
138
+ * visible in the base64 text — a useful heuristic but not a substitute for
139
+ * real ASN.1 parsing).
140
+ */
141
+ function extractSpiffeIdFromHeaders(headers) {
142
+ const normalised = {};
143
+ for (const [k, v] of Object.entries(headers)) {
144
+ if (v !== undefined) {
145
+ normalised[k.toLowerCase()] = v;
146
+ }
147
+ }
148
+ // 1. Explicit header set by the proxy.
149
+ const explicit = normalised["x-client-cert-spiffe-id"];
150
+ if (explicit && isValidSpiffeId(explicit)) {
151
+ return explicit;
152
+ }
153
+ // 2. XFCC URI field (Envoy sets `URI=spiffe://...`).
154
+ const xfcc = normalised["x-forwarded-client-cert"];
155
+ if (xfcc) {
156
+ const uriMatch = xfcc.match(/URI=(spiffe:\/\/[^;,"]+)/i);
157
+ if (uriMatch?.[1] && isValidSpiffeId(uriMatch[1])) {
158
+ return uriMatch[1];
159
+ }
160
+ }
161
+ return null;
162
+ }
163
+ // ---------------------------------------------------------------------------
164
+ // Public API
165
+ // ---------------------------------------------------------------------------
166
+ /**
167
+ * Extract a workload identity from client certificate information provided
168
+ * via request headers.
169
+ *
170
+ * In a typical deployment the mTLS termination happens at a reverse proxy
171
+ * (Envoy, Nginx, Istio ingress, etc.) which forwards the client certificate
172
+ * and its SPIFFE ID through HTTP headers.
173
+ *
174
+ * Supported header combinations:
175
+ *
176
+ * | SPIFFE ID source | Cert source |
177
+ * |---------------------------------------|--------------------------|
178
+ * | `X-Client-Cert-Spiffe-Id` header | `X-Client-Cert` |
179
+ * | `URI=` in `X-Forwarded-Client-Cert` | `X-Forwarded-Client-Cert`|
180
+ *
181
+ * @param certOrHeaders PEM-encoded client certificate string, or a
182
+ * header map (e.g. from `Object.fromEntries(request.headers)`).
183
+ * @param trustedDomains Whitelist of SPIFFE trust domains to accept.
184
+ * @returns The parsed `WorkloadIdentity` if the certificate is present, the
185
+ * SPIFFE ID is valid, and the trust domain is in the whitelist.
186
+ * Returns `null` otherwise.
187
+ */
188
+ export function extractWorkloadIdentity(certOrHeaders, trustedDomains) {
189
+ if (trustedDomains.length === 0)
190
+ return null;
191
+ let pem;
192
+ let spiffeId;
193
+ if (typeof certOrHeaders === "string") {
194
+ // Raw PEM string provided directly — cannot extract SPIFFE ID from
195
+ // headers, so this path is only useful if the caller also provides
196
+ // the SPIFFE ID separately. For header-based flows use the object form.
197
+ pem = certOrHeaders.length > 0 ? certOrHeaders : null;
198
+ spiffeId = null;
199
+ }
200
+ else {
201
+ pem = extractPemFromHeaders(certOrHeaders);
202
+ spiffeId = extractSpiffeIdFromHeaders(certOrHeaders);
203
+ }
204
+ // We need both a certificate (for fingerprinting) and a SPIFFE ID.
205
+ if (!pem || !spiffeId)
206
+ return null;
207
+ if (!isValidSpiffeId(spiffeId))
208
+ return null;
209
+ // Parse the trust domain from the SPIFFE ID.
210
+ const rest = spiffeId.slice("spiffe://".length);
211
+ const slashIndex = rest.indexOf("/");
212
+ if (slashIndex <= 0)
213
+ return null;
214
+ const trustDomain = rest.slice(0, slashIndex);
215
+ const workloadPath = rest.slice(slashIndex);
216
+ // Verify the trust domain is in the whitelist.
217
+ if (!trustedDomains.includes(trustDomain))
218
+ return null;
219
+ const certFingerprint = computeCertFingerprint(pem);
220
+ return {
221
+ spiffeId,
222
+ trustDomain,
223
+ workloadPath,
224
+ certFingerprint,
225
+ };
226
+ }
227
+ //# sourceMappingURL=workload.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workload.js","sourceRoot":"","sources":["../src/workload.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zauso-ai/capstan-auth",
3
- "version": "1.0.0-beta.5",
3
+ "version": "1.0.0-beta.6",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -12,7 +12,8 @@
12
12
  },
13
13
  "scripts": {
14
14
  "build": "tsc -p tsconfig.json",
15
- "typecheck": "tsc -p tsconfig.json --noEmit"
15
+ "typecheck": "tsc -p tsconfig.json --noEmit",
16
+ "clean": "rm -rf dist"
16
17
  },
17
18
  "description": "Authentication for Capstan — JWT sessions, API key auth for AI agents, permissions",
18
19
  "files": [