@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,453 @@
|
|
|
1
|
+
# Authentication Example: Email/Password with JWT Signing
|
|
2
|
+
|
|
3
|
+
This example shows how to use xAuthJWSK for complete authentication workflows, including:
|
|
4
|
+
- Token signing for user login
|
|
5
|
+
- Token validation for protected routes
|
|
6
|
+
- All using a single JWK key
|
|
7
|
+
|
|
8
|
+
## Architecture
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
┌────────────────────────────────────────────┐
|
|
12
|
+
│ Your Application │
|
|
13
|
+
├────────────────────────────────────────────┤
|
|
14
|
+
│ │
|
|
15
|
+
│ Private Key (for signing) ────────┐ │
|
|
16
|
+
│ │ │
|
|
17
|
+
│ Public Key (for validation) ──────┐ │ │
|
|
18
|
+
│ │ │ │
|
|
19
|
+
│ xAuthJWSK (validates) ◄─────┴───┘ │
|
|
20
|
+
│ │
|
|
21
|
+
│ Login Route (signs tokens) ───────────┐ │
|
|
22
|
+
│ Protected Routes (verify) ◄──────────┘ │
|
|
23
|
+
│ │
|
|
24
|
+
└────────────────────────────────────────────┘
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Step 1: Generate Your Key Pair
|
|
28
|
+
|
|
29
|
+
```javascript
|
|
30
|
+
// generate-auth-keys.js
|
|
31
|
+
import * as jose from 'jose';
|
|
32
|
+
import fs from 'fs';
|
|
33
|
+
|
|
34
|
+
async function generateAuthKeys() {
|
|
35
|
+
// Generate RSA key pair
|
|
36
|
+
const { publicKey, privateKey } = await jose.generateKeyPair('RS256');
|
|
37
|
+
|
|
38
|
+
// Export keys
|
|
39
|
+
const publicJwk = await jose.exportJWK(publicKey);
|
|
40
|
+
const privateKeyPem = await jose.exportPKCS8(privateKey);
|
|
41
|
+
|
|
42
|
+
// Create JWK with metadata for validation
|
|
43
|
+
const publicJwk_with_metadata = {
|
|
44
|
+
...publicJwk,
|
|
45
|
+
use: 'sig',
|
|
46
|
+
alg: 'RS256',
|
|
47
|
+
kid: `auth-key-${Date.now()}`
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Save files
|
|
51
|
+
fs.writeFileSync('auth-key-private.pem', privateKeyPem);
|
|
52
|
+
fs.writeFileSync('auth-key-public.json', JSON.stringify(publicJwk_with_metadata, null, 2));
|
|
53
|
+
|
|
54
|
+
console.log('✅ Authentication keys generated');
|
|
55
|
+
console.log(' Private: auth-key-private.pem (keep secret!)');
|
|
56
|
+
console.log(' Public: auth-key-public.json');
|
|
57
|
+
console.log(' Kid: ' + publicJwk_with_metadata.kid);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
generateAuthKeys().catch(console.error);
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Run it:
|
|
64
|
+
```bash
|
|
65
|
+
node generate-auth-keys.js
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Step 2: Configure xAuthJWSK with Public Key
|
|
69
|
+
|
|
70
|
+
```javascript
|
|
71
|
+
// server.js
|
|
72
|
+
import Fastify from 'fastify';
|
|
73
|
+
import xAuthJWSK from '@xenterprises/fastify-xauth-jwks';
|
|
74
|
+
import publicKeyJwk from './auth-key-public.json' assert { type: 'json' };
|
|
75
|
+
import * as jose from 'jose';
|
|
76
|
+
import fs from 'fs';
|
|
77
|
+
|
|
78
|
+
const fastify = Fastify();
|
|
79
|
+
|
|
80
|
+
// Register xAuthJWSK with the PUBLIC key (for validation)
|
|
81
|
+
await fastify.register(xAuthJWSK, {
|
|
82
|
+
paths: {
|
|
83
|
+
api: {
|
|
84
|
+
pathPattern: '/api',
|
|
85
|
+
jwksData: publicKeyJwk, // Single JWK for validation
|
|
86
|
+
excludedPaths: ['/login', '/register']
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Load private key for signing
|
|
92
|
+
const privateKeyPem = fs.readFileSync('auth-key-private.pem', 'utf8');
|
|
93
|
+
const privateKey = await jose.importPKCS8(privateKeyPem, 'RS256');
|
|
94
|
+
|
|
95
|
+
// ============================================================================
|
|
96
|
+
// Authentication Routes
|
|
97
|
+
// ============================================================================
|
|
98
|
+
|
|
99
|
+
// Login endpoint - signs JWT tokens
|
|
100
|
+
fastify.post('/api/login', async (request, reply) => {
|
|
101
|
+
const { email, password } = request.body;
|
|
102
|
+
|
|
103
|
+
// TODO: Validate email/password against database
|
|
104
|
+
if (!email || !password) {
|
|
105
|
+
return reply.code(400).send({ error: 'Email and password required' });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Verify user credentials (example)
|
|
109
|
+
const user = await validateCredentials(email, password);
|
|
110
|
+
if (!user) {
|
|
111
|
+
return reply.code(401).send({ error: 'Invalid credentials' });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Create JWT token
|
|
115
|
+
const token = await new jose.SignJWT({
|
|
116
|
+
sub: user.id,
|
|
117
|
+
email: user.email,
|
|
118
|
+
name: user.name,
|
|
119
|
+
roles: user.roles || ['user'],
|
|
120
|
+
iat: Math.floor(Date.now() / 1000),
|
|
121
|
+
exp: Math.floor(Date.now() / 1000) + 86400, // 24 hours
|
|
122
|
+
})
|
|
123
|
+
.setProtectedHeader({
|
|
124
|
+
alg: 'RS256',
|
|
125
|
+
kid: publicKeyJwk.kid,
|
|
126
|
+
typ: 'JWT'
|
|
127
|
+
})
|
|
128
|
+
.sign(privateKey);
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
token,
|
|
132
|
+
user: {
|
|
133
|
+
id: user.id,
|
|
134
|
+
email: user.email,
|
|
135
|
+
name: user.name
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Register endpoint - signs JWT for new users
|
|
141
|
+
fastify.post('/api/register', async (request, reply) => {
|
|
142
|
+
const { email, password, name } = request.body;
|
|
143
|
+
|
|
144
|
+
// TODO: Validate and create user in database
|
|
145
|
+
const user = await createUser(email, password, name);
|
|
146
|
+
|
|
147
|
+
// Sign token for new user
|
|
148
|
+
const token = await new jose.SignJWT({
|
|
149
|
+
sub: user.id,
|
|
150
|
+
email: user.email,
|
|
151
|
+
name: user.name,
|
|
152
|
+
roles: ['user'],
|
|
153
|
+
iat: Math.floor(Date.now() / 1000),
|
|
154
|
+
exp: Math.floor(Date.now() / 1000) + 86400,
|
|
155
|
+
})
|
|
156
|
+
.setProtectedHeader({
|
|
157
|
+
alg: 'RS256',
|
|
158
|
+
kid: publicKeyJwk.kid,
|
|
159
|
+
typ: 'JWT'
|
|
160
|
+
})
|
|
161
|
+
.sign(privateKey);
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
token,
|
|
165
|
+
user: { id: user.id, email: user.email, name: user.name }
|
|
166
|
+
};
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Protected route - uses xAuthJWSK for validation
|
|
170
|
+
fastify.get('/api/profile', async (request) => {
|
|
171
|
+
// xAuthJWSK automatically validates and populates request.auth
|
|
172
|
+
return {
|
|
173
|
+
message: 'Your profile',
|
|
174
|
+
userId: request.auth.userId,
|
|
175
|
+
email: request.auth.payload.email,
|
|
176
|
+
name: request.auth.payload.name
|
|
177
|
+
};
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Token refresh endpoint
|
|
181
|
+
fastify.post('/api/refresh', async (request) => {
|
|
182
|
+
// xAuthJWSK validates the token first
|
|
183
|
+
const oldToken = request.auth.payload;
|
|
184
|
+
|
|
185
|
+
// Create new token with updated expiration
|
|
186
|
+
const newToken = await new jose.SignJWT({
|
|
187
|
+
sub: oldToken.sub,
|
|
188
|
+
email: oldToken.email,
|
|
189
|
+
name: oldToken.name,
|
|
190
|
+
roles: oldToken.roles,
|
|
191
|
+
iat: Math.floor(Date.now() / 1000),
|
|
192
|
+
exp: Math.floor(Date.now() / 1000) + 86400,
|
|
193
|
+
})
|
|
194
|
+
.setProtectedHeader({
|
|
195
|
+
alg: 'RS256',
|
|
196
|
+
kid: publicKeyJwk.kid,
|
|
197
|
+
typ: 'JWT'
|
|
198
|
+
})
|
|
199
|
+
.sign(privateKey);
|
|
200
|
+
|
|
201
|
+
return { token: newToken };
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// ============================================================================
|
|
205
|
+
// Start Server
|
|
206
|
+
// ============================================================================
|
|
207
|
+
|
|
208
|
+
await fastify.listen({ port: 3000 });
|
|
209
|
+
console.log('✅ Server running on http://localhost:3000');
|
|
210
|
+
console.log('\nAuthentication Endpoints:');
|
|
211
|
+
console.log(' POST /api/login - Login with email/password');
|
|
212
|
+
console.log(' POST /api/register - Create new account');
|
|
213
|
+
console.log(' GET /api/profile - View profile (requires token)');
|
|
214
|
+
console.log(' POST /api/refresh - Refresh token expiration');
|
|
215
|
+
|
|
216
|
+
// Stub functions - implement with your database
|
|
217
|
+
async function validateCredentials(email, password) {
|
|
218
|
+
// TODO: Query database, verify password hash, return user object
|
|
219
|
+
// Example implementation with a database:
|
|
220
|
+
// 1. Query user by email: const user = await db.users.findOne({ email });
|
|
221
|
+
// 2. Verify password: const valid = await bcrypt.compare(password, user.passwordHash);
|
|
222
|
+
// 3. Return null if invalid, otherwise return user object with id, email, name, roles
|
|
223
|
+
|
|
224
|
+
// For testing - replace with actual database query
|
|
225
|
+
if (email === 'user@example.com' && password === 'password123') {
|
|
226
|
+
return {
|
|
227
|
+
id: 'user-123',
|
|
228
|
+
email: email,
|
|
229
|
+
name: 'Test User',
|
|
230
|
+
roles: ['user']
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function createUser(email, password, name) {
|
|
237
|
+
// TODO: Create user in database with hashed password
|
|
238
|
+
// Example implementation:
|
|
239
|
+
// 1. Validate email doesn't already exist
|
|
240
|
+
// 2. Hash password with bcrypt: const hash = await bcrypt.hash(password, 10);
|
|
241
|
+
// 3. Create user in database with email, passwordHash, name
|
|
242
|
+
// 4. Return user object with id, email, name, roles
|
|
243
|
+
|
|
244
|
+
// For testing - replace with actual database creation
|
|
245
|
+
return {
|
|
246
|
+
id: 'user-' + Date.now(),
|
|
247
|
+
email: email,
|
|
248
|
+
name: name,
|
|
249
|
+
roles: ['user']
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## Step 3: Test the Authentication Flow
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
# 1. Register a new user
|
|
258
|
+
curl -X POST http://localhost:3000/api/register \
|
|
259
|
+
-H "Content-Type: application/json" \
|
|
260
|
+
-d '{"email":"user@example.com","password":"secret123","name":"John Doe"}'
|
|
261
|
+
|
|
262
|
+
# Response:
|
|
263
|
+
# {
|
|
264
|
+
# "token": "eyJhbGciOiJSUzI1NiI...",
|
|
265
|
+
# "user": {
|
|
266
|
+
# "id": "user-1234567890",
|
|
267
|
+
# "email": "user@example.com",
|
|
268
|
+
# "name": "John Doe"
|
|
269
|
+
# }
|
|
270
|
+
# }
|
|
271
|
+
|
|
272
|
+
# 2. Login with credentials
|
|
273
|
+
curl -X POST http://localhost:3000/api/login \
|
|
274
|
+
-H "Content-Type: application/json" \
|
|
275
|
+
-d '{"email":"user@example.com","password":"secret123"}'
|
|
276
|
+
|
|
277
|
+
# Response: Same format as register
|
|
278
|
+
|
|
279
|
+
# 3. Access protected endpoint with token
|
|
280
|
+
TOKEN="eyJhbGciOiJSUzI1NiI..."
|
|
281
|
+
|
|
282
|
+
curl http://localhost:3000/api/profile \
|
|
283
|
+
-H "Authorization: Bearer $TOKEN"
|
|
284
|
+
|
|
285
|
+
# Response:
|
|
286
|
+
# {
|
|
287
|
+
# "message": "Your profile",
|
|
288
|
+
# "userId": "user-1234567890",
|
|
289
|
+
# "email": "user@example.com",
|
|
290
|
+
# "name": "John Doe"
|
|
291
|
+
# }
|
|
292
|
+
|
|
293
|
+
# 4. Refresh token
|
|
294
|
+
curl -X POST http://localhost:3000/api/refresh \
|
|
295
|
+
-H "Authorization: Bearer $TOKEN"
|
|
296
|
+
|
|
297
|
+
# Response:
|
|
298
|
+
# {
|
|
299
|
+
# "token": "eyJhbGciOiJSUzI1NiI..." # New token with extended expiration
|
|
300
|
+
# }
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
## Architecture Advantages
|
|
304
|
+
|
|
305
|
+
### Single Key Management
|
|
306
|
+
|
|
307
|
+
```javascript
|
|
308
|
+
// One key for signing AND validation
|
|
309
|
+
const publicKeyJwk = { ... }; // Use in xAuthJWSK for validation
|
|
310
|
+
const privateKey = importPKCS8(...); // Use for signing
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### Self-Contained Service
|
|
314
|
+
|
|
315
|
+
- No external auth provider needed
|
|
316
|
+
- No network calls for token validation
|
|
317
|
+
- Complete control over token claims
|
|
318
|
+
- Easy to test and develop
|
|
319
|
+
|
|
320
|
+
### Production Ready
|
|
321
|
+
|
|
322
|
+
```bash
|
|
323
|
+
# Set environment variables for private key
|
|
324
|
+
export AUTH_PRIVATE_KEY=$(cat auth-key-private.pem)
|
|
325
|
+
export AUTH_KEY_ID="prod-key-$(date +%s)"
|
|
326
|
+
|
|
327
|
+
# Load from environment in production
|
|
328
|
+
const privateKey = await jose.importPKCS8(
|
|
329
|
+
process.env.AUTH_PRIVATE_KEY,
|
|
330
|
+
'RS256'
|
|
331
|
+
);
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### Multiple Tokens per User
|
|
335
|
+
|
|
336
|
+
```javascript
|
|
337
|
+
// Create different tokens for different purposes
|
|
338
|
+
const accessToken = await signToken({
|
|
339
|
+
type: 'access',
|
|
340
|
+
sub: user.id,
|
|
341
|
+
exp: Math.floor(Date.now() / 1000) + 3600 // 1 hour
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
const refreshToken = await signToken({
|
|
345
|
+
type: 'refresh',
|
|
346
|
+
sub: user.id,
|
|
347
|
+
exp: Math.floor(Date.now() / 1000) + 604800 // 7 days
|
|
348
|
+
});
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
## Integration with Email/Password Flows
|
|
352
|
+
|
|
353
|
+
### Registration Flow
|
|
354
|
+
|
|
355
|
+
```
|
|
356
|
+
User Registration Form
|
|
357
|
+
↓
|
|
358
|
+
POST /api/register (email, password, name)
|
|
359
|
+
↓
|
|
360
|
+
Hash password, create user record
|
|
361
|
+
↓
|
|
362
|
+
Sign JWT with private key
|
|
363
|
+
↓
|
|
364
|
+
Return token + user info
|
|
365
|
+
↓
|
|
366
|
+
Client stores token (localStorage, httpOnly cookie, etc)
|
|
367
|
+
↓
|
|
368
|
+
Client uses token for API requests
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### Login Flow
|
|
372
|
+
|
|
373
|
+
```
|
|
374
|
+
User Login Form
|
|
375
|
+
↓
|
|
376
|
+
POST /api/login (email, password)
|
|
377
|
+
↓
|
|
378
|
+
Verify password hash against database
|
|
379
|
+
↓
|
|
380
|
+
Sign JWT with private key
|
|
381
|
+
↓
|
|
382
|
+
Return token + user info
|
|
383
|
+
↓
|
|
384
|
+
Client stores token
|
|
385
|
+
↓
|
|
386
|
+
Client uses token for API requests
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
### Protected Route Access
|
|
390
|
+
|
|
391
|
+
```
|
|
392
|
+
Client Request
|
|
393
|
+
Header: Authorization: Bearer <token>
|
|
394
|
+
↓
|
|
395
|
+
xAuthJWSK validates signature with PUBLIC key
|
|
396
|
+
↓
|
|
397
|
+
JWT claims extracted and attached to request.auth
|
|
398
|
+
↓
|
|
399
|
+
Route handler accesses request.auth.userId, payload, etc.
|
|
400
|
+
↓
|
|
401
|
+
Process request and return response
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
## Security Considerations
|
|
405
|
+
|
|
406
|
+
✅ **DO:**
|
|
407
|
+
- Keep private key in environment variables or secure key storage
|
|
408
|
+
- Use HTTPS in production
|
|
409
|
+
- Set reasonable token expiration times
|
|
410
|
+
- Hash passwords before storing
|
|
411
|
+
- Use httpOnly cookies for tokens (if web app)
|
|
412
|
+
- Implement logout by invalidating tokens server-side
|
|
413
|
+
|
|
414
|
+
❌ **DON'T:**
|
|
415
|
+
- Commit private keys to version control
|
|
416
|
+
- Use weak passwords or short token expiration
|
|
417
|
+
- Store private key in client-side code
|
|
418
|
+
- Trust token claims without signature verification (xAuthJWSK handles this)
|
|
419
|
+
- Use same key for signing and client authentication
|
|
420
|
+
|
|
421
|
+
## Scaling to Multiple Services
|
|
422
|
+
|
|
423
|
+
If you need multiple services to validate tokens:
|
|
424
|
+
|
|
425
|
+
```javascript
|
|
426
|
+
// Service A: Signs tokens with private key
|
|
427
|
+
const token = await signToken({ sub: user.id, ... });
|
|
428
|
+
|
|
429
|
+
// Service B, C, D: Validate with public key
|
|
430
|
+
await fastify.register(xAuthJWSK, {
|
|
431
|
+
paths: {
|
|
432
|
+
api: {
|
|
433
|
+
pathPattern: '/api',
|
|
434
|
+
jwksData: publicKeyJwk // Same public key
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
// All services can verify tokens signed by Service A
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
## Complete Example Repository
|
|
443
|
+
|
|
444
|
+
See the full working example in `server/` directory:
|
|
445
|
+
- `app.js` - Demo server with protected routes
|
|
446
|
+
- `generate-demo-token.js` - Token generation utility
|
|
447
|
+
- `example-jwks.json` - Example public key
|
|
448
|
+
|
|
449
|
+
Run it:
|
|
450
|
+
```bash
|
|
451
|
+
node server/app.js
|
|
452
|
+
node server/generate-demo-token.js admin user-123 admin
|
|
453
|
+
```
|