@xenterprises/fastify-xauth-jwks 1.0.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/DEVELOPMENT.md ADDED
@@ -0,0 +1,385 @@
1
+ # Development Guide - xAuthJWSK
2
+
3
+ Quick reference for developing with xAuthJWSK locally using test tokens.
4
+
5
+ ## 🚀 Quick Start (30 seconds)
6
+
7
+ ### 1. Start the demo server (uses local JWKS by default)
8
+
9
+ ```bash
10
+ cd server
11
+ node app.js
12
+ ```
13
+
14
+ You'll see:
15
+ ```
16
+ 🚀 xAuthJWSK Demo Server Started!
17
+
18
+ Mode: 🏠 LOCAL (Development)
19
+ Using example-jwks.json - perfect for testing!
20
+ Generate test tokens: node server/generate-demo-token.js [path] [userId] [role]
21
+ ```
22
+
23
+ ### 2. Generate a test token (in another terminal)
24
+
25
+ ```bash
26
+ cd server
27
+ node generate-demo-token.js admin user-123 admin
28
+ ```
29
+
30
+ Output:
31
+ ```
32
+ 🎫 Generated Test Token:
33
+
34
+ eyJhbGciOiJSUzI1NiIsImtpZCI6ImRlbW8ta2V5LWFkbWluIiwidHlwIjoiSldUIn0...
35
+
36
+ 📋 Token Details:
37
+ Path: admin
38
+ User ID: user-123
39
+ Role: admin
40
+ Expires: 1 hour
41
+
42
+ 🔗 Usage Examples:
43
+
44
+ # cURL:
45
+ curl -H "Authorization: Bearer eyJhbGciOiJSUzI1NiI..." \
46
+ http://localhost:3000/admin/dashboard
47
+ ```
48
+
49
+ ### 3. Test the protected endpoint
50
+
51
+ ```bash
52
+ # Replace TOKEN with actual token from step 2
53
+ TOKEN="eyJhbGciOiJSUzI1NiI..."
54
+
55
+ curl -H "Authorization: Bearer $TOKEN" \
56
+ http://localhost:3000/admin/dashboard
57
+ ```
58
+
59
+ Response:
60
+ ```json
61
+ {
62
+ "message": "Admin Dashboard",
63
+ "userId": "user-123",
64
+ "user": {
65
+ "sub": "user-123",
66
+ "userId": "user-123",
67
+ "name": "Test User (admin)",
68
+ "email": "user-123@example.com",
69
+ "roles": ["admin"],
70
+ "iat": 1702000000,
71
+ "exp": 1702003600
72
+ },
73
+ "authenticatedVia": "admin"
74
+ }
75
+ ```
76
+
77
+ ## 📝 Generate Test Tokens
78
+
79
+ ### Different paths and roles
80
+
81
+ ```bash
82
+ # Admin with admin role
83
+ node server/generate-demo-token.js admin user-123 admin
84
+
85
+ # Portal user with user role
86
+ node server/generate-demo-token.js portal user-456 user
87
+
88
+ # Partner with default user role
89
+ node server/generate-demo-token.js partner partner-789
90
+ ```
91
+
92
+ ### Decode a token (inspect claims)
93
+
94
+ ```bash
95
+ # Extract the payload (middle part) and decode
96
+ TOKEN="eyJhbGciOiJSUzI1NiI...eyJzdWIiOiJ1c2VyLTEyMyI...abc123"
97
+
98
+ # Extract payload and decode
99
+ node -e "console.log(JSON.stringify(JSON.parse(Buffer.from('$TOKEN'.split('.')[1], 'base64').toString()), null, 2))"
100
+ ```
101
+
102
+ ## 🏗️ Architecture
103
+
104
+ ### Local JWKS Mode (Development)
105
+
106
+ ```
107
+ ┌─────────────────────────────────────┐
108
+ │ server/app.js │
109
+ ├─────────────────────────────────────┤
110
+ │ USE_LOCAL_JWKS=true (default) │
111
+ │ Reads: example-jwks.json │
112
+ └────────────┬────────────────────────┘
113
+
114
+
115
+ ┌─────────────────────────────────────┐
116
+ │ xAuthJWSK Plugin │
117
+ ├─────────────────────────────────────┤
118
+ │ paths: { │
119
+ │ admin: { │
120
+ │ jwksData: exampleJwks ← LOCAL │
121
+ │ } │
122
+ │ } │
123
+ └────────────┬────────────────────────┘
124
+
125
+
126
+ ┌─────────────────────────────────────┐
127
+ │ Test Token │
128
+ ├─────────────────────────────────────┤
129
+ │ Signed with: private key │
130
+ │ Verified against: jwksData │
131
+ │ No network calls needed! │
132
+ └─────────────────────────────────────┘
133
+ ```
134
+
135
+ ### Remote JWKS Mode (Production)
136
+
137
+ ```
138
+ USE_LOCAL_JWKS=false
139
+ ADMIN_JWKS_URL=https://auth.example.com/.well-known/jwks.json
140
+
141
+ xAuthJWSK fetches remote JWKS
142
+
143
+ Validates tokens against remote keys
144
+ ```
145
+
146
+ ## 🔧 Server Configuration
147
+
148
+ ### Use Local JWKS (Default)
149
+
150
+ ```bash
151
+ # Default - no env vars needed
152
+ node app.js
153
+
154
+ # Explicit
155
+ USE_LOCAL_JWKS=true node app.js
156
+ ```
157
+
158
+ ### Use Remote JWKS URLs
159
+
160
+ ```bash
161
+ export ADMIN_JWKS_URL="https://auth.example.com/admin/.well-known/jwks.json"
162
+ export PORTAL_JWKS_URL="https://auth.example.com/portal/.well-known/jwks.json"
163
+ export PARTNER_JWKS_URL="https://auth.example.com/partner/.well-known/jwks.json"
164
+
165
+ node app.js
166
+ ```
167
+
168
+ ## 🧪 Testing Workflows
169
+
170
+ ### Test Missing Token
171
+
172
+ ```bash
173
+ curl http://localhost:3000/admin/dashboard
174
+ # Response: 401 Access token required
175
+ ```
176
+
177
+ ### Test Invalid Token
178
+
179
+ ```bash
180
+ curl -H "Authorization: Bearer invalid-token" \
181
+ http://localhost:3000/admin/dashboard
182
+ # Response: 401 Invalid token
183
+ ```
184
+
185
+ ### Test Excluded Paths (no token needed)
186
+
187
+ ```bash
188
+ # These don't require tokens
189
+ curl http://localhost:3000/admin/health
190
+ curl http://localhost:3000/admin/status
191
+
192
+ # Response: 200 OK
193
+ ```
194
+
195
+ ### Test Role-Based Access
196
+
197
+ ```bash
198
+ # Generate user token (not admin)
199
+ TOKEN=$(node server/generate-demo-token.js admin user-456 user | grep "Bearer" -A 1 | head -1)
200
+
201
+ # Try admin-only endpoint
202
+ curl -H "Authorization: Bearer $TOKEN" \
203
+ http://localhost:3000/admin/settings
204
+ # Response: 403 Insufficient permissions (requireRole('admin') failed)
205
+ ```
206
+
207
+ ### Test Token Expiration
208
+
209
+ Check expiration in generated tokens:
210
+
211
+ ```bash
212
+ # Token has exp claim set to 1 hour from now
213
+ # After 1 hour, the same token will fail validation
214
+ ```
215
+
216
+ ## 🔐 Key Management
217
+
218
+ ### Current Demo Key
219
+
220
+ The demo uses a single RSA key defined in `generate-demo-token.js`:
221
+ - Kid: `demo-key-admin`
222
+ - Algorithm: RS256
223
+ - Public key: stored in `example-jwks.json`
224
+ - Private key: used only to sign test tokens
225
+
226
+ ### Generate Your Own Keys
227
+
228
+ For production or custom development:
229
+
230
+ ```bash
231
+ node generate-keys.js
232
+ ```
233
+
234
+ See [KEYS_GENERATION.md](./KEYS_GENERATION.md) for details.
235
+
236
+ ## 🔍 Debugging
237
+
238
+ ### View Cache Statistics
239
+
240
+ ```bash
241
+ curl http://localhost:3000/admin/cache/stats
242
+ ```
243
+
244
+ Response:
245
+ ```json
246
+ {
247
+ "cacheStats": {
248
+ "admin": {
249
+ "size": 2,
250
+ "enabled": true,
251
+ "ttl": 300000
252
+ },
253
+ "portal": {
254
+ "size": 0,
255
+ "enabled": true,
256
+ "ttl": 300000
257
+ },
258
+ "partner": {
259
+ "size": 0,
260
+ "enabled": true,
261
+ "ttl": 600000
262
+ }
263
+ }
264
+ }
265
+ ```
266
+
267
+ ### View Validator Configuration
268
+
269
+ ```bash
270
+ curl http://localhost:3000/admin/validators
271
+ ```
272
+
273
+ ### Decode Token (server-side)
274
+
275
+ ```bash
276
+ TOKEN="eyJhbGciOiJSUzI1NiI..."
277
+
278
+ curl -X POST http://localhost:3000/debug/decode-token \
279
+ -H "Content-Type: application/json" \
280
+ -d "{\"token\": \"$TOKEN\"}"
281
+ ```
282
+
283
+ ### Extract Token from Header
284
+
285
+ ```bash
286
+ curl -X POST http://localhost:3000/debug/extract-token \
287
+ -H "Authorization: Bearer $TOKEN"
288
+ ```
289
+
290
+ ## 📊 Endpoints for Development
291
+
292
+ ### Public (no token needed)
293
+
294
+ | Method | Path | Purpose |
295
+ |--------|------|---------|
296
+ | GET | `/` | API info |
297
+ | GET | `/health` | Health check |
298
+ | POST | `/debug/decode-token` | Decode JWT payload |
299
+ | POST | `/debug/extract-token` | Extract Bearer token |
300
+ | GET | `/admin/validators` | View validator configs |
301
+
302
+ ### Protected - Admin Path
303
+
304
+ | Method | Path | Notes |
305
+ |--------|------|-------|
306
+ | GET | `/admin/dashboard` | Requires token |
307
+ | GET | `/admin/health` | Excluded (no token needed) |
308
+ | GET | `/admin/status` | Excluded (no token needed) |
309
+ | GET | `/admin/users` | List users |
310
+ | GET | `/admin/settings` | Requires `admin` role |
311
+ | POST | `/admin/cache/clear` | Clear JWT cache |
312
+ | GET | `/admin/cache/stats` | View cache stats |
313
+
314
+ ### Protected - Portal Path
315
+
316
+ | Method | Path | Notes |
317
+ |--------|------|-------|
318
+ | GET | `/portal/dashboard` | Requires token |
319
+ | GET | `/portal/profile` | User profile |
320
+ | GET | `/portal/public/docs` | Excluded (no token needed) |
321
+ | GET | `/portal/public/pricing` | Excluded (no token needed) |
322
+
323
+ ### Protected - Partner Path
324
+
325
+ | Method | Path | Notes |
326
+ |--------|------|-------|
327
+ | GET | `/partner/api/data` | Partner data |
328
+ | GET | `/partner/api/webhooks` | Partner webhooks |
329
+
330
+ ## 🐛 Troubleshooting
331
+
332
+ ### "Invalid token" on all requests
333
+
334
+ 1. Check token is valid:
335
+ ```bash
336
+ node -e "console.log(JSON.stringify(JSON.parse(Buffer.from('TOKEN'.split('.')[1], 'base64').toString()), null, 2))" | jq .
337
+ ```
338
+
339
+ 2. Check token hasn't expired:
340
+ ```bash
341
+ node -e "const token = 'TOKEN'; const exp = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()).exp; console.log('Expires:', new Date(exp * 1000).toISOString(), 'Valid:', exp * 1000 > Date.now());"
342
+ ```
343
+
344
+ 3. Check kid matches:
345
+ ```bash
346
+ # Token header kid
347
+ node -e "const token = 'TOKEN'; const header = JSON.parse(Buffer.from(token.split('.')[0], 'base64').toString()); console.log('Token kid:', header.kid);"
348
+
349
+ # JWKS kids
350
+ cat example-jwks.json | jq '.keys[].kid'
351
+ ```
352
+
353
+ ### "Local JWKS not working"
354
+
355
+ 1. Check `example-jwks.json` exists:
356
+ ```bash
357
+ ls -la server/example-jwks.json
358
+ ```
359
+
360
+ 2. Validate JWKS JSON:
361
+ ```bash
362
+ cat server/example-jwks.json | jq .
363
+ ```
364
+
365
+ 3. Ensure server is using local mode:
366
+ ```bash
367
+ # Should say "🏠 LOCAL (Development)"
368
+ node app.js | head -10
369
+ ```
370
+
371
+ ### Cache not working
372
+
373
+ Check cache is enabled:
374
+ ```bash
375
+ curl http://localhost:3000/admin/cache/stats | jq .
376
+ ```
377
+
378
+ If `enabled: false`, restart server or check `enablePayloadCache` config.
379
+
380
+ ## 📚 Related Documentation
381
+
382
+ - [KEYS_GENERATION.md](./KEYS_GENERATION.md) - Generate your own keys
383
+ - [QUICK_START.md](./QUICK_START.md) - Production setup
384
+ - [CACHING.md](./CACHING.md) - Caching configuration
385
+ - [JOSE_UTILITIES.md](./JOSE_UTILITIES.md) - Token inspection utilities
@@ -0,0 +1,204 @@
1
+ # Jose Utilities Reference
2
+
3
+ xAuthJWSK re-exports useful jose functions for advanced JWT manipulation without requiring a separate jose import.
4
+
5
+ ## Available Utilities
6
+
7
+ ### decodeToken(token)
8
+
9
+ Decode JWT payload without verification. Useful for inspecting claims without validation overhead.
10
+
11
+ **⚠️ Warning**: This does NOT verify the signature. Use only for inspection, never trust the claims without verification.
12
+
13
+ ```javascript
14
+ import { decodeToken } from '@xenterprises/fastify-xauth-jwks/utils';
15
+
16
+ const token = request.headers.authorization?.split(' ')[1];
17
+ const payload = decodeToken(token);
18
+
19
+ console.log(payload.sub); // User ID
20
+ console.log(payload.exp); // Expiration time
21
+ console.log(payload.roles); // User roles
22
+ ```
23
+
24
+ **Use Cases:**
25
+ - Inspecting token expiration before full validation
26
+ - Checking user ID in headers before processing
27
+ - Debugging JWT content
28
+ - Conditional routing based on claims
29
+
30
+ **Example: Check Token Expiration**
31
+
32
+ ```javascript
33
+ import { decodeToken } from '@xenterprises/fastify-xauth-jwks/utils';
34
+
35
+ fastify.addHook('onRequest', async (request, reply) => {
36
+ const token = extractToken(request);
37
+ if (!token) return;
38
+
39
+ const payload = decodeToken(token);
40
+
41
+ // Check if token is about to expire
42
+ if (payload.exp && Date.now() / 1000 > payload.exp - 60) {
43
+ // Token expires in less than 1 minute
44
+ request.tokenAboutToExpire = true;
45
+ }
46
+ });
47
+ ```
48
+
49
+ ### decodeHeader(token)
50
+
51
+ Decode JWT header without verification. Useful for inspecting algorithm, key ID, and other header claims.
52
+
53
+ ```javascript
54
+ import { decodeHeader } from '@xenterprises/fastify-xauth-jwks/utils';
55
+
56
+ const token = request.headers.authorization?.split(' ')[1];
57
+ const header = decodeHeader(token);
58
+
59
+ console.log(header.alg); // Algorithm (HS256, RS256, etc.)
60
+ console.log(header.kid); // Key ID
61
+ console.log(header.typ); // Type (JWT)
62
+ ```
63
+
64
+ **Use Cases:**
65
+ - Finding which key signed the token (kid)
66
+ - Verifying algorithm matches expected value
67
+ - Debugging JWT issues
68
+ - Routing tokens to different validators
69
+
70
+ **Example: Check Key ID**
71
+
72
+ ```javascript
73
+ import { decodeHeader, extractToken } from '@xenterprises/fastify-xauth-jwks/utils';
74
+
75
+ fastify.addHook('onRequest', async (request, reply) => {
76
+ const token = extractToken(request);
77
+ if (!token) return;
78
+
79
+ const header = decodeHeader(token);
80
+
81
+ // Route to different validator based on key ID
82
+ if (header.kid === 'old-key') {
83
+ request.oldKeyWarning = true;
84
+ }
85
+ });
86
+ ```
87
+
88
+ **Example: Validate Algorithm**
89
+
90
+ ```javascript
91
+ import { decodeHeader, extractToken } from '@xenterprises/fastify-xauth-jwks/utils';
92
+
93
+ fastify.addHook('onRequest', async (request, reply) => {
94
+ const token = extractToken(request);
95
+ if (!token) return;
96
+
97
+ const header = decodeHeader(token);
98
+
99
+ // Only accept RS256, reject HS256
100
+ if (header.alg === 'HS256') {
101
+ return reply.code(401).send({ error: 'Insecure algorithm' });
102
+ }
103
+ });
104
+ ```
105
+
106
+ ## When to Use vs When NOT to Use
107
+
108
+ ### ✅ DO use decodeToken/decodeHeader for:
109
+
110
+ - Pre-validation inspection of claims
111
+ - Extracting metadata from token headers
112
+ - Conditional routing decisions
113
+ - Token debugging and inspection
114
+ - Displaying token info to user (read-only)
115
+
116
+ ### ❌ DON'T use decodeToken/decodeHeader for:
117
+
118
+ - Making authorization decisions (trust signature verification via xAuthJWSK)
119
+ - Verifying user identity without validation
120
+ - Accepting claims as fact without verification
121
+ - Security-critical decisions
122
+
123
+ ## Complete Example
124
+
125
+ ```javascript
126
+ import Fastify from 'fastify';
127
+ import xAuthJWSK from '@xenterprises/fastify-xauth-jwks';
128
+ import { decodeToken, decodeHeader, extractToken } from '@xenterprises/fastify-xauth-jwks/utils';
129
+
130
+ const fastify = Fastify();
131
+
132
+ await fastify.register(xAuthJWSK, {
133
+ paths: {
134
+ api: {
135
+ pathPattern: "/api",
136
+ jwksUrl: "https://your-auth.com/.well-known/jwks.json",
137
+ }
138
+ }
139
+ });
140
+
141
+ // Pre-validation inspection
142
+ fastify.addHook('onRequest', async (request, reply) => {
143
+ const token = extractToken(request);
144
+ if (!token) return;
145
+
146
+ const header = decodeHeader(token);
147
+
148
+ // Log key ID for audit
149
+ request.keyId = header.kid;
150
+
151
+ const payload = decodeToken(token);
152
+
153
+ // Check expiration before full validation
154
+ const expiresIn = (payload.exp * 1000) - Date.now();
155
+ if (expiresIn < 60000) {
156
+ request.tokenWarnings = ['Token expires in less than 1 minute'];
157
+ }
158
+ });
159
+
160
+ // Protected routes use request.auth (after xAuthJWSK validation)
161
+ fastify.get('/api/profile', (request) => {
162
+ return {
163
+ userId: request.auth.userId, // Verified via signature
164
+ warnings: request.tokenWarnings, // From pre-validation
165
+ keyId: request.keyId // From pre-validation
166
+ };
167
+ });
168
+ ```
169
+
170
+ ## Performance Considerations
171
+
172
+ - `decodeToken()` and `decodeHeader()` are **very fast** (no crypto operations)
173
+ - Use liberally for inspection and logging
174
+ - They complete in microseconds (vs milliseconds for signature verification)
175
+ - Safe to call in every request without performance impact
176
+
177
+ ## Error Handling
178
+
179
+ Both functions throw if token is malformed:
180
+
181
+ ```javascript
182
+ import { decodeToken } from '@xenterprises/fastify-xauth-jwks/utils';
183
+
184
+ try {
185
+ const payload = decodeToken(badToken);
186
+ } catch (error) {
187
+ // Invalid JWT format
188
+ console.error('Malformed token:', error.message);
189
+ }
190
+ ```
191
+
192
+ ## No Other Jose Exports
193
+
194
+ xAuthJWSK intentionally does NOT export:
195
+ - `SignJWT`, `CompactSign` - Not needed (tokens come from auth provider)
196
+ - `generateKeyPair`, `generateSecret` - Not needed (use auth provider)
197
+ - `importJWK`, `importPKCS8` - Not needed (JWKS are fetched remotely)
198
+ - Encryption functions - Out of scope for validation only
199
+
200
+ This keeps the module slim and focused. If you need advanced jose features, import jose directly:
201
+
202
+ ```javascript
203
+ import * as jose from 'jose';
204
+ ```