@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
|
@@ -0,0 +1,189 @@
|
|
|
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
|
+
/**
|
|
29
|
+
* Drop-in transport for the management API that authenticates via a per-request
|
|
30
|
+
* EdDSA JWT (`private_key_jwt`, Path A).
|
|
31
|
+
*
|
|
32
|
+
* The `send` method has the same name and signature as `DelegationTransport.send`
|
|
33
|
+
* so the factory can swap transports behind `StepUpClient` without any change to
|
|
34
|
+
* the business-method layer.
|
|
35
|
+
*/
|
|
36
|
+
class JwtBearerTransport {
|
|
37
|
+
constructor(options) {
|
|
38
|
+
this.baseUrl = options.baseUrl.replace(/\/$/, '');
|
|
39
|
+
this.configId = options.configId;
|
|
40
|
+
this.kid = options.kid;
|
|
41
|
+
this.clientId = options.clientId;
|
|
42
|
+
this.jwtTtlSeconds = options.jwtTtlSeconds ?? 60;
|
|
43
|
+
this.timeoutMs = (options.timeoutSeconds ?? 15) * 1000;
|
|
44
|
+
this.http = options.http ?? ((...args) => fetch(...args));
|
|
45
|
+
const key = options.oidcSigningKey;
|
|
46
|
+
if (typeof key === 'string') {
|
|
47
|
+
// PKCS#8 PEM
|
|
48
|
+
this.importKeyFn = () => (0, jose_1.importPKCS8)(key, 'EdDSA');
|
|
49
|
+
}
|
|
50
|
+
else if (isJwk(key)) {
|
|
51
|
+
// Plain JWK object
|
|
52
|
+
this.importKeyFn = () => (0, jose_1.importJWK)(key, 'EdDSA');
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
// Already a jose KeyLike / CryptoKey — wrap in a resolved promise.
|
|
56
|
+
const resolved = Promise.resolve(key);
|
|
57
|
+
this.importKeyFn = () => resolved;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// ── Key loading ────────────────────────────────────────────────────────────
|
|
61
|
+
importKey() {
|
|
62
|
+
if (!this.keyPromise) {
|
|
63
|
+
// Don't memoise a rejection — reset so the next call retries.
|
|
64
|
+
this.keyPromise = this.importKeyFn().catch((err) => {
|
|
65
|
+
this.keyPromise = undefined;
|
|
66
|
+
throw err;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return this.keyPromise;
|
|
70
|
+
}
|
|
71
|
+
// ── JWT minting ────────────────────────────────────────────────────────────
|
|
72
|
+
/**
|
|
73
|
+
* Mint a per-request EdDSA JWT.
|
|
74
|
+
*
|
|
75
|
+
* @param audience - full request URL (`baseUrl + endpoint`), pinned per-call
|
|
76
|
+
* @param bodySha256 - base64url (no-padding) of SHA-256 over the exact body bytes
|
|
77
|
+
*/
|
|
78
|
+
async mintJwt(audience, bodySha256) {
|
|
79
|
+
const key = await this.importKey();
|
|
80
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
81
|
+
return new jose_1.SignJWT({ body_sha256: bodySha256 })
|
|
82
|
+
.setProtectedHeader({ alg: 'EdDSA', kid: this.kid, typ: 'JWT' })
|
|
83
|
+
.setIssuer(this.clientId)
|
|
84
|
+
.setSubject(this.clientId)
|
|
85
|
+
.setAudience(audience)
|
|
86
|
+
.setIssuedAt(nowSec)
|
|
87
|
+
.setExpirationTime(nowSec + this.jwtTtlSeconds)
|
|
88
|
+
.setJti((0, node_crypto_1.randomUUID)())
|
|
89
|
+
.sign(key);
|
|
90
|
+
}
|
|
91
|
+
// ── Transport ──────────────────────────────────────────────────────────────
|
|
92
|
+
/**
|
|
93
|
+
* Send an authenticated POST to `endpoint` with `payload` serialised as JSON.
|
|
94
|
+
*
|
|
95
|
+
* Matches the signature of `DelegationTransport.send` so the two transports
|
|
96
|
+
* are drop-in substitutes behind `StepUpClient`.
|
|
97
|
+
*
|
|
98
|
+
* @param options.endpoint - API path, e.g. `/v1/management/stepup/start-token`
|
|
99
|
+
* @param options.payload - request body; serialised once and hashed; the
|
|
100
|
+
* EXACT same bytes are sent and hashed — no double-serialisation.
|
|
101
|
+
* @param options.headers - optional extra headers merged last (win over built-ins)
|
|
102
|
+
*/
|
|
103
|
+
async send(options) {
|
|
104
|
+
const { endpoint, payload, headers: extraHeaders = {} } = options;
|
|
105
|
+
// Canonicalise body ONCE. The same Buffer is both hashed and sent.
|
|
106
|
+
const bodyBytes = Buffer.from(JSON.stringify(payload));
|
|
107
|
+
// SHA-256 → base64url without padding (RFC 7515 §2).
|
|
108
|
+
const hash = (0, node_crypto_2.createHash)('sha256').update(bodyBytes).digest();
|
|
109
|
+
const bodySha256 = hash.toString('base64url').replace(/=+$/, '');
|
|
110
|
+
const url = this.baseUrl + endpoint;
|
|
111
|
+
let jwt;
|
|
112
|
+
try {
|
|
113
|
+
jwt = await this.mintJwt(url, bodySha256);
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
throw new exceptions_1.DelegationRequestError('Failed to sign management request', { cause: err });
|
|
117
|
+
}
|
|
118
|
+
// Extra headers are merged first so the transport's own auth/routing
|
|
119
|
+
// headers (Authorization, X-Config-Id, X-Hawcx-Protocol-Version) cannot
|
|
120
|
+
// be accidentally overridden by the caller's header bag.
|
|
121
|
+
const requestHeaders = {
|
|
122
|
+
...extraHeaders,
|
|
123
|
+
'Content-Type': 'application/json',
|
|
124
|
+
'Authorization': `Bearer ${jwt}`,
|
|
125
|
+
'X-Config-Id': this.configId,
|
|
126
|
+
'X-Hawcx-Protocol-Version': '2',
|
|
127
|
+
};
|
|
128
|
+
let response;
|
|
129
|
+
let responseData;
|
|
130
|
+
// Bound every request with a timeout: a connected-but-silent server would
|
|
131
|
+
// otherwise hang the call indefinitely and can exhaust connection pools.
|
|
132
|
+
const controller = new AbortController();
|
|
133
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
134
|
+
try {
|
|
135
|
+
response = await this.http(url, {
|
|
136
|
+
method: 'POST',
|
|
137
|
+
headers: requestHeaders,
|
|
138
|
+
body: bodyBytes,
|
|
139
|
+
signal: controller.signal,
|
|
140
|
+
});
|
|
141
|
+
responseData = Buffer.from(await response.arrayBuffer());
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
if (err instanceof exceptions_1.DelegationRequestError)
|
|
145
|
+
throw err;
|
|
146
|
+
throw new exceptions_1.DelegationRequestError('Request to management API failed', { cause: err });
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
clearTimeout(timer);
|
|
150
|
+
}
|
|
151
|
+
const text = responseData.toString('utf-8');
|
|
152
|
+
// Check the HTTP status BEFORE parsing JSON. A non-JSON error body (e.g. a
|
|
153
|
+
// WAF/proxy HTML 403) must still surface its status as a
|
|
154
|
+
// DelegationRequestError — not get masked as a JSON parse failure that the
|
|
155
|
+
// caller can't distinguish from a malformed 200.
|
|
156
|
+
if (response.status >= 400) {
|
|
157
|
+
let errorBody = text;
|
|
158
|
+
try {
|
|
159
|
+
const errJson = JSON.parse(text);
|
|
160
|
+
if (errJson?.detail)
|
|
161
|
+
errorBody = String(errJson.detail);
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// Non-JSON error body — keep the raw text.
|
|
165
|
+
}
|
|
166
|
+
throw new exceptions_1.DelegationRequestError(`Management API returned ${response.status}`, {
|
|
167
|
+
statusCode: response.status,
|
|
168
|
+
responseBody: errorBody,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
let parsed;
|
|
172
|
+
try {
|
|
173
|
+
parsed = JSON.parse(text);
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
throw new exceptions_1.DelegationResponseError('Failed to parse management API response', { cause: err });
|
|
177
|
+
}
|
|
178
|
+
return parsed;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
exports.JwtBearerTransport = JwtBearerTransport;
|
|
182
|
+
// ── helpers ──────────────────────────────────────────────────────────────────
|
|
183
|
+
/** Narrow check: is this value a plain JWK object (has `kty` string)? */
|
|
184
|
+
function isJwk(value) {
|
|
185
|
+
return (typeof value === 'object' &&
|
|
186
|
+
value !== null &&
|
|
187
|
+
typeof value.kty === 'string');
|
|
188
|
+
}
|
|
189
|
+
//# 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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"}
|
package/dist/oauth.d.ts
CHANGED
|
@@ -1,16 +1,71 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Hawcx OAuth Client
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Two construction paths:
|
|
5
|
+
*
|
|
6
|
+
* 1. OIDC discovery (recommended) — `HawcxOAuth.fromIssuer(...)` fetches
|
|
7
|
+
* `<issuer>/.well-known/openid-configuration` once at startup and wires
|
|
8
|
+
* up a verifier that enforces signature + iss + aud + exp + nbf on every
|
|
9
|
+
* subsequent `verifyToken` call.
|
|
10
|
+
*
|
|
11
|
+
* 2. Legacy — `new HawcxOAuth({ configId, baseUrl })` hits the fixed
|
|
12
|
+
* endpoints `/oauth2/token` and `/keys` directly. Backward-compatible
|
|
13
|
+
* with v4.x callers, but does NOT verify `iss` or `aud` — the JWT's
|
|
14
|
+
* signature is the only check.
|
|
15
|
+
*
|
|
16
|
+
* Both modes are safe to share across requests.
|
|
5
17
|
*/
|
|
6
18
|
import { type JWTPayload } from 'jose';
|
|
19
|
+
import { ClientAssertionSigner } from './client-assertion';
|
|
7
20
|
export interface HawcxOAuthConfig {
|
|
8
|
-
/** Your Hawcx
|
|
21
|
+
/** Your Hawcx config-id (sent as the `X-Config-Id` header on token exchange) */
|
|
9
22
|
configId: string;
|
|
10
23
|
/** Base URL override (default: https://api.hawcx.com) */
|
|
11
24
|
baseUrl?: string;
|
|
12
25
|
/** Request timeout in ms (default: 10000) */
|
|
13
26
|
timeout?: number;
|
|
27
|
+
/**
|
|
28
|
+
* Tolerated clock skew in seconds when verifying `exp` and `nbf` claims.
|
|
29
|
+
* Default 10. Set to 0 to require exact-time validation; raise above 10
|
|
30
|
+
* if you regularly see NTP drift between the issuer and this host.
|
|
31
|
+
* Spec.md §3.3.
|
|
32
|
+
*/
|
|
33
|
+
clockToleranceSeconds?: number;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Options for the discovery-mode factory.
|
|
37
|
+
*
|
|
38
|
+
* **configId vs clientId.** Hawcx separates the tenant routing key from the
|
|
39
|
+
* audience claim — the SDK forwards each to the right place:
|
|
40
|
+
*
|
|
41
|
+
* - `configId` — opaque per-tenant routing key. Sent verbatim as the
|
|
42
|
+
* `X-Config-Id` header on the token-exchange POST so the OAuth server can
|
|
43
|
+
* look up the tenant.
|
|
44
|
+
* - `clientId` — whatever value the tenant has configured (in the Hawcx admin
|
|
45
|
+
* console) as the `aud` claim on issued id_tokens. The verifier checks the
|
|
46
|
+
* token's `aud` against this exact string.
|
|
47
|
+
*
|
|
48
|
+
* Passing the same value for both fails verification with an audience-mismatch
|
|
49
|
+
* error unless the tenant has configured them identically.
|
|
50
|
+
*/
|
|
51
|
+
export interface HawcxOAuthIssuerConfig {
|
|
52
|
+
/** Hawcx issuer URL (e.g. `https://api.hawcx.com`). Used for OIDC discovery and as the expected `iss` claim. */
|
|
53
|
+
issuer: string;
|
|
54
|
+
/** Hawcx tenant config-id; sent as the `X-Config-Id` header on token exchange. */
|
|
55
|
+
configId: string;
|
|
56
|
+
/** Hawcx project / tenant client-id; matched as the expected `aud` claim on id_tokens. */
|
|
57
|
+
clientId: string;
|
|
58
|
+
/** Per-request timeout in ms for token exchange and JWKS fetch (default: 10000) */
|
|
59
|
+
timeout?: number;
|
|
60
|
+
/** Timeout in ms for the one-shot discovery fetch (default: 5000) */
|
|
61
|
+
discoveryTimeout?: number;
|
|
62
|
+
/**
|
|
63
|
+
* Tolerated clock skew in seconds when verifying `exp` and `nbf` claims.
|
|
64
|
+
* Default 10. Set to 0 to require exact-time validation; raise above 10
|
|
65
|
+
* if you regularly see NTP drift between the issuer and this host.
|
|
66
|
+
* Spec.md §3.3.
|
|
67
|
+
*/
|
|
68
|
+
clockToleranceSeconds?: number;
|
|
14
69
|
}
|
|
15
70
|
export interface JwtClaims extends JWTPayload {
|
|
16
71
|
/** Subject (user ID) */
|
|
@@ -44,55 +99,167 @@ export interface ExchangeResult {
|
|
|
44
99
|
/** Verified JWT claims */
|
|
45
100
|
claims: JwtClaims;
|
|
46
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Subset of the OIDC discovery document the SDK actually consumes.
|
|
104
|
+
* The raw response may include additional fields; they're preserved on
|
|
105
|
+
* `HawcxOAuth.discoveryMetadata` but the SDK only relies on these.
|
|
106
|
+
*/
|
|
107
|
+
export interface DiscoveryMetadata {
|
|
108
|
+
issuer: string;
|
|
109
|
+
token_endpoint: string;
|
|
110
|
+
jwks_uri: string;
|
|
111
|
+
id_token_signing_alg_values_supported: string[];
|
|
112
|
+
[key: string]: unknown;
|
|
113
|
+
}
|
|
114
|
+
export interface VerifyTokenOptions {
|
|
115
|
+
/**
|
|
116
|
+
* If set, the JWT's `nonce` claim must equal this value. Use when your
|
|
117
|
+
* authorization request bound a nonce. Checked in both discovery and
|
|
118
|
+
* legacy modes — legacy mode skips `iss` / `aud` but the explicit nonce
|
|
119
|
+
* comparison applies unconditionally when this option is provided.
|
|
120
|
+
*/
|
|
121
|
+
nonce?: string;
|
|
122
|
+
}
|
|
47
123
|
/**
|
|
48
124
|
* Hawcx OAuth Client
|
|
49
125
|
*
|
|
50
|
-
*
|
|
126
|
+
* Discovery-mode usage (recommended):
|
|
51
127
|
*
|
|
52
|
-
* @example
|
|
53
128
|
* ```typescript
|
|
54
|
-
* const oauth =
|
|
55
|
-
*
|
|
56
|
-
*
|
|
129
|
+
* const oauth = await HawcxOAuth.fromIssuer({
|
|
130
|
+
* issuer: 'https://api.hawcx.com',
|
|
131
|
+
* configId: process.env.HAWCX_CONFIG_ID!,
|
|
132
|
+
* clientId: process.env.HAWCX_CLIENT_ID!,
|
|
133
|
+
* });
|
|
57
134
|
* const { idToken, claims } = await oauth.exchangeCode(code, codeVerifier);
|
|
58
|
-
*
|
|
135
|
+
* ```
|
|
136
|
+
*
|
|
137
|
+
* Legacy usage (no discovery; aud/iss are not checked):
|
|
59
138
|
*
|
|
60
|
-
*
|
|
61
|
-
* const
|
|
139
|
+
* ```typescript
|
|
140
|
+
* const oauth = new HawcxOAuth({ configId: process.env.HAWCX_CONFIG_ID! });
|
|
141
|
+
* const { idToken, claims } = await oauth.exchangeCode(code, codeVerifier);
|
|
62
142
|
* ```
|
|
63
143
|
*/
|
|
64
144
|
export declare class HawcxOAuth {
|
|
65
145
|
private readonly configId;
|
|
66
|
-
private readonly
|
|
146
|
+
private readonly tokenEndpoint;
|
|
147
|
+
private readonly jwksUrl;
|
|
67
148
|
private readonly timeout;
|
|
149
|
+
private readonly clockToleranceSeconds;
|
|
150
|
+
/** Expected `aud` claim. `null` in legacy mode (audience is not checked). */
|
|
151
|
+
private readonly expectedAudience;
|
|
152
|
+
/** Expected `iss` claim. `null` in legacy mode (issuer is not checked). */
|
|
153
|
+
private readonly expectedIssuer;
|
|
154
|
+
/**
|
|
155
|
+
* id_token signing algorithms accepted by `verifyToken`. In discovery mode
|
|
156
|
+
* this is the intersection of the issuer's advertised algs with the SDK's
|
|
157
|
+
* closed allowlist ({@link DEFAULT_ID_TOKEN_ALGS}). `null` in legacy mode
|
|
158
|
+
* (we have no discovery doc to constrain against, so jose's default
|
|
159
|
+
* "accept whatever the JWKS provides" remains — same as 4.x).
|
|
160
|
+
*/
|
|
161
|
+
private readonly allowedAlgorithms;
|
|
162
|
+
/** Resolved OIDC discovery document. `null` in legacy mode. */
|
|
163
|
+
readonly discoveryMetadata: DiscoveryMetadata | null;
|
|
68
164
|
private jwks;
|
|
165
|
+
/**
|
|
166
|
+
* Optional `private_key_jwt` signer (discovery mode only). `null` ⇒ the
|
|
167
|
+
* token exchange stays public-client + PKCE. Set via {@link withClientAssertion},
|
|
168
|
+
* which returns a *new* configured client (this field is `readonly`).
|
|
169
|
+
*/
|
|
170
|
+
private readonly clientAssertionSigner;
|
|
171
|
+
/**
|
|
172
|
+
* Construct a legacy-mode client. For discovery mode, prefer
|
|
173
|
+
* {@link HawcxOAuth.fromIssuer}.
|
|
174
|
+
*/
|
|
69
175
|
constructor(config: HawcxOAuthConfig);
|
|
176
|
+
/**
|
|
177
|
+
* Construct a client via OIDC discovery.
|
|
178
|
+
*
|
|
179
|
+
* Fetches `<issuer>/.well-known/openid-configuration`, reads the token
|
|
180
|
+
* endpoint, JWKS URI, and supported id_token signing algorithm, and wires
|
|
181
|
+
* a verifier that enforces signature + `iss` + `aud` + `exp` + `nbf` on
|
|
182
|
+
* subsequent {@link verifyToken} calls.
|
|
183
|
+
*
|
|
184
|
+
* **Discovery is bounded** at `discoveryTimeout` ms (default 5000) so an
|
|
185
|
+
* unreachable issuer fails fast with {@link DiscoveryError} rather than
|
|
186
|
+
* hanging your bootstrap.
|
|
187
|
+
*
|
|
188
|
+
* @throws {DiscoveryError} if discovery fails to resolve or the document
|
|
189
|
+
* is missing required fields (`token_endpoint`, `jwks_uri`,
|
|
190
|
+
* `id_token_signing_alg_values_supported`).
|
|
191
|
+
*/
|
|
192
|
+
static fromIssuer(config: HawcxOAuthIssuerConfig): Promise<HawcxOAuth>;
|
|
193
|
+
/**
|
|
194
|
+
* Opt into `private_key_jwt` client authentication (discovery mode only).
|
|
195
|
+
* When set, the token exchange signs and attaches an RFC 7523
|
|
196
|
+
* `client_assertion` (in addition to PKCE). Without it, the exchange stays
|
|
197
|
+
* public-client + PKCE.
|
|
198
|
+
*
|
|
199
|
+
* Intended to be called once right after {@link fromIssuer}, before the
|
|
200
|
+
* client is shared. Returns `this` for chaining.
|
|
201
|
+
*/
|
|
202
|
+
/**
|
|
203
|
+
* @beta Experimental `private_key_jwt` surface; may change before stable.
|
|
204
|
+
*
|
|
205
|
+
* Returns a **new** discovery-mode client configured with the signer (sharing
|
|
206
|
+
* this client's resolved discovery state); the original is unchanged, so a
|
|
207
|
+
* configured client is immutable. Chained usage
|
|
208
|
+
* (`(await fromIssuer(...)).withClientAssertion(...)`) is unaffected.
|
|
209
|
+
*
|
|
210
|
+
* Refuses if the OP's discovery doc advertises
|
|
211
|
+
* `token_endpoint_auth_methods_supported` without `private_key_jwt`
|
|
212
|
+
* (the assertion would be silently ignored).
|
|
213
|
+
*/
|
|
214
|
+
withClientAssertion(signer: ClientAssertionSigner): HawcxOAuth;
|
|
70
215
|
/**
|
|
71
216
|
* Exchange authorization code for id_token and verified claims.
|
|
72
217
|
*
|
|
73
218
|
* @param code - Authorization code from auth flow
|
|
74
219
|
* @param codeVerifier - PKCE code verifier
|
|
75
|
-
* @
|
|
220
|
+
* @param redirectUri - Optional. If your original authorization request
|
|
221
|
+
* registered a `redirect_uri`, RFC 6749 §4.1.3 requires the token
|
|
222
|
+
* request to carry the same value. Omit if the auth request also omitted it.
|
|
223
|
+
* @param expectedNonce - Optional. If your authorization request bound a
|
|
224
|
+
* `nonce`, pass it here so the returned id_token's `nonce` is verified.
|
|
225
|
+
* Required whenever the token carries a nonce (verification refuses
|
|
226
|
+
* otherwise — OIDC Core §3.1.3.7).
|
|
76
227
|
*/
|
|
77
|
-
exchangeCode(code: string, codeVerifier: string): Promise<ExchangeResult>;
|
|
228
|
+
exchangeCode(code: string, codeVerifier: string, redirectUri?: string, expectedNonce?: string): Promise<ExchangeResult>;
|
|
78
229
|
/**
|
|
79
230
|
* Verify a JWT token and return its claims.
|
|
80
231
|
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
232
|
+
* In **discovery mode**, enforces signature + `iss` + `aud` + `exp` + `nbf`
|
|
233
|
+
* (and the optional `nonce` claim if `options.nonce` is provided).
|
|
83
234
|
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
235
|
+
* In **legacy mode**, only the signature is verified. Audience and issuer
|
|
236
|
+
* claims are not checked, matching the pre-5.x behavior.
|
|
86
237
|
*/
|
|
87
|
-
verifyToken(token: string): Promise<JwtClaims>;
|
|
238
|
+
verifyToken(token: string, options?: VerifyTokenOptions): Promise<JwtClaims>;
|
|
88
239
|
/**
|
|
89
|
-
* Get or create the remote JWKS.
|
|
90
|
-
*
|
|
240
|
+
* Get or create the remote JWKS. jose handles caching + concurrent-fetch
|
|
241
|
+
* coalescing internally.
|
|
242
|
+
*
|
|
243
|
+
* No single-flight guard is needed here: this method is fully synchronous
|
|
244
|
+
* (no `await` between the null-check and the assignment), so within Node's
|
|
245
|
+
* single-threaded event loop the first caller assigns `this.jwks` before any
|
|
246
|
+
* other code runs. Two concurrent `verifyToken` calls therefore cannot create
|
|
247
|
+
* duplicate `RemoteJWKSet` instances.
|
|
91
248
|
*/
|
|
92
249
|
private getJwks;
|
|
93
250
|
/**
|
|
94
|
-
*
|
|
251
|
+
* Drop the cached JWKS so the next `verifyToken` re-fetches it. Useful after
|
|
252
|
+
* an operator-triggered signing-key rotation, or in tests.
|
|
253
|
+
*
|
|
254
|
+
* Canonical name, aligned with the Java (`refreshJwks`) and Python
|
|
255
|
+
* (`refresh_jwks`) SDKs.
|
|
256
|
+
*/
|
|
257
|
+
refreshJwks(): void;
|
|
258
|
+
/**
|
|
259
|
+
* @deprecated since 5.1.0 — renamed to {@link refreshJwks} for cross-SDK
|
|
260
|
+
* consistency. Will be removed in 6.0.0. Behaviour is identical.
|
|
95
261
|
*/
|
|
96
262
|
clearCache(): void;
|
|
263
|
+
private clearCacheWarned;
|
|
97
264
|
}
|
|
98
265
|
//# sourceMappingURL=oauth.d.ts.map
|
package/dist/oauth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../src/oauth.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../src/oauth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAiC,KAAK,UAAU,EAAE,MAAM,MAAM,CAAC;AACtE,OAAO,EAAE,qBAAqB,EAAyB,MAAM,oBAAoB,CAAC;AA6BlF,MAAM,WAAW,gBAAgB;IAC/B,gFAAgF;IAChF,QAAQ,EAAE,MAAM,CAAC;IACjB,yDAAyD;IACzD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6CAA6C;IAC7C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,sBAAsB;IACrC,gHAAgH;IAChH,MAAM,EAAE,MAAM,CAAC;IACf,kFAAkF;IAClF,QAAQ,EAAE,MAAM,CAAC;IACjB,0FAA0F;IAC1F,QAAQ,EAAE,MAAM,CAAC;IACjB,mFAAmF;IACnF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qEAAqE;IACrE,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;OAKG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAED,MAAM,WAAW,SAAU,SAAQ,UAAU;IAC3C,wBAAwB;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,aAAa;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,eAAe;IACf,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACxB,iCAAiC;IACjC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,kCAAkC;IAClC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,kCAAkC;IAClC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,aAAa;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,YAAY;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6BAA6B;IAC7B,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;IACf,sBAAsB;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW;IACX,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,cAAc;IAC7B,uBAAuB;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,0BAA0B;IAC1B,MAAM,EAAE,SAAS,CAAC;CACnB;AAED;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,qCAAqC,EAAE,MAAM,EAAE,CAAC;IAChD,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,kBAAkB;IACjC;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAiDD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAS;IAE/C,6EAA6E;IAC7E,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAgB;IACjD,2EAA2E;IAC3E,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAgB;IAC/C;;;;;;OAMG;IACH,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAA2B;IAE7D,+DAA+D;IAC/D,QAAQ,CAAC,iBAAiB,EAAE,iBAAiB,GAAG,IAAI,CAAC;IAErD,OAAO,CAAC,IAAI,CAAsD;IAElE;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAA+B;IAErE;;;OAGG;gBACS,MAAM,EAAE,gBAAgB;IAqCpC;;;;;;;;;;;;;;;OAeG;WACU,UAAU,CAAC,MAAM,EAAE,sBAAsB,GAAG,OAAO,CAAC,UAAU,CAAC;IAyH5E;;;;;;;;OAQG;IACH;;;;;;;;;;;OAWG;IACH,mBAAmB,CAAC,MAAM,EAAE,qBAAqB,GAAG,UAAU;IA6B9D;;;;;;;;;;;;OAYG;IACG,YAAY,CAChB,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,MAAM,EACpB,WAAW,CAAC,EAAE,MAAM,EACpB,aAAa,CAAC,EAAE,MAAM,GACrB,OAAO,CAAC,cAAc,CAAC;IAkF1B;;;;;;;;OAQG;IACG,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,SAAS,CAAC;IA8ElF;;;;;;;;;OASG;IACH,OAAO,CAAC,OAAO;IAoBf;;;;;;OAMG;IACH,WAAW,IAAI,IAAI;IAInB;;;OAGG;IACH,UAAU,IAAI,IAAI;IAclB,OAAO,CAAC,gBAAgB,CAAS;CAClC"}
|