@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/AUTHENTICATION_EXAMPLE.md +453 -0
- package/CACHING.md +282 -0
- package/CONFIGURATION.md +545 -0
- package/DEVELOPMENT.md +385 -0
- package/JOSE_UTILITIES.md +204 -0
- package/KEYS_GENERATION.md +359 -0
- package/QUICK_START.md +334 -0
- package/README.md +73 -0
- package/package.json +44 -0
- package/server/app.js +370 -0
- package/server/example-jwks.json +12 -0
- package/server/generate-demo-token.js +232 -0
- package/src/index.js +9 -0
- package/src/services/pathValidator.js +175 -0
- package/src/utils/index.js +145 -0
- package/src/xAuth.js +36 -0
- package/test/integration.test.js +259 -0
- package/test/utils.test.js +195 -0
- package/test/xAuth.test.js +439 -0
|
@@ -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.
|