@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,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
+ ```