@veloxts/auth 0.3.3 → 0.3.5

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.
Files changed (66) hide show
  1. package/README.md +1157 -30
  2. package/dist/__integration__/fixtures.d.ts +41 -0
  3. package/dist/__integration__/fixtures.d.ts.map +1 -0
  4. package/dist/__integration__/fixtures.js +79 -0
  5. package/dist/__integration__/fixtures.js.map +1 -0
  6. package/dist/__integration__/setup.d.ts +26 -0
  7. package/dist/__integration__/setup.d.ts.map +1 -0
  8. package/dist/__integration__/setup.js +28 -0
  9. package/dist/__integration__/setup.js.map +1 -0
  10. package/dist/adapter.d.ts +710 -0
  11. package/dist/adapter.d.ts.map +1 -0
  12. package/dist/adapter.js +581 -0
  13. package/dist/adapter.js.map +1 -0
  14. package/dist/adapters/better-auth.d.ts +271 -0
  15. package/dist/adapters/better-auth.d.ts.map +1 -0
  16. package/dist/adapters/better-auth.js +341 -0
  17. package/dist/adapters/better-auth.js.map +1 -0
  18. package/dist/adapters/index.d.ts +28 -0
  19. package/dist/adapters/index.d.ts.map +1 -0
  20. package/dist/adapters/index.js +28 -0
  21. package/dist/adapters/index.js.map +1 -0
  22. package/dist/csrf.d.ts +300 -0
  23. package/dist/csrf.d.ts.map +1 -0
  24. package/dist/csrf.js +402 -0
  25. package/dist/csrf.js.map +1 -0
  26. package/dist/guards.d.ts +142 -0
  27. package/dist/guards.d.ts.map +1 -0
  28. package/dist/guards.js +259 -0
  29. package/dist/guards.js.map +1 -0
  30. package/dist/hash.d.ts +91 -0
  31. package/dist/hash.d.ts.map +1 -0
  32. package/dist/hash.js +236 -0
  33. package/dist/hash.js.map +1 -0
  34. package/dist/index.d.ts +27 -32
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +94 -36
  37. package/dist/index.js.map +1 -1
  38. package/dist/jwt.d.ts +157 -0
  39. package/dist/jwt.d.ts.map +1 -0
  40. package/dist/jwt.js +489 -0
  41. package/dist/jwt.js.map +1 -0
  42. package/dist/middleware.d.ts +99 -0
  43. package/dist/middleware.d.ts.map +1 -0
  44. package/dist/middleware.js +253 -0
  45. package/dist/middleware.js.map +1 -0
  46. package/dist/plugin.d.ts +125 -0
  47. package/dist/plugin.d.ts.map +1 -0
  48. package/dist/plugin.js +193 -0
  49. package/dist/plugin.js.map +1 -0
  50. package/dist/policies.d.ts +137 -0
  51. package/dist/policies.d.ts.map +1 -0
  52. package/dist/policies.js +240 -0
  53. package/dist/policies.js.map +1 -0
  54. package/dist/rate-limit.d.ts +231 -0
  55. package/dist/rate-limit.d.ts.map +1 -0
  56. package/dist/rate-limit.js +352 -0
  57. package/dist/rate-limit.js.map +1 -0
  58. package/dist/session.d.ts +500 -0
  59. package/dist/session.d.ts.map +1 -0
  60. package/dist/session.js +801 -0
  61. package/dist/session.js.map +1 -0
  62. package/dist/types.d.ts +261 -0
  63. package/dist/types.d.ts.map +1 -0
  64. package/dist/types.js +33 -0
  65. package/dist/types.js.map +1 -0
  66. package/package.json +61 -7
package/README.md CHANGED
@@ -4,51 +4,1178 @@
4
4
 
5
5
  Authentication and authorization system for VeloxTS Framework.
6
6
 
7
- > **Note:** This package is a placeholder in v0.1.0. Full implementation coming in v1.1.
7
+ ## Features
8
8
 
9
- ## Status
9
+ - **Pluggable Auth Adapters** - Integrate external providers like BetterAuth, Clerk, Auth0
10
+ - **Session Management** - Cookie-based sessions with pluggable storage backends
11
+ - **JWT Authentication** - Stateless token-based authentication with refresh tokens
12
+ - **Password Hashing** - Secure bcrypt hashing with configurable cost factors
13
+ - **CSRF Protection** - Signed double-submit cookie pattern
14
+ - **Guards and Policies** - Declarative authorization for procedures
15
+ - **Rate Limiting** - Built-in request rate limiting middleware
10
16
 
11
- This package is reserved for future functionality. The current release includes only type definitions and stub implementations to allow other packages to reference it.
17
+ ## Table of Contents
12
18
 
13
- ## Planned Features (v1.1+)
19
+ - [Installation](#installation)
20
+ - [Auth Adapters](#auth-adapters)
21
+ - [Authentication Strategies](#authentication-strategies)
22
+ - [Session Management](#session-management)
23
+ - [JWT Authentication](#jwt-authentication)
24
+ - [CSRF Protection](#csrf-protection)
25
+ - [Guards and Policies](#guards-and-policies)
26
+ - [User Roles and Permissions](#user-roles-and-permissions)
27
+ - [Role-Based Guards](#role-based-guards)
28
+ - [Permission-Based Guards](#permission-based-guards)
29
+ - [Combining Guards](#combining-guards)
30
+ - [Custom Guards](#custom-guards)
31
+ - [Policies](#policies)
32
+ - [Password Hashing](#password-hashing)
33
+ - [Rate Limiting](#rate-limiting)
14
34
 
15
- - **Session Management** - Secure session handling with configurable stores
16
- - **JWT Authentication** - Token-based authentication with refresh tokens
17
- - **Guards and Policies** - Declarative authorization for procedures
18
- - **Role-Based Access Control** - Define roles and permissions
19
- - **User Model Integration** - Seamless integration with Prisma user models
20
- - **OAuth Providers** - Social login support (Google, GitHub, etc.)
35
+ ## Installation
36
+
37
+ ```bash
38
+ npm install @veloxts/auth
39
+ ```
40
+
41
+ Required peer dependencies:
42
+
43
+ ```bash
44
+ npm install @veloxts/core @veloxts/router fastify @fastify/cookie
45
+ ```
46
+
47
+ ## Auth Adapters
48
+
49
+ Auth adapters allow you to integrate external authentication providers like BetterAuth, Clerk, or Auth0 with VeloxTS. Instead of building authentication from scratch, you can leverage battle-tested auth solutions.
50
+
51
+ ### BetterAuth Adapter
52
+
53
+ [BetterAuth](https://better-auth.com) is a comprehensive, framework-agnostic TypeScript authentication library. The BetterAuth adapter seamlessly integrates it with VeloxTS.
54
+
55
+ #### Installation
56
+
57
+ ```bash
58
+ npm install better-auth @veloxts/auth
59
+ ```
60
+
61
+ #### Basic Setup
62
+
63
+ ```typescript
64
+ import { veloxApp } from '@veloxts/core';
65
+ import { createAuthAdapterPlugin, createBetterAuthAdapter } from '@veloxts/auth';
66
+ import { betterAuth } from 'better-auth';
67
+ import { prismaAdapter } from 'better-auth/adapters/prisma';
68
+ import { PrismaClient } from '@prisma/client';
69
+
70
+ const prisma = new PrismaClient();
71
+
72
+ // Create BetterAuth instance
73
+ const auth = betterAuth({
74
+ database: prismaAdapter(prisma, {
75
+ provider: 'postgresql', // or 'mysql', 'sqlite'
76
+ }),
77
+ trustedOrigins: ['http://localhost:3000'],
78
+ emailAndPassword: {
79
+ enabled: true,
80
+ },
81
+ });
82
+
83
+ // Create the adapter
84
+ const betterAuthAdapter = createBetterAuthAdapter({
85
+ name: 'better-auth',
86
+ auth,
87
+ debug: process.env.NODE_ENV === 'development',
88
+ });
89
+
90
+ // Create the plugin
91
+ const authAdapterPlugin = createAuthAdapterPlugin({
92
+ adapter: betterAuthAdapter,
93
+ config: betterAuthAdapter.config,
94
+ });
95
+
96
+ // Create app and register plugin
97
+ const app = await veloxApp();
98
+ await app.register(authAdapterPlugin);
99
+ ```
100
+
101
+ #### Using Authentication in Procedures
102
+
103
+ ```typescript
104
+ import { createAdapterAuthMiddleware } from '@veloxts/auth';
105
+ import { defineProcedures, procedure } from '@veloxts/router';
106
+
107
+ const authMiddleware = createAdapterAuthMiddleware();
108
+
109
+ export const userProcedures = defineProcedures('users', {
110
+ // Require authentication - throws 401 if not logged in
111
+ getProfile: procedure
112
+ .use(authMiddleware.requireAuth())
113
+ .query(async ({ ctx }) => {
114
+ // ctx.user is guaranteed to exist
115
+ return {
116
+ id: ctx.user.id,
117
+ email: ctx.user.email,
118
+ name: ctx.user.name,
119
+ };
120
+ }),
121
+
122
+ // Optional authentication - user may or may not be logged in
123
+ getPublicPosts: procedure
124
+ .use(authMiddleware.optionalAuth())
125
+ .query(async ({ ctx }) => {
126
+ const posts = await db.post.findMany({ where: { published: true } });
127
+ return {
128
+ posts,
129
+ isAuthenticated: ctx.isAuthenticated,
130
+ userId: ctx.user?.id,
131
+ };
132
+ }),
133
+ });
134
+ ```
135
+
136
+ #### BetterAuth Configuration Options
137
+
138
+ ```typescript
139
+ const adapter = createBetterAuthAdapter({
140
+ // Required: Adapter name (for logging)
141
+ name: 'better-auth',
142
+
143
+ // Required: BetterAuth instance
144
+ auth: betterAuth({ ... }),
145
+
146
+ // Optional: Base path for auth routes (default: '/api/auth')
147
+ basePath: '/api/auth',
148
+
149
+ // Optional: Enable debug logging
150
+ debug: true,
151
+
152
+ // Optional: Handle all HTTP methods (default: GET, POST only)
153
+ handleAllMethods: true,
154
+
155
+ // Optional: Routes to exclude from session loading
156
+ excludeRoutes: ['/api/health', '/api/public/*'],
157
+
158
+ // Optional: Custom user transformation
159
+ transformUser: (adapterUser) => ({
160
+ id: adapterUser.id,
161
+ email: adapterUser.email,
162
+ role: adapterUser.providerData?.role as string ?? 'user',
163
+ permissions: adapterUser.providerData?.permissions as string[] ?? [],
164
+ }),
165
+ });
166
+ ```
167
+
168
+ #### Auth Routes
169
+
170
+ BetterAuth automatically mounts its routes at the configured base path. Common routes include:
171
+
172
+ - `POST /api/auth/sign-up` - User registration
173
+ - `POST /api/auth/sign-in/email` - Email/password login
174
+ - `POST /api/auth/sign-out` - Logout
175
+ - `GET /api/auth/session` - Get current session
176
+ - `POST /api/auth/magic-link` - Send magic link (if enabled)
177
+ - `GET /api/auth/callback/:provider` - OAuth callbacks
178
+
179
+ See the [BetterAuth documentation](https://better-auth.com/docs) for all available routes and configuration options.
180
+
181
+ ### Creating Custom Adapters
182
+
183
+ You can create adapters for other authentication providers by implementing the `AuthAdapter` interface:
184
+
185
+ ```typescript
186
+ import {
187
+ AuthAdapter,
188
+ AuthAdapterConfig,
189
+ AdapterSessionResult,
190
+ BaseAuthAdapter,
191
+ defineAuthAdapter,
192
+ } from '@veloxts/auth';
193
+ import type { FastifyInstance, FastifyRequest } from 'fastify';
194
+
195
+ // Define your adapter-specific config
196
+ interface MyAuthConfig extends AuthAdapterConfig {
197
+ apiKey: string;
198
+ domain: string;
199
+ }
200
+
201
+ // Option 1: Use defineAuthAdapter helper
202
+ export const myAuthAdapter = defineAuthAdapter<MyAuthConfig>({
203
+ name: 'my-auth',
204
+ version: '1.0.0',
205
+
206
+ async initialize(fastify: FastifyInstance, config: MyAuthConfig) {
207
+ // Initialize your auth client
208
+ this.client = new MyAuthClient({
209
+ apiKey: config.apiKey,
210
+ domain: config.domain,
211
+ });
212
+ },
213
+
214
+ async getSession(request: FastifyRequest): Promise<AdapterSessionResult | null> {
215
+ const token = request.headers.authorization?.replace('Bearer ', '');
216
+ if (!token) return null;
217
+
218
+ const session = await this.client.verifySession(token);
219
+ if (!session) return null;
220
+
221
+ return {
222
+ user: {
223
+ id: session.user.id,
224
+ email: session.user.email,
225
+ name: session.user.name,
226
+ },
227
+ session: {
228
+ sessionId: session.id,
229
+ userId: session.user.id,
230
+ expiresAt: session.expiresAt,
231
+ isActive: true,
232
+ },
233
+ };
234
+ },
235
+
236
+ getRoutes() {
237
+ return [
238
+ {
239
+ path: '/api/auth/*',
240
+ methods: ['GET', 'POST'],
241
+ handler: async (request, reply) => {
242
+ // Forward to your auth provider
243
+ },
244
+ },
245
+ ];
246
+ },
247
+ });
248
+
249
+ // Option 2: Extend BaseAuthAdapter class
250
+ class MyAuthAdapter extends BaseAuthAdapter<MyAuthConfig> {
251
+ private client: MyAuthClient | null = null;
252
+
253
+ constructor() {
254
+ super('my-auth', '1.0.0');
255
+ }
256
+
257
+ override async initialize(fastify: FastifyInstance, config: MyAuthConfig) {
258
+ await super.initialize(fastify, config);
259
+ this.client = new MyAuthClient(config);
260
+ }
261
+
262
+ override async getSession(request: FastifyRequest): Promise<AdapterSessionResult | null> {
263
+ // Implementation...
264
+ }
265
+
266
+ override getRoutes() {
267
+ return [];
268
+ }
269
+ }
270
+ ```
271
+
272
+ ### Adapter Type Utilities
273
+
274
+ ```typescript
275
+ import {
276
+ isAuthAdapter,
277
+ InferAdapterConfig,
278
+ AuthAdapterError,
279
+ } from '@veloxts/auth';
280
+
281
+ // Type guard to check if value is a valid adapter
282
+ if (isAuthAdapter(maybeAdapter)) {
283
+ const plugin = createAuthAdapterPlugin({
284
+ adapter: maybeAdapter,
285
+ config: maybeAdapter.config,
286
+ });
287
+ }
288
+
289
+ // Infer config type from adapter
290
+ type BetterAuthConfig = InferAdapterConfig<typeof betterAuthAdapter>;
291
+
292
+ // Handle adapter errors
293
+ try {
294
+ const session = await adapter.getSession(request);
295
+ } catch (error) {
296
+ if (error instanceof AuthAdapterError) {
297
+ console.error(`Adapter error: ${error.code} - ${error.message}`);
298
+ console.error(`Cause:`, error.cause);
299
+ }
300
+ }
301
+ ```
302
+
303
+ ## Authentication Strategies
304
+
305
+ VeloxTS Auth provides two primary authentication strategies. Choose the one that fits your architecture:
306
+
307
+ ### Session-Based Authentication
308
+
309
+ **Use when:**
310
+ - Building traditional server-rendered applications
311
+ - You need server-side state and fine-grained session control
312
+ - Single-server or shared session store architecture (Redis, database)
313
+ - You want Laravel-style flash data and session management
314
+
315
+ **Advantages:**
316
+ - Server controls session lifecycle (can revoke sessions immediately)
317
+ - No token storage needed on client
318
+ - Easy "logout all devices" functionality
319
+ - Built-in flash data support
320
+
321
+ **Trade-offs:**
322
+ - Requires session store (Redis, database, etc. for production)
323
+ - Slightly more server load (session lookups)
324
+ - Requires sticky sessions or shared storage for horizontal scaling
325
+
326
+ ### JWT-Based Authentication
327
+
328
+ **Use when:**
329
+ - Building stateless APIs for mobile apps or SPAs
330
+ - Microservices architecture with distributed authentication
331
+ - You need cross-domain authentication
332
+ - Horizontal scaling without shared state
333
+
334
+ **Advantages:**
335
+ - Stateless (no server-side storage required)
336
+ - Works seamlessly across multiple servers
337
+ - Can include custom claims and metadata
338
+
339
+ **Trade-offs:**
340
+ - Cannot revoke tokens before expiration (without additional infrastructure)
341
+ - Requires secure client-side token storage
342
+ - Larger payload size (tokens in every request)
343
+
344
+ ## Session Management
345
+
346
+ Cookie-based session management with secure defaults and pluggable storage backends.
347
+
348
+ ### Quick Start
349
+
350
+ ```typescript
351
+ import { sessionMiddleware, createInMemorySessionStore } from '@veloxts/auth';
352
+ import { defineProcedures, procedure } from '@veloxts/router';
353
+
354
+ // Create session middleware
355
+ const session = sessionMiddleware({
356
+ secret: process.env.SESSION_SECRET!, // Min 32 characters
357
+ cookie: {
358
+ secure: process.env.NODE_ENV === 'production',
359
+ sameSite: 'lax',
360
+ },
361
+ expiration: {
362
+ ttl: 86400, // 24 hours
363
+ sliding: true, // Refresh on each request
364
+ },
365
+ });
366
+
367
+ // Use in procedures
368
+ export const userProcedures = defineProcedures('users', {
369
+ // Get shopping cart from session
370
+ getCart: procedure
371
+ .use(session.middleware())
372
+ .query(async ({ ctx }) => {
373
+ return ctx.session.get('cart') ?? [];
374
+ }),
375
+
376
+ // Add item to cart
377
+ addToCart: procedure
378
+ .use(session.middleware())
379
+ .input(AddToCartSchema)
380
+ .mutation(async ({ input, ctx }) => {
381
+ const cart = ctx.session.get('cart') ?? [];
382
+ cart.push(input.item);
383
+ ctx.session.set('cart', cart);
384
+ return { success: true };
385
+ }),
386
+ });
387
+ ```
388
+
389
+ ### Configuration Options
390
+
391
+ ```typescript
392
+ import { createSessionManager, createInMemorySessionStore } from '@veloxts/auth';
393
+
394
+ const sessionManager = createSessionManager({
395
+ // Required: Cryptographically secure secret (min 32 chars)
396
+ // Generate with: openssl rand -base64 32
397
+ secret: process.env.SESSION_SECRET!,
398
+
399
+ // Optional: Storage backend (default: InMemorySessionStore)
400
+ store: createInMemorySessionStore(),
401
+
402
+ // Optional: Cookie configuration
403
+ cookie: {
404
+ name: 'myapp.session', // Cookie name (default: 'velox.session')
405
+ path: '/', // Cookie path (default: '/')
406
+ domain: 'example.com', // Cookie domain (optional)
407
+ secure: true, // HTTPS only (default: NODE_ENV === 'production')
408
+ httpOnly: true, // Prevent JS access (default: true)
409
+ sameSite: 'strict', // CSRF protection (default: 'lax')
410
+ },
411
+
412
+ // Optional: Expiration configuration
413
+ expiration: {
414
+ ttl: 3600, // Session TTL in seconds (default: 86400)
415
+ sliding: true, // Refresh TTL on each request (default: true)
416
+ absoluteTimeout: 604800, // Max session lifetime (7 days), forces re-auth
417
+ },
418
+
419
+ // Optional: User loader function
420
+ userLoader: async (userId) => {
421
+ return db.user.findUnique({ where: { id: userId } });
422
+ },
423
+ });
424
+ ```
425
+
426
+ ### Middleware Variants
427
+
428
+ ```typescript
429
+ const session = sessionMiddleware(config);
430
+
431
+ // Basic session middleware - creates session for all requests
432
+ const getPreferences = procedure
433
+ .use(session.middleware())
434
+ .query(async ({ ctx }) => {
435
+ return ctx.session.get('theme') ?? 'light';
436
+ });
437
+
438
+ // Require authentication - throws 401 if no userId in session
439
+ const getProfile = procedure
440
+ .use(session.requireAuth())
441
+ .query(async ({ ctx }) => {
442
+ // ctx.user is guaranteed to exist
443
+ return ctx.user;
444
+ });
445
+
446
+ // Optional authentication - user may or may not be logged in
447
+ const getHomePage = procedure
448
+ .use(session.optionalAuth())
449
+ .query(async ({ ctx }) => {
450
+ if (ctx.isAuthenticated) {
451
+ return { greeting: `Welcome back, ${ctx.user.email}!` };
452
+ }
453
+ return { greeting: 'Welcome, guest!' };
454
+ });
455
+ ```
456
+
457
+ ### Login and Logout
458
+
459
+ ```typescript
460
+ import { loginSession, logoutSession } from '@veloxts/auth';
461
+ import { hashPassword, verifyPassword } from '@veloxts/auth';
462
+
463
+ export const authProcedures = defineProcedures('auth', {
464
+ // Login procedure
465
+ login: procedure
466
+ .use(session.middleware())
467
+ .input(z.object({ email: z.string().email(), password: z.string() }))
468
+ .mutation(async ({ input, ctx }) => {
469
+ // Find user
470
+ const user = await db.user.findUnique({ where: { email: input.email } });
471
+ if (!user) {
472
+ throw new AuthError('Invalid credentials', 401);
473
+ }
474
+
475
+ // Verify password
476
+ const valid = await verifyPassword(input.password, user.passwordHash);
477
+ if (!valid) {
478
+ throw new AuthError('Invalid credentials', 401);
479
+ }
480
+
481
+ // Login - regenerates session ID to prevent fixation attacks
482
+ await loginSession(ctx.session, user);
483
+
484
+ return { success: true, user };
485
+ }),
486
+
487
+ // Logout procedure
488
+ logout: procedure
489
+ .use(session.requireAuth())
490
+ .mutation(async ({ ctx }) => {
491
+ await logoutSession(ctx.session);
492
+ return { success: true };
493
+ }),
494
+
495
+ // Logout from all devices
496
+ logoutAll: procedure
497
+ .use(session.requireAuth())
498
+ .mutation(async ({ ctx }) => {
499
+ await session.manager.destroyUserSessions(ctx.user.id);
500
+ return { success: true };
501
+ }),
502
+ });
503
+ ```
504
+
505
+ ### Flash Data
506
+
507
+ Flash data persists for exactly one request - perfect for success messages after redirects.
508
+
509
+ ```typescript
510
+ // Set flash data
511
+ const createPost = procedure
512
+ .use(session.middleware())
513
+ .input(CreatePostSchema)
514
+ .mutation(async ({ input, ctx }) => {
515
+ const post = await db.post.create({ data: input });
516
+
517
+ // Flash message for next request
518
+ ctx.session.flash('success', 'Post created successfully!');
519
+
520
+ return post;
521
+ });
522
+
523
+ // Read flash data (automatically cleared after this request)
524
+ const getFlashMessages = procedure
525
+ .use(session.middleware())
526
+ .query(async ({ ctx }) => {
527
+ const messages = ctx.session.getAllFlash();
528
+ return messages;
529
+ });
530
+ ```
531
+
532
+ ### Session Handle API
533
+
534
+ ```typescript
535
+ // Get value
536
+ const theme = session.get('theme');
537
+
538
+ // Set value (marks session as modified)
539
+ session.set('theme', 'dark');
540
+
541
+ // Delete value
542
+ session.delete('theme');
543
+
544
+ // Check if key exists
545
+ if (session.has('cart')) {
546
+ // ...
547
+ }
548
+
549
+ // Flash data
550
+ session.flash('message', 'Success!');
551
+ const message = session.getFlash('message');
552
+
553
+ // Regenerate session ID (security: call after privilege changes)
554
+ await session.regenerate();
555
+
556
+ // Destroy session completely
557
+ await session.destroy();
558
+
559
+ // Save session manually (auto-saved by middleware)
560
+ await session.save();
561
+
562
+ // Reload session from store
563
+ await session.reload();
564
+
565
+ // Session metadata
566
+ session.id; // Session ID
567
+ session.isNew; // True for new sessions
568
+ session.isModified; // True if data changed
569
+ session.isDestroyed; // True after destroy()
570
+ ```
571
+
572
+ ### Custom Session Storage
573
+
574
+ For production, implement a custom store backed by Redis, PostgreSQL, or other persistent storage.
575
+
576
+ ```typescript
577
+ import { SessionStore, StoredSession } from '@veloxts/auth';
578
+ import { Redis } from 'ioredis';
579
+
580
+ class RedisSessionStore implements SessionStore {
581
+ constructor(private redis: Redis) {}
582
+
583
+ async get(sessionId: string): Promise<StoredSession | null> {
584
+ const data = await this.redis.get(`session:${sessionId}`);
585
+ return data ? JSON.parse(data) : null;
586
+ }
587
+
588
+ async set(sessionId: string, session: StoredSession): Promise<void> {
589
+ const ttl = Math.ceil((session.expiresAt - Date.now()) / 1000);
590
+ await this.redis.setex(
591
+ `session:${sessionId}`,
592
+ ttl,
593
+ JSON.stringify(session)
594
+ );
595
+ }
596
+
597
+ async delete(sessionId: string): Promise<void> {
598
+ await this.redis.del(`session:${sessionId}`);
599
+ }
600
+
601
+ async touch(sessionId: string, expiresAt: number): Promise<void> {
602
+ const session = await this.get(sessionId);
603
+ if (session) {
604
+ session.expiresAt = expiresAt;
605
+ session.data._lastAccessedAt = Date.now();
606
+ await this.set(sessionId, session);
607
+ }
608
+ }
609
+
610
+ async clear(): Promise<void> {
611
+ const keys = await this.redis.keys('session:*');
612
+ if (keys.length > 0) {
613
+ await this.redis.del(...keys);
614
+ }
615
+ }
616
+
617
+ async getSessionsByUser(userId: string): Promise<string[]> {
618
+ return this.redis.smembers(`user:${userId}:sessions`);
619
+ }
620
+
621
+ async deleteSessionsByUser(userId: string): Promise<void> {
622
+ const sessions = await this.getSessionsByUser(userId);
623
+ if (sessions.length > 0) {
624
+ await this.redis.del(...sessions.map(id => `session:${id}`));
625
+ await this.redis.del(`user:${userId}:sessions`);
626
+ }
627
+ }
628
+ }
629
+
630
+ // Use custom store
631
+ const redisStore = new RedisSessionStore(redisClient);
632
+ const session = sessionMiddleware({
633
+ secret: process.env.SESSION_SECRET!,
634
+ store: redisStore,
635
+ });
636
+ ```
637
+
638
+ ### Security Best Practices
639
+
640
+ **1. Always regenerate session ID after login**
641
+
642
+ ```typescript
643
+ // loginSession() does this automatically
644
+ await loginSession(ctx.session, user);
645
+ ```
646
+
647
+ **2. Destroy sessions on logout**
648
+
649
+ ```typescript
650
+ await logoutSession(ctx.session);
651
+ ```
652
+
653
+ **3. Use strong, random secrets**
21
654
 
22
- ## Current Exports
655
+ ```bash
656
+ # Generate a secure secret
657
+ openssl rand -base64 32
658
+ ```
23
659
 
24
- The following are available but not fully implemented:
660
+ **4. Enable secure cookies in production**
25
661
 
26
662
  ```typescript
27
- import { createAuth, guard, User } from '@veloxts/auth';
663
+ cookie: {
664
+ secure: process.env.NODE_ENV === 'production',
665
+ httpOnly: true,
666
+ sameSite: 'strict', // or 'lax'
667
+ }
668
+ ```
28
669
 
29
- // User interface for type definitions
30
- interface User {
31
- id: string;
32
- email: string;
33
- [key: string]: unknown;
670
+ **5. Set absolute timeout for sensitive operations**
671
+
672
+ ```typescript
673
+ expiration: {
674
+ absoluteTimeout: 3600, // Force re-auth after 1 hour
34
675
  }
676
+ ```
677
+
678
+ **6. Use environment variables for secrets**
679
+
680
+ ```typescript
681
+ // NEVER hardcode secrets
682
+ secret: process.env.SESSION_SECRET!,
683
+ ```
684
+
685
+ **7. Implement CSRF protection for state-changing operations**
686
+
687
+ See [CSRF Protection](#csrf-protection) section below.
688
+
689
+ **Built-in Security Features:**
690
+
691
+ The session implementation includes several security protections by default:
692
+
693
+ - **HMAC-SHA256 signing** - All session IDs are cryptographically signed to prevent tampering
694
+ - **Timing-safe comparison** - Session ID verification uses constant-time comparison to prevent timing attacks
695
+ - **Entropy validation** - Session IDs are validated for sufficient randomness (32 bytes, 256 bits)
696
+ - **Session fixation protection** - `loginSession()` automatically regenerates session IDs
697
+ - **SameSite enforcement** - `SameSite=none` requires `Secure` flag per RFC 6265bis
698
+
699
+ ### When to Use Sessions vs JWT
700
+
701
+ **Choose Sessions when:**
702
+ - You need immediate session revocation (logout all devices)
703
+ - Building server-rendered applications with traditional workflows
704
+ - Flash data and server-side state are important to your application
705
+ - You have infrastructure for shared session storage (Redis, etc.)
706
+
707
+ **Choose JWT when:**
708
+ - Building stateless APIs for mobile apps or microservices
709
+ - You need cross-domain authentication
710
+ - Horizontal scaling without shared state is critical
711
+ - You prefer client-side session storage
712
+
713
+ ## JWT Authentication
714
+
715
+ Stateless token-based authentication using HMAC-SHA256 signed JWTs.
716
+
717
+ ### Quick Start
718
+
719
+ ```typescript
720
+ import { jwtManager, authMiddleware } from '@veloxts/auth';
721
+ import { defineProcedures, procedure } from '@veloxts/router';
722
+
723
+ // Create JWT manager
724
+ const jwt = jwtManager({
725
+ secret: process.env.JWT_SECRET!, // Min 64 characters
726
+ accessTokenExpiry: '15m',
727
+ refreshTokenExpiry: '7d',
728
+ issuer: 'my-app',
729
+ audience: 'my-app-users',
730
+ });
731
+
732
+ // Create auth middleware
733
+ const auth = authMiddleware({
734
+ jwt: {
735
+ secret: process.env.JWT_SECRET!,
736
+ accessTokenExpiry: '15m',
737
+ refreshTokenExpiry: '7d',
738
+ },
739
+ userLoader: async (userId) => {
740
+ return db.user.findUnique({ where: { id: userId } });
741
+ },
742
+ });
743
+
744
+ // Use in procedures
745
+ export const authProcedures = defineProcedures('auth', {
746
+ // Login - return tokens
747
+ login: procedure
748
+ .input(z.object({ email: z.string().email(), password: z.string() }))
749
+ .mutation(async ({ input }) => {
750
+ const user = await db.user.findUnique({ where: { email: input.email } });
751
+ if (!user || !await verifyPassword(input.password, user.passwordHash)) {
752
+ throw new AuthError('Invalid credentials', 401);
753
+ }
754
+ return jwt.createTokenPair(user);
755
+ }),
756
+
757
+ // Protected route
758
+ getProfile: procedure
759
+ .use(auth.requireAuth())
760
+ .query(async ({ ctx }) => {
761
+ return ctx.user; // Guaranteed to exist
762
+ }),
763
+
764
+ // Refresh tokens
765
+ refresh: procedure
766
+ .input(z.object({ refreshToken: z.string() }))
767
+ .mutation(async ({ input }) => {
768
+ return jwt.refreshTokens(input.refreshToken);
769
+ }),
770
+ });
771
+ ```
772
+
773
+ ### Configuration Options
774
+
775
+ ```typescript
776
+ import { jwtManager } from '@veloxts/auth';
777
+
778
+ const jwt = jwtManager({
779
+ // Required: Secret for signing tokens (min 64 chars)
780
+ // Generate with: openssl rand -base64 64
781
+ secret: process.env.JWT_SECRET!,
35
782
 
36
- // Placeholder - returns stub implementation
37
- const auth = createAuth({ secret: 'your-secret' });
783
+ // Optional: Token expiration times
784
+ accessTokenExpiry: '15m', // Default: 15 minutes
785
+ refreshTokenExpiry: '7d', // Default: 7 days
38
786
 
39
- // Placeholder decorator
40
- @guard(['admin'])
41
- class AdminProcedures { }
787
+ // Optional: Token claims
788
+ issuer: 'my-app', // iss claim
789
+ audience: 'my-app-users', // aud claim
790
+ });
42
791
  ```
43
792
 
44
- ## Roadmap
793
+ ### Token Operations
45
794
 
46
- | Version | Features |
47
- |---------|----------|
48
- | v0.1.0 | Placeholder with type definitions |
49
- | v1.1.0 | JWT authentication, session management |
50
- | v1.2.0 | Guards, policies, RBAC |
51
- | v1.3.0 | OAuth providers |
795
+ ```typescript
796
+ // Create token pair for user
797
+ const tokens = jwt.createTokenPair(user);
798
+ // Returns: { accessToken, refreshToken, expiresIn, tokenType }
799
+
800
+ // Add custom claims (cannot override reserved claims)
801
+ const tokens = jwt.createTokenPair(user, {
802
+ role: 'admin',
803
+ permissions: ['read', 'write'],
804
+ });
805
+
806
+ // Verify access token
807
+ const payload = jwt.verifyToken(accessToken);
808
+ // Returns: { sub, email, iat, exp, type, jti, ... }
809
+
810
+ // Refresh tokens using refresh token
811
+ const newTokens = jwt.refreshTokens(refreshToken);
812
+
813
+ // Extract token from Authorization header
814
+ const token = jwt.extractFromHeader(request.headers.authorization);
815
+ ```
816
+
817
+ ### Token Revocation
818
+
819
+ For security-critical applications, implement token revocation:
820
+
821
+ ```typescript
822
+ import { createInMemoryTokenStore } from '@veloxts/auth';
823
+
824
+ // Development/testing (NOT for production!)
825
+ const tokenStore = createInMemoryTokenStore();
826
+
827
+ // Configure auth to check revocation
828
+ const auth = authMiddleware({
829
+ jwt: { secret: process.env.JWT_SECRET! },
830
+ isTokenRevoked: tokenStore.isRevoked,
831
+ });
832
+
833
+ // Revoke on logout
834
+ const logout = procedure
835
+ .use(auth.requireAuth())
836
+ .mutation(async ({ ctx }) => {
837
+ if (ctx.auth.token?.jti) {
838
+ tokenStore.revoke(ctx.auth.token.jti);
839
+ }
840
+ return { success: true };
841
+ });
842
+ ```
843
+
844
+ For production, use Redis or database-backed storage instead of the in-memory store.
845
+
846
+ ### Auth Middleware Options
847
+
848
+ ```typescript
849
+ const auth = authMiddleware(config);
850
+
851
+ // Require authentication (throws 401 if no valid token)
852
+ const getProfile = procedure
853
+ .use(auth.requireAuth())
854
+ .query(({ ctx }) => ctx.user);
855
+
856
+ // Optional authentication (user may be undefined)
857
+ const getPosts = procedure
858
+ .use(auth.optionalAuth())
859
+ .query(({ ctx }) => {
860
+ if (ctx.user) {
861
+ return getPrivatePosts(ctx.user.id);
862
+ }
863
+ return getPublicPosts();
864
+ });
865
+
866
+ // With guards (after authentication)
867
+ const adminOnly = procedure
868
+ .use(auth.middleware({ guards: [hasRole('admin')] }))
869
+ .query(({ ctx }) => getAdminData());
870
+ ```
871
+
872
+ ### Security Features
873
+
874
+ The JWT implementation includes several security protections:
875
+
876
+ - **HS256 algorithm enforcement** - Rejects `none`, RS256, and other algorithms to prevent confusion attacks
877
+ - **Timing-safe signature verification** - Prevents timing attacks on token validation
878
+ - **Secret entropy validation** - Requires at least 64 characters with 16+ unique characters
879
+ - **Reserved claim protection** - Prevents overriding `sub`, `exp`, `iat`, etc. via custom claims
880
+ - **Token expiration** - Access and refresh tokens have separate expiration times
881
+ - **Not-before claim support** - Optionally delay token validity
882
+
883
+ ## CSRF Protection
884
+
885
+ CSRF protection is already implemented using the signed double-submit cookie pattern with timing-safe comparison and entropy validation.
886
+
887
+ ```typescript
888
+ import { csrfMiddleware } from '@veloxts/auth';
889
+
890
+ const csrf = csrfMiddleware({
891
+ secret: process.env.CSRF_SECRET!,
892
+ });
893
+
894
+ // Use in procedures that modify state
895
+ const deletePost = procedure
896
+ .use(csrf.middleware())
897
+ .mutation(async ({ ctx }) => {
898
+ // CSRF token validated automatically
899
+ });
900
+ ```
901
+
902
+ See the CSRF documentation for complete details on configuration and usage.
903
+
904
+ ## Guards and Policies
905
+
906
+ Guards and policies provide declarative authorization for procedures.
907
+
908
+ ### User Roles and Permissions
909
+
910
+ Users can have multiple roles and permissions:
911
+
912
+ ```typescript
913
+ import type { User } from '@veloxts/auth';
914
+
915
+ // User with multiple roles
916
+ const user: User = {
917
+ id: '1',
918
+ email: 'admin@example.com',
919
+ roles: ['admin', 'editor'], // Multiple roles
920
+ permissions: ['posts.read', 'posts.write', 'users.manage'],
921
+ };
922
+ ```
923
+
924
+ ### Role-Based Guards
925
+
926
+ The `hasRole` guard checks if the user has ANY of the specified roles:
927
+
928
+ ```typescript
929
+ import { hasRole, hasPermission, allOf, anyOf } from '@veloxts/auth';
930
+
931
+ // Require a single role
932
+ const adminOnly = procedure
933
+ .use(auth.middleware({ guards: [hasRole('admin')] }))
934
+ .query(async ({ ctx }) => {
935
+ // Only users with 'admin' role can access
936
+ });
937
+
938
+ // Require ANY of multiple roles (OR logic)
939
+ const staffAccess = procedure
940
+ .use(auth.middleware({ guards: [hasRole(['admin', 'moderator', 'editor'])] }))
941
+ .query(async ({ ctx }) => {
942
+ // Users with 'admin', 'moderator', OR 'editor' role can access
943
+ });
944
+
945
+ // User with roles: ['editor', 'reviewer'] passes hasRole(['admin', 'editor'])
946
+ // because they have the 'editor' role
947
+ ```
948
+
949
+ ### Permission-Based Guards
950
+
951
+ ```typescript
952
+ // Require ALL specified permissions (AND logic)
953
+ const canManagePosts = procedure
954
+ .use(auth.middleware({ guards: [hasPermission(['posts.read', 'posts.write'])] }))
955
+ .query(async ({ ctx }) => {
956
+ // User must have BOTH permissions
957
+ });
958
+
959
+ // Require ANY of the permissions (OR logic)
960
+ const canViewPosts = procedure
961
+ .use(auth.middleware({ guards: [hasAnyPermission(['posts.read', 'posts.admin'])] }))
962
+ .query(async ({ ctx }) => {
963
+ // User needs at least one of these permissions
964
+ });
965
+ ```
966
+
967
+ ### Combining Guards
968
+
969
+ ```typescript
970
+ // Require BOTH role AND permission (AND logic)
971
+ const adminWithPermission = procedure
972
+ .use(auth.middleware({
973
+ guards: [hasRole('admin'), hasPermission('users.delete')]
974
+ }))
975
+ .mutation(async ({ ctx }) => {
976
+ // Must be admin AND have users.delete permission
977
+ });
978
+
979
+ // Using allOf for explicit AND
980
+ const strictAccess = procedure
981
+ .use(auth.middleware({
982
+ guards: [allOf([hasRole('admin'), hasPermission('sensitive.access')])]
983
+ }))
984
+ .query(async ({ ctx }) => {
985
+ // Both conditions must pass
986
+ });
987
+
988
+ // Using anyOf for explicit OR
989
+ const flexibleAccess = procedure
990
+ .use(auth.middleware({
991
+ guards: [anyOf([hasRole('admin'), hasPermission('special.access')])]
992
+ }))
993
+ .query(async ({ ctx }) => {
994
+ // Either condition can pass
995
+ });
996
+ ```
997
+
998
+ ### Custom Guards
999
+
1000
+ ```typescript
1001
+ import { guard, defineGuard } from '@veloxts/auth';
1002
+
1003
+ // Simple custom guard
1004
+ const isVerifiedEmail = guard('isVerifiedEmail', (ctx) => {
1005
+ return ctx.user?.emailVerified === true;
1006
+ });
1007
+
1008
+ // Guard with configuration
1009
+ const isPremiumUser = defineGuard({
1010
+ name: 'isPremiumUser',
1011
+ check: (ctx) => ctx.user?.subscription === 'premium',
1012
+ message: 'Premium subscription required',
1013
+ statusCode: 403,
1014
+ });
1015
+
1016
+ // Use in procedures
1017
+ const premiumContent = procedure
1018
+ .use(auth.middleware({ guards: [isPremiumUser] }))
1019
+ .query(async ({ ctx }) => {
1020
+ return getPremiumContent();
1021
+ });
1022
+ ```
1023
+
1024
+ ### Policies
1025
+
1026
+ Define resource-specific authorization logic:
1027
+
1028
+ ```typescript
1029
+ import { definePolicy } from '@veloxts/auth';
1030
+
1031
+ const postPolicy = definePolicy<{ postId: string }>('post', {
1032
+ view: async (user, { postId }) => {
1033
+ // Anyone can view public posts
1034
+ return true;
1035
+ },
1036
+ edit: async (user, { postId }) => {
1037
+ const post = await db.post.findUnique({ where: { id: postId } });
1038
+ // Only author or admin can edit
1039
+ return post?.authorId === user.id || user.roles?.includes('admin');
1040
+ },
1041
+ delete: async (user, { postId }) => {
1042
+ const post = await db.post.findUnique({ where: { id: postId } });
1043
+ // Only admin can delete
1044
+ return user.roles?.includes('admin') ?? false;
1045
+ },
1046
+ });
1047
+ ```
1048
+
1049
+ ## Password Hashing
1050
+
1051
+ Secure password hashing with bcrypt:
1052
+
1053
+ ```typescript
1054
+ import { hashPassword, verifyPassword } from '@veloxts/auth';
1055
+
1056
+ // Hash password
1057
+ const hash = await hashPassword('user-password', { cost: 12 });
1058
+
1059
+ // Verify password
1060
+ const valid = await verifyPassword('user-password', hash);
1061
+ ```
1062
+
1063
+ ## Rate Limiting
1064
+
1065
+ Protect your endpoints from abuse with request rate limiting.
1066
+
1067
+ ### Quick Start
1068
+
1069
+ ```typescript
1070
+ import { rateLimitMiddleware } from '@veloxts/auth';
1071
+ import { defineProcedures, procedure } from '@veloxts/router';
1072
+
1073
+ // Create rate limit middleware
1074
+ const rateLimit = rateLimitMiddleware({
1075
+ max: 100, // Maximum requests per window
1076
+ windowMs: 60000, // Window size in milliseconds (1 minute)
1077
+ });
1078
+
1079
+ // Stricter limit for auth endpoints
1080
+ const authRateLimit = rateLimitMiddleware({
1081
+ max: 5,
1082
+ windowMs: 60000,
1083
+ message: 'Too many login attempts, please try again later',
1084
+ });
1085
+
1086
+ export const authProcedures = defineProcedures('auth', {
1087
+ // Protected with stricter rate limit
1088
+ login: procedure
1089
+ .use(authRateLimit)
1090
+ .input(LoginSchema)
1091
+ .mutation(async ({ input }) => {
1092
+ // Login logic
1093
+ }),
1094
+
1095
+ // Normal rate limit
1096
+ getProfile: procedure
1097
+ .use(rateLimit)
1098
+ .use(auth.requireAuth())
1099
+ .query(({ ctx }) => ctx.user),
1100
+ });
1101
+ ```
1102
+
1103
+ ### Configuration Options
1104
+
1105
+ ```typescript
1106
+ const rateLimit = rateLimitMiddleware({
1107
+ // Maximum requests allowed in window
1108
+ max: 100, // Default: 100
1109
+
1110
+ // Time window in milliseconds
1111
+ windowMs: 60000, // Default: 60000 (1 minute)
1112
+
1113
+ // Custom key generator (default: request IP)
1114
+ keyGenerator: (ctx) => {
1115
+ // Rate limit by user ID if authenticated
1116
+ return ctx.user?.id ?? ctx.request.ip ?? 'anonymous';
1117
+ },
1118
+
1119
+ // Custom error message
1120
+ message: 'Rate limit exceeded',
1121
+ });
1122
+ ```
1123
+
1124
+ ### Response Headers
1125
+
1126
+ Rate limit info is included in response headers:
1127
+
1128
+ ```
1129
+ X-RateLimit-Limit: 100 # Max requests allowed
1130
+ X-RateLimit-Remaining: 95 # Remaining requests in window
1131
+ X-RateLimit-Reset: 1234567890 # Unix timestamp when window resets
1132
+ ```
1133
+
1134
+ ### Production Considerations
1135
+
1136
+ The built-in rate limiter uses in-memory storage, which:
1137
+ - Does **not** persist across server restarts
1138
+ - Does **not** work across multiple server instances
1139
+
1140
+ For production, implement a custom middleware using Redis:
1141
+
1142
+ ```typescript
1143
+ import { Redis } from 'ioredis';
1144
+ import type { MiddlewareFunction } from '@veloxts/router';
1145
+ import { AuthError } from '@veloxts/auth';
1146
+
1147
+ const redis = new Redis();
1148
+
1149
+ function redisRateLimitMiddleware(options: {
1150
+ max: number;
1151
+ windowMs: number;
1152
+ prefix?: string;
1153
+ }): MiddlewareFunction {
1154
+ const { max, windowMs, prefix = 'ratelimit:' } = options;
1155
+
1156
+ return async ({ ctx, next }) => {
1157
+ const key = `${prefix}${ctx.request.ip}`;
1158
+ const current = await redis.incr(key);
1159
+
1160
+ if (current === 1) {
1161
+ await redis.pexpire(key, windowMs);
1162
+ }
1163
+
1164
+ const ttl = await redis.pttl(key);
1165
+ const remaining = Math.max(0, max - current);
1166
+
1167
+ ctx.reply.header('X-RateLimit-Limit', String(max));
1168
+ ctx.reply.header('X-RateLimit-Remaining', String(remaining));
1169
+ ctx.reply.header('X-RateLimit-Reset', String(Math.ceil((Date.now() + ttl) / 1000)));
1170
+
1171
+ if (current > max) {
1172
+ throw new AuthError('Too many requests', 429, 'RATE_LIMIT_EXCEEDED');
1173
+ }
1174
+
1175
+ return next();
1176
+ };
1177
+ }
1178
+ ```
52
1179
 
53
1180
  ## Related Packages
54
1181