@hawcx/oauth-client 0.2.0 โ 5.1.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/README.md +587 -204
- 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/credentials.d.ts +92 -0
- package/dist/delegation/credentials.d.ts.map +1 -0
- package/dist/delegation/credentials.js +215 -0
- package/dist/delegation/credentials.js.map +1 -0
- package/dist/delegation/crypto.d.ts +2 -2
- package/dist/delegation/crypto.d.ts.map +1 -1
- package/dist/delegation/crypto.js +41 -32
- package/dist/delegation/crypto.js.map +1 -1
- package/dist/delegation/exceptions.d.ts +12 -5
- package/dist/delegation/exceptions.d.ts.map +1 -1
- package/dist/delegation/exceptions.js +12 -14
- package/dist/delegation/exceptions.js.map +1 -1
- package/dist/delegation/factory.d.ts +20 -0
- package/dist/delegation/factory.d.ts.map +1 -0
- package/dist/delegation/factory.js +49 -0
- package/dist/delegation/factory.js.map +1 -0
- package/dist/delegation/hawcx.d.ts +59 -224
- package/dist/delegation/hawcx.d.ts.map +1 -1
- package/dist/delegation/hawcx.js +99 -299
- package/dist/delegation/hawcx.js.map +1 -1
- package/dist/delegation/index.d.ts +4 -9
- package/dist/delegation/index.d.ts.map +1 -1
- package/dist/delegation/index.js +5 -17
- package/dist/delegation/index.js.map +1 -1
- package/dist/delegation/transport.d.ts +40 -0
- package/dist/delegation/transport.d.ts.map +1 -0
- package/dist/delegation/transport.js +120 -0
- package/dist/delegation/transport.js.map +1 -0
- package/dist/errors.d.ts +62 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +73 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +12 -15
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -27
- package/dist/index.js.map +1 -1
- package/dist/oauth.d.ts +265 -0
- package/dist/oauth.d.ts.map +1 -0
- package/dist/oauth.js +497 -0
- package/dist/oauth.js.map +1 -0
- package/dist/stepup/client.d.ts +26 -0
- package/dist/stepup/client.d.ts.map +1 -0
- package/dist/stepup/client.js +78 -0
- package/dist/stepup/client.js.map +1 -0
- package/dist/stepup/index.d.ts +3 -0
- package/dist/stepup/index.d.ts.map +1 -0
- package/dist/stepup/index.js +19 -0
- package/dist/stepup/index.js.map +1 -0
- package/dist/stepup/types.d.ts +35 -0
- package/dist/stepup/types.d.ts.map +1 -0
- package/dist/stepup/types.js +3 -0
- package/dist/stepup/types.js.map +1 -0
- package/package.json +13 -16
- package/dist/client.d.ts +0 -134
- package/dist/client.d.ts.map +0 -1
- package/dist/client.js +0 -300
- package/dist/client.js.map +0 -1
- package/dist/delegation/client.d.ts +0 -116
- package/dist/delegation/client.d.ts.map +0 -1
- package/dist/delegation/client.js +0 -227
- package/dist/delegation/client.js.map +0 -1
- package/dist/exceptions.d.ts +0 -52
- package/dist/exceptions.d.ts.map +0 -1
- package/dist/exceptions.js +0 -81
- package/dist/exceptions.js.map +0 -1
- package/dist/utils.d.ts +0 -61
- package/dist/utils.d.ts.map +0 -1
- package/dist/utils.js +0 -263
- package/dist/utils.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,310 +1,693 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @hawcx/oauth-client
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Simple, production-ready OAuth client SDK for Hawcx authentication.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
- ๐ง **Express/Fastify Compatible**: Works with any Node.js framework
|
|
15
|
-
- ๐ **Detailed Logging**: Structured logging for debugging (no sensitive data logged)
|
|
7
|
+
- **OIDC Discovery** - `HawcxOAuth.fromIssuer(...)` resolves endpoints from `/.well-known/openid-configuration` and enforces signature + `iss` + `aud` + `exp` + `nbf` on verify
|
|
8
|
+
- **Simple JWT Verification** - Uses `jose` library for automatic JWKS caching and signature handling
|
|
9
|
+
- **PKCE Support** - Native PKCE (RFC 7636) support for enhanced OAuth security
|
|
10
|
+
- **Delegation API** - Server-to-server user management (MFA, devices)
|
|
11
|
+
- **Step-Up Auth** - Step-up authentication flows for sensitive operations
|
|
12
|
+
- **TypeScript First** - Full type definitions included
|
|
13
|
+
- **Zero Config Crypto** - All cryptographic complexity hidden behind a single secret key
|
|
16
14
|
|
|
17
15
|
## Installation
|
|
18
16
|
|
|
19
17
|
```bash
|
|
20
18
|
npm install @hawcx/oauth-client
|
|
19
|
+
```
|
|
20
|
+
|
|
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.
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { HawcxOAuth } from '@hawcx/oauth-client';
|
|
29
|
+
|
|
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
|
+
});
|
|
35
|
+
|
|
36
|
+
const { idToken, claims } = await oauth.exchangeCode(code, codeVerifier);
|
|
37
|
+
console.log(claims.sub); // user ID
|
|
38
|
+
console.log(claims.email); // user email
|
|
39
|
+
```
|
|
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';
|
|
21
63
|
|
|
22
|
-
|
|
23
|
-
yarn add @hawcx/oauth-client
|
|
64
|
+
const oauth = new HawcxOAuth({ configId: 'your-config-id' });
|
|
24
65
|
|
|
25
|
-
|
|
26
|
-
pnpm add @hawcx/oauth-client
|
|
66
|
+
const { idToken, claims } = await oauth.exchangeCode(code, codeVerifier);
|
|
27
67
|
```
|
|
28
68
|
|
|
29
|
-
|
|
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
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Modules
|
|
30
107
|
|
|
31
|
-
|
|
108
|
+
| Module | Purpose |
|
|
109
|
+
|--------|---------|
|
|
110
|
+
| **OAuth** | Token exchange and JWT verification |
|
|
111
|
+
| **Delegation** | Server-to-server user management (MFA, devices) |
|
|
112
|
+
| **Step-Up** | Step-up authentication flows |
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## OAuth Module
|
|
117
|
+
|
|
118
|
+
Core module for OAuth 2.1 + PKCE authentication.
|
|
119
|
+
|
|
120
|
+
### Construction
|
|
121
|
+
|
|
122
|
+
#### Discovery mode (recommended)
|
|
32
123
|
|
|
33
124
|
```typescript
|
|
34
|
-
import {
|
|
125
|
+
import { HawcxOAuth } from '@hawcx/oauth-client';
|
|
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
|
+
});
|
|
35
134
|
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
145
|
+
const oauth = new HawcxOAuth({
|
|
146
|
+
configId: 'your-config-id', // Required
|
|
147
|
+
baseUrl: 'https://api.hawcx.com', // Optional: defaults to api.hawcx.com
|
|
148
|
+
timeout: 10000, // Optional
|
|
48
149
|
});
|
|
150
|
+
```
|
|
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
|
+
|
|
156
|
+
### Methods
|
|
157
|
+
|
|
158
|
+
#### `exchangeCode(code, codeVerifier, redirectUri?): Promise<ExchangeResult>`
|
|
159
|
+
|
|
160
|
+
Exchange an authorization code for tokens.
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
const { idToken, claims } = await oauth.exchangeCode(
|
|
164
|
+
'authorization-code',
|
|
165
|
+
'pkce-code-verifier'
|
|
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
|
+
);
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**Returns:**
|
|
177
|
+
```typescript
|
|
178
|
+
interface ExchangeResult {
|
|
179
|
+
idToken: string; // Raw JWT token
|
|
180
|
+
claims: JwtClaims; // Verified claims
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
#### `verifyToken(token, options?): Promise<JwtClaims>`
|
|
49
185
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
186
|
+
Verify an existing JWT token. In discovery mode, enforces signature + `iss` +
|
|
187
|
+
`aud` + `exp` + `nbf`. In legacy mode, only the signature is checked.
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
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' });
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**Returns:**
|
|
197
|
+
```typescript
|
|
198
|
+
interface JwtClaims {
|
|
199
|
+
sub?: string; // User ID
|
|
200
|
+
iss?: string; // Issuer
|
|
201
|
+
aud?: string | string[]; // Audience
|
|
202
|
+
iat?: number; // Issued at (unix timestamp)
|
|
203
|
+
exp?: number; // Expiration (unix timestamp)
|
|
204
|
+
email?: string; // User email
|
|
205
|
+
name?: string; // User name
|
|
206
|
+
amr?: string[]; // Authentication methods
|
|
207
|
+
mfa_method?: string; // MFA method used
|
|
208
|
+
nonce?: string; // Nonce (if bound)
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
#### `refreshJwks(): void`
|
|
213
|
+
|
|
214
|
+
Reset the JWKS cache so the next verification refetches keys (useful for testing
|
|
215
|
+
or key rotation).
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
oauth.refreshJwks();
|
|
219
|
+
```
|
|
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
|
+
|
|
225
|
+
### Errors
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
import {
|
|
229
|
+
HawcxOAuthError, // Base error class
|
|
230
|
+
DiscoveryError, // OIDC discovery resolution failed (fromIssuer only)
|
|
231
|
+
TokenExchangeError, // Code exchange failed
|
|
232
|
+
TokenVerificationError // JWT verification failed
|
|
233
|
+
} from '@hawcx/oauth-client';
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
const oauth = await HawcxOAuth.fromIssuer({ issuer, configId, clientId });
|
|
237
|
+
await oauth.exchangeCode(code, verifier);
|
|
238
|
+
} catch (error) {
|
|
239
|
+
if (error instanceof DiscoveryError) {
|
|
240
|
+
console.log(error.cause); // original network / parse error
|
|
241
|
+
}
|
|
242
|
+
if (error instanceof TokenExchangeError) {
|
|
243
|
+
console.log(error.statusCode); // HTTP status code
|
|
244
|
+
}
|
|
245
|
+
if (error instanceof TokenVerificationError) {
|
|
246
|
+
// "Token expired", "Invalid signature", "Unexpected JWT audience: ...", etc.
|
|
247
|
+
console.log(error.message);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
53
250
|
```
|
|
54
251
|
|
|
55
|
-
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## Delegation Module
|
|
56
255
|
|
|
57
|
-
|
|
256
|
+
Server-to-server API for managing users, MFA, and devices. Requires a Hawcx secret key.
|
|
257
|
+
|
|
258
|
+
### `DelegationClient`
|
|
58
259
|
|
|
59
260
|
```typescript
|
|
60
|
-
import {
|
|
261
|
+
import { DelegationClient, MfaMethod } from '@hawcx/oauth-client';
|
|
262
|
+
|
|
263
|
+
const client = DelegationClient.fromSecretKey({
|
|
264
|
+
baseUrl: 'https://api.hawcx.com',
|
|
265
|
+
secretKey: process.env.HAWCX_SECRET_KEY!,
|
|
266
|
+
apiKey: 'optional-api-key', // Optional
|
|
267
|
+
timeoutSeconds: 15, // Optional (default: 15)
|
|
268
|
+
clockSkewSeconds: 300, // Optional (default: 300)
|
|
269
|
+
});
|
|
270
|
+
```
|
|
61
271
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
272
|
+
### MFA Operations
|
|
273
|
+
|
|
274
|
+
#### `client.mfa.initiate(options): Promise<object>`
|
|
275
|
+
|
|
276
|
+
Initiate MFA setup or change for a user.
|
|
277
|
+
|
|
278
|
+
```typescript
|
|
279
|
+
// Email MFA
|
|
280
|
+
const result = await client.mfa.initiate({
|
|
281
|
+
userId: 'user@example.com',
|
|
282
|
+
mfaMethod: MfaMethod.EMAIL,
|
|
70
283
|
});
|
|
71
284
|
|
|
72
|
-
//
|
|
73
|
-
const result = await client.
|
|
74
|
-
|
|
75
|
-
mfaMethod: MfaMethod.SMS,
|
|
76
|
-
phoneNumber:
|
|
285
|
+
// SMS MFA (requires phone number)
|
|
286
|
+
const result = await client.mfa.initiate({
|
|
287
|
+
userId: 'user@example.com',
|
|
288
|
+
mfaMethod: MfaMethod.SMS,
|
|
289
|
+
phoneNumber: '+1234567890',
|
|
77
290
|
});
|
|
78
291
|
|
|
79
|
-
//
|
|
80
|
-
await client.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
otp: "123456"
|
|
292
|
+
// TOTP MFA
|
|
293
|
+
const result = await client.mfa.initiate({
|
|
294
|
+
userId: 'user@example.com',
|
|
295
|
+
mfaMethod: MfaMethod.TOTP,
|
|
84
296
|
});
|
|
85
297
|
|
|
86
|
-
//
|
|
87
|
-
const
|
|
88
|
-
|
|
298
|
+
// Change MFA method
|
|
299
|
+
const result = await client.mfa.initiate({
|
|
300
|
+
userId: 'user@example.com',
|
|
301
|
+
mfaMethod: MfaMethod.EMAIL, // Current method for verification
|
|
302
|
+
mfaChangeTo: MfaMethod.TOTP, // New method to switch to
|
|
303
|
+
});
|
|
89
304
|
```
|
|
90
305
|
|
|
91
|
-
**
|
|
306
|
+
**MFA Methods:**
|
|
307
|
+
```typescript
|
|
308
|
+
enum MfaMethod {
|
|
309
|
+
EMAIL = 'email',
|
|
310
|
+
SMS = 'sms',
|
|
311
|
+
TOTP = 'totp',
|
|
312
|
+
}
|
|
313
|
+
```
|
|
92
314
|
|
|
93
|
-
|
|
315
|
+
#### `client.mfa.verify(options): Promise<object>`
|
|
94
316
|
|
|
95
|
-
|
|
317
|
+
Verify MFA setup with OTP.
|
|
96
318
|
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
# Visit http://localhost:3000
|
|
319
|
+
```typescript
|
|
320
|
+
const result = await client.mfa.verify({
|
|
321
|
+
userId: 'user@example.com',
|
|
322
|
+
sessionId: 'session-from-initiate',
|
|
323
|
+
otp: '123456', // 6-digit code
|
|
324
|
+
});
|
|
104
325
|
```
|
|
105
326
|
|
|
106
|
-
|
|
327
|
+
### User Operations
|
|
107
328
|
|
|
108
|
-
|
|
329
|
+
#### `client.users.getCredentials(userId): Promise<object>`
|
|
109
330
|
|
|
110
|
-
|
|
331
|
+
Get user credentials/metadata.
|
|
111
332
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
OAUTH_CLIENT_ID="your-client-id"
|
|
116
|
-
OAUTH_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\n..."
|
|
117
|
-
OAUTH_ISSUER="https://oauth.example.com" # Optional but recommended
|
|
118
|
-
OAUTH_AUDIENCE="your-client-id" # Optional but recommended
|
|
333
|
+
```typescript
|
|
334
|
+
const creds = await client.users.getCredentials('user@example.com');
|
|
335
|
+
console.log(creds.mfa_method);
|
|
119
336
|
```
|
|
120
337
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
338
|
+
### Device Operations
|
|
339
|
+
|
|
340
|
+
#### `client.devices.list(userId): Promise<object>`
|
|
341
|
+
|
|
342
|
+
List all devices for a user.
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
const { devices } = await client.devices.list('user@example.com');
|
|
127
346
|
```
|
|
128
347
|
|
|
129
|
-
|
|
348
|
+
#### `client.devices.revoke(options): Promise<object>`
|
|
130
349
|
|
|
131
|
-
|
|
350
|
+
Revoke a device (block it from authenticating).
|
|
132
351
|
|
|
133
|
-
**From file:**
|
|
134
352
|
```typescript
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
publicKey: '/path/to/public.pem' // Absolute path
|
|
353
|
+
await client.devices.revoke({
|
|
354
|
+
userId: 'user@example.com',
|
|
355
|
+
deviceId: 'device-h2index',
|
|
139
356
|
});
|
|
140
357
|
```
|
|
141
358
|
|
|
142
|
-
|
|
359
|
+
#### `client.devices.unrevoke(options): Promise<object>`
|
|
360
|
+
|
|
361
|
+
Unrevoke a previously revoked device.
|
|
362
|
+
|
|
363
|
+
```typescript
|
|
364
|
+
await client.devices.unrevoke({
|
|
365
|
+
userId: 'user@example.com',
|
|
366
|
+
deviceId: 'device-h2index',
|
|
367
|
+
});
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
#### `client.devices.delete(options): Promise<object>`
|
|
371
|
+
|
|
372
|
+
Permanently delete a device.
|
|
373
|
+
|
|
143
374
|
```typescript
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
publicKey: process.env.OAUTH_PUBLIC_KEY! // PEM string
|
|
375
|
+
await client.devices.delete({
|
|
376
|
+
userId: 'user@example.com',
|
|
377
|
+
deviceId: 'device-h2index',
|
|
148
378
|
});
|
|
149
379
|
```
|
|
150
380
|
|
|
151
|
-
|
|
381
|
+
### Generic Request
|
|
382
|
+
|
|
383
|
+
#### `client.request<TReq, TRes>(options): Promise<TRes>`
|
|
384
|
+
|
|
385
|
+
Send a custom encrypted request to any delegation endpoint.
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
const result = await client.request({
|
|
389
|
+
endpoint: '/hc_auth/v5/custom/endpoint',
|
|
390
|
+
payload: { userid: 'user@example.com', custom_field: 'value' },
|
|
391
|
+
headers: { 'X-Custom-Header': 'value' },
|
|
392
|
+
includeBaseUrl: true, // Optional (default: true)
|
|
393
|
+
});
|
|
152
394
|
```
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
395
|
+
|
|
396
|
+
### Credential Utilities
|
|
397
|
+
|
|
398
|
+
#### `parseHawcxSecretKey(secretKey): ParsedCredentials`
|
|
399
|
+
|
|
400
|
+
Parse a secret key into usable key material (advanced usage).
|
|
401
|
+
|
|
402
|
+
```typescript
|
|
403
|
+
import { parseHawcxSecretKey } from '@hawcx/oauth-client';
|
|
404
|
+
|
|
405
|
+
const creds = parseHawcxSecretKey(process.env.HAWCX_SECRET_KEY!);
|
|
406
|
+
// Returns: { kid, hkid, signingKeyPem, verifyKeyPem, encryptKeyPem, decryptKeyPem }
|
|
156
407
|
```
|
|
157
408
|
|
|
158
|
-
|
|
409
|
+
#### `generateCredentialBlob(options): string`
|
|
410
|
+
|
|
411
|
+
Generate a new credential blob (for admin tooling).
|
|
412
|
+
|
|
413
|
+
```typescript
|
|
414
|
+
import { generateCredentialBlob } from '@hawcx/oauth-client';
|
|
415
|
+
|
|
416
|
+
const secretKey = generateCredentialBlob({
|
|
417
|
+
kid: 'customer-key-id',
|
|
418
|
+
hkid: 'hawcx-key-id',
|
|
419
|
+
edPrivate: Buffer.alloc(32), // Ed25519 private key (32 bytes)
|
|
420
|
+
xPrivate: Buffer.alloc(32), // X25519 private key (32 bytes)
|
|
421
|
+
hawcxEdPublic: Buffer.alloc(32), // Hawcx Ed25519 public key (32 bytes)
|
|
422
|
+
hawcxXPublic: Buffer.alloc(32), // Hawcx X25519 public key (32 bytes)
|
|
423
|
+
});
|
|
424
|
+
// Returns: "hwx_sk_v1_<base64url>"
|
|
425
|
+
```
|
|
159
426
|
|
|
160
|
-
|
|
427
|
+
### Delegation Errors
|
|
161
428
|
|
|
162
429
|
```typescript
|
|
163
430
|
import {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
431
|
+
DelegationError, // Base delegation error
|
|
432
|
+
DelegationCryptoError, // Encryption/signing failed
|
|
433
|
+
DelegationRequestError, // Request to IDP failed
|
|
434
|
+
DelegationResponseError, // Invalid response from IDP
|
|
168
435
|
} from '@hawcx/oauth-client';
|
|
169
436
|
|
|
170
437
|
try {
|
|
171
|
-
|
|
172
|
-
code,
|
|
173
|
-
oauthTokenUrl,
|
|
174
|
-
clientId,
|
|
175
|
-
apiKey,
|
|
176
|
-
publicKey
|
|
177
|
-
});
|
|
438
|
+
await client.mfa.initiate({ userId: 'user@example.com', mfaMethod: MfaMethod.EMAIL });
|
|
178
439
|
} catch (error) {
|
|
179
|
-
if (error instanceof
|
|
180
|
-
|
|
181
|
-
console.
|
|
182
|
-
console.error(`HTTP Status: ${error.statusCode}`);
|
|
183
|
-
console.error(`Response: ${error.responseBody}`);
|
|
184
|
-
} else if (error instanceof JWTVerificationError) {
|
|
185
|
-
// JWT verification failed (invalid signature, expired, etc.)
|
|
186
|
-
console.error(`Verification failed: ${error.message}`);
|
|
187
|
-
console.error(`Original error: ${error.originalError}`);
|
|
188
|
-
} else if (error instanceof InvalidPublicKeyError) {
|
|
189
|
-
// Public key is invalid or unreadable
|
|
190
|
-
console.error(`Key error: ${error.message}`);
|
|
440
|
+
if (error instanceof DelegationRequestError) {
|
|
441
|
+
console.log(error.statusCode); // HTTP status
|
|
442
|
+
console.log(error.responseBody); // Response body
|
|
191
443
|
}
|
|
192
444
|
}
|
|
193
445
|
```
|
|
194
446
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
| Exception | Common Causes | Recommended Action |
|
|
198
|
-
|-----------|--------------|-------------------|
|
|
199
|
-
| `OAuthExchangeError` | Invalid/expired code, network issues | Ask user to re-authenticate |
|
|
200
|
-
| `JWTVerificationError` | Token tampering, expired token | Log incident, ask user to re-authenticate |
|
|
201
|
-
| `InvalidPublicKeyError` | Wrong key, file not found | Check configuration, verify key format |
|
|
447
|
+
---
|
|
202
448
|
|
|
203
|
-
##
|
|
449
|
+
## Step-Up Module
|
|
204
450
|
|
|
205
|
-
|
|
451
|
+
For step-up authentication flows (e.g., changing MFA method requires re-authentication).
|
|
206
452
|
|
|
207
|
-
|
|
208
|
-
- **Use HTTPS** for all OAuth endpoints
|
|
209
|
-
- **Store keys securely** (environment variables, secrets manager)
|
|
210
|
-
- **Set appropriate leeway** for clock skew (5-10 seconds typical)
|
|
211
|
-
- **Log authentication failures** for security monitoring
|
|
212
|
-
- **Rotate keys regularly** following your security policy
|
|
453
|
+
### `StepUpClient`
|
|
213
454
|
|
|
214
|
-
|
|
455
|
+
```typescript
|
|
456
|
+
import { StepUpClient } from '@hawcx/oauth-client';
|
|
457
|
+
|
|
458
|
+
const stepUp = StepUpClient.fromSecretKey({
|
|
459
|
+
baseUrl: 'https://api.hawcx.com',
|
|
460
|
+
secretKey: process.env.HAWCX_SECRET_KEY!,
|
|
461
|
+
relyingParty: 'your-app.com', // Required
|
|
462
|
+
apiKey: 'optional-api-key', // Optional
|
|
463
|
+
apiPrefix: '/v1', // Optional (default: '/v1')
|
|
464
|
+
timeoutSeconds: 15, // Optional
|
|
465
|
+
clockSkewSeconds: 300, // Optional
|
|
466
|
+
});
|
|
467
|
+
```
|
|
215
468
|
|
|
216
|
-
|
|
217
|
-
- **Don't disable signature verification** in production
|
|
218
|
-
- **Don't use the id_token as your application's access token** (mint your own)
|
|
219
|
-
- **Don't commit keys** to version control
|
|
220
|
-
- **Don't ignore verification errors** or catch them silently
|
|
469
|
+
### Methods
|
|
221
470
|
|
|
222
|
-
|
|
471
|
+
#### `stepUp.startToken(options): Promise<StepUpStartTokenResponse>`
|
|
223
472
|
|
|
224
|
-
|
|
473
|
+
Start a step-up authentication flow.
|
|
225
474
|
|
|
226
475
|
```typescript
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
476
|
+
const { start_token, expires_in } = await stepUp.startToken({
|
|
477
|
+
userId: 'user@example.com',
|
|
478
|
+
purpose: 'change_mfa_method',
|
|
479
|
+
newMfaMethod: 'totp', // 'email_otp' | 'sms_otp' | 'totp'
|
|
480
|
+
});
|
|
481
|
+
// Send start_token to client for step-up authentication
|
|
230
482
|
```
|
|
231
483
|
|
|
232
|
-
|
|
233
|
-
- `code` (string): Authorization code from OAuth flow
|
|
234
|
-
- `oauthTokenUrl` (string): Token endpoint URL
|
|
235
|
-
- `clientId` (string): OAuth client identifier
|
|
236
|
-
- `apiKey` (string): API key for Hawcx OAuth
|
|
237
|
-
- `publicKey` (string, optional): RS256 public key (PEM string or file path) - optional if jwksUrl provided
|
|
238
|
-
- `jwksUrl` (string, optional): JWKS URL for dynamic key fetching (alternative to publicKey)
|
|
239
|
-
- `codeVerifier` (string, optional): PKCE code verifier (RFC 7636)
|
|
240
|
-
- `redirectUri` (string, optional): OAuth redirect URI
|
|
241
|
-
- `timeout` (number, optional): Request timeout in seconds (default: 20)
|
|
242
|
-
- `audience` (string, optional): Expected 'aud' claim (recommended)
|
|
243
|
-
- `issuer` (string, optional): Expected 'iss' claim (recommended)
|
|
244
|
-
- `verifyExp` (boolean, optional): Verify token expiration (default: true)
|
|
245
|
-
- `leeway` (number, optional): Clock skew tolerance in seconds (default: 0)
|
|
246
|
-
- `algorithms` (string[], optional): Allowed signing algorithms (default: RS256, ES256/384/512, EdDSA)
|
|
484
|
+
#### `stepUp.consumeReceipt(options): Promise<StepUpConsumeResponse>`
|
|
247
485
|
|
|
248
|
-
|
|
486
|
+
Consume a step-up receipt after user completes authentication.
|
|
249
487
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
-
|
|
253
|
-
|
|
488
|
+
```typescript
|
|
489
|
+
const { ok } = await stepUp.consumeReceipt({
|
|
490
|
+
receipt: 'step-up-receipt-from-client',
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
if (ok) {
|
|
494
|
+
// Step-up verified, proceed with sensitive operation
|
|
495
|
+
}
|
|
496
|
+
```
|
|
254
497
|
|
|
255
|
-
###
|
|
498
|
+
### Types
|
|
256
499
|
|
|
257
500
|
```typescript
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
501
|
+
type StepUpPurpose = 'change_mfa_method';
|
|
502
|
+
|
|
503
|
+
type HxAuthMfaMethod = 'sms_otp' | 'email_otp' | 'totp';
|
|
504
|
+
|
|
505
|
+
interface StepUpStartTokenResponse {
|
|
506
|
+
start_token: string;
|
|
507
|
+
expires_in: number;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
interface StepUpConsumeResponse {
|
|
511
|
+
ok: boolean;
|
|
512
|
+
}
|
|
261
513
|
```
|
|
262
514
|
|
|
263
|
-
|
|
515
|
+
---
|
|
516
|
+
|
|
517
|
+
## Examples
|
|
518
|
+
|
|
519
|
+
### Next.js Auth Callback
|
|
264
520
|
|
|
265
|
-
|
|
521
|
+
```typescript
|
|
522
|
+
// app/api/auth/callback/route.ts
|
|
523
|
+
import { HawcxOAuth, TokenExchangeError } from '@hawcx/oauth-client';
|
|
524
|
+
import { cookies } from 'next/headers';
|
|
525
|
+
|
|
526
|
+
const oauth = new HawcxOAuth({
|
|
527
|
+
configId: process.env.HAWCX_CONFIG_ID!,
|
|
528
|
+
});
|
|
266
529
|
|
|
267
|
-
|
|
530
|
+
export async function GET(request: Request) {
|
|
531
|
+
const url = new URL(request.url);
|
|
532
|
+
const code = url.searchParams.get('code');
|
|
268
533
|
|
|
269
|
-
|
|
534
|
+
const cookieStore = cookies();
|
|
535
|
+
const codeVerifier = cookieStore.get('code_verifier')?.value;
|
|
270
536
|
|
|
271
|
-
|
|
537
|
+
if (!code || !codeVerifier) {
|
|
538
|
+
return Response.redirect('/login?error=missing_params');
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
try {
|
|
542
|
+
const { idToken, claims } = await oauth.exchangeCode(code, codeVerifier);
|
|
543
|
+
// Create session with claims.sub, claims.email, etc.
|
|
544
|
+
return Response.redirect('/dashboard');
|
|
545
|
+
} catch (error) {
|
|
546
|
+
if (error instanceof TokenExchangeError) {
|
|
547
|
+
console.error('Token exchange failed:', error.message);
|
|
548
|
+
}
|
|
549
|
+
return Response.redirect('/login?error=auth_failed');
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
### Admin MFA Management
|
|
272
555
|
|
|
273
556
|
```typescript
|
|
274
|
-
import
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
557
|
+
import { DelegationClient, MfaMethod } from '@hawcx/oauth-client';
|
|
558
|
+
|
|
559
|
+
const admin = DelegationClient.fromSecretKey({
|
|
560
|
+
baseUrl: process.env.HAWCX_API_URL!,
|
|
561
|
+
secretKey: process.env.HAWCX_SECRET_KEY!,
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
// Reset user MFA to email
|
|
565
|
+
async function resetUserMfa(userId: string, otp: string) {
|
|
566
|
+
const initResult = await admin.mfa.initiate({
|
|
567
|
+
userId,
|
|
568
|
+
mfaMethod: MfaMethod.EMAIL,
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
const verifyResult = await admin.mfa.verify({
|
|
572
|
+
userId,
|
|
573
|
+
sessionId: initResult.session_id,
|
|
574
|
+
otp,
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
return verifyResult;
|
|
578
|
+
}
|
|
278
579
|
|
|
279
|
-
//
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
580
|
+
// Revoke all user devices
|
|
581
|
+
async function revokeAllDevices(userId: string) {
|
|
582
|
+
const { devices } = await admin.devices.list(userId);
|
|
583
|
+
|
|
584
|
+
for (const device of devices) {
|
|
585
|
+
await admin.devices.revoke({
|
|
586
|
+
userId,
|
|
587
|
+
deviceId: device.h2index,
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
}
|
|
287
591
|
```
|
|
288
592
|
|
|
289
|
-
|
|
593
|
+
### Step-Up for MFA Change
|
|
290
594
|
|
|
291
|
-
|
|
595
|
+
```typescript
|
|
596
|
+
import { StepUpClient, DelegationClient, MfaMethod } from '@hawcx/oauth-client';
|
|
292
597
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
598
|
+
const stepUp = StepUpClient.fromSecretKey({
|
|
599
|
+
baseUrl: process.env.HAWCX_API_URL!,
|
|
600
|
+
secretKey: process.env.HAWCX_SECRET_KEY!,
|
|
601
|
+
relyingParty: 'myapp.com',
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
const delegation = DelegationClient.fromSecretKey({
|
|
605
|
+
baseUrl: process.env.HAWCX_API_URL!,
|
|
606
|
+
secretKey: process.env.HAWCX_SECRET_KEY!,
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
async function changeMfaMethod(userId: string, newMethod: 'totp' | 'sms_otp' | 'email_otp') {
|
|
610
|
+
// 1. Start step-up flow
|
|
611
|
+
const { start_token } = await stepUp.startToken({
|
|
612
|
+
userId,
|
|
613
|
+
purpose: 'change_mfa_method',
|
|
614
|
+
newMfaMethod: newMethod,
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
// 2. Send start_token to client, user completes step-up auth
|
|
618
|
+
// 3. Client sends back receipt
|
|
619
|
+
|
|
620
|
+
// 4. Verify step-up receipt
|
|
621
|
+
const { ok } = await stepUp.consumeReceipt({ receipt: 'receipt-from-client' });
|
|
622
|
+
|
|
623
|
+
if (!ok) {
|
|
624
|
+
throw new Error('Step-up verification failed');
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// 5. Now safe to change MFA
|
|
628
|
+
await delegation.mfa.initiate({
|
|
629
|
+
userId,
|
|
630
|
+
mfaMethod: MfaMethod.EMAIL,
|
|
631
|
+
mfaChangeTo: newMethod,
|
|
632
|
+
});
|
|
633
|
+
}
|
|
296
634
|
```
|
|
297
635
|
|
|
298
|
-
|
|
636
|
+
---
|
|
299
637
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
638
|
+
## Environment Variables
|
|
639
|
+
|
|
640
|
+
| Variable | Description |
|
|
641
|
+
|----------|-------------|
|
|
642
|
+
| `HAWCX_CONFIG_ID` | Your Hawcx OAuth config ID (for token verification) |
|
|
643
|
+
| `HAWCX_SECRET_KEY` | Your Hawcx secret key (for delegation/step-up APIs) |
|
|
644
|
+
| `HAWCX_API_URL` | API base URL (default: `https://api.hawcx.com`) |
|
|
645
|
+
|
|
646
|
+
### Secret Key Format
|
|
647
|
+
|
|
648
|
+
The `HAWCX_SECRET_KEY` is a compact credential blob:
|
|
649
|
+
|
|
650
|
+
```
|
|
651
|
+
hwx_sk_v1_<base64url-encoded-json>
|
|
303
652
|
```
|
|
304
653
|
|
|
305
|
-
|
|
654
|
+
It contains:
|
|
655
|
+
- Your Ed25519 signing key (for request authentication)
|
|
656
|
+
- Your X25519 decryption key (for response decryption)
|
|
657
|
+
- Hawcx's Ed25519 public key (for response verification)
|
|
658
|
+
- Hawcx's X25519 public key (for request encryption)
|
|
659
|
+
- Key IDs for rotation tracking
|
|
306
660
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
661
|
+
Generate via the Hawcx Dashboard or admin tools.
|
|
662
|
+
|
|
663
|
+
---
|
|
664
|
+
|
|
665
|
+
## JWKS Caching
|
|
666
|
+
|
|
667
|
+
The SDK automatically caches JWKS (JSON Web Key Set) for JWT verification:
|
|
668
|
+
|
|
669
|
+
- Keys are fetched lazily on first `verifyToken()` call
|
|
670
|
+
- Cached in memory for the lifetime of the `HawcxOAuth` instance
|
|
671
|
+
- Auto-refreshes if a key ID isn't found (handles key rotation)
|
|
672
|
+
- Call `refreshJwks()` to force a refresh (`clearCache()` is a deprecated alias โ see above)
|
|
673
|
+
|
|
674
|
+
```typescript
|
|
675
|
+
// Keys fetched once, cached thereafter
|
|
676
|
+
const claims1 = await oauth.verifyToken(token1); // Fetches JWKS
|
|
677
|
+
const claims2 = await oauth.verifyToken(token2); // Uses cache
|
|
678
|
+
const claims3 = await oauth.verifyToken(token3); // Uses cache
|
|
679
|
+
|
|
680
|
+
// Force refresh if needed
|
|
681
|
+
oauth.refreshJwks();
|
|
682
|
+
const claims4 = await oauth.verifyToken(token4); // Fetches JWKS again
|
|
310
683
|
```
|
|
684
|
+
|
|
685
|
+
---
|
|
686
|
+
|
|
687
|
+
## Requirements
|
|
688
|
+
|
|
689
|
+
- Node.js >= 18.0.0
|
|
690
|
+
|
|
691
|
+
## License
|
|
692
|
+
|
|
693
|
+
MIT
|