@veloxts/auth 0.3.3 → 0.3.4
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 +755 -30
- package/dist/adapter.d.ts +710 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +581 -0
- package/dist/adapter.js.map +1 -0
- package/dist/adapters/better-auth.d.ts +271 -0
- package/dist/adapters/better-auth.d.ts.map +1 -0
- package/dist/adapters/better-auth.js +341 -0
- package/dist/adapters/better-auth.js.map +1 -0
- package/dist/adapters/index.d.ts +28 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +28 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/csrf.d.ts +294 -0
- package/dist/csrf.d.ts.map +1 -0
- package/dist/csrf.js +396 -0
- package/dist/csrf.js.map +1 -0
- package/dist/guards.d.ts +139 -0
- package/dist/guards.d.ts.map +1 -0
- package/dist/guards.js +247 -0
- package/dist/guards.js.map +1 -0
- package/dist/hash.d.ts +85 -0
- package/dist/hash.d.ts.map +1 -0
- package/dist/hash.js +220 -0
- package/dist/hash.js.map +1 -0
- package/dist/index.d.ts +25 -32
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +63 -36
- package/dist/index.js.map +1 -1
- package/dist/jwt.d.ts +128 -0
- package/dist/jwt.d.ts.map +1 -0
- package/dist/jwt.js +363 -0
- package/dist/jwt.js.map +1 -0
- package/dist/middleware.d.ts +87 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +241 -0
- package/dist/middleware.js.map +1 -0
- package/dist/plugin.d.ts +107 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +174 -0
- package/dist/plugin.js.map +1 -0
- package/dist/policies.d.ts +137 -0
- package/dist/policies.d.ts.map +1 -0
- package/dist/policies.js +240 -0
- package/dist/policies.js.map +1 -0
- package/dist/session.d.ts +494 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +795 -0
- package/dist/session.js.map +1 -0
- package/dist/types.d.ts +251 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +33 -0
- package/dist/types.js.map +1 -0
- package/package.json +38 -7
package/README.md
CHANGED
|
@@ -4,51 +4,776 @@
|
|
|
4
4
|
|
|
5
5
|
Authentication and authorization system for VeloxTS Framework.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## Features
|
|
8
8
|
|
|
9
|
-
|
|
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
|
-
|
|
17
|
+
## Table of Contents
|
|
12
18
|
|
|
13
|
-
|
|
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
|
+
- [Password Hashing](#password-hashing)
|
|
27
|
+
- [Rate Limiting](#rate-limiting)
|
|
14
28
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install @veloxts/auth
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Required peer dependencies:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm install @veloxts/core @veloxts/router fastify @fastify/cookie
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Auth Adapters
|
|
42
|
+
|
|
43
|
+
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.
|
|
44
|
+
|
|
45
|
+
### BetterAuth Adapter
|
|
46
|
+
|
|
47
|
+
[BetterAuth](https://better-auth.com) is a comprehensive, framework-agnostic TypeScript authentication library. The BetterAuth adapter seamlessly integrates it with VeloxTS.
|
|
48
|
+
|
|
49
|
+
#### Installation
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npm install better-auth @veloxts/auth
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
#### Basic Setup
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import { createVeloxApp } from '@veloxts/core';
|
|
59
|
+
import { createAuthAdapterPlugin, createBetterAuthAdapter } from '@veloxts/auth';
|
|
60
|
+
import { betterAuth } from 'better-auth';
|
|
61
|
+
import { prismaAdapter } from 'better-auth/adapters/prisma';
|
|
62
|
+
import { PrismaClient } from '@prisma/client';
|
|
63
|
+
|
|
64
|
+
const prisma = new PrismaClient();
|
|
65
|
+
|
|
66
|
+
// Create BetterAuth instance
|
|
67
|
+
const auth = betterAuth({
|
|
68
|
+
database: prismaAdapter(prisma, {
|
|
69
|
+
provider: 'postgresql', // or 'mysql', 'sqlite'
|
|
70
|
+
}),
|
|
71
|
+
trustedOrigins: ['http://localhost:3000'],
|
|
72
|
+
emailAndPassword: {
|
|
73
|
+
enabled: true,
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Create the adapter
|
|
78
|
+
const betterAuthAdapter = createBetterAuthAdapter({
|
|
79
|
+
name: 'better-auth',
|
|
80
|
+
auth,
|
|
81
|
+
debug: process.env.NODE_ENV === 'development',
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Create the plugin
|
|
85
|
+
const authPlugin = createAuthAdapterPlugin({
|
|
86
|
+
adapter: betterAuthAdapter,
|
|
87
|
+
config: betterAuthAdapter.config,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Create app and register plugin
|
|
91
|
+
const app = createVeloxApp();
|
|
92
|
+
await app.register(authPlugin);
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
#### Using Authentication in Procedures
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
import { createAdapterAuthMiddleware } from '@veloxts/auth';
|
|
99
|
+
import { defineProcedures, procedure } from '@veloxts/router';
|
|
100
|
+
|
|
101
|
+
const authMiddleware = createAdapterAuthMiddleware();
|
|
102
|
+
|
|
103
|
+
export const userProcedures = defineProcedures('users', {
|
|
104
|
+
// Require authentication - throws 401 if not logged in
|
|
105
|
+
getProfile: procedure
|
|
106
|
+
.use(authMiddleware.requireAuth())
|
|
107
|
+
.query(async ({ ctx }) => {
|
|
108
|
+
// ctx.user is guaranteed to exist
|
|
109
|
+
return {
|
|
110
|
+
id: ctx.user.id,
|
|
111
|
+
email: ctx.user.email,
|
|
112
|
+
name: ctx.user.name,
|
|
113
|
+
};
|
|
114
|
+
}),
|
|
115
|
+
|
|
116
|
+
// Optional authentication - user may or may not be logged in
|
|
117
|
+
getPublicPosts: procedure
|
|
118
|
+
.use(authMiddleware.optionalAuth())
|
|
119
|
+
.query(async ({ ctx }) => {
|
|
120
|
+
const posts = await db.post.findMany({ where: { published: true } });
|
|
121
|
+
return {
|
|
122
|
+
posts,
|
|
123
|
+
isAuthenticated: ctx.isAuthenticated,
|
|
124
|
+
userId: ctx.user?.id,
|
|
125
|
+
};
|
|
126
|
+
}),
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
#### BetterAuth Configuration Options
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
const adapter = createBetterAuthAdapter({
|
|
134
|
+
// Required: Adapter name (for logging)
|
|
135
|
+
name: 'better-auth',
|
|
136
|
+
|
|
137
|
+
// Required: BetterAuth instance
|
|
138
|
+
auth: betterAuth({ ... }),
|
|
139
|
+
|
|
140
|
+
// Optional: Base path for auth routes (default: '/api/auth')
|
|
141
|
+
basePath: '/api/auth',
|
|
142
|
+
|
|
143
|
+
// Optional: Enable debug logging
|
|
144
|
+
debug: true,
|
|
145
|
+
|
|
146
|
+
// Optional: Handle all HTTP methods (default: GET, POST only)
|
|
147
|
+
handleAllMethods: true,
|
|
148
|
+
|
|
149
|
+
// Optional: Routes to exclude from session loading
|
|
150
|
+
excludeRoutes: ['/api/health', '/api/public/*'],
|
|
151
|
+
|
|
152
|
+
// Optional: Custom user transformation
|
|
153
|
+
transformUser: (adapterUser) => ({
|
|
154
|
+
id: adapterUser.id,
|
|
155
|
+
email: adapterUser.email,
|
|
156
|
+
role: adapterUser.providerData?.role as string ?? 'user',
|
|
157
|
+
permissions: adapterUser.providerData?.permissions as string[] ?? [],
|
|
158
|
+
}),
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
#### Auth Routes
|
|
163
|
+
|
|
164
|
+
BetterAuth automatically mounts its routes at the configured base path. Common routes include:
|
|
165
|
+
|
|
166
|
+
- `POST /api/auth/sign-up` - User registration
|
|
167
|
+
- `POST /api/auth/sign-in/email` - Email/password login
|
|
168
|
+
- `POST /api/auth/sign-out` - Logout
|
|
169
|
+
- `GET /api/auth/session` - Get current session
|
|
170
|
+
- `POST /api/auth/magic-link` - Send magic link (if enabled)
|
|
171
|
+
- `GET /api/auth/callback/:provider` - OAuth callbacks
|
|
172
|
+
|
|
173
|
+
See the [BetterAuth documentation](https://better-auth.com/docs) for all available routes and configuration options.
|
|
174
|
+
|
|
175
|
+
### Creating Custom Adapters
|
|
176
|
+
|
|
177
|
+
You can create adapters for other authentication providers by implementing the `AuthAdapter` interface:
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
import {
|
|
181
|
+
AuthAdapter,
|
|
182
|
+
AuthAdapterConfig,
|
|
183
|
+
AdapterSessionResult,
|
|
184
|
+
BaseAuthAdapter,
|
|
185
|
+
defineAuthAdapter,
|
|
186
|
+
} from '@veloxts/auth';
|
|
187
|
+
import type { FastifyInstance, FastifyRequest } from 'fastify';
|
|
188
|
+
|
|
189
|
+
// Define your adapter-specific config
|
|
190
|
+
interface MyAuthConfig extends AuthAdapterConfig {
|
|
191
|
+
apiKey: string;
|
|
192
|
+
domain: string;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Option 1: Use defineAuthAdapter helper
|
|
196
|
+
export const myAuthAdapter = defineAuthAdapter<MyAuthConfig>({
|
|
197
|
+
name: 'my-auth',
|
|
198
|
+
version: '1.0.0',
|
|
21
199
|
|
|
22
|
-
|
|
200
|
+
async initialize(fastify: FastifyInstance, config: MyAuthConfig) {
|
|
201
|
+
// Initialize your auth client
|
|
202
|
+
this.client = new MyAuthClient({
|
|
203
|
+
apiKey: config.apiKey,
|
|
204
|
+
domain: config.domain,
|
|
205
|
+
});
|
|
206
|
+
},
|
|
23
207
|
|
|
24
|
-
|
|
208
|
+
async getSession(request: FastifyRequest): Promise<AdapterSessionResult | null> {
|
|
209
|
+
const token = request.headers.authorization?.replace('Bearer ', '');
|
|
210
|
+
if (!token) return null;
|
|
211
|
+
|
|
212
|
+
const session = await this.client.verifySession(token);
|
|
213
|
+
if (!session) return null;
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
user: {
|
|
217
|
+
id: session.user.id,
|
|
218
|
+
email: session.user.email,
|
|
219
|
+
name: session.user.name,
|
|
220
|
+
},
|
|
221
|
+
session: {
|
|
222
|
+
sessionId: session.id,
|
|
223
|
+
userId: session.user.id,
|
|
224
|
+
expiresAt: session.expiresAt,
|
|
225
|
+
isActive: true,
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
getRoutes() {
|
|
231
|
+
return [
|
|
232
|
+
{
|
|
233
|
+
path: '/api/auth/*',
|
|
234
|
+
methods: ['GET', 'POST'],
|
|
235
|
+
handler: async (request, reply) => {
|
|
236
|
+
// Forward to your auth provider
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
];
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Option 2: Extend BaseAuthAdapter class
|
|
244
|
+
class MyAuthAdapter extends BaseAuthAdapter<MyAuthConfig> {
|
|
245
|
+
private client: MyAuthClient | null = null;
|
|
246
|
+
|
|
247
|
+
constructor() {
|
|
248
|
+
super('my-auth', '1.0.0');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
override async initialize(fastify: FastifyInstance, config: MyAuthConfig) {
|
|
252
|
+
await super.initialize(fastify, config);
|
|
253
|
+
this.client = new MyAuthClient(config);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
override async getSession(request: FastifyRequest): Promise<AdapterSessionResult | null> {
|
|
257
|
+
// Implementation...
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
override getRoutes() {
|
|
261
|
+
return [];
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Adapter Type Utilities
|
|
25
267
|
|
|
26
268
|
```typescript
|
|
27
|
-
import {
|
|
269
|
+
import {
|
|
270
|
+
isAuthAdapter,
|
|
271
|
+
InferAdapterConfig,
|
|
272
|
+
AuthAdapterError,
|
|
273
|
+
} from '@veloxts/auth';
|
|
274
|
+
|
|
275
|
+
// Type guard to check if value is a valid adapter
|
|
276
|
+
if (isAuthAdapter(maybeAdapter)) {
|
|
277
|
+
const plugin = createAuthAdapterPlugin({
|
|
278
|
+
adapter: maybeAdapter,
|
|
279
|
+
config: maybeAdapter.config,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
28
282
|
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
283
|
+
// Infer config type from adapter
|
|
284
|
+
type BetterAuthConfig = InferAdapterConfig<typeof betterAuthAdapter>;
|
|
285
|
+
|
|
286
|
+
// Handle adapter errors
|
|
287
|
+
try {
|
|
288
|
+
const session = await adapter.getSession(request);
|
|
289
|
+
} catch (error) {
|
|
290
|
+
if (error instanceof AuthAdapterError) {
|
|
291
|
+
console.error(`Adapter error: ${error.code} - ${error.message}`);
|
|
292
|
+
console.error(`Cause:`, error.cause);
|
|
293
|
+
}
|
|
34
294
|
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
## Authentication Strategies
|
|
298
|
+
|
|
299
|
+
VeloxTS Auth provides two primary authentication strategies. Choose the one that fits your architecture:
|
|
300
|
+
|
|
301
|
+
### Session-Based Authentication
|
|
302
|
+
|
|
303
|
+
**Use when:**
|
|
304
|
+
- Building traditional server-rendered applications
|
|
305
|
+
- You need server-side state and fine-grained session control
|
|
306
|
+
- Single-server or shared session store architecture (Redis, database)
|
|
307
|
+
- You want Laravel-style flash data and session management
|
|
308
|
+
|
|
309
|
+
**Advantages:**
|
|
310
|
+
- Server controls session lifecycle (can revoke sessions immediately)
|
|
311
|
+
- No token storage needed on client
|
|
312
|
+
- Easy "logout all devices" functionality
|
|
313
|
+
- Built-in flash data support
|
|
314
|
+
|
|
315
|
+
**Trade-offs:**
|
|
316
|
+
- Requires session store (Redis, database, etc. for production)
|
|
317
|
+
- Slightly more server load (session lookups)
|
|
318
|
+
- Requires sticky sessions or shared storage for horizontal scaling
|
|
319
|
+
|
|
320
|
+
### JWT-Based Authentication
|
|
321
|
+
|
|
322
|
+
**Use when:**
|
|
323
|
+
- Building stateless APIs for mobile apps or SPAs
|
|
324
|
+
- Microservices architecture with distributed authentication
|
|
325
|
+
- You need cross-domain authentication
|
|
326
|
+
- Horizontal scaling without shared state
|
|
327
|
+
|
|
328
|
+
**Advantages:**
|
|
329
|
+
- Stateless (no server-side storage required)
|
|
330
|
+
- Works seamlessly across multiple servers
|
|
331
|
+
- Can include custom claims and metadata
|
|
332
|
+
|
|
333
|
+
**Trade-offs:**
|
|
334
|
+
- Cannot revoke tokens before expiration (without additional infrastructure)
|
|
335
|
+
- Requires secure client-side token storage
|
|
336
|
+
- Larger payload size (tokens in every request)
|
|
337
|
+
|
|
338
|
+
## Session Management
|
|
339
|
+
|
|
340
|
+
Cookie-based session management with secure defaults and pluggable storage backends.
|
|
341
|
+
|
|
342
|
+
### Quick Start
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
import { createSessionMiddleware, createInMemorySessionStore } from '@veloxts/auth';
|
|
346
|
+
import { defineProcedures, procedure } from '@veloxts/router';
|
|
347
|
+
|
|
348
|
+
// Create session middleware
|
|
349
|
+
const session = createSessionMiddleware({
|
|
350
|
+
secret: process.env.SESSION_SECRET!, // Min 32 characters
|
|
351
|
+
cookie: {
|
|
352
|
+
secure: process.env.NODE_ENV === 'production',
|
|
353
|
+
sameSite: 'lax',
|
|
354
|
+
},
|
|
355
|
+
expiration: {
|
|
356
|
+
ttl: 86400, // 24 hours
|
|
357
|
+
sliding: true, // Refresh on each request
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// Use in procedures
|
|
362
|
+
export const userProcedures = defineProcedures('users', {
|
|
363
|
+
// Get shopping cart from session
|
|
364
|
+
getCart: procedure
|
|
365
|
+
.use(session.middleware())
|
|
366
|
+
.query(async ({ ctx }) => {
|
|
367
|
+
return ctx.session.get('cart') ?? [];
|
|
368
|
+
}),
|
|
369
|
+
|
|
370
|
+
// Add item to cart
|
|
371
|
+
addToCart: procedure
|
|
372
|
+
.use(session.middleware())
|
|
373
|
+
.input(AddToCartSchema)
|
|
374
|
+
.mutation(async ({ input, ctx }) => {
|
|
375
|
+
const cart = ctx.session.get('cart') ?? [];
|
|
376
|
+
cart.push(input.item);
|
|
377
|
+
ctx.session.set('cart', cart);
|
|
378
|
+
return { success: true };
|
|
379
|
+
}),
|
|
380
|
+
});
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
### Configuration Options
|
|
384
|
+
|
|
385
|
+
```typescript
|
|
386
|
+
import { createSessionManager, createInMemorySessionStore } from '@veloxts/auth';
|
|
387
|
+
|
|
388
|
+
const sessionManager = createSessionManager({
|
|
389
|
+
// Required: Cryptographically secure secret (min 32 chars)
|
|
390
|
+
// Generate with: openssl rand -base64 32
|
|
391
|
+
secret: process.env.SESSION_SECRET!,
|
|
392
|
+
|
|
393
|
+
// Optional: Storage backend (default: InMemorySessionStore)
|
|
394
|
+
store: createInMemorySessionStore(),
|
|
395
|
+
|
|
396
|
+
// Optional: Cookie configuration
|
|
397
|
+
cookie: {
|
|
398
|
+
name: 'myapp.session', // Cookie name (default: 'velox.session')
|
|
399
|
+
path: '/', // Cookie path (default: '/')
|
|
400
|
+
domain: 'example.com', // Cookie domain (optional)
|
|
401
|
+
secure: true, // HTTPS only (default: NODE_ENV === 'production')
|
|
402
|
+
httpOnly: true, // Prevent JS access (default: true)
|
|
403
|
+
sameSite: 'strict', // CSRF protection (default: 'lax')
|
|
404
|
+
},
|
|
405
|
+
|
|
406
|
+
// Optional: Expiration configuration
|
|
407
|
+
expiration: {
|
|
408
|
+
ttl: 3600, // Session TTL in seconds (default: 86400)
|
|
409
|
+
sliding: true, // Refresh TTL on each request (default: true)
|
|
410
|
+
absoluteTimeout: 604800, // Max session lifetime (7 days), forces re-auth
|
|
411
|
+
},
|
|
412
|
+
|
|
413
|
+
// Optional: User loader function
|
|
414
|
+
userLoader: async (userId) => {
|
|
415
|
+
return db.user.findUnique({ where: { id: userId } });
|
|
416
|
+
},
|
|
417
|
+
});
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
### Middleware Variants
|
|
421
|
+
|
|
422
|
+
```typescript
|
|
423
|
+
const session = createSessionMiddleware(config);
|
|
424
|
+
|
|
425
|
+
// Basic session middleware - creates session for all requests
|
|
426
|
+
const getPreferences = procedure
|
|
427
|
+
.use(session.middleware())
|
|
428
|
+
.query(async ({ ctx }) => {
|
|
429
|
+
return ctx.session.get('theme') ?? 'light';
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// Require authentication - throws 401 if no userId in session
|
|
433
|
+
const getProfile = procedure
|
|
434
|
+
.use(session.requireAuth())
|
|
435
|
+
.query(async ({ ctx }) => {
|
|
436
|
+
// ctx.user is guaranteed to exist
|
|
437
|
+
return ctx.user;
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
// Optional authentication - user may or may not be logged in
|
|
441
|
+
const getHomePage = procedure
|
|
442
|
+
.use(session.optionalAuth())
|
|
443
|
+
.query(async ({ ctx }) => {
|
|
444
|
+
if (ctx.isAuthenticated) {
|
|
445
|
+
return { greeting: `Welcome back, ${ctx.user.email}!` };
|
|
446
|
+
}
|
|
447
|
+
return { greeting: 'Welcome, guest!' };
|
|
448
|
+
});
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
### Login and Logout
|
|
452
|
+
|
|
453
|
+
```typescript
|
|
454
|
+
import { loginSession, logoutSession } from '@veloxts/auth';
|
|
455
|
+
import { hashPassword, verifyPassword } from '@veloxts/auth';
|
|
456
|
+
|
|
457
|
+
export const authProcedures = defineProcedures('auth', {
|
|
458
|
+
// Login procedure
|
|
459
|
+
login: procedure
|
|
460
|
+
.use(session.middleware())
|
|
461
|
+
.input(z.object({ email: z.string().email(), password: z.string() }))
|
|
462
|
+
.mutation(async ({ input, ctx }) => {
|
|
463
|
+
// Find user
|
|
464
|
+
const user = await db.user.findUnique({ where: { email: input.email } });
|
|
465
|
+
if (!user) {
|
|
466
|
+
throw new AuthError('Invalid credentials', 401);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Verify password
|
|
470
|
+
const valid = await verifyPassword(input.password, user.passwordHash);
|
|
471
|
+
if (!valid) {
|
|
472
|
+
throw new AuthError('Invalid credentials', 401);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Login - regenerates session ID to prevent fixation attacks
|
|
476
|
+
await loginSession(ctx.session, user);
|
|
477
|
+
|
|
478
|
+
return { success: true, user };
|
|
479
|
+
}),
|
|
480
|
+
|
|
481
|
+
// Logout procedure
|
|
482
|
+
logout: procedure
|
|
483
|
+
.use(session.requireAuth())
|
|
484
|
+
.mutation(async ({ ctx }) => {
|
|
485
|
+
await logoutSession(ctx.session);
|
|
486
|
+
return { success: true };
|
|
487
|
+
}),
|
|
488
|
+
|
|
489
|
+
// Logout from all devices
|
|
490
|
+
logoutAll: procedure
|
|
491
|
+
.use(session.requireAuth())
|
|
492
|
+
.mutation(async ({ ctx }) => {
|
|
493
|
+
await session.manager.destroyUserSessions(ctx.user.id);
|
|
494
|
+
return { success: true };
|
|
495
|
+
}),
|
|
496
|
+
});
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
### Flash Data
|
|
500
|
+
|
|
501
|
+
Flash data persists for exactly one request - perfect for success messages after redirects.
|
|
502
|
+
|
|
503
|
+
```typescript
|
|
504
|
+
// Set flash data
|
|
505
|
+
const createPost = procedure
|
|
506
|
+
.use(session.middleware())
|
|
507
|
+
.input(CreatePostSchema)
|
|
508
|
+
.mutation(async ({ input, ctx }) => {
|
|
509
|
+
const post = await db.post.create({ data: input });
|
|
510
|
+
|
|
511
|
+
// Flash message for next request
|
|
512
|
+
ctx.session.flash('success', 'Post created successfully!');
|
|
513
|
+
|
|
514
|
+
return post;
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
// Read flash data (automatically cleared after this request)
|
|
518
|
+
const getFlashMessages = procedure
|
|
519
|
+
.use(session.middleware())
|
|
520
|
+
.query(async ({ ctx }) => {
|
|
521
|
+
const messages = ctx.session.getAllFlash();
|
|
522
|
+
return messages;
|
|
523
|
+
});
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
### Session Handle API
|
|
527
|
+
|
|
528
|
+
```typescript
|
|
529
|
+
// Get value
|
|
530
|
+
const theme = session.get('theme');
|
|
531
|
+
|
|
532
|
+
// Set value (marks session as modified)
|
|
533
|
+
session.set('theme', 'dark');
|
|
534
|
+
|
|
535
|
+
// Delete value
|
|
536
|
+
session.delete('theme');
|
|
537
|
+
|
|
538
|
+
// Check if key exists
|
|
539
|
+
if (session.has('cart')) {
|
|
540
|
+
// ...
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Flash data
|
|
544
|
+
session.flash('message', 'Success!');
|
|
545
|
+
const message = session.getFlash('message');
|
|
546
|
+
|
|
547
|
+
// Regenerate session ID (security: call after privilege changes)
|
|
548
|
+
await session.regenerate();
|
|
549
|
+
|
|
550
|
+
// Destroy session completely
|
|
551
|
+
await session.destroy();
|
|
552
|
+
|
|
553
|
+
// Save session manually (auto-saved by middleware)
|
|
554
|
+
await session.save();
|
|
555
|
+
|
|
556
|
+
// Reload session from store
|
|
557
|
+
await session.reload();
|
|
558
|
+
|
|
559
|
+
// Session metadata
|
|
560
|
+
session.id; // Session ID
|
|
561
|
+
session.isNew; // True for new sessions
|
|
562
|
+
session.isModified; // True if data changed
|
|
563
|
+
session.isDestroyed; // True after destroy()
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
### Custom Session Storage
|
|
567
|
+
|
|
568
|
+
For production, implement a custom store backed by Redis, PostgreSQL, or other persistent storage.
|
|
569
|
+
|
|
570
|
+
```typescript
|
|
571
|
+
import { SessionStore, StoredSession } from '@veloxts/auth';
|
|
572
|
+
import { Redis } from 'ioredis';
|
|
573
|
+
|
|
574
|
+
class RedisSessionStore implements SessionStore {
|
|
575
|
+
constructor(private redis: Redis) {}
|
|
576
|
+
|
|
577
|
+
async get(sessionId: string): Promise<StoredSession | null> {
|
|
578
|
+
const data = await this.redis.get(`session:${sessionId}`);
|
|
579
|
+
return data ? JSON.parse(data) : null;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
async set(sessionId: string, session: StoredSession): Promise<void> {
|
|
583
|
+
const ttl = Math.ceil((session.expiresAt - Date.now()) / 1000);
|
|
584
|
+
await this.redis.setex(
|
|
585
|
+
`session:${sessionId}`,
|
|
586
|
+
ttl,
|
|
587
|
+
JSON.stringify(session)
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
async delete(sessionId: string): Promise<void> {
|
|
592
|
+
await this.redis.del(`session:${sessionId}`);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
async touch(sessionId: string, expiresAt: number): Promise<void> {
|
|
596
|
+
const session = await this.get(sessionId);
|
|
597
|
+
if (session) {
|
|
598
|
+
session.expiresAt = expiresAt;
|
|
599
|
+
session.data._lastAccessedAt = Date.now();
|
|
600
|
+
await this.set(sessionId, session);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
async clear(): Promise<void> {
|
|
605
|
+
const keys = await this.redis.keys('session:*');
|
|
606
|
+
if (keys.length > 0) {
|
|
607
|
+
await this.redis.del(...keys);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
async getSessionsByUser(userId: string): Promise<string[]> {
|
|
612
|
+
return this.redis.smembers(`user:${userId}:sessions`);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
async deleteSessionsByUser(userId: string): Promise<void> {
|
|
616
|
+
const sessions = await this.getSessionsByUser(userId);
|
|
617
|
+
if (sessions.length > 0) {
|
|
618
|
+
await this.redis.del(...sessions.map(id => `session:${id}`));
|
|
619
|
+
await this.redis.del(`user:${userId}:sessions`);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Use custom store
|
|
625
|
+
const redisStore = new RedisSessionStore(redisClient);
|
|
626
|
+
const session = createSessionMiddleware({
|
|
627
|
+
secret: process.env.SESSION_SECRET!,
|
|
628
|
+
store: redisStore,
|
|
629
|
+
});
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
### Security Best Practices
|
|
633
|
+
|
|
634
|
+
**1. Always regenerate session ID after login**
|
|
635
|
+
|
|
636
|
+
```typescript
|
|
637
|
+
// loginSession() does this automatically
|
|
638
|
+
await loginSession(ctx.session, user);
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
**2. Destroy sessions on logout**
|
|
642
|
+
|
|
643
|
+
```typescript
|
|
644
|
+
await logoutSession(ctx.session);
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
**3. Use strong, random secrets**
|
|
648
|
+
|
|
649
|
+
```bash
|
|
650
|
+
# Generate a secure secret
|
|
651
|
+
openssl rand -base64 32
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
**4. Enable secure cookies in production**
|
|
655
|
+
|
|
656
|
+
```typescript
|
|
657
|
+
cookie: {
|
|
658
|
+
secure: process.env.NODE_ENV === 'production',
|
|
659
|
+
httpOnly: true,
|
|
660
|
+
sameSite: 'strict', // or 'lax'
|
|
661
|
+
}
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
**5. Set absolute timeout for sensitive operations**
|
|
665
|
+
|
|
666
|
+
```typescript
|
|
667
|
+
expiration: {
|
|
668
|
+
absoluteTimeout: 3600, // Force re-auth after 1 hour
|
|
669
|
+
}
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
**6. Use environment variables for secrets**
|
|
673
|
+
|
|
674
|
+
```typescript
|
|
675
|
+
// NEVER hardcode secrets
|
|
676
|
+
secret: process.env.SESSION_SECRET!,
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
**7. Implement CSRF protection for state-changing operations**
|
|
680
|
+
|
|
681
|
+
See [CSRF Protection](#csrf-protection) section below.
|
|
682
|
+
|
|
683
|
+
**Built-in Security Features:**
|
|
684
|
+
|
|
685
|
+
The session implementation includes several security protections by default:
|
|
686
|
+
|
|
687
|
+
- **HMAC-SHA256 signing** - All session IDs are cryptographically signed to prevent tampering
|
|
688
|
+
- **Timing-safe comparison** - Session ID verification uses constant-time comparison to prevent timing attacks
|
|
689
|
+
- **Entropy validation** - Session IDs are validated for sufficient randomness (32 bytes, 256 bits)
|
|
690
|
+
- **Session fixation protection** - `loginSession()` automatically regenerates session IDs
|
|
691
|
+
- **SameSite enforcement** - `SameSite=none` requires `Secure` flag per RFC 6265bis
|
|
692
|
+
|
|
693
|
+
### When to Use Sessions vs JWT
|
|
694
|
+
|
|
695
|
+
**Choose Sessions when:**
|
|
696
|
+
- You need immediate session revocation (logout all devices)
|
|
697
|
+
- Building server-rendered applications with traditional workflows
|
|
698
|
+
- Flash data and server-side state are important to your application
|
|
699
|
+
- You have infrastructure for shared session storage (Redis, etc.)
|
|
700
|
+
|
|
701
|
+
**Choose JWT when:**
|
|
702
|
+
- Building stateless APIs for mobile apps or microservices
|
|
703
|
+
- You need cross-domain authentication
|
|
704
|
+
- Horizontal scaling without shared state is critical
|
|
705
|
+
- You prefer client-side session storage
|
|
706
|
+
|
|
707
|
+
## JWT Authentication
|
|
708
|
+
|
|
709
|
+
Coming soon in v1.1.0. For now, use session-based authentication.
|
|
710
|
+
|
|
711
|
+
## CSRF Protection
|
|
712
|
+
|
|
713
|
+
CSRF protection is already implemented using the signed double-submit cookie pattern with timing-safe comparison and entropy validation.
|
|
714
|
+
|
|
715
|
+
```typescript
|
|
716
|
+
import { createCsrfMiddleware } from '@veloxts/auth';
|
|
717
|
+
|
|
718
|
+
const csrf = createCsrfMiddleware({
|
|
719
|
+
secret: process.env.CSRF_SECRET!,
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
// Use in procedures that modify state
|
|
723
|
+
const deletePost = procedure
|
|
724
|
+
.use(csrf.middleware())
|
|
725
|
+
.mutation(async ({ ctx }) => {
|
|
726
|
+
// CSRF token validated automatically
|
|
727
|
+
});
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
See the CSRF documentation for complete details on configuration and usage.
|
|
731
|
+
|
|
732
|
+
## Guards and Policies
|
|
733
|
+
|
|
734
|
+
Guards and policies provide declarative authorization for procedures.
|
|
735
|
+
|
|
736
|
+
```typescript
|
|
737
|
+
import { authenticated, hasRole, definePolicy } from '@veloxts/auth';
|
|
738
|
+
|
|
739
|
+
// Use built-in guards
|
|
740
|
+
const adminOnly = procedure
|
|
741
|
+
.use(session.requireAuth())
|
|
742
|
+
.use(guard(hasRole('admin')))
|
|
743
|
+
.query(async ({ ctx }) => {
|
|
744
|
+
// Only admins can access
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
// Define custom policies
|
|
748
|
+
const postPolicy = definePolicy<{ postId: string }>('post', {
|
|
749
|
+
view: async (user, { postId }) => {
|
|
750
|
+
// Anyone can view public posts
|
|
751
|
+
return true;
|
|
752
|
+
},
|
|
753
|
+
edit: async (user, { postId }) => {
|
|
754
|
+
const post = await db.post.findUnique({ where: { id: postId } });
|
|
755
|
+
return post?.authorId === user.id;
|
|
756
|
+
},
|
|
757
|
+
});
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
## Password Hashing
|
|
761
|
+
|
|
762
|
+
Secure password hashing with bcrypt:
|
|
763
|
+
|
|
764
|
+
```typescript
|
|
765
|
+
import { hashPassword, verifyPassword } from '@veloxts/auth';
|
|
35
766
|
|
|
36
|
-
//
|
|
37
|
-
const
|
|
767
|
+
// Hash password
|
|
768
|
+
const hash = await hashPassword('user-password', { cost: 12 });
|
|
38
769
|
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
class AdminProcedures { }
|
|
770
|
+
// Verify password
|
|
771
|
+
const valid = await verifyPassword('user-password', hash);
|
|
42
772
|
```
|
|
43
773
|
|
|
44
|
-
##
|
|
774
|
+
## Rate Limiting
|
|
45
775
|
|
|
46
|
-
|
|
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 |
|
|
776
|
+
Coming soon.
|
|
52
777
|
|
|
53
778
|
## Related Packages
|
|
54
779
|
|