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