@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.
@@ -0,0 +1,359 @@
1
+ # Generating JWKS Keys
2
+
3
+ This guide shows how to generate and use cryptographic keys with xAuthJWSK for local development and testing.
4
+
5
+ ## Quick Start: Using Node.js
6
+
7
+ ### 1. Generate RSA Key Pair
8
+
9
+ Create a file called `generate-keys.js`:
10
+
11
+ ```javascript
12
+ import * as jose from 'jose';
13
+ import fs from 'fs';
14
+
15
+ async function generateKeys() {
16
+ // Generate RSA key pair
17
+ const { publicKey, privateKey } = await jose.generateKeyPair('RS256');
18
+
19
+ // Export keys to JWK format
20
+ const publicJwk = await jose.exportSPKI(publicKey);
21
+ const privateJwk = await jose.exportPKCS8(privateKey);
22
+
23
+ // Create JWKS (public keys only - safe to share)
24
+ const jwks = {
25
+ keys: [
26
+ {
27
+ ...(await jose.exportJWK(publicKey)),
28
+ use: 'sig',
29
+ alg: 'RS256',
30
+ kid: 'dev-key-' + Date.now(),
31
+ },
32
+ ],
33
+ };
34
+
35
+ // Save files
36
+ fs.writeFileSync('private-key.pem', privateJwk);
37
+ fs.writeFileSync('public-key.pem', publicJwk);
38
+ fs.writeFileSync('jwks.json', JSON.stringify(jwks, null, 2));
39
+
40
+ console.log('✅ Keys generated successfully!');
41
+ console.log(' - private-key.pem (Keep secret - for signing tokens)');
42
+ console.log(' - public-key.pem (Safe to share)');
43
+ console.log(' - jwks.json (Use in xAuthJWSK config)');
44
+ console.log(`\nKey ID: ${jwks.keys[0].kid}`);
45
+ }
46
+
47
+ generateKeys().catch(console.error);
48
+ ```
49
+
50
+ Run it:
51
+
52
+ ```bash
53
+ node generate-keys.js
54
+ ```
55
+
56
+ Output files:
57
+ - **private-key.pem** - Use this to sign test tokens
58
+ - **public-key.pem** - Public key format
59
+ - **jwks.json** - JWKS format for xAuthJWSK
60
+
61
+ ### 2. Using the JWKS in xAuthJWSK
62
+
63
+ ```javascript
64
+ import Fastify from 'fastify';
65
+ import xAuthJWSK from '@xenterprises/fastify-xauth-jwks';
66
+ import localJwks from './jwks.json' assert { type: 'json' };
67
+
68
+ const fastify = Fastify();
69
+
70
+ await fastify.register(xAuthJWSK, {
71
+ paths: {
72
+ api: {
73
+ pathPattern: '/api',
74
+ jwksData: localJwks, // Use local JWKS
75
+ }
76
+ }
77
+ });
78
+
79
+ // Routes now protected with JWT validation against local keys
80
+ fastify.get('/api/data', (request) => {
81
+ return { userId: request.auth.userId };
82
+ });
83
+
84
+ await fastify.listen({ port: 3000 });
85
+ ```
86
+
87
+ ### 3. Create Test Tokens
88
+
89
+ Create a file called `create-token.js`:
90
+
91
+ ```javascript
92
+ import * as jose from 'jose';
93
+ import fs from 'fs';
94
+
95
+ async function createToken() {
96
+ // Read private key
97
+ const privateKeyPem = fs.readFileSync('private-key.pem', 'utf8');
98
+ const privateKey = await jose.importPKCS8(privateKeyPem, 'RS256');
99
+
100
+ // Read JWKS to get key ID
101
+ const jwks = JSON.parse(fs.readFileSync('jwks.json', 'utf8'));
102
+ const kid = jwks.keys[0].kid;
103
+
104
+ // Create token
105
+ const token = await new jose.SignJWT({
106
+ sub: 'user-123',
107
+ name: 'Test User',
108
+ email: 'test@example.com',
109
+ roles: ['admin'],
110
+ iat: Math.floor(Date.now() / 1000),
111
+ exp: Math.floor(Date.now() / 1000) + 3600, // 1 hour
112
+ })
113
+ .setProtectedHeader({
114
+ alg: 'RS256',
115
+ kid: kid, // Must match key ID in JWKS
116
+ typ: 'JWT',
117
+ })
118
+ .sign(privateKey);
119
+
120
+ console.log('🎫 Test Token:');
121
+ console.log(token);
122
+ console.log('\n📋 Usage:');
123
+ console.log('curl -H "Authorization: Bearer ' + token + '" http://localhost:3000/api/data');
124
+ }
125
+
126
+ createToken().catch(console.error);
127
+ ```
128
+
129
+ Run it:
130
+
131
+ ```bash
132
+ node create-token.js
133
+ ```
134
+
135
+ ### 4. Test the Protected Route
136
+
137
+ ```bash
138
+ # Get token
139
+ TOKEN=$(node create-token.js | grep -A 100 "Test Token:" | tail -1)
140
+
141
+ # Call protected endpoint
142
+ curl -H "Authorization: Bearer $TOKEN" http://localhost:3000/api/data
143
+ ```
144
+
145
+ ## Using OpenSSL (Alternative)
146
+
147
+ If you prefer command-line tools:
148
+
149
+ ### Generate RSA Key Pair
150
+
151
+ ```bash
152
+ # Generate private key
153
+ openssl genrsa -out private-key.pem 2048
154
+
155
+ # Extract public key
156
+ openssl rsa -in private-key.pem -pubout -out public-key.pem
157
+ ```
158
+
159
+ ### Convert to JWKS Format
160
+
161
+ Create `keys-to-jwks.js`:
162
+
163
+ ```javascript
164
+ import * as jose from 'jose';
165
+ import fs from 'fs';
166
+
167
+ async function convertToJwks() {
168
+ const publicKeyPem = fs.readFileSync('public-key.pem', 'utf8');
169
+ const publicKey = await jose.importSPKI(publicKeyPem, 'RS256');
170
+
171
+ const jwks = {
172
+ keys: [
173
+ {
174
+ ...(await jose.exportJWK(publicKey)),
175
+ use: 'sig',
176
+ alg: 'RS256',
177
+ kid: 'dev-key-' + Date.now(),
178
+ },
179
+ ],
180
+ };
181
+
182
+ fs.writeFileSync('jwks.json', JSON.stringify(jwks, null, 2));
183
+ console.log('✅ JWKS generated from existing keys');
184
+ }
185
+
186
+ convertToJwks().catch(console.error);
187
+ ```
188
+
189
+ ## Key Algorithms
190
+
191
+ ### RS256 (Recommended)
192
+
193
+ Asymmetric RSA signature. Use this for:
194
+ - Development and testing
195
+ - Multiple independent services
196
+ - Key rotation scenarios
197
+
198
+ ```javascript
199
+ const { publicKey, privateKey } = await jose.generateKeyPair('RS256');
200
+ ```
201
+
202
+ ### HS256
203
+
204
+ Symmetric HMAC. Use this only for:
205
+ - Single monolithic application
206
+ - When performance is critical
207
+ - Development with shared secret
208
+
209
+ ```javascript
210
+ const secret = jose.base64url.decode('your-secret-here');
211
+ const key = await jose.importJWK({ kty: 'oct', k: jose.base64url.encode(secret) });
212
+ ```
213
+
214
+ ## JWKS File Format
215
+
216
+ Here's what a typical JWKS looks like:
217
+
218
+ ```json
219
+ {
220
+ "keys": [
221
+ {
222
+ "kty": "RSA",
223
+ "use": "sig",
224
+ "alg": "RS256",
225
+ "kid": "dev-key-1702000000000",
226
+ "n": "very-long-base64-number...",
227
+ "e": "AQAB",
228
+ "crv": null,
229
+ "x": null,
230
+ "y": null,
231
+ "d": null,
232
+ "p": null,
233
+ "q": null,
234
+ "dp": null,
235
+ "dq": null,
236
+ "qi": null,
237
+ "oth": null,
238
+ "ext": true,
239
+ "key_ops": [],
240
+ "x5c": null,
241
+ "x5t": null,
242
+ "x5t#S256": null,
243
+ "x5u": null
244
+ }
245
+ ]
246
+ }
247
+ ```
248
+
249
+ **Key fields:**
250
+ - `kty` - Key type (`RSA`, `EC`, `oct`)
251
+ - `use` - Key usage (`sig` for signing, `enc` for encryption)
252
+ - `alg` - Algorithm (`RS256`, `ES256`, `HS256`)
253
+ - `kid` - Key ID (must match token header `kid`)
254
+ - `n`, `e` - RSA components (modulus, exponent)
255
+ - `x`, `y` - EC coordinates (for elliptic curve)
256
+ - `k` - Symmetric key (for HMAC)
257
+
258
+ ## Token Header Format
259
+
260
+ Tokens signed with your keys should have headers like:
261
+
262
+ ```json
263
+ {
264
+ "alg": "RS256",
265
+ "kid": "dev-key-1702000000000",
266
+ "typ": "JWT"
267
+ }
268
+ ```
269
+
270
+ The `kid` (Key ID) **must match** a key in your JWKS.
271
+
272
+ ## Best Practices
273
+
274
+ ### ✅ DO
275
+
276
+ - Generate unique keys for each environment (dev, staging, prod)
277
+ - Rotate keys periodically
278
+ - Keep private keys in `.env` or secrets manager (not in code)
279
+ - Use strong algorithms (RS256, ES256)
280
+ - Set appropriate expiration times on tokens
281
+ - Log key rotations for audit trails
282
+
283
+ ### ❌ DON'T
284
+
285
+ - Commit private keys to git
286
+ - Use HS256 with static shared secrets
287
+ - Reuse the same key for years
288
+ - Put keys in public URLs
289
+ - Share JWKS files containing private key material
290
+ - Use weak algorithms (HS256 with short secrets)
291
+
292
+ ## Example: Development Workflow
293
+
294
+ ```bash
295
+ # 1. Generate keys
296
+ node generate-keys.js
297
+
298
+ # 2. Add to .gitignore
299
+ echo "private-key.pem" >> .gitignore
300
+ echo "*.token" >> .gitignore
301
+
302
+ # 3. Start server with local JWKS
303
+ node server.js
304
+
305
+ # 4. Create test token in another terminal
306
+ TOKEN=$(node create-token.js | grep "Bearer" | awk '{print $NF}')
307
+ echo $TOKEN > test.token
308
+
309
+ # 5. Test endpoint
310
+ curl -H "Authorization: Bearer $(cat test.token)" http://localhost:3000/api/data
311
+
312
+ # 6. Decode and inspect token
313
+ node -e "console.log(require('util').inspect(JSON.parse(Buffer.from(process.argv[1].split('.')[1], 'base64').toString()), false, 10))" "$(cat test.token)"
314
+ ```
315
+
316
+ ## Rotating Keys
317
+
318
+ To rotate keys with zero downtime:
319
+
320
+ 1. **Generate new key pair**
321
+ ```bash
322
+ node generate-keys.js
323
+ ```
324
+
325
+ 2. **Add new key to JWKS** (keep old keys)
326
+ ```json
327
+ {
328
+ "keys": [
329
+ { "kid": "old-key-...", ... },
330
+ { "kid": "new-key-...", ... }
331
+ ]
332
+ }
333
+ ```
334
+
335
+ 3. **Start signing new tokens with new key**
336
+ 4. **Wait for old tokens to expire**
337
+ 5. **Remove old keys from JWKS** after expiration period
338
+
339
+ xAuthJWSK will accept tokens signed with any key in the JWKS, enabling smooth rotation.
340
+
341
+ ## Troubleshooting
342
+
343
+ ### "Invalid token" errors
344
+
345
+ 1. Check `kid` in token header matches JWKS
346
+ 2. Verify token hasn't expired (`exp` claim)
347
+ 3. Ensure private key used to sign matches public key in JWKS
348
+
349
+ ### "JWKS format error"
350
+
351
+ 1. Validate JSON syntax: `cat jwks.json | jq .`
352
+ 2. Ensure `keys` is an array
353
+ 3. Check required fields: `kty`, `use`, `alg`, `kid`
354
+
355
+ ### "Cannot verify token"
356
+
357
+ 1. Make sure token was signed with corresponding private key
358
+ 2. Check algorithm matches (`alg` in token header vs JWKS)
359
+ 3. Verify public key `n` and `e` values match original key
package/QUICK_START.md ADDED
@@ -0,0 +1,334 @@
1
+ # xAuthJWSK Quick Start
2
+
3
+ Lightweight path-based JWT/JWKS validation for Fastify v5.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @xenterprises/fastify-xauth-jwks
9
+ ```
10
+
11
+ ## Basic Usage
12
+
13
+ ```javascript
14
+ import Fastify from 'fastify';
15
+ import xAuthJWSK from '@xenterprises/fastify-xauth-jwks';
16
+
17
+ const fastify = Fastify();
18
+
19
+ await fastify.register(xAuthJWSK, {
20
+ paths: {
21
+ admin: {
22
+ pathPattern: "/admin",
23
+ jwksUrl: "https://your-auth.com/.well-known/jwks.json",
24
+ }
25
+ }
26
+ });
27
+
28
+ // All routes under /admin now require Bearer tokens
29
+ fastify.get('/admin/users', (request) => {
30
+ return {
31
+ userId: request.auth.userId, // from JWT 'sub' claim
32
+ payload: request.auth.payload, // full JWT payload
33
+ path: request.auth.path, // 'admin'
34
+ };
35
+ });
36
+ ```
37
+
38
+ ## Multiple Paths
39
+
40
+ Protect different paths with different JWKS providers:
41
+
42
+ ```javascript
43
+ await fastify.register(xAuthJWSK, {
44
+ paths: {
45
+ admin: {
46
+ pathPattern: "/admin",
47
+ jwksUrl: "https://your-auth.com/admin/.well-known/jwks.json",
48
+ },
49
+ portal: {
50
+ pathPattern: "/portal",
51
+ jwksUrl: "https://your-auth.com/portal/.well-known/jwks.json",
52
+ }
53
+ }
54
+ });
55
+ ```
56
+
57
+ ## Local JWKS (Development/Testing)
58
+
59
+ Use local JWKS data instead of remote endpoints. `jwksData` accepts either a full JWKS object or a single JWK key:
60
+
61
+ ### Option 1: Full JWKS Object (multiple keys)
62
+
63
+ ```javascript
64
+ import localJwks from './jwks.json' assert { type: 'json' };
65
+
66
+ await fastify.register(xAuthJWSK, {
67
+ paths: {
68
+ api: {
69
+ pathPattern: "/api",
70
+ jwksData: localJwks, // { keys: [...] }
71
+ }
72
+ }
73
+ });
74
+ ```
75
+
76
+ ### Option 2: Single JWK Key (for token signing AND validation)
77
+
78
+ Perfect for email/password auth, passwordless flows, or self-contained services:
79
+
80
+ ```javascript
81
+ import { exportSPKI, exportJWK, generateKeyPair } from 'jose';
82
+
83
+ const { publicKey } = await generateKeyPair('RS256');
84
+ const publicJwk = await exportJWK(publicKey);
85
+
86
+ await fastify.register(xAuthJWSK, {
87
+ paths: {
88
+ api: {
89
+ pathPattern: "/api",
90
+ jwksData: { // Single JWK - automatically wrapped in JWKS format
91
+ ...publicJwk,
92
+ use: "sig",
93
+ alg: "RS256",
94
+ kid: "my-key-id"
95
+ }
96
+ }
97
+ }
98
+ });
99
+ ```
100
+
101
+ ### Option 3: Inline JWKS Object
102
+
103
+ ```javascript
104
+ await fastify.register(xAuthJWSK, {
105
+ paths: {
106
+ api: {
107
+ pathPattern: "/api",
108
+ jwksData: {
109
+ keys: [
110
+ {
111
+ kty: "RSA",
112
+ use: "sig",
113
+ kid: "your-key-id",
114
+ n: "...",
115
+ e: "AQAB"
116
+ }
117
+ ]
118
+ }
119
+ }
120
+ }
121
+ });
122
+ ```
123
+
124
+ **Key Points:**
125
+ - `jwksData` accepts both `{ keys: [...] }` (full JWKS) and single JWK objects
126
+ - Single keys are automatically wrapped in JWKS format internally
127
+ - Perfect for self-contained services that sign AND validate their own tokens
128
+ - Use `jwksUrl` for production (remote endpoints) or `jwksData` for development/testing (local data)
129
+ - Cannot use both `jwksUrl` and `jwksData` simultaneously
130
+
131
+ ## Excluded Paths
132
+
133
+ Skip authentication for specific routes:
134
+
135
+ ```javascript
136
+ await fastify.register(xAuthJWSK, {
137
+ paths: {
138
+ api: {
139
+ pathPattern: "/api",
140
+ jwksUrl: "https://your-auth.com/.well-known/jwks.json",
141
+ excludedPaths: ["/health", "/status", "/docs"],
142
+ }
143
+ }
144
+ });
145
+
146
+ // These do NOT require tokens
147
+ fastify.get('/api/health', () => ({ ok: true }));
148
+ fastify.get('/api/status', () => ({ status: 'active' }));
149
+ ```
150
+
151
+ ## Caching Configuration
152
+
153
+ ### Default Caching (Recommended)
154
+
155
+ ```javascript
156
+ {
157
+ paths: {
158
+ api: {
159
+ pathPattern: "/api",
160
+ jwksUrl: "https://your-auth.com/.well-known/jwks.json",
161
+ // Default: JWKS cached 30 mins, tokens cached 5 mins
162
+ }
163
+ }
164
+ }
165
+ ```
166
+
167
+ ### Custom Caching
168
+
169
+ ```javascript
170
+ {
171
+ paths: {
172
+ api: {
173
+ pathPattern: "/api",
174
+ jwksUrl: "https://your-auth.com/.well-known/jwks.json",
175
+ // JWKS caching
176
+ jwksCooldownDuration: 60000, // 1 min between JWKS refetches
177
+ jwksCacheMaxAge: 3600000, // 1 hour JWKS cache
178
+ // JWT payload caching
179
+ enablePayloadCache: true, // cache token payloads
180
+ payloadCacheTTL: 600000, // 10 min token cache
181
+ }
182
+ }
183
+ }
184
+ ```
185
+
186
+ ### Disable Caching
187
+
188
+ ```javascript
189
+ {
190
+ paths: {
191
+ admin: {
192
+ pathPattern: "/admin",
193
+ jwksUrl: "https://your-auth.com/.well-known/jwks.json",
194
+ enablePayloadCache: false, // no token payload caching
195
+ }
196
+ }
197
+ }
198
+ ```
199
+
200
+ ## Accessing User Info
201
+
202
+ In any protected route:
203
+
204
+ ```javascript
205
+ fastify.get('/admin/profile', (request) => {
206
+ const userId = request.auth.userId; // JWT 'sub' claim
207
+ const tokenPayload = request.auth.payload; // full JWT payload
208
+ const pathName = request.auth.path; // 'admin'
209
+
210
+ return { userId, tokenPayload, pathName };
211
+ });
212
+ ```
213
+
214
+ ## Cache Management
215
+
216
+ ```javascript
217
+ const validator = fastify.xAuth.validators.admin;
218
+
219
+ // Get cache stats
220
+ const stats = validator.getPayloadCacheStats();
221
+ console.log(stats); // { size: 42, enabled: true, ttl: 300000 }
222
+
223
+ // Clear cache manually
224
+ validator.clearPayloadCache();
225
+
226
+ // Access configuration
227
+ console.log(validator.config);
228
+ ```
229
+
230
+ ## Utilities
231
+
232
+ Import helper functions:
233
+
234
+ ```javascript
235
+ import { extractToken, hasRole, hasPermission } from '@xenterprises/fastify-xauth-jwks/utils';
236
+
237
+ // Extract token from Authorization header
238
+ const token = extractToken(request);
239
+
240
+ // Check roles (if JWT includes 'roles' claim)
241
+ if (hasRole(request.user, 'admin')) {
242
+ // user is admin
243
+ }
244
+
245
+ // Check permissions (if JWT includes 'permissions' claim)
246
+ if (hasPermission(request.user, 'users:write')) {
247
+ // user can write users
248
+ }
249
+
250
+ // Get user ID
251
+ const userId = getUserId(request);
252
+
253
+ // Get auth path name
254
+ const pathName = getAuthEndpoint(request);
255
+ ```
256
+
257
+ ## Error Responses
258
+
259
+ ### Missing Token
260
+ ```javascript
261
+ {
262
+ statusCode: 401,
263
+ error: "Access token required",
264
+ path: "admin"
265
+ }
266
+ ```
267
+
268
+ ### Invalid Token
269
+ ```javascript
270
+ {
271
+ statusCode: 401,
272
+ error: "Invalid token",
273
+ path: "admin"
274
+ }
275
+ ```
276
+
277
+ ## Performance Tips
278
+
279
+ 1. **Enable caching** (default): Reduces verification overhead by ~90%
280
+ 2. **Increase `jwksCacheMaxAge`** for stable JWKS endpoints (up to 1 hour)
281
+ 3. **Match `payloadCacheTTL`** to your token lifetime (usually 5-15 mins)
282
+ 4. **Monitor cache** with `getPayloadCacheStats()` in production
283
+
284
+ ## Examples
285
+
286
+ ### Simple API Protection
287
+
288
+ ```javascript
289
+ await fastify.register(xAuthJWSK, {
290
+ paths: {
291
+ api: {
292
+ pathPattern: "/api",
293
+ jwksUrl: "https://auth.example.com/.well-known/jwks.json",
294
+ }
295
+ }
296
+ });
297
+
298
+ fastify.get('/api/data', (request) => ({ data: 'sensitive' }));
299
+ ```
300
+
301
+ ### Multiple Environments
302
+
303
+ ```javascript
304
+ await fastify.register(xAuthJWSK, {
305
+ paths: {
306
+ admin: {
307
+ pathPattern: "/admin",
308
+ jwksUrl: process.env.ADMIN_JWKS_URL,
309
+ },
310
+ portal: {
311
+ pathPattern: "/portal",
312
+ jwksUrl: process.env.PORTAL_JWKS_URL,
313
+ }
314
+ }
315
+ });
316
+ ```
317
+
318
+ ### High-Traffic Setup
319
+
320
+ ```javascript
321
+ await fastify.register(xAuthJWSK, {
322
+ paths: {
323
+ api: {
324
+ pathPattern: "/api",
325
+ jwksUrl: "https://auth.example.com/.well-known/jwks.json",
326
+ jwksCooldownDuration: 60000,
327
+ jwksCacheMaxAge: 3600000,
328
+ payloadCacheTTL: 600000,
329
+ }
330
+ }
331
+ });
332
+ ```
333
+
334
+ See [CACHING.md](./CACHING.md) for detailed caching documentation.