@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 +22 -1150
- package/dist/csrf.d.ts +2 -18
- package/dist/csrf.d.ts.map +1 -1
- package/dist/csrf.js +9 -3
- package/dist/csrf.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/password-policy.d.ts +3 -0
- package/dist/password-policy.d.ts.map +1 -1
- package/dist/password-policy.js +41 -26
- package/dist/password-policy.js.map +1 -1
- package/dist/session.d.ts +1 -18
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +13 -6
- package/dist/session.js.map +1 -1
- package/dist/utils/cookie-support.d.ts +61 -0
- package/dist/utils/cookie-support.d.ts.map +1 -0
- package/dist/utils/cookie-support.js +38 -0
- package/dist/utils/cookie-support.js.map +1 -0
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -1,1189 +1,61 @@
|
|
|
1
1
|
# @veloxts/auth
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
5
|
+
## What is this?
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Authentication and authorization package for the VeloxTS Framework, providing JWT, sessions, guards, and auth adapters.
|
|
8
8
|
|
|
9
|
-
|
|
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
|
-
|
|
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/
|
|
14
|
+
npm install @veloxts/velox
|
|
40
15
|
```
|
|
41
16
|
|
|
42
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
#### Installation
|
|
19
|
+
## Standalone Installation
|
|
57
20
|
|
|
58
21
|
```bash
|
|
59
|
-
npm install
|
|
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
|
-
|
|
25
|
+
Required peer dependencies:
|
|
655
26
|
|
|
656
27
|
```bash
|
|
657
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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 {
|
|
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
|
-
|
|
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
|
-
##
|
|
53
|
+
## Learn More
|
|
1182
54
|
|
|
1183
|
-
- [
|
|
1184
|
-
- [
|
|
1185
|
-
- [
|
|
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
|
-
|
|
61
|
+
MIT
|