@hawcx/oauth-client 4.0.1 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,253 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [5.2.0] - 2026-06-26
9
+
10
+ ### Added — Management API authentication via `private_key_jwt` (Path A)
11
+
12
+ - **`StepUpClient.withPrivateKeyJwt(...)`** — authenticate `/v1/management/*`
13
+ (step-up, MFA enforcement, phone-number changes) with a per-request EdDSA
14
+ Bearer JWT: `iss` = `sub` = `clientId`, `aud` = the full endpoint URL, ~60s
15
+ `exp`, random `jti`, and a `body_sha256` claim binding the JWT to the exact
16
+ request body bytes. Reuses the same Ed25519 OIDC `private_key_jwt` signing key
17
+ already registered in the admin console — one key, standards-based (RFC 7523),
18
+ no ECIES credential blob required. Tenant routing uses the `X-Config-Id`
19
+ header. Options include `timeoutSeconds` (default 15) and `jwtTtlSeconds`
20
+ (default 60).
21
+ - **`JwtBearerTransport`** — the transport backing the Path A client. Responses
22
+ carry an `X-Response-Signature` (ES256) which callers **may** verify against
23
+ the OIDC JWKS they already trust (optional, customer-side per the Path A spec).
24
+
25
+ ### Changed
26
+
27
+ - The ECIES factories **`StepUpClient.fromSecretKey(...)` / `fromKeys(...)`**
28
+ (the proprietary `hwx_sk_v1_…` blob) are now **deprecated** — they emit a
29
+ one-time `DeprecationWarning` but remain fully functional and will only be
30
+ removed in a future major version.
31
+
32
+ ### Migration (ECIES → `private_key_jwt`)
33
+
34
+ Existing ECIES integrations keep working unchanged; adopting `private_key_jwt`
35
+ is opt-in (spec §10.3):
36
+
37
+ 1. Register your Ed25519 `private_key_jwt` public key in the Admin Console
38
+ (OAuth tab → **Client authentication**) — the same key used for OIDC login.
39
+ 2. Switch the project's Management API auth to **Migrating** (accepts both
40
+ `ecies` and `jwt`) so existing and new callers coexist during rollout.
41
+ 3. Replace `StepUpClient.fromSecretKey(...)` / `fromKeys(...)` with
42
+ `StepUpClient.withPrivateKeyJwt({ baseUrl, configId, clientId, oidcSigningKey, kid })`.
43
+ 4. Once every caller uses `private_key_jwt`, move the project to **Signed
44
+ tokens** (`jwt` only). ECIES EOL date: TBA (removed no earlier than the next
45
+ major version).
46
+
47
+ ### Hardening
48
+
49
+ - `JwtBearerTransport` now bounds every request with a timeout (`timeoutSeconds`,
50
+ default 15) via an `AbortController`, so a connected-but-silent server can no
51
+ longer hang the call or exhaust connection pools. Mirrors the ECIES
52
+ `DelegationTransport`.
53
+ - The HTTP status is now checked **before** parsing the response body, so a
54
+ non-JSON error body (e.g. a WAF/proxy HTML 403) surfaces its HTTP status as a
55
+ `DelegationRequestError` instead of being masked as a JSON parse failure.
56
+
57
+ ## [5.1.0] - 2026-06-13
58
+
59
+ > The `private_key_jwt` / `ClientAssertionSigner` surface is **experimental** (`@beta`)
60
+ > and may change before it is marked stable.
61
+
62
+ ### Cross-SDK consistency
63
+
64
+ - Added `HawcxError` as the canonical root exception (aligns with Java/Python);
65
+ `HawcxOAuthError` now extends it (back-compat — catch either).
66
+ - Added a shared **golden test vector** (`tests/fixtures/golden-assertion.json`,
67
+ byte-identical across the three SDKs) pinning the assertion header/claims/signature
68
+ for a fixed key.
69
+ - Added `refreshJwks()` as the canonical JWKS-cache reset (aligns with the Java
70
+ `refreshJwks()` / Python `refresh_jwks()` SDKs). `clearCache()` is now
71
+ **deprecated** (passthrough; one-time `console.warn`) and will be removed in 6.0.0.
72
+
73
+ ### Production-readiness hardening
74
+
75
+ - `ClientAssertionSigner` no longer memoizes a *rejected* key import — a transient/
76
+ structural import failure no longer permanently poisons the signer.
77
+ - `jti` now sourced from `node:crypto` `randomUUID` (explicit import; was the
78
+ `globalThis.crypto` global).
79
+ - `withClientAssertion` refuses to attach a signer when the OP's discovery doc
80
+ advertises `token_endpoint_auth_methods_supported` without `private_key_jwt`
81
+ (prevents a silent downgrade to public-client).
82
+ - `withClientAssertion` is now **immutable** — returns a *new* configured client
83
+ (the backing `clientAssertionSigner` field is `readonly`) rather than mutating
84
+ in place. Chained usage (`(await fromIssuer(...)).withClientAssertion(...)`) is
85
+ unaffected; callers relying on in-place mutation of the original must use the
86
+ return value.
87
+ - `ClientAssertionSigner` gains `ed25519FromPem(pkcs8Pem, kid)` and
88
+ `ed25519(keyObject, kid)` factories (parity with the Java SDK).
89
+ - Replaced the `expectedAudience ?? ''` dead-code fallback with a non-null assertion.
90
+
91
+ ### Added
92
+
93
+ - **`private_key_jwt` client authentication** (RFC 7523 / OIDC Core §9). New
94
+ `ClientAssertionSigner.ed25519FromJwk(...)` + `HawcxOAuth.withClientAssertion(signer)`
95
+ (discovery mode). When set, `exchangeCode` signs and attaches a `client_assertion`
96
+ (EdDSA/Ed25519 only) alongside PKCE; `aud` is the discovered `token_endpoint`.
97
+ Without a signer, the request is unchanged (public-client + PKCE).
98
+ - `exchangeCode` gains an optional 4th `expectedNonce` argument, threaded to verification.
99
+
100
+ ### Changed / hardened
101
+
102
+ - **https required** on the issuer, `token_endpoint`, and `jwks_uri` (loopback http
103
+ still allowed for local dev/tests) — an http issuer let an on-path attacker swap
104
+ the discovery document.
105
+ - **Nonce**: verification now refuses an id_token that carries a `nonce` when none
106
+ was supplied to verify against (OIDC Core §3.1.3.7), instead of silently skipping it.
107
+ - **`exp` and `iat` required** in discovery mode — a token with no `exp` (never-expiring)
108
+ or no `iat` (no issuance anchor) is rejected.
109
+ - **Structured error fields** on the `HawcxOAuthError` hierarchy: new
110
+ `HawcxOAuthErrorFields` carrying optional `error` / `errorDescription`. The
111
+ token-endpoint error path now populates these from the OAuth `error` /
112
+ `error_description` body so callers can dispatch on a stable code without parsing
113
+ the message string (the message string composition is unchanged for back-compat).
114
+ - JWKS fetch timeout pinned explicitly; token-exchange error bodies that aren't JSON
115
+ are preserved in the error message; negative `clockToleranceSeconds` rejected.
116
+
117
+ ## [5.0.0] - 2026-05-23
118
+
119
+ OIDC discovery support. The legacy `new HawcxOAuth({ configId, baseUrl })`
120
+ constructor is unchanged at the signature level — but verification gains a
121
+ default 10-second clock-tolerance window (see `clockToleranceSeconds`
122
+ below). Apps that depend on strict zero-skew validation should pass
123
+ `clockToleranceSeconds: 0`.
124
+
125
+ ### Added
126
+
127
+ #### OIDC discovery construction path
128
+
129
+ - `HawcxOAuth.fromIssuer({ issuer, configId, clientId })` — static async
130
+ factory that fetches `<issuer>/.well-known/openid-configuration` once at
131
+ startup (5s default timeout) and wires a verifier that enforces signature
132
+ + `iss` + `aud` + `exp` + `nbf` on every subsequent `verifyToken` call.
133
+ The legacy path verifies the signature only.
134
+
135
+ **`configId` vs `clientId`.** Hawcx separates the tenant routing key from
136
+ the audience claim, and the SDK forwards each to the right place:
137
+ - `configId` — sent as `X-Config-Id` on the token-exchange POST (opaque
138
+ tenant routing key)
139
+ - `clientId` — the value the tenant has configured (in the Hawcx admin
140
+ console) as the `aud` claim on issued id_tokens. The verifier matches
141
+ the token's `aud` against this exact string.
142
+
143
+ Passing the same value for both fails verification with
144
+ `Unexpected JWT audience` unless the tenant happens to configure them
145
+ identically.
146
+
147
+ - `HawcxOAuth.prototype.exchangeCode(code, codeVerifier, redirectUri?)` —
148
+ new optional third argument. If your authorization request registered a
149
+ `redirect_uri`, RFC 6749 §4.1.3 requires the token request to carry the
150
+ same value. Omit for flows that didn't include it.
151
+
152
+ - `HawcxOAuth.prototype.verifyToken(token, { nonce })` — new options object
153
+ with optional `nonce` field for callers who bind a nonce to the
154
+ authorization request. Has no effect in legacy mode.
155
+
156
+ - `HawcxOAuth.prototype.discoveryMetadata` — the resolved OIDC discovery
157
+ document. `null` on legacy clients; useful for diagnostics ("which
158
+ token endpoint did we resolve to?", "what signing algorithms?").
159
+
160
+ - `clockToleranceSeconds` option on both `HawcxOAuthConfig` (legacy) and
161
+ `HawcxOAuthIssuerConfig` (discovery) — tolerated clock skew in seconds
162
+ when verifying `exp` and `nbf`. Spec.md §3.3 mandates this knob with a
163
+ recommended default of 10 seconds, which the SDK now applies. Without
164
+ it, jose's default of 0 rejected tokens minted under normal NTP drift
165
+ between issuer and verifier — the root cause of the
166
+ `exchangeCodeTolerant` workaround in `hawcx_admin_console` pre-5.0.
167
+ Pass `clockToleranceSeconds: 0` to opt back into strict exact-time
168
+ validation; raise above 10 if drift on your hosts is wider than that.
169
+
170
+ - **Algorithm allowlist (spec.md §3.3) — discovery mode.** `fromIssuer`
171
+ now intersects the issuer's advertised
172
+ `id_token_signing_alg_values_supported` with a closed SDK allowlist
173
+ (`{RS256, ES256, ES384, ES512, EdDSA}`). If no advertised alg is in
174
+ the allowlist, `DiscoveryError` is thrown at construction time. The
175
+ intersection is then passed to `jose.jwtVerify`'s `algorithms` option
176
+ on every `verifyToken` call, so verify-time enforcement matches what
177
+ was resolved at construction — forecloses key-confusion /
178
+ alg-substitution where the JWKS happens to serve a key type matching
179
+ whatever the JWT header claims. Mirrors the Java SDK
180
+ (`JwtVerifier.DEFAULT_ALGORITHMS`). Legacy mode behavior is unchanged
181
+ (no closed allowlist, no `algorithms:` constraint — same as 4.x).
182
+
183
+ - `DiscoveryError` — new error class for discovery-resolution failures
184
+ (DNS / TLS / network failure, non-2xx response, malformed JSON, missing
185
+ required fields). Underlying cause is preserved as `err.cause`.
186
+
187
+ - New exported types: `HawcxOAuthIssuerConfig`, `DiscoveryMetadata`,
188
+ `VerifyTokenOptions`.
189
+
190
+ ### Changed
191
+
192
+ - `TokenVerificationError.message` now distinguishes audience / issuer
193
+ mismatches from generic signature failures by parsing jose's
194
+ `ERR_JWT_CLAIM_VALIDATION_FAILED` code. Code that pattern-matched on
195
+ the legacy "Invalid signature" message still sees it for signature
196
+ failures; new "Unexpected JWT audience" / "Unexpected JWT issuer"
197
+ messages are now used when those specific claims fail.
198
+
199
+ - `exchangeCode` now sets `grant_type=authorization_code` on the form body.
200
+ 4.x callers won't notice — the OAuth server already inferred the grant
201
+ type from the presence of `code`. Listed here for completeness.
202
+
203
+ ### Notes for upgraders
204
+
205
+ If you only construct via `new HawcxOAuth({ configId })`, nothing changes —
206
+ the legacy path is signature-only verification, same as 4.x. If you want
207
+ proper `aud`/`iss` enforcement, switch to `await HawcxOAuth.fromIssuer(...)`
208
+ and pass both `configId` and `clientId` from the admin console.
209
+
210
+ ## [0.2.0] - 2024-11-20
211
+
212
+ ### Breaking Changes
213
+ - **BREAKING**: `apiKey` parameter is now required in `exchangeCodeForClaims()` and `exchangeCodeForTokenAndClaims()`
214
+ - **BREAKING**: At least one of `publicKey` or `jwksUrl` must be provided for JWT verification
215
+
216
+ ### Added
217
+ - Validation to ensure `apiKey` is provided and non-empty
218
+ - Validation to ensure at least one of `publicKey` or `jwksUrl` is provided
219
+ - Clear error messages for missing required parameters
220
+ - Priority logic: `jwksUrl` takes precedence over `publicKey` when both are provided
221
+
222
+ ### Changed
223
+ - Updated all documentation and examples to use `jwksUrl` as the primary recommended method
224
+ - Updated demo application to include `apiKey` parameter
225
+ - Improved error messages for parameter validation
226
+
227
+ ### Fixed
228
+ - Ensures API key is always sent with OAuth token exchange requests
229
+ - Prevents authentication failures due to missing API key
230
+
231
+ ## [0.1.0] - 2024-01-XX
232
+
233
+ ### Added
234
+ - Initial release of Node.js/TypeScript OAuth client SDK
235
+ - OAuth authorization code exchange with PKCE support
236
+ - JWT verification with RS256, ES256/384/512, and EdDSA algorithms
237
+ - Comprehensive error handling with custom exception classes
238
+ - Hawcx delegation client for MFA setup and management
239
+ - ECIES (X25519 + AES-GCM) encryption for secure backend-to-backend communication
240
+ - Ed25519 and RSA-PSS signing support
241
+ - Full TypeScript type definitions
242
+ - Comprehensive documentation and examples
243
+ - Feature parity with Python SDK
244
+
245
+ ### Features
246
+ - 🔐 Secure JWT verification with configurable validation
247
+ - 🆕 Native PKCE (RFC 7636) support
248
+ - 🚀 Simple, async/await API
249
+ - 🎯 High-level Hawcx client with automatic encryption/signing
250
+ - 🛡️ Comprehensive error handling
251
+ - 📝 Full TypeScript support
252
+ - 🔧 Framework-agnostic (works with Express, Fastify, etc.)
253
+
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Hawcx
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
package/README.md CHANGED
@@ -4,6 +4,7 @@ Simple, production-ready OAuth client SDK for Hawcx authentication.
4
4
 
5
5
  ## Features
6
6
 
7
+ - **OIDC Discovery** - `HawcxOAuth.fromIssuer(...)` resolves endpoints from `/.well-known/openid-configuration` and enforces signature + `iss` + `aud` + `exp` + `nbf` on verify
7
8
  - **Simple JWT Verification** - Uses `jose` library for automatic JWKS caching and signature handling
8
9
  - **PKCE Support** - Native PKCE (RFC 7636) support for enhanced OAuth security
9
10
  - **Delegation API** - Server-to-server user management (MFA, devices)
@@ -17,19 +18,89 @@ Simple, production-ready OAuth client SDK for Hawcx authentication.
17
18
  npm install @hawcx/oauth-client
18
19
  ```
19
20
 
20
- ## Quick Start
21
+ ## Quick Start (OIDC discovery, recommended)
22
+
23
+ `HawcxOAuth.fromIssuer(...)` does the discovery fetch once at startup and
24
+ gives you a client whose `exchangeCode` + `verifyToken` enforce all the
25
+ standard OIDC claim checks. Construct once and reuse.
21
26
 
22
27
  ```typescript
23
28
  import { HawcxOAuth } from '@hawcx/oauth-client';
24
29
 
25
- const oauth = new HawcxOAuth({ configId: 'your-config-id' });
30
+ const oauth = await HawcxOAuth.fromIssuer({
31
+ issuer: 'https://api.hawcx.com', // → discovery + expected `iss`
32
+ configId: process.env.HAWCX_CONFIG_ID!, // → X-Config-Id header
33
+ clientId: process.env.HAWCX_CLIENT_ID!, // → expected `aud` on id_tokens
34
+ });
26
35
 
27
- // Exchange authorization code for tokens
28
36
  const { idToken, claims } = await oauth.exchangeCode(code, codeVerifier);
29
37
  console.log(claims.sub); // user ID
30
38
  console.log(claims.email); // user email
31
39
  ```
32
40
 
41
+ > **`configId` vs `clientId` — they are different values.**
42
+ > Hawcx separates the tenant routing key from the audience claim:
43
+ >
44
+ > - **`configId`** — the per-tenant config identifier. Sent as the
45
+ > `X-Config-Id` header on the token-exchange POST so the OAuth server
46
+ > can look up the tenant.
47
+ > - **`clientId`** — the value the tenant has configured (in the Hawcx
48
+ > admin console) as the `aud` claim on issued id_tokens. The verifier
49
+ > matches the token's `aud` against this exact string.
50
+ >
51
+ > Passing the same value for both fails verification with
52
+ > `Unexpected JWT audience` unless your tenant happens to use identical
53
+ > strings.
54
+
55
+ ## Quick Start (legacy)
56
+
57
+ The legacy single-arg constructor still works for backward compatibility
58
+ with 4.x callers. It verifies the JWT signature only — `iss` and `aud`
59
+ are not checked. Prefer `fromIssuer` for new code.
60
+
61
+ ```typescript
62
+ import { HawcxOAuth } from '@hawcx/oauth-client';
63
+
64
+ const oauth = new HawcxOAuth({ configId: 'your-config-id' });
65
+
66
+ const { idToken, claims } = await oauth.exchangeCode(code, codeVerifier);
67
+ ```
68
+
69
+ ---
70
+
71
+ ## Confidential clients (`private_key_jwt`)
72
+
73
+ If your project is registered in the Hawcx admin console with **Client
74
+ authentication = Signing key (`private_key_jwt`)**, attach an Ed25519 signer.
75
+ The SDK then signs an RFC 7523 `client_assertion` on every token exchange
76
+ (PKCE is still sent). Public clients omit this and keep using PKCE only.
77
+
78
+ ```typescript
79
+ import { HawcxOAuth, ClientAssertionSigner } from '@hawcx/oauth-client';
80
+
81
+ const privateJwk = JSON.parse(process.env.HAWCX_PRIVATE_JWK!); // OKP/Ed25519, with kid
82
+
83
+ const oauth = (
84
+ await HawcxOAuth.fromIssuer({
85
+ issuer: 'https://dev-demo-api.hawcx.com',
86
+ configId: process.env.HAWCX_CONFIG_ID!,
87
+ clientId: process.env.HAWCX_CLIENT_ID!,
88
+ })
89
+ ).withClientAssertion(ClientAssertionSigner.ed25519FromJwk(privateJwk));
90
+
91
+ const { idToken, claims } = await oauth.exchangeCode(code, codeVerifier);
92
+ ```
93
+
94
+ The private key must be Ed25519 (`kty: 'OKP', crv: 'Ed25519'`, with a `kid`);
95
+ its public JWK is what you register in the admin console. The assertion's `aud`
96
+ is the discovered `token_endpoint`, and EdDSA is the only accepted algorithm.
97
+ `clientId` must be the confidential client's id — it becomes the assertion
98
+ `iss`/`sub` and the id_token `aud`.
99
+
100
+ > If you bound a `nonce` at `/authorize`, pass it as the 4th argument to
101
+ > `exchangeCode(code, verifier, redirectUri?, expectedNonce)` — verification
102
+ > refuses a nonce-bearing token when none is supplied (OIDC Core §3.1.3.7).
103
+
33
104
  ---
34
105
 
35
106
  ## Modules
@@ -46,21 +117,45 @@ console.log(claims.email); // user email
46
117
 
47
118
  Core module for OAuth 2.1 + PKCE authentication.
48
119
 
49
- ### `HawcxOAuth`
120
+ ### Construction
121
+
122
+ #### Discovery mode (recommended)
50
123
 
51
124
  ```typescript
52
125
  import { HawcxOAuth } from '@hawcx/oauth-client';
53
126
 
127
+ const oauth = await HawcxOAuth.fromIssuer({
128
+ issuer: 'https://api.hawcx.com', // Required: discovery + expected `iss`
129
+ configId: 'your-config-id', // Required: X-Config-Id header
130
+ clientId: 'your-client-id', // Required: expected `aud` on id_tokens
131
+ timeout: 10000, // Optional: per-request timeout in ms
132
+ discoveryTimeout: 5000, // Optional: discovery fetch timeout in ms
133
+ });
134
+
135
+ oauth.discoveryMetadata; // resolved OIDC discovery doc, for diagnostics
136
+ ```
137
+
138
+ Throws `DiscoveryError` if the discovery document is unreachable, returns
139
+ non-2xx, isn't valid JSON, or is missing any of `token_endpoint`,
140
+ `jwks_uri`, `id_token_signing_alg_values_supported`.
141
+
142
+ #### Legacy mode
143
+
144
+ ```typescript
54
145
  const oauth = new HawcxOAuth({
55
- configId: 'your-config-id', // Required: Your Hawcx config ID
56
- baseUrl: 'https://api.hawcx.com', // Optional: API base URL
57
- timeout: 10000, // Optional: Request timeout in ms
146
+ configId: 'your-config-id', // Required
147
+ baseUrl: 'https://api.hawcx.com', // Optional: defaults to api.hawcx.com
148
+ timeout: 10000, // Optional
58
149
  });
59
150
  ```
60
151
 
152
+ Hits the fixed endpoints `<baseUrl>/oauth2/token` and `<baseUrl>/keys`
153
+ directly. **Signature-only verification** — `iss` and `aud` are not
154
+ checked. Kept for backward compatibility with 4.x callers.
155
+
61
156
  ### Methods
62
157
 
63
- #### `exchangeCode(code, codeVerifier): Promise<ExchangeResult>`
158
+ #### `exchangeCode(code, codeVerifier, redirectUri?): Promise<ExchangeResult>`
64
159
 
65
160
  Exchange an authorization code for tokens.
66
161
 
@@ -69,6 +164,13 @@ const { idToken, claims } = await oauth.exchangeCode(
69
164
  'authorization-code',
70
165
  'pkce-code-verifier'
71
166
  );
167
+
168
+ // With redirect_uri (RFC 6749 §4.1.3) when your auth request registered one:
169
+ const { idToken, claims } = await oauth.exchangeCode(
170
+ 'authorization-code',
171
+ 'pkce-code-verifier',
172
+ 'https://app.example.com/auth/callback'
173
+ );
72
174
  ```
73
175
 
74
176
  **Returns:**
@@ -79,12 +181,16 @@ interface ExchangeResult {
79
181
  }
80
182
  ```
81
183
 
82
- #### `verifyToken(token): Promise<JwtClaims>`
184
+ #### `verifyToken(token, options?): Promise<JwtClaims>`
83
185
 
84
- Verify an existing JWT token.
186
+ Verify an existing JWT token. In discovery mode, enforces signature + `iss` +
187
+ `aud` + `exp` + `nbf`. In legacy mode, only the signature is checked.
85
188
 
86
189
  ```typescript
87
190
  const claims = await oauth.verifyToken(idToken);
191
+
192
+ // With expected nonce (useful for flows that bind a nonce to the auth request):
193
+ const claims = await oauth.verifyToken(idToken, { nonce: 'expected-nonce' });
88
194
  ```
89
195
 
90
196
  **Returns:**
@@ -99,34 +205,46 @@ interface JwtClaims {
99
205
  name?: string; // User name
100
206
  amr?: string[]; // Authentication methods
101
207
  mfa_method?: string; // MFA method used
208
+ nonce?: string; // Nonce (if bound)
102
209
  }
103
210
  ```
104
211
 
105
- #### `clearCache(): void`
212
+ #### `refreshJwks(): void`
106
213
 
107
- Clear the JWKS cache (useful for testing or key rotation).
214
+ Reset the JWKS cache so the next verification refetches keys (useful for testing
215
+ or key rotation).
108
216
 
109
217
  ```typescript
110
- oauth.clearCache();
218
+ oauth.refreshJwks();
111
219
  ```
112
220
 
221
+ > **Deprecated:** `clearCache()` is a deprecated passthrough that calls
222
+ > `refreshJwks()` (and emits a one-time `console.warn`). It still works but will be
223
+ > removed in 6.0.0 — prefer `refreshJwks()`.
224
+
113
225
  ### Errors
114
226
 
115
227
  ```typescript
116
228
  import {
117
229
  HawcxOAuthError, // Base error class
230
+ DiscoveryError, // OIDC discovery resolution failed (fromIssuer only)
118
231
  TokenExchangeError, // Code exchange failed
119
232
  TokenVerificationError // JWT verification failed
120
233
  } from '@hawcx/oauth-client';
121
234
 
122
235
  try {
236
+ const oauth = await HawcxOAuth.fromIssuer({ issuer, configId, clientId });
123
237
  await oauth.exchangeCode(code, verifier);
124
238
  } catch (error) {
239
+ if (error instanceof DiscoveryError) {
240
+ console.log(error.cause); // original network / parse error
241
+ }
125
242
  if (error instanceof TokenExchangeError) {
126
243
  console.log(error.statusCode); // HTTP status code
127
244
  }
128
245
  if (error instanceof TokenVerificationError) {
129
- console.log(error.message); // "Token expired", "Invalid signature", etc.
246
+ // "Token expired", "Invalid signature", "Unexpected JWT audience: ...", etc.
247
+ console.log(error.message);
130
248
  }
131
249
  }
132
250
  ```
@@ -332,22 +450,35 @@ try {
332
450
 
333
451
  For step-up authentication flows (e.g., changing MFA method requires re-authentication).
334
452
 
335
- ### `StepUpClient`
453
+ ### `StepUpClient` (recommended: `withPrivateKeyJwt`)
454
+
455
+ `StepUpClient.withPrivateKeyJwt` is the recommended construction path. It reuses
456
+ the same Ed25519 `private_key_jwt` signing key already registered in the Hawcx admin
457
+ console for OIDC — one standards-based key, no separate credential blob required.
336
458
 
337
459
  ```typescript
338
460
  import { StepUpClient } from '@hawcx/oauth-client';
339
461
 
340
- const stepUp = StepUpClient.fromSecretKey({
462
+ const privateJwk = JSON.parse(process.env.HAWCX_PRIVATE_JWK!); // OKP/Ed25519, with kid
463
+
464
+ const stepUp = StepUpClient.withPrivateKeyJwt({
341
465
  baseUrl: 'https://api.hawcx.com',
342
- secretKey: process.env.HAWCX_SECRET_KEY!,
343
- relyingParty: 'your-app.com', // Required
344
- apiKey: 'optional-api-key', // Optional
345
- apiPrefix: '/v1', // Optional (default: '/v1')
346
- timeoutSeconds: 15, // Optional
347
- clockSkewSeconds: 300, // Optional
466
+ configId: process.env.HAWCX_CONFIG_ID!, // X-Config-Id tenant routing
467
+ clientId: process.env.HAWCX_CLIENT_ID!, // iss/sub in per-request JWT
468
+ oidcSigningKey: privateJwk, // same key as ClientAssertionSigner
469
+ kid: privateJwk.kid,
470
+ relyingParty: 'your-app.com', // Optional
471
+ apiKey: 'optional-api-key', // Optional
472
+ apiPrefix: '/v1', // Optional (default: '/v1')
348
473
  });
349
474
  ```
350
475
 
476
+ The signing key must be Ed25519 (`kty: 'OKP', crv: 'Ed25519'`). It accepts a JWK
477
+ object, a PKCS#8 PEM string, or a pre-imported jose `KeyLike`.
478
+
479
+ Each request is signed with a short-lived per-request EdDSA JWT whose `aud` is the
480
+ full request URL and whose `body_sha256` claim covers the exact bytes sent.
481
+
351
482
  ### Methods
352
483
 
353
484
  #### `stepUp.startToken(options): Promise<StepUpStartTokenResponse>`
@@ -394,6 +525,23 @@ interface StepUpConsumeResponse {
394
525
  }
395
526
  ```
396
527
 
528
+ ### Legacy (deprecated): `fromSecretKey`
529
+
530
+ `StepUpClient.fromSecretKey` and `StepUpClient.fromKeys` are deprecated since 5.2.0
531
+ and will be removed in a future major version. They use a proprietary ECIES credential
532
+ format. Migrate to `withPrivateKeyJwt`.
533
+
534
+ ```typescript
535
+ // DEPRECATED — migrate to withPrivateKeyJwt above
536
+ import { StepUpClient } from '@hawcx/oauth-client';
537
+
538
+ const stepUp = StepUpClient.fromSecretKey({
539
+ baseUrl: 'https://api.hawcx.com',
540
+ secretKey: process.env.HAWCX_SECRET_KEY!, // hwx_sk_v1_… blob
541
+ relyingParty: 'your-app.com',
542
+ });
543
+ ```
544
+
397
545
  ---
398
546
 
399
547
  ## Examples
@@ -477,9 +625,14 @@ async function revokeAllDevices(userId: string) {
477
625
  ```typescript
478
626
  import { StepUpClient, DelegationClient, MfaMethod } from '@hawcx/oauth-client';
479
627
 
480
- const stepUp = StepUpClient.fromSecretKey({
628
+ const privateJwk = JSON.parse(process.env.HAWCX_PRIVATE_JWK!);
629
+
630
+ const stepUp = StepUpClient.withPrivateKeyJwt({
481
631
  baseUrl: process.env.HAWCX_API_URL!,
482
- secretKey: process.env.HAWCX_SECRET_KEY!,
632
+ configId: process.env.HAWCX_CONFIG_ID!,
633
+ clientId: process.env.HAWCX_CLIENT_ID!,
634
+ oidcSigningKey: privateJwk,
635
+ kid: privateJwk.kid,
483
636
  relyingParty: 'myapp.com',
484
637
  });
485
638
 
@@ -551,7 +704,7 @@ The SDK automatically caches JWKS (JSON Web Key Set) for JWT verification:
551
704
  - Keys are fetched lazily on first `verifyToken()` call
552
705
  - Cached in memory for the lifetime of the `HawcxOAuth` instance
553
706
  - Auto-refreshes if a key ID isn't found (handles key rotation)
554
- - Call `clearCache()` to force a refresh
707
+ - Call `refreshJwks()` to force a refresh (`clearCache()` is a deprecated alias — see above)
555
708
 
556
709
  ```typescript
557
710
  // Keys fetched once, cached thereafter
@@ -560,7 +713,7 @@ const claims2 = await oauth.verifyToken(token2); // Uses cache
560
713
  const claims3 = await oauth.verifyToken(token3); // Uses cache
561
714
 
562
715
  // Force refresh if needed
563
- oauth.clearCache();
716
+ oauth.refreshJwks();
564
717
  const claims4 = await oauth.verifyToken(token4); // Fetches JWKS again
565
718
  ```
566
719
 
@@ -0,0 +1,45 @@
1
+ import type { KeyObject } from 'node:crypto';
2
+ import { type JWK } from 'jose';
3
+ /** RFC 7523 §2.2 assertion type for the `private_key_jwt` method. */
4
+ export declare const CLIENT_ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
5
+ export declare class ClientAssertionSigner {
6
+ private readonly importKeyFn;
7
+ private readonly kid;
8
+ private readonly lifetimeSeconds;
9
+ /** Lazily-imported signing key, memoised across sign() calls. */
10
+ private keyPromise?;
11
+ private constructor();
12
+ /**
13
+ * Build a signer from an Ed25519 *private* JWK (RFC 8037):
14
+ * `{ kty: 'OKP', crv: 'Ed25519', d, x, kid }`. The `kid` is read from the
15
+ * JWK. This is the canonical Hawcx key format (what the admin console
16
+ * generates/downloads). Accepts either a JWK object or its JSON string.
17
+ */
18
+ static ed25519FromJwk(privateJwk: JWK | string): ClientAssertionSigner;
19
+ /**
20
+ * Build a signer from a PKCS#8 PEM-encoded Ed25519 private key plus its `kid`.
21
+ * The key type is validated (must be Ed25519) on first use.
22
+ */
23
+ static ed25519FromPem(pkcs8Pem: string, kid: string): ClientAssertionSigner;
24
+ /**
25
+ * Build a signer from an already-loaded Ed25519 private `KeyObject`
26
+ * (e.g. from `crypto.createPrivateKey(...)`) plus its `kid`.
27
+ */
28
+ static ed25519(privateKey: KeyObject, kid: string): ClientAssertionSigner;
29
+ /**
30
+ * Return a copy of this signer using `seconds` as the assertion lifetime
31
+ * (iat→exp). Clamped to [1, 120] (the OP maximum). Defaults to 60; most
32
+ * callers never need this.
33
+ */
34
+ withLifetimeSeconds(seconds: number): ClientAssertionSigner;
35
+ private importKey;
36
+ /**
37
+ * Build and Ed25519-sign the `client_assertion` JWT for one token exchange.
38
+ *
39
+ * @param clientId - becomes `iss` and `sub` (RFC 7523 §3)
40
+ * @param audience - the OP's token endpoint, exactly as advertised in
41
+ * discovery (the OP pins `aud` to that string)
42
+ */
43
+ sign(clientId: string, audience: string): Promise<string>;
44
+ }
45
+ //# sourceMappingURL=client-assertion.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-assertion.d.ts","sourceRoot":"","sources":["../src/client-assertion.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAmC,KAAK,GAAG,EAAE,MAAM,MAAM,CAAC;AAKjE,qEAAqE;AACrE,eAAO,MAAM,qBAAqB,2DACwB,CAAC;AAK3D,qBAAa,qBAAqB;IAChC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA6B;IACzD,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAC7B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,iEAAiE;IACjE,OAAO,CAAC,UAAU,CAAC,CAAuB;IAE1C,OAAO;IAUP;;;;;OAKG;IACH,MAAM,CAAC,cAAc,CAAC,UAAU,EAAE,GAAG,GAAG,MAAM,GAAG,qBAAqB;IA0BtE;;;OAGG;IACH,MAAM,CAAC,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,qBAAqB;IAkB3E;;;OAGG;IACH,MAAM,CAAC,OAAO,CAAC,UAAU,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,GAAG,qBAAqB;IAazE;;;;OAIG;IACH,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,qBAAqB;IAK3D,OAAO,CAAC,SAAS;IAYjB;;;;;;OAMG;IACG,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAYhE"}