@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 +253 -0
- package/LICENSE +22 -0
- package/README.md +179 -26
- package/dist/client-assertion.d.ts +45 -0
- package/dist/client-assertion.d.ts.map +1 -0
- package/dist/client-assertion.js +141 -0
- package/dist/client-assertion.js.map +1 -0
- package/dist/delegation/hawcx.d.ts +0 -8
- package/dist/delegation/hawcx.d.ts.map +1 -1
- package/dist/delegation/hawcx.js +0 -17
- package/dist/delegation/hawcx.js.map +1 -1
- package/dist/delegation/index.d.ts +1 -1
- package/dist/delegation/index.d.ts.map +1 -1
- package/dist/delegation/index.js.map +1 -1
- package/dist/errors.d.ts +51 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +50 -6
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -2
- package/dist/index.js.map +1 -1
- package/dist/management/jwt_transport.d.ts +109 -0
- package/dist/management/jwt_transport.d.ts.map +1 -0
- package/dist/management/jwt_transport.js +189 -0
- package/dist/management/jwt_transport.js.map +1 -0
- package/dist/oauth.d.ts +188 -21
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js +390 -43
- package/dist/oauth.js.map +1 -1
- package/dist/stepup/client.d.ts +61 -2
- package/dist/stepup/client.d.ts.map +1 -1
- package/dist/stepup/client.js +69 -11
- package/dist/stepup/client.js.map +1 -1
- package/dist/stepup/types.d.ts +2 -3
- package/dist/stepup/types.d.ts.map +1 -1
- package/package.json +12 -13
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 =
|
|
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
|
-
###
|
|
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
|
|
56
|
-
baseUrl: 'https://api.hawcx.com', // Optional:
|
|
57
|
-
timeout: 10000, // Optional
|
|
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
|
-
#### `
|
|
212
|
+
#### `refreshJwks(): void`
|
|
106
213
|
|
|
107
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
|
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
|
-
|
|
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 `
|
|
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.
|
|
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"}
|