@veloxts/auth 0.4.0 → 0.4.2

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