@hawcx/oauth-client 5.1.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +479 -0
  2. package/README.md +75 -10
  3. package/dist/client-assertion.d.ts.map +1 -1
  4. package/dist/client-assertion.js +6 -2
  5. package/dist/client-assertion.js.map +1 -1
  6. package/dist/delegation/exceptions.d.ts +34 -0
  7. package/dist/delegation/exceptions.d.ts.map +1 -1
  8. package/dist/delegation/exceptions.js +39 -1
  9. package/dist/delegation/exceptions.js.map +1 -1
  10. package/dist/delegation/hawcx.d.ts +6 -2
  11. package/dist/delegation/hawcx.d.ts.map +1 -1
  12. package/dist/delegation/hawcx.js.map +1 -1
  13. package/dist/delegation/index.d.ts +2 -1
  14. package/dist/delegation/index.d.ts.map +1 -1
  15. package/dist/delegation/index.js +3 -1
  16. package/dist/delegation/index.js.map +1 -1
  17. package/dist/delegation/transport.d.ts +6 -2
  18. package/dist/delegation/transport.d.ts.map +1 -1
  19. package/dist/delegation/transport.js +9 -2
  20. package/dist/delegation/transport.js.map +1 -1
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.js +1 -1
  23. package/dist/internal/url.d.ts +66 -0
  24. package/dist/internal/url.d.ts.map +1 -0
  25. package/dist/internal/url.js +155 -0
  26. package/dist/internal/url.js.map +1 -0
  27. package/dist/management/jwt_transport.d.ts +150 -0
  28. package/dist/management/jwt_transport.d.ts.map +1 -0
  29. package/dist/management/jwt_transport.js +245 -0
  30. package/dist/management/jwt_transport.js.map +1 -0
  31. package/dist/management/response_signature.d.ts +65 -0
  32. package/dist/management/response_signature.d.ts.map +1 -0
  33. package/dist/management/response_signature.js +170 -0
  34. package/dist/management/response_signature.js.map +1 -0
  35. package/dist/oauth.d.ts +1 -1
  36. package/dist/oauth.d.ts.map +1 -1
  37. package/dist/oauth.js +35 -24
  38. package/dist/oauth.js.map +1 -1
  39. package/dist/stepup/client.d.ts +72 -2
  40. package/dist/stepup/client.d.ts.map +1 -1
  41. package/dist/stepup/client.js +69 -0
  42. package/dist/stepup/client.js.map +1 -1
  43. package/package.json +2 -1
@@ -0,0 +1,150 @@
1
+ /**
2
+ * JWT-bearer transport for the Hawcx management API (Path A: private_key_jwt).
3
+ *
4
+ * Each request is authenticated with a per-request EdDSA JWT whose `aud` is
5
+ * the full request URL, carrying a `body_sha256` claim over the EXACT bytes
6
+ * sent. This mirrors the wire contract verified end-to-end against hx_auth
7
+ * (see spec.md §management-api-auth / Python SDK Path A implementation).
8
+ *
9
+ * Construction:
10
+ * const transport = new JwtBearerTransport({
11
+ * baseUrl: 'https://api.hawcx.com',
12
+ * configId: 'your-config-id',
13
+ * clientId: 'your-client-id',
14
+ * oidcSigningKey: pkcs8Pem, // Ed25519 PKCS#8 PEM — same key registered in admin console
15
+ * kid: 'key-id',
16
+ * });
17
+ *
18
+ * The `http` constructor option accepts an injectable fetch-compatible function
19
+ * (same signature as the global `fetch`) for testing.
20
+ */
21
+ import { type JWK } from 'jose';
22
+ import type { KeyLike } from 'jose';
23
+ import type { JsonObject } from '../delegation/transport';
24
+ /**
25
+ * A fetch-compatible callable — accepts `(url: string, init: RequestInit)`
26
+ * and returns a `Promise<Response>`. Mirrors the global `fetch` signature so
27
+ * the real `fetch` can be passed unchanged.
28
+ */
29
+ export type HttpFetch = (url: string, init: RequestInit) => Promise<Response>;
30
+ export interface JwtBearerTransportOptions {
31
+ /**
32
+ * Base URL of the management API, e.g. `https://api.hawcx.com`. Must be
33
+ * `https:` (plain `http:` only for `localhost` / `127.0.0.1` / `[::1]`),
34
+ * without credentials, query or fragment. May carry a path prefix
35
+ * (e.g. `https://gateway.example.com/hawcx`). Validated at construction;
36
+ * an invalid value throws `DelegationRequestError`.
37
+ */
38
+ baseUrl: string;
39
+ /** Tenant config identifier; forwarded as `X-Config-Id`. */
40
+ configId: string;
41
+ /**
42
+ * Ed25519 private signing key. Accepts:
43
+ * - PKCS#8 PEM string (e.g. `"-----BEGIN PRIVATE KEY-----\n…"`)
44
+ * - JWK object `{ kty:'OKP', crv:'Ed25519', d, x, kid? }`
45
+ * - jose `KeyLike` / `CryptoKey` already imported
46
+ */
47
+ oidcSigningKey: string | JWK | KeyLike;
48
+ /** Key ID embedded in the JWT `kid` header. */
49
+ kid: string;
50
+ /** OAuth client ID; used as both `iss` and `sub` in the per-request JWT. */
51
+ clientId: string;
52
+ /**
53
+ * JWT time-to-live in seconds (default: 60). The server rejects tokens
54
+ * older than its configured TTL; do not set this above 120.
55
+ */
56
+ jwtTtlSeconds?: number;
57
+ /**
58
+ * Per-request timeout in seconds (default: 15). A stalled
59
+ * (connected-but-silent) server would otherwise hang the call forever and
60
+ * can exhaust connection pools server-side. Mirrors the ECIES
61
+ * `DelegationTransport`'s 15s `AbortController`.
62
+ */
63
+ timeoutSeconds?: number;
64
+ /**
65
+ * Injectable fetch implementation. Defaults to the global `fetch`
66
+ * (Node >= 18). Pass a capturing fake in tests.
67
+ */
68
+ http?: HttpFetch;
69
+ /**
70
+ * OIDC issuer whose `${issuer}/.well-known/jwks.json` publishes the key that
71
+ * signs management responses (the same JWKS that verifies ID tokens).
72
+ * Defaults to `baseUrl` — in every Hawcx environment the management API and
73
+ * the OP share one public host (e.g. `https://api.hawcx.com`). Set this
74
+ * only when they differ (e.g. local dev with separate ports). Same URL
75
+ * rules as `baseUrl`.
76
+ */
77
+ issuer?: string;
78
+ /**
79
+ * Verify the `X-Response-Signature` JWS on every successful response
80
+ * (default: `true`). A response with a missing or invalid signature is
81
+ * rejected with `DelegationResponseError`.
82
+ *
83
+ * Failures throw `ResponseSignatureInvalidError` (bad/missing signature) or
84
+ * `ResponseSignatureUnavailableError` (signing keys could not be fetched —
85
+ * the request may already have been applied server-side); both extend
86
+ * `DelegationResponseError` and expose a stable `code`.
87
+ *
88
+ * **Emergency escape hatch only.** Setting `false` makes the SDK trust any
89
+ * response body it receives, unauthenticated; a one-time process warning is
90
+ * emitted. Do not disable in production.
91
+ */
92
+ verifyResponseSignature?: boolean;
93
+ /**
94
+ * Allowed difference in seconds between local time and the response
95
+ * signature's `iat` (default: 300) — same default as the ECIES transport's
96
+ * response timestamp skew.
97
+ */
98
+ clockSkewSeconds?: number;
99
+ }
100
+ /**
101
+ * Drop-in transport for the management API that authenticates via a per-request
102
+ * EdDSA JWT (`private_key_jwt`, Path A).
103
+ *
104
+ * The `send` method has the same name and signature as `DelegationTransport.send`
105
+ * so the factory can swap transports behind `StepUpClient` without any change to
106
+ * the business-method layer.
107
+ */
108
+ export declare class JwtBearerTransport {
109
+ private readonly baseUrl;
110
+ private readonly configId;
111
+ private readonly kid;
112
+ private readonly clientId;
113
+ private readonly jwtTtlSeconds;
114
+ private readonly timeoutMs;
115
+ private readonly http;
116
+ /** `null` only when verification was explicitly disabled. */
117
+ private readonly responseVerifier;
118
+ /** Lazily-imported (and memoised) signing key. */
119
+ private keyPromise?;
120
+ private readonly importKeyFn;
121
+ constructor(options: JwtBearerTransportOptions);
122
+ private importKey;
123
+ /**
124
+ * Mint a per-request EdDSA JWT.
125
+ *
126
+ * @param audience - full request URL (base joined with endpoint), pinned per-call
127
+ * @param bodySha256 - base64url (no-padding) of SHA-256 over the exact body bytes
128
+ */
129
+ private mintJwt;
130
+ /**
131
+ * Send an authenticated POST to `endpoint` with `payload` serialised as JSON.
132
+ *
133
+ * Matches the signature of `DelegationTransport.send` so the two transports
134
+ * are drop-in substitutes behind `StepUpClient`.
135
+ *
136
+ * @param options.endpoint - API path relative to `baseUrl`, e.g.
137
+ * `/v1/management/stepup/start-token`. Must start with a single `/`;
138
+ * anything that would resolve off the configured origin or out of its path
139
+ * prefix throws `DelegationRequestError` before the request is signed.
140
+ * @param options.payload - request body; serialised once and hashed; the
141
+ * EXACT same bytes are sent and hashed — no double-serialisation.
142
+ * @param options.headers - optional extra headers merged last (win over built-ins)
143
+ */
144
+ send<TReq extends JsonObject, TRes extends JsonObject>(options: {
145
+ endpoint: string;
146
+ payload: TReq;
147
+ headers?: Record<string, string>;
148
+ }): Promise<TRes>;
149
+ }
150
+ //# sourceMappingURL=jwt_transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jwt_transport.d.ts","sourceRoot":"","sources":["../../src/management/jwt_transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAIH,OAAO,EAAmC,KAAK,GAAG,EAAE,MAAM,MAAM,CAAC;AACjE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAEpC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAc1D;;;;GAIG;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAE9E,MAAM,WAAW,yBAAyB;IACxC;;;;;;OAMG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB,4DAA4D;IAC5D,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,cAAc,EAAE,MAAM,GAAG,GAAG,GAAG,OAAO,CAAC;IACvC,+CAA+C;IAC/C,GAAG,EAAE,MAAM,CAAC;IACZ,4EAA4E;IAC5E,QAAQ,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;;;;;;;;OAaG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;GAOG;AACH,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAgB;IACxC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAC7B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAY;IACjC,6DAA6D;IAC7D,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAmC;IAEpE,kDAAkD;IAClD,OAAO,CAAC,UAAU,CAAC,CAAuB;IAC1C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA6B;gBAE7C,OAAO,EAAE,yBAAyB;IAgE9C,OAAO,CAAC,SAAS;IAajB;;;;;OAKG;YACW,OAAO;IAiBrB;;;;;;;;;;;;;OAaG;IACG,IAAI,CAAC,IAAI,SAAS,UAAU,EAAE,IAAI,SAAS,UAAU,EAAE,OAAO,EAAE;QACpE,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,IAAI,CAAC;QACd,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;KAClC,GAAG,OAAO,CAAC,IAAI,CAAC;CAgGlB"}
@@ -0,0 +1,245 @@
1
+ "use strict";
2
+ /**
3
+ * JWT-bearer transport for the Hawcx management API (Path A: private_key_jwt).
4
+ *
5
+ * Each request is authenticated with a per-request EdDSA JWT whose `aud` is
6
+ * the full request URL, carrying a `body_sha256` claim over the EXACT bytes
7
+ * sent. This mirrors the wire contract verified end-to-end against hx_auth
8
+ * (see spec.md §management-api-auth / Python SDK Path A implementation).
9
+ *
10
+ * Construction:
11
+ * const transport = new JwtBearerTransport({
12
+ * baseUrl: 'https://api.hawcx.com',
13
+ * configId: 'your-config-id',
14
+ * clientId: 'your-client-id',
15
+ * oidcSigningKey: pkcs8Pem, // Ed25519 PKCS#8 PEM — same key registered in admin console
16
+ * kid: 'key-id',
17
+ * });
18
+ *
19
+ * The `http` constructor option accepts an injectable fetch-compatible function
20
+ * (same signature as the global `fetch`) for testing.
21
+ */
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.JwtBearerTransport = void 0;
24
+ const node_crypto_1 = require("node:crypto");
25
+ const node_crypto_2 = require("node:crypto");
26
+ const jose_1 = require("jose");
27
+ const exceptions_1 = require("../delegation/exceptions");
28
+ const url_1 = require("../internal/url");
29
+ const response_signature_1 = require("./response_signature");
30
+ /** Config/URL errors surface as `DelegationRequestError`, like other request failures. */
31
+ const urlError = (message, cause) => new exceptions_1.DelegationRequestError(message, cause === undefined ? {} : { cause });
32
+ /** Emit the "verification disabled" warning at most once per process. */
33
+ let responseSignatureDisabledWarned = false;
34
+ /**
35
+ * Drop-in transport for the management API that authenticates via a per-request
36
+ * EdDSA JWT (`private_key_jwt`, Path A).
37
+ *
38
+ * The `send` method has the same name and signature as `DelegationTransport.send`
39
+ * so the factory can swap transports behind `StepUpClient` without any change to
40
+ * the business-method layer.
41
+ */
42
+ class JwtBearerTransport {
43
+ constructor(options) {
44
+ // Parsed and validated once (TOB-HSDK-13): every signed request is built
45
+ // from this structured origin + path prefix, never by string concatenation.
46
+ this.baseUrl = (0, url_1.parseSecureBaseUrl)('baseUrl', options.baseUrl, urlError);
47
+ this.configId = options.configId;
48
+ this.kid = options.kid;
49
+ this.clientId = options.clientId;
50
+ this.jwtTtlSeconds = options.jwtTtlSeconds ?? 60;
51
+ this.timeoutMs = (options.timeoutSeconds ?? 15) * 1000;
52
+ this.http = options.http ?? ((...args) => fetch(...args));
53
+ // Response authentication (TOB-HSDK-11). The JWKS URL is derived from the
54
+ // validated issuer with the same structured join as request URLs.
55
+ //
56
+ // DEFAULT ISSUER = baseUrl, i.e. JWKS = ${baseUrl}/.well-known/jwks.json,
57
+ // fetched without X-Config-Id. This holds today because the OP's single
58
+ // global ISSUER is the management API host and its global JWKS carries the
59
+ // response-signing kid. REVISIT under hawcx_core_oauth#67 (per-tenant
60
+ // issuer / discovery_mode cutover): if a tenant's signing kid moves to a
61
+ // per-tenant JWKS, this default stops containing it and every management
62
+ // call fails closed (ResponseSignatureUnavailable/Invalid). Callers can
63
+ // already override via the `issuer` option; whoever lands #67 must update
64
+ // this default (e.g. derive a per-tenant issuer) and the README.
65
+ const issuer = options.issuer === undefined
66
+ ? this.baseUrl
67
+ : (0, url_1.parseSecureBaseUrl)('issuer', options.issuer, urlError);
68
+ if (options.verifyResponseSignature === false) {
69
+ this.responseVerifier = null;
70
+ if (!responseSignatureDisabledWarned) {
71
+ responseSignatureDisabledWarned = true;
72
+ process.emitWarning('[@hawcx/oauth-client] JwtBearerTransport: verifyResponseSignature is disabled — ' +
73
+ 'management API responses will NOT be authenticated. Use only as a temporary escape hatch.', { type: 'Warning', code: 'HAWCX_RESPONSE_SIGNATURE_DISABLED' });
74
+ }
75
+ }
76
+ else {
77
+ const clockSkewSeconds = options.clockSkewSeconds ?? 300;
78
+ if (!Number.isFinite(clockSkewSeconds) || clockSkewSeconds < 0) {
79
+ throw new exceptions_1.DelegationRequestError('clockSkewSeconds must be a non-negative number');
80
+ }
81
+ this.responseVerifier = new response_signature_1.ResponseSignatureVerifier({
82
+ jwksUri: (0, url_1.safeJoinUrl)(issuer, '/.well-known/jwks.json', 'JWKS URL', urlError),
83
+ clockSkewSeconds,
84
+ timeoutMs: this.timeoutMs,
85
+ });
86
+ }
87
+ const key = options.oidcSigningKey;
88
+ if (typeof key === 'string') {
89
+ // PKCS#8 PEM
90
+ this.importKeyFn = () => (0, jose_1.importPKCS8)(key, 'EdDSA');
91
+ }
92
+ else if (isJwk(key)) {
93
+ // Plain JWK object
94
+ this.importKeyFn = () => (0, jose_1.importJWK)(key, 'EdDSA');
95
+ }
96
+ else {
97
+ // Already a jose KeyLike / CryptoKey — wrap in a resolved promise.
98
+ const resolved = Promise.resolve(key);
99
+ this.importKeyFn = () => resolved;
100
+ }
101
+ }
102
+ // ── Key loading ────────────────────────────────────────────────────────────
103
+ importKey() {
104
+ if (!this.keyPromise) {
105
+ // Don't memoise a rejection — reset so the next call retries.
106
+ this.keyPromise = this.importKeyFn().catch((err) => {
107
+ this.keyPromise = undefined;
108
+ throw err;
109
+ });
110
+ }
111
+ return this.keyPromise;
112
+ }
113
+ // ── JWT minting ────────────────────────────────────────────────────────────
114
+ /**
115
+ * Mint a per-request EdDSA JWT.
116
+ *
117
+ * @param audience - full request URL (base joined with endpoint), pinned per-call
118
+ * @param bodySha256 - base64url (no-padding) of SHA-256 over the exact body bytes
119
+ */
120
+ async mintJwt(audience, bodySha256) {
121
+ const key = await this.importKey();
122
+ const nowSec = Math.floor(Date.now() / 1000);
123
+ return new jose_1.SignJWT({ body_sha256: bodySha256 })
124
+ .setProtectedHeader({ alg: 'EdDSA', kid: this.kid, typ: 'JWT' })
125
+ .setIssuer(this.clientId)
126
+ .setSubject(this.clientId)
127
+ .setAudience(audience)
128
+ .setIssuedAt(nowSec)
129
+ .setExpirationTime(nowSec + this.jwtTtlSeconds)
130
+ .setJti((0, node_crypto_1.randomUUID)())
131
+ .sign(key);
132
+ }
133
+ // ── Transport ──────────────────────────────────────────────────────────────
134
+ /**
135
+ * Send an authenticated POST to `endpoint` with `payload` serialised as JSON.
136
+ *
137
+ * Matches the signature of `DelegationTransport.send` so the two transports
138
+ * are drop-in substitutes behind `StepUpClient`.
139
+ *
140
+ * @param options.endpoint - API path relative to `baseUrl`, e.g.
141
+ * `/v1/management/stepup/start-token`. Must start with a single `/`;
142
+ * anything that would resolve off the configured origin or out of its path
143
+ * prefix throws `DelegationRequestError` before the request is signed.
144
+ * @param options.payload - request body; serialised once and hashed; the
145
+ * EXACT same bytes are sent and hashed — no double-serialisation.
146
+ * @param options.headers - optional extra headers merged last (win over built-ins)
147
+ */
148
+ async send(options) {
149
+ const { endpoint, payload, headers: extraHeaders = {} } = options;
150
+ // Canonicalise body ONCE. The same Buffer is both hashed and sent.
151
+ const bodyBytes = Buffer.from(JSON.stringify(payload));
152
+ // SHA-256 → base64url without padding (RFC 7515 §2).
153
+ const hash = (0, node_crypto_2.createHash)('sha256').update(bodyBytes).digest();
154
+ const bodySha256 = hash.toString('base64url').replace(/=+$/, '');
155
+ // Join structurally and assert the result stays on the configured origin
156
+ // BEFORE signing: the JWT `aud` and the request target are this one value.
157
+ const url = (0, url_1.safeJoinUrl)(this.baseUrl, endpoint, 'endpoint', urlError);
158
+ let jwt;
159
+ try {
160
+ jwt = await this.mintJwt(url, bodySha256);
161
+ }
162
+ catch (err) {
163
+ throw new exceptions_1.DelegationRequestError('Failed to sign management request', { cause: err });
164
+ }
165
+ // Extra headers are merged first so the transport's own auth/routing
166
+ // headers (Authorization, X-Config-Id, X-Hawcx-Protocol-Version) cannot
167
+ // be accidentally overridden by the caller's header bag.
168
+ const requestHeaders = {
169
+ ...extraHeaders,
170
+ 'Content-Type': 'application/json',
171
+ 'Authorization': `Bearer ${jwt}`,
172
+ 'X-Config-Id': this.configId,
173
+ 'X-Hawcx-Protocol-Version': '2',
174
+ };
175
+ let response;
176
+ let responseData;
177
+ // Bound every request with a timeout: a connected-but-silent server would
178
+ // otherwise hang the call indefinitely and can exhaust connection pools.
179
+ const controller = new AbortController();
180
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
181
+ try {
182
+ response = await this.http(url, {
183
+ method: 'POST',
184
+ headers: requestHeaders,
185
+ body: bodyBytes,
186
+ signal: controller.signal,
187
+ });
188
+ responseData = Buffer.from(await response.arrayBuffer());
189
+ }
190
+ catch (err) {
191
+ if (err instanceof exceptions_1.DelegationRequestError)
192
+ throw err;
193
+ throw new exceptions_1.DelegationRequestError('Request to management API failed', { cause: err });
194
+ }
195
+ finally {
196
+ clearTimeout(timer);
197
+ }
198
+ const text = responseData.toString('utf-8');
199
+ // Check the HTTP status BEFORE parsing JSON. A non-JSON error body (e.g. a
200
+ // WAF/proxy HTML 403) must still surface its status as a
201
+ // DelegationRequestError — not get masked as a JSON parse failure that the
202
+ // caller can't distinguish from a malformed 200.
203
+ if (response.status >= 400) {
204
+ let errorBody = text;
205
+ try {
206
+ const errJson = JSON.parse(text);
207
+ if (errJson?.detail)
208
+ errorBody = String(errJson.detail);
209
+ }
210
+ catch {
211
+ // Non-JSON error body — keep the raw text.
212
+ }
213
+ throw new exceptions_1.DelegationRequestError(`Management API returned ${response.status}`, {
214
+ statusCode: response.status,
215
+ responseBody: errorBody,
216
+ });
217
+ }
218
+ // Authenticate every response we are about to return as success — any
219
+ // status below 400, i.e. every 2xx (and any stray 1xx/3xx, which the
220
+ // server never sends signed and so fail closed). Error responses (>= 400)
221
+ // are unsigned by design and were rejected above. The hash covers the raw
222
+ // bytes from `arrayBuffer()`, not `text`: UTF-8 decoding replaces invalid
223
+ // sequences and would let distinct byte strings hash the same.
224
+ if (this.responseVerifier !== null) {
225
+ await this.responseVerifier.verify(response.headers.get(response_signature_1.RESPONSE_SIGNATURE_HEADER), responseData);
226
+ }
227
+ let parsed;
228
+ try {
229
+ parsed = JSON.parse(text);
230
+ }
231
+ catch (err) {
232
+ throw new exceptions_1.DelegationResponseError('Failed to parse management API response', { cause: err });
233
+ }
234
+ return parsed;
235
+ }
236
+ }
237
+ exports.JwtBearerTransport = JwtBearerTransport;
238
+ // ── helpers ──────────────────────────────────────────────────────────────────
239
+ /** Narrow check: is this value a plain JWK object (has `kty` string)? */
240
+ function isJwk(value) {
241
+ return (typeof value === 'object' &&
242
+ value !== null &&
243
+ typeof value.kty === 'string');
244
+ }
245
+ //# sourceMappingURL=jwt_transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jwt_transport.js","sourceRoot":"","sources":["../../src/management/jwt_transport.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Verification of the management API's `X-Response-Signature` (TOB-HSDK-11).
3
+ *
4
+ * Every successful jwt-mode (Path A) management response from hx_auth carries
5
+ * `X-Response-Signature`: a compact JWS signed with the shared OAuth signing
6
+ * key that the OP publishes at `${issuer}/.well-known/jwks.json` — the same
7
+ * JWKS that verifies ID tokens. Its payload is exactly
8
+ *
9
+ * { "body_sha256": base64url(sha256(<exact response body bytes>)), "iat": <unix s> }
10
+ *
11
+ * Verifying it restores the response authentication the legacy ECIES
12
+ * transport always had (signature + timestamp skew on every response).
13
+ *
14
+ * Checks, all fail-closed:
15
+ * - the header is present and is a compact JWS whose protected header names
16
+ * a `kid`;
17
+ * - `alg` is one of ES256 / ES384 / ES512 (the server's KMS key is an EC key
18
+ * and the alg follows its curve). `none`, HS*, RS*, EdDSA are rejected —
19
+ * this forecloses alg-substitution / key-confusion;
20
+ * - the signature verifies against the issuer JWKS (jose `RemoteJWKSet`:
21
+ * cached, and refetched once when the `kid` is unknown — key rotation);
22
+ * - `iat` is present and within ±clockSkewSeconds of local time;
23
+ * - the payload is a response signature, not some other token minted with
24
+ * the same key (TOB-HSDK-9 type confusion): `body_sha256` is required and
25
+ * `aud`, `sub`, `nonce` must be absent — an ID token always carries
26
+ * `aud`/`sub`;
27
+ * - `body_sha256` equals base64url(sha256(raw body bytes)), compared in
28
+ * constant time. The hash is taken over the raw bytes as received, before
29
+ * any decoding or JSON parsing.
30
+ *
31
+ * Failures are split so callers can react correctly:
32
+ * - {@link ResponseSignatureInvalidError} (`code: 'response_signature_invalid'`)
33
+ * — missing/empty header or a signature that does not verify;
34
+ * - {@link ResponseSignatureUnavailableError}
35
+ * (`code: 'response_signature_unavailable'`) — the signing keys could not
36
+ * be obtained (JWKS fetch failed / timed out / non-200 / malformed, or the
37
+ * kid is unknown while jose's refetch cooldown suppresses a refetch). The
38
+ * request may already have been applied server-side.
39
+ * Both subclass `DelegationResponseError` and carry fixed, non-leaky messages.
40
+ *
41
+ * Not part of the public API (not re-exported from the package index).
42
+ */
43
+ /** Response-signature header name (HTTP headers are case-insensitive). */
44
+ export declare const RESPONSE_SIGNATURE_HEADER = "X-Response-Signature";
45
+ export interface ResponseSignatureVerifierOptions {
46
+ /** Absolute, already-validated JWKS URL (`${issuer}/.well-known/jwks.json`). */
47
+ jwksUri: string;
48
+ /** Allowed |now − iat| in seconds. */
49
+ clockSkewSeconds: number;
50
+ /** Bound on each JWKS fetch, in ms. */
51
+ timeoutMs: number;
52
+ }
53
+ export declare class ResponseSignatureVerifier {
54
+ private readonly jwks;
55
+ private readonly clockSkewSeconds;
56
+ constructor(options: ResponseSignatureVerifierOptions);
57
+ /**
58
+ * Verify `signature` (the header value, or null when absent) over the EXACT
59
+ * response body bytes. Resolves on success; throws
60
+ * {@link ResponseSignatureInvalidError} or
61
+ * {@link ResponseSignatureUnavailableError} otherwise.
62
+ */
63
+ verify(signature: string | null, body: Uint8Array): Promise<void>;
64
+ }
65
+ //# sourceMappingURL=response_signature.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"response_signature.d.ts","sourceRoot":"","sources":["../../src/management/response_signature.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AASH,0EAA0E;AAC1E,eAAO,MAAM,yBAAyB,yBAAyB,CAAC;AAoBhE,MAAM,WAAW,gCAAgC;IAC/C,gFAAgF;IAChF,OAAO,EAAE,MAAM,CAAC;IAChB,sCAAsC;IACtC,gBAAgB,EAAE,MAAM,CAAC;IACzB,uCAAuC;IACvC,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,qBAAa,yBAAyB;IACpC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAwC;IAC7D,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;gBAE9B,OAAO,EAAE,gCAAgC;IAUrD;;;;;OAKG;IACG,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;CAgGxE"}
@@ -0,0 +1,170 @@
1
+ "use strict";
2
+ /**
3
+ * Verification of the management API's `X-Response-Signature` (TOB-HSDK-11).
4
+ *
5
+ * Every successful jwt-mode (Path A) management response from hx_auth carries
6
+ * `X-Response-Signature`: a compact JWS signed with the shared OAuth signing
7
+ * key that the OP publishes at `${issuer}/.well-known/jwks.json` — the same
8
+ * JWKS that verifies ID tokens. Its payload is exactly
9
+ *
10
+ * { "body_sha256": base64url(sha256(<exact response body bytes>)), "iat": <unix s> }
11
+ *
12
+ * Verifying it restores the response authentication the legacy ECIES
13
+ * transport always had (signature + timestamp skew on every response).
14
+ *
15
+ * Checks, all fail-closed:
16
+ * - the header is present and is a compact JWS whose protected header names
17
+ * a `kid`;
18
+ * - `alg` is one of ES256 / ES384 / ES512 (the server's KMS key is an EC key
19
+ * and the alg follows its curve). `none`, HS*, RS*, EdDSA are rejected —
20
+ * this forecloses alg-substitution / key-confusion;
21
+ * - the signature verifies against the issuer JWKS (jose `RemoteJWKSet`:
22
+ * cached, and refetched once when the `kid` is unknown — key rotation);
23
+ * - `iat` is present and within ±clockSkewSeconds of local time;
24
+ * - the payload is a response signature, not some other token minted with
25
+ * the same key (TOB-HSDK-9 type confusion): `body_sha256` is required and
26
+ * `aud`, `sub`, `nonce` must be absent — an ID token always carries
27
+ * `aud`/`sub`;
28
+ * - `body_sha256` equals base64url(sha256(raw body bytes)), compared in
29
+ * constant time. The hash is taken over the raw bytes as received, before
30
+ * any decoding or JSON parsing.
31
+ *
32
+ * Failures are split so callers can react correctly:
33
+ * - {@link ResponseSignatureInvalidError} (`code: 'response_signature_invalid'`)
34
+ * — missing/empty header or a signature that does not verify;
35
+ * - {@link ResponseSignatureUnavailableError}
36
+ * (`code: 'response_signature_unavailable'`) — the signing keys could not
37
+ * be obtained (JWKS fetch failed / timed out / non-200 / malformed, or the
38
+ * kid is unknown while jose's refetch cooldown suppresses a refetch). The
39
+ * request may already have been applied server-side.
40
+ * Both subclass `DelegationResponseError` and carry fixed, non-leaky messages.
41
+ *
42
+ * Not part of the public API (not re-exported from the package index).
43
+ */
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.ResponseSignatureVerifier = exports.RESPONSE_SIGNATURE_HEADER = void 0;
46
+ const node_crypto_1 = require("node:crypto");
47
+ const jose_1 = require("jose");
48
+ const exceptions_1 = require("../delegation/exceptions");
49
+ /** Response-signature header name (HTTP headers are case-insensitive). */
50
+ exports.RESPONSE_SIGNATURE_HEADER = 'X-Response-Signature';
51
+ /** EC algorithms the server's KMS signing key can produce — nothing else. */
52
+ const ALLOWED_ALGS = Object.freeze(['ES256', 'ES384', 'ES512']);
53
+ /**
54
+ * Claims that mark a token as something other than a response signature.
55
+ * Deliberately `aud` is forbidden rather than repurposed: request binding
56
+ * (anti-replay within the iat window) is designed server-side in
57
+ * hawcx/hx_auth#278 as dedicated custom claims (`req_jti` / `htm` / `htu` /
58
+ * `cid`), explicitly NOT `aud` — keep this list as is.
59
+ */
60
+ const FORBIDDEN_CLAIMS = Object.freeze(['aud', 'sub', 'nonce']);
61
+ /** Generic, non-leaky messages; detail (if any) is attached as `cause`. */
62
+ const MSG_MISSING = 'Management API response is missing its signature';
63
+ const MSG_INVALID = 'Management API response signature verification failed';
64
+ const MSG_UNAVAILABLE = 'Management API response signature could not be verified: signing keys unavailable';
65
+ class ResponseSignatureVerifier {
66
+ constructor(options) {
67
+ // jose handles caching (10 min max age), concurrent-fetch coalescing, and a
68
+ // single refetch on an unknown `kid` (rate-limited by its 30s cooldown) —
69
+ // the same machinery HawcxOAuth uses for ID-token verification.
70
+ this.jwks = (0, jose_1.createRemoteJWKSet)(new URL(options.jwksUri), {
71
+ timeoutDuration: options.timeoutMs,
72
+ });
73
+ this.clockSkewSeconds = options.clockSkewSeconds;
74
+ }
75
+ /**
76
+ * Verify `signature` (the header value, or null when absent) over the EXACT
77
+ * response body bytes. Resolves on success; throws
78
+ * {@link ResponseSignatureInvalidError} or
79
+ * {@link ResponseSignatureUnavailableError} otherwise.
80
+ */
81
+ async verify(signature, body) {
82
+ if (!signature) {
83
+ throw new exceptions_1.ResponseSignatureInvalidError(MSG_MISSING);
84
+ }
85
+ // Set by the key resolver when the keys could not be obtained, as opposed
86
+ // to a signature that was checked and found bad.
87
+ let keysUnavailable = false;
88
+ let payloadBytes;
89
+ try {
90
+ const result = await (0, jose_1.compactVerify)(signature, async (protectedHeader, token) => {
91
+ if (typeof protectedHeader.kid !== 'string' || protectedHeader.kid.length === 0) {
92
+ throw new Error('response signature has no kid');
93
+ }
94
+ // If jose may refetch on a miss (not cooling down), a miss means the
95
+ // kid is absent from a freshly fetched JWKS → invalid. If the
96
+ // cooldown suppresses the refetch, the kid may simply be newer than
97
+ // our cache (rotation) → unavailable.
98
+ const canRefetch = !this.jwks.coolingDown;
99
+ try {
100
+ return await this.jwks(protectedHeader, token);
101
+ }
102
+ catch (err) {
103
+ if (err instanceof jose_1.errors.JWKSNoMatchingKey) {
104
+ keysUnavailable = !canRefetch;
105
+ }
106
+ else if (!(err instanceof jose_1.errors.JWKSMultipleMatchingKeys)) {
107
+ // Fetch failure, timeout, non-200, malformed JWKS, network error.
108
+ keysUnavailable = true;
109
+ }
110
+ throw err;
111
+ }
112
+ }, { algorithms: [...ALLOWED_ALGS] });
113
+ payloadBytes = result.payload;
114
+ }
115
+ catch (err) {
116
+ if (keysUnavailable) {
117
+ throw new exceptions_1.ResponseSignatureUnavailableError(MSG_UNAVAILABLE, { cause: err });
118
+ }
119
+ throw new exceptions_1.ResponseSignatureInvalidError(MSG_INVALID, { cause: err });
120
+ }
121
+ let claims;
122
+ try {
123
+ const parsed = JSON.parse(Buffer.from(payloadBytes).toString('utf-8'));
124
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
125
+ throw new Error('payload is not a JSON object');
126
+ }
127
+ claims = parsed;
128
+ }
129
+ catch (err) {
130
+ throw new exceptions_1.ResponseSignatureInvalidError(MSG_INVALID, { cause: err });
131
+ }
132
+ // Type-confusion guard (TOB-HSDK-9): only a response signature qualifies.
133
+ for (const claim of FORBIDDEN_CLAIMS) {
134
+ if (Object.prototype.hasOwnProperty.call(claims, claim)) {
135
+ throw new exceptions_1.ResponseSignatureInvalidError(MSG_INVALID, {
136
+ cause: new Error(`unexpected '${claim}' claim in response signature`),
137
+ });
138
+ }
139
+ }
140
+ const claimedHash = claims['body_sha256'];
141
+ if (typeof claimedHash !== 'string' || claimedHash.length === 0) {
142
+ throw new exceptions_1.ResponseSignatureInvalidError(MSG_INVALID, {
143
+ cause: new Error("response signature has no 'body_sha256' claim"),
144
+ });
145
+ }
146
+ // Freshness: `iat` required, within the configured skew either way.
147
+ const iat = claims['iat'];
148
+ if (typeof iat !== 'number' || !Number.isFinite(iat)) {
149
+ throw new exceptions_1.ResponseSignatureInvalidError(MSG_INVALID, {
150
+ cause: new Error("response signature has no numeric 'iat' claim"),
151
+ });
152
+ }
153
+ const nowSec = Date.now() / 1000;
154
+ if (iat > nowSec + this.clockSkewSeconds || iat < nowSec - this.clockSkewSeconds) {
155
+ throw new exceptions_1.ResponseSignatureInvalidError(MSG_INVALID, {
156
+ cause: new Error(`response signature 'iat' outside allowed skew (${this.clockSkewSeconds}s)`),
157
+ });
158
+ }
159
+ // Body binding: base64url(sha256(raw bytes)), no padding, constant-time.
160
+ const actual = Buffer.from((0, node_crypto_1.createHash)('sha256').update(body).digest('base64url').replace(/=+$/, ''), 'utf-8');
161
+ const claimed = Buffer.from(claimedHash, 'utf-8');
162
+ if (actual.length !== claimed.length || !(0, node_crypto_1.timingSafeEqual)(actual, claimed)) {
163
+ throw new exceptions_1.ResponseSignatureInvalidError(MSG_INVALID, {
164
+ cause: new Error('response body does not match the signed body_sha256'),
165
+ });
166
+ }
167
+ }
168
+ }
169
+ exports.ResponseSignatureVerifier = ResponseSignatureVerifier;
170
+ //# sourceMappingURL=response_signature.js.map