Node.js SDK API Reference
Complete API reference for Hawcx OAuth Client SDK for Node.js
The SDK exposes two construction paths on HawcxOAuth:
| Path | Constructor | When to use |
|---|---|---|
| Discovery (recommended) | await HawcxOAuth.fromIssuer({ issuer, configId, clientId }) | New code. Discovers /.well-known/openid-configuration once at startup and enforces signature + iss + aud + exp + nbf on every verify. |
| Legacy | new HawcxOAuth({ configId, baseUrl }) | 4.x callers. Hits fixed endpoints <baseUrl>/oauth2/token and <baseUrl>/keys; verifies signature only — iss and aud are not checked. |
Both modes share the same exchangeCode / verifyToken / refreshJwks methods.
HawcxOAuth (discovery mode)
HawcxOAuth.fromIssuer(config)
Async factory. Fetches <issuer>/.well-known/openid-configuration once at
construction, reads the token endpoint + JWKS URI + supported id_token
signing algorithm, and wires a verifier that enforces signature, iss,
aud, exp, and nbf on subsequent verifyToken calls.
import { HawcxOAuth } from '@hawcx/oauth-client';
const oauth = await HawcxOAuth.fromIssuer({
issuer: process.env.HAWCX_BASE_URL!, // discovery + expected `iss`
configId: process.env.HAWCX_CONFIG_ID!, // → X-Config-Id header
clientId: process.env.HAWCX_CLIENT_ID!, // → expected `aud` on id_tokens
});Config:
| Property | Type | Required | Description |
|---|---|---|---|
issuer | string | Yes | Hawcx issuer URL. Discovery target and expected iss claim. |
configId | string | Yes | Tenant routing key. Sent as X-Config-Id header on token exchange. |
clientId | string | Yes | The value the tenant has configured (Admin Console) as the aud claim on issued id_tokens. |
timeout | number | No | Per-request timeout in ms (default 10_000). |
discoveryTimeout | number | No | Discovery fetch timeout in ms (default 5_000). |
configId and clientId are distinct
configId routes the token exchange (header); clientId is matched
against the aud claim. They are configured separately in the Admin
Console and may be different strings. Passing the same value for both
fails verification with Unexpected JWT audience unless the tenant has
configured them identically.
Throws DiscoveryError if the discovery document is unreachable,
returns non-2xx, isn't valid JSON, or is missing any of token_endpoint,
jwks_uri, id_token_signing_alg_values_supported. The underlying
network / parse error is preserved as err.cause.
oauth.discoveryMetadata
The resolved OIDC discovery document. Useful for diagnostics — confirm which endpoints and signing algs were picked up.
oauth.discoveryMetadata?.token_endpoint;
oauth.discoveryMetadata?.jwks_uri;
oauth.discoveryMetadata?.id_token_signing_alg_values_supported;null on clients constructed via the legacy constructor.
HawcxOAuth (legacy mode)
For 4.x callers; new code should prefer the discovery path above.
import { HawcxOAuth } from '@hawcx/oauth-client';
const oauth = new HawcxOAuth({
configId: process.env.HAWCX_CONFIG_ID!,
baseUrl: process.env.HAWCX_BASE_URL!,
timeout: 10_000,
});Config:
| Property | Type | Required | Description |
|---|---|---|---|
configId | string | Yes | Hawcx Config ID. |
baseUrl | string | No | Defaults to https://api.hawcx.com. |
timeout | number | No | Request timeout in ms (default 10_000). |
Verifies signature + exp + nbf. iss and aud are not checked
(the legacy constructor has no way to know what to expect).
Shared methods
exchangeCode(code, codeVerifier, redirectUri?)
Exchange an authorization code for verified claims.
const { idToken, claims } = await oauth.exchangeCode(authCode, codeVerifier);
// With redirect_uri (RFC 6749 §4.1.3) when your auth request registered one:
const { idToken, claims } = await oauth.exchangeCode(
authCode,
codeVerifier,
'https://app.example.com/auth/callback',
);Returns:
| Property | Type | Description |
|---|---|---|
idToken | string | Raw JWT (do not use as access token) |
claims | object | Verified JWT claims |
verifyToken(token, options?)
Verify a JWT and return its claims.
- Discovery mode enforces signature +
iss+aud+exp+nbf. - Legacy mode enforces signature +
exp+nbf.issandaudare not checked — the legacy SDK has no way to know what to expect. options.nonceis checked in both modes when provided. If the token'snonceclaim doesn't match,TokenVerificationErroris thrown regardless of construction path.
const claims = await oauth.verifyToken(idToken);
// With expected nonce, when your auth request bound one:
const claims = await oauth.verifyToken(idToken, { nonce: 'expected-nonce' });refreshJwks()
Refresh the JWKS cache (useful for key rotation or tests).
oauth.refreshJwks();clearCache() is deprecated
clearCache() is a deprecated alias for refreshJwks(). It was renamed in 5.1.0 for cross-SDK consistency (Java refreshJwks, Python refresh_jwks). It is still present in 6.0.0 but will be removed in a future major version. Use refreshJwks().
Error Types
import {
HawcxOAuthError,
DiscoveryError,
TokenExchangeError,
TokenVerificationError,
} from '@hawcx/oauth-client';DiscoveryError—fromIssuercould not resolve a usable discovery document. Underlying network / parse error is preserved aserr.cause.TokenExchangeError— code exchange failed (invalid/expired code, network error, missingid_tokenin response). CarriesstatusCode.TokenVerificationError— JWT verification failed. Messages includeToken expired,Invalid signature,Token not yet valid, and (in discovery mode)Unexpected JWT audience/Unexpected JWT issuer.
Step-Up Client (Management API)
StepUpClient is the entry point for the management / step-up API (/v1/management/*) — startToken and consumeReceipt.
Recommended: reuse your OIDC signing key
withPrivateKeyJwt (Path A) is the recommended way to authenticate management API calls. It reuses the same Ed25519 private_key_jwt signing key you already registered for OIDC login — one key instead of the legacy four-key ECIES blob — and is fully standards-based (RFC 7523).
Prerequisite: enable jwt on the Management API Authentication card
Before management calls can authenticate with private_key_jwt, enable it for the project in the Admin Console → Project Settings → Management API Authentication:
- Migrating — accepts both the legacy ECIES path and signed
private_key_jwt. Use this during rollout so existing and new callers coexist. - Signed tokens —
private_key_jwtonly (after every caller has migrated).
If the project is left on Legacy (ECIES only), private_key_jwt management calls are rejected with management_auth_mode_not_allowed (HTTP 401) — even with a correctly registered key. Note: regenerating an ECIES secret-key blob resets the project to Legacy, so set this after any blob regeneration.
StepUpClient.withPrivateKeyJwt(options) (recommended)
Mints a short-lived per-request EdDSA Bearer JWT (iss = sub = clientId, aud = the full endpoint URL, ~60s lifetime, random jti, and a body_sha256 claim that binds the JWT to the exact request body bytes). Routing uses the X-Config-Id header.
The signing key must be Ed25519 (kty: 'OKP', crv: 'Ed25519'). It accepts a JWK object, a PKCS#8 PEM string, or a pre-imported jose KeyLike.
import { StepUpClient } from '@hawcx/oauth-client';
const privateJwk = JSON.parse(process.env.HAWCX_PRIVATE_JWK!); // OKP/Ed25519, with kid
const stepUp = StepUpClient.withPrivateKeyJwt({
baseUrl: 'https://api.hawcx.com',
configId: process.env.HAWCX_CONFIG_ID!, // X-Config-Id tenant routing
clientId: process.env.HAWCX_CLIENT_ID!, // iss/sub in the per-request JWT
oidcSigningKey: privateJwk, // same key as your OIDC ClientAssertionSigner
kid: privateJwk.kid,
});
// Begin a step-up flow
const { start_token, expires_in } = await stepUp.startToken({
userId: '[email protected]',
purpose: 'change_mfa_method',
newMfaMethod: 'totp', // 'email_otp' | 'sms_otp' | 'totp'
});
// Finalize after the user completes the MFA challenge
const { ok } = await stepUp.consumeReceipt({
receipt: 'step-up-receipt-from-client',
});Options (StepUpClientPrivateKeyJwtOptions):
| Option | Type | Default | Description |
|---|---|---|---|
baseUrl | string | required | Management API base URL, e.g. https://api.hawcx.com. See URL rules. |
configId | string | required | Sent as X-Config-Id for tenant routing |
clientId | string | required | iss / sub of the per-request JWT |
oidcSigningKey | string | JWK | KeyLike | required | Ed25519 private key: PKCS#8 PEM, JWK object, or jose KeyLike |
kid | string | required | Key ID in the JWT header |
apiKey | string | none | Optional, sent as the apikey header |
jwtTtlSeconds | number | 60 | Lifetime of each request JWT. Do not set above 120. |
timeoutSeconds | number | 15 | Per-request timeout |
issuer | string | baseUrl | Issuer whose {issuer}/.well-known/jwks.json holds the response-signing key. Same URL rules as baseUrl. |
verifyResponseSignature | boolean | true | Verify X-Response-Signature on every successful response. Emergency escape hatch only, see below. |
clockSkewSeconds | number | 300 | Allowed difference between local time and the response signature's iat. Must be zero or more. |
The step-up base options (apiPrefix, default /v1; relyingParty; extraHeaders; tenantHeaderName / tenantHeaderValue, sent as a request header when both are set) are accepted too.
Response signature verification
Since 6.0.0 the SDK verifies every successful management response before returning
it. Each 2xx response carries an X-Response-Signature header, a compact JWS signed
with the same key the OIDC JWKS publishes. The SDK accepts the response only when:
- the JWS uses
ES256,ES384orES512(the algorithm follows the curve of the signing key; production usesES512) and verifies against{issuer}/.well-known/jwks.json; - its
body_sha256claim matches the SHA-256 of the exact response bytes; - its
iatis withinclockSkewSecondsof local time; - it carries no
aud,subornonceclaim, so an ID token signed by the same key cannot pass as a response signature.
Responses with status 400 and above are not signed and are not verified.
By default the JWKS is fetched from {baseUrl}/.well-known/jwks.json, because the
management API and the OIDC issuer share one host in every Hawcx environment. Set
issuer only if your baseUrl is not that host, for example an internal gateway URL
or local development with separate ports.
Verification detects a response that was changed in transit, for example by a proxy that terminates TLS. Because the keys come from the same origin as the API by default, it does not protect against someone who can impersonate that origin itself. The signature binds the response body, not the request it answers.
A failure throws one of two errors, both subclasses of DelegationResponseError with a
stable code:
import {
ResponseSignatureInvalidError,
ResponseSignatureUnavailableError,
} from '@hawcx/oauth-client';
try {
await stepUp.consumeReceipt({ receipt });
} catch (err) {
if (err instanceof ResponseSignatureUnavailableError) {
// err.code === 'response_signature_unavailable'
// The signing keys could not be fetched. The request may already have been
// applied server-side: reconcile state before retrying.
} else if (err instanceof ResponseSignatureInvalidError) {
// err.code === 'response_signature_invalid'
// Missing or bad signature. Treat the response as untrusted.
}
throw err;
}verifyResponseSignature: false is an escape hatch
Setting verifyResponseSignature: false restores the 5.x behaviour: the SDK trusts any
response body it receives, and emits a one-time process warning. Use it only to
recover from an outage, or in a test harness whose stubs return unsigned responses.
If a gateway or CDN in front of Hawcx strips response headers, every call fails with
ResponseSignatureInvalidError: fix the gateway instead.
URL rules
Every base URL the SDK sends credentials to is validated at construction: baseUrl
and issuer here, the ECIES fromSecretKey / fromKeys factories, the legacy
new HawcxOAuth({ baseUrl }) and HawcxOAuth.fromIssuer. Since 6.0.0 a base URL must:
- use
https:. Plainhttp:is accepted only forlocalhost,127.0.0.1and[::1]; - carry no userinfo (
user:pw@), query or fragment.
It may include a path prefix, such as https://gateway.example.com/hawcx. Request URLs
are joined structurally and must stay on the base's origin and under that prefix. An
invalid value throws DelegationRequestError from the management clients,
HawcxOAuthError from the legacy constructor and DiscoveryError from fromIssuer.
Host case and default ports are normalised, so https://API.Hawcx.com:443 and
https://api.hawcx.com produce the same request URL and the same signed aud.
StepUpClient.fromSecretKey(options) (legacy, deprecated)
Legacy ECIES path — deprecated
StepUpClient.fromSecretKey and StepUpClient.fromKeys use the proprietary ECIES credential blob (hwx_sk_v1_…). They are deprecated but still functional and will only be removed in a future major release. Migrate to withPrivateKeyJwt at your leisure.
// DEPRECATED — migrate to withPrivateKeyJwt above
import { StepUpClient } from '@hawcx/oauth-client';
const stepUp = StepUpClient.fromSecretKey({
baseUrl: 'https://api.hawcx.com',
secretKey: process.env.HAWCX_SECRET_KEY!, // hwx_sk_v1_… blob
relyingParty: 'your-app.com',
});Delegation Client
Use the delegation client for backend-driven MFA setup and user/device management.
DelegationClient.fromSecretKey(options)
import { DelegationClient } from '@hawcx/oauth-client';
const client = DelegationClient.fromSecretKey({
secretKey: process.env.HAWCX_SECRET_KEY!,
baseUrl: process.env.HAWCX_BASE_URL!,
apiKey: process.env.HAWCX_CONFIG_ID
});DelegationClient.fromKeys(options)
const client = DelegationClient.fromKeys({
spSigningKey: process.env.SP_ED25519_PRIVATE_KEY_PEM!,
spEncryptionKey: process.env.SP_X25519_PRIVATE_KEY_PEM!,
idpVerifyKey: process.env.IDP_ED25519_PUBLIC_KEY_PEM!,
idpEncryptionKey: process.env.IDP_X25519_PUBLIC_KEY_PEM!,
spKid: process.env.SP_KEY_ID!,
idpKid: process.env.IDP_KEY_ID!,
baseUrl: process.env.HAWCX_BASE_URL!,
apiKey: process.env.HAWCX_CONFIG_ID
});MFA
import { MfaMethod } from '@hawcx/oauth-client';
const result = await client.mfa.initiate({
userId: '[email protected]',
mfaMethod: MfaMethod.SMS,
phoneNumber: '+15551234567'
});
await client.mfa.verify({
userId: '[email protected]',
sessionId: result.session_id,
otp: '123456'
});Users
const creds = await client.users.getCredentials('[email protected]');Devices
const devices = await client.devices.list('[email protected]');
await client.devices.revoke({ userId: '[email protected]', deviceId: 'h2index' });Last updated on