@hawcx/oauth-client 0.1.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 -204
- 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 -301
- 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,304 +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
|
-
publicKey: process.env.OAUTH_PUBLIC_KEY!,
|
|
42
|
-
// Optional (recommended for production):
|
|
43
|
-
codeVerifier: session.pkceVerifier, // PKCE support
|
|
44
|
-
audience: 'my-app', // Validate 'aud' claim
|
|
45
|
-
issuer: 'https://oauth.example.com', // Validate 'iss' claim
|
|
46
|
-
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
|
|
47
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';
|
|
48
121
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
+
}
|
|
52
132
|
```
|
|
53
133
|
|
|
54
|
-
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Delegation Module
|
|
55
137
|
|
|
56
|
-
|
|
138
|
+
Server-to-server API for managing users, MFA, and devices. Requires a Hawcx secret key.
|
|
139
|
+
|
|
140
|
+
### `DelegationClient`
|
|
57
141
|
|
|
58
142
|
```typescript
|
|
59
|
-
import {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
baseUrl: 'https://ceasar-api.hawcx.com',
|
|
68
|
-
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)
|
|
69
151
|
});
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### MFA Operations
|
|
155
|
+
|
|
156
|
+
#### `client.mfa.initiate(options): Promise<object>`
|
|
157
|
+
|
|
158
|
+
Initiate MFA setup or change for a user.
|
|
70
159
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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',
|
|
76
172
|
});
|
|
77
173
|
|
|
78
|
-
//
|
|
79
|
-
await client.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
otp: "123456"
|
|
174
|
+
// TOTP MFA
|
|
175
|
+
const result = await client.mfa.initiate({
|
|
176
|
+
userId: 'user@example.com',
|
|
177
|
+
mfaMethod: MfaMethod.TOTP,
|
|
83
178
|
});
|
|
84
179
|
|
|
85
|
-
//
|
|
86
|
-
const
|
|
87
|
-
|
|
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
|
+
});
|
|
88
186
|
```
|
|
89
187
|
|
|
90
|
-
**
|
|
188
|
+
**MFA Methods:**
|
|
189
|
+
```typescript
|
|
190
|
+
enum MfaMethod {
|
|
191
|
+
EMAIL = 'email',
|
|
192
|
+
SMS = 'sms',
|
|
193
|
+
TOTP = 'totp',
|
|
194
|
+
}
|
|
195
|
+
```
|
|
91
196
|
|
|
92
|
-
|
|
197
|
+
#### `client.mfa.verify(options): Promise<object>`
|
|
93
198
|
|
|
94
|
-
|
|
199
|
+
Verify MFA setup with OTP.
|
|
95
200
|
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
# 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
|
+
});
|
|
103
207
|
```
|
|
104
208
|
|
|
105
|
-
|
|
209
|
+
### User Operations
|
|
106
210
|
|
|
107
|
-
|
|
211
|
+
#### `client.users.getCredentials(userId): Promise<object>`
|
|
108
212
|
|
|
109
|
-
|
|
213
|
+
Get user credentials/metadata.
|
|
110
214
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
OAUTH_CLIENT_ID="your-client-id"
|
|
115
|
-
OAUTH_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\n..."
|
|
116
|
-
OAUTH_ISSUER="https://oauth.example.com" # Optional but recommended
|
|
117
|
-
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);
|
|
118
218
|
```
|
|
119
219
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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');
|
|
126
228
|
```
|
|
127
229
|
|
|
128
|
-
|
|
230
|
+
#### `client.devices.revoke(options): Promise<object>`
|
|
129
231
|
|
|
130
|
-
|
|
232
|
+
Revoke a device (block it from authenticating).
|
|
131
233
|
|
|
132
|
-
**From file:**
|
|
133
234
|
```typescript
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
235
|
+
await client.devices.revoke({
|
|
236
|
+
userId: 'user@example.com',
|
|
237
|
+
deviceId: 'device-h2index',
|
|
137
238
|
});
|
|
138
239
|
```
|
|
139
240
|
|
|
140
|
-
|
|
241
|
+
#### `client.devices.unrevoke(options): Promise<object>`
|
|
242
|
+
|
|
243
|
+
Unrevoke a previously revoked device.
|
|
244
|
+
|
|
141
245
|
```typescript
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
246
|
+
await client.devices.unrevoke({
|
|
247
|
+
userId: 'user@example.com',
|
|
248
|
+
deviceId: 'device-h2index',
|
|
145
249
|
});
|
|
146
250
|
```
|
|
147
251
|
|
|
148
|
-
|
|
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
|
+
});
|
|
149
276
|
```
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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 }
|
|
153
289
|
```
|
|
154
290
|
|
|
155
|
-
|
|
291
|
+
#### `generateCredentialBlob(options): string`
|
|
156
292
|
|
|
157
|
-
|
|
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
|
|
158
310
|
|
|
159
311
|
```typescript
|
|
160
312
|
import {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
313
|
+
DelegationError, // Base delegation error
|
|
314
|
+
DelegationCryptoError, // Encryption/signing failed
|
|
315
|
+
DelegationRequestError, // Request to IDP failed
|
|
316
|
+
DelegationResponseError, // Invalid response from IDP
|
|
165
317
|
} from '@hawcx/oauth-client';
|
|
166
318
|
|
|
167
319
|
try {
|
|
168
|
-
|
|
169
|
-
code,
|
|
170
|
-
oauthTokenUrl,
|
|
171
|
-
clientId,
|
|
172
|
-
publicKey
|
|
173
|
-
});
|
|
320
|
+
await client.mfa.initiate({ userId: 'user@example.com', mfaMethod: MfaMethod.EMAIL });
|
|
174
321
|
} catch (error) {
|
|
175
|
-
if (error instanceof
|
|
176
|
-
|
|
177
|
-
console.
|
|
178
|
-
console.error(`HTTP Status: ${error.statusCode}`);
|
|
179
|
-
console.error(`Response: ${error.responseBody}`);
|
|
180
|
-
} else if (error instanceof JWTVerificationError) {
|
|
181
|
-
// JWT verification failed (invalid signature, expired, etc.)
|
|
182
|
-
console.error(`Verification failed: ${error.message}`);
|
|
183
|
-
console.error(`Original error: ${error.originalError}`);
|
|
184
|
-
} else if (error instanceof InvalidPublicKeyError) {
|
|
185
|
-
// Public key is invalid or unreadable
|
|
186
|
-
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
|
|
187
325
|
}
|
|
188
326
|
}
|
|
189
327
|
```
|
|
190
328
|
|
|
191
|
-
|
|
329
|
+
---
|
|
192
330
|
|
|
193
|
-
|
|
194
|
-
|-----------|--------------|-------------------|
|
|
195
|
-
| `OAuthExchangeError` | Invalid/expired code, network issues | Ask user to re-authenticate |
|
|
196
|
-
| `JWTVerificationError` | Token tampering, expired token | Log incident, ask user to re-authenticate |
|
|
197
|
-
| `InvalidPublicKeyError` | Wrong key, file not found | Check configuration, verify key format |
|
|
331
|
+
## Step-Up Module
|
|
198
332
|
|
|
199
|
-
|
|
333
|
+
For step-up authentication flows (e.g., changing MFA method requires re-authentication).
|
|
200
334
|
|
|
201
|
-
###
|
|
335
|
+
### `StepUpClient`
|
|
202
336
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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>`
|
|
209
354
|
|
|
210
|
-
|
|
355
|
+
Start a step-up authentication flow.
|
|
211
356
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
+
```
|
|
217
365
|
|
|
218
|
-
|
|
366
|
+
#### `stepUp.consumeReceipt(options): Promise<StepUpConsumeResponse>`
|
|
219
367
|
|
|
220
|
-
|
|
368
|
+
Consume a step-up receipt after user completes authentication.
|
|
221
369
|
|
|
222
370
|
```typescript
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
)
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
- `codeVerifier` (string, optional): PKCE code verifier (RFC 7636)
|
|
234
|
-
- `redirectUri` (string, optional): OAuth redirect URI
|
|
235
|
-
- `timeout` (number, optional): Request timeout in seconds (default: 20)
|
|
236
|
-
- `audience` (string, optional): Expected 'aud' claim (recommended)
|
|
237
|
-
- `issuer` (string, optional): Expected 'iss' claim (recommended)
|
|
238
|
-
- `verifyExp` (boolean, optional): Verify token expiration (default: true)
|
|
239
|
-
- `leeway` (number, optional): Clock skew tolerance in seconds (default: 0)
|
|
240
|
-
- `algorithms` (string[], optional): Allowed signing algorithms (default: RS256, ES256/384/512, EdDSA)
|
|
241
|
-
|
|
242
|
-
**Returns:** `Promise<Record<string, any>>` - Verified JWT claims
|
|
243
|
-
|
|
244
|
-
**Throws:**
|
|
245
|
-
- `OAuthExchangeError`: Code exchange failed
|
|
246
|
-
- `JWTVerificationError`: JWT verification failed
|
|
247
|
-
- `InvalidPublicKeyError`: Public key invalid
|
|
248
|
-
|
|
249
|
-
### `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
|
|
250
381
|
|
|
251
382
|
```typescript
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
+
}
|
|
255
395
|
```
|
|
256
396
|
|
|
257
|
-
|
|
397
|
+
---
|
|
258
398
|
|
|
259
|
-
|
|
399
|
+
## Examples
|
|
260
400
|
|
|
261
|
-
|
|
401
|
+
### Next.js Auth Callback
|
|
262
402
|
|
|
263
|
-
|
|
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
|
+
});
|
|
264
411
|
|
|
265
|
-
|
|
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
|
|
266
437
|
|
|
267
438
|
```typescript
|
|
268
|
-
import
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
+
});
|
|
272
452
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
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
|
+
}
|
|
281
473
|
```
|
|
282
474
|
|
|
283
|
-
|
|
475
|
+
### Step-Up for MFA Change
|
|
284
476
|
|
|
285
|
-
|
|
477
|
+
```typescript
|
|
478
|
+
import { StepUpClient, DelegationClient, MfaMethod } from '@hawcx/oauth-client';
|
|
286
479
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
+
}
|
|
290
516
|
```
|
|
291
517
|
|
|
292
|
-
|
|
518
|
+
---
|
|
293
519
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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>
|
|
297
534
|
```
|
|
298
535
|
|
|
299
|
-
|
|
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
|
|
300
542
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
|
304
565
|
```
|
|
566
|
+
|
|
567
|
+
---
|
|
568
|
+
|
|
569
|
+
## Requirements
|
|
570
|
+
|
|
571
|
+
- Node.js >= 18.0.0
|
|
572
|
+
|
|
573
|
+
## License
|
|
574
|
+
|
|
575
|
+
MIT
|