@veloxts/auth 0.3.4 → 0.3.6
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 +425 -22
- package/dist/__integration__/fixtures.d.ts +41 -0
- package/dist/__integration__/fixtures.d.ts.map +1 -0
- package/dist/__integration__/fixtures.js +79 -0
- package/dist/__integration__/fixtures.js.map +1 -0
- package/dist/__integration__/setup.d.ts +26 -0
- package/dist/__integration__/setup.d.ts.map +1 -0
- package/dist/__integration__/setup.js +28 -0
- package/dist/__integration__/setup.js.map +1 -0
- package/dist/csrf.d.ts +9 -3
- package/dist/csrf.d.ts.map +1 -1
- package/dist/csrf.js +9 -3
- package/dist/csrf.js.map +1 -1
- package/dist/guards.d.ts +12 -9
- package/dist/guards.d.ts.map +1 -1
- package/dist/guards.js +17 -5
- package/dist/guards.js.map +1 -1
- package/dist/hash.d.ts +7 -1
- package/dist/hash.d.ts.map +1 -1
- package/dist/hash.js +20 -4
- package/dist/hash.js.map +1 -1
- package/dist/index.d.ts +10 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +43 -7
- package/dist/index.js.map +1 -1
- package/dist/jwt.d.ts +34 -5
- package/dist/jwt.d.ts.map +1 -1
- package/dist/jwt.js +154 -28
- package/dist/jwt.js.map +1 -1
- package/dist/middleware.d.ts +18 -6
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +23 -11
- package/dist/middleware.js.map +1 -1
- package/dist/password-policy.d.ts +259 -0
- package/dist/password-policy.d.ts.map +1 -0
- package/dist/password-policy.js +529 -0
- package/dist/password-policy.js.map +1 -0
- package/dist/plugin.d.ts +25 -7
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +28 -9
- package/dist/plugin.js.map +1 -1
- package/dist/rate-limit.d.ts +231 -0
- package/dist/rate-limit.d.ts.map +1 -0
- package/dist/rate-limit.js +352 -0
- package/dist/rate-limit.js.map +1 -0
- package/dist/session.d.ts +9 -3
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +9 -3
- package/dist/session.js.map +1 -1
- package/dist/types.d.ts +11 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +30 -7
package/README.md
CHANGED
|
@@ -9,10 +9,11 @@ Authentication and authorization system for VeloxTS Framework.
|
|
|
9
9
|
- **Pluggable Auth Adapters** - Integrate external providers like BetterAuth, Clerk, Auth0
|
|
10
10
|
- **Session Management** - Cookie-based sessions with pluggable storage backends
|
|
11
11
|
- **JWT Authentication** - Stateless token-based authentication with refresh tokens
|
|
12
|
-
- **Password Hashing** - Secure bcrypt hashing with configurable cost factors
|
|
13
|
-
- **
|
|
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
|
|
14
15
|
- **Guards and Policies** - Declarative authorization for procedures
|
|
15
|
-
- **Rate Limiting** -
|
|
16
|
+
- **Rate Limiting** - Auth-specific rate limiting with progressive backoff and lockout detection
|
|
16
17
|
|
|
17
18
|
## Table of Contents
|
|
18
19
|
|
|
@@ -23,6 +24,12 @@ Authentication and authorization system for VeloxTS Framework.
|
|
|
23
24
|
- [JWT Authentication](#jwt-authentication)
|
|
24
25
|
- [CSRF Protection](#csrf-protection)
|
|
25
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)
|
|
26
33
|
- [Password Hashing](#password-hashing)
|
|
27
34
|
- [Rate Limiting](#rate-limiting)
|
|
28
35
|
|
|
@@ -55,7 +62,7 @@ npm install better-auth @veloxts/auth
|
|
|
55
62
|
#### Basic Setup
|
|
56
63
|
|
|
57
64
|
```typescript
|
|
58
|
-
import {
|
|
65
|
+
import { veloxApp } from '@veloxts/core';
|
|
59
66
|
import { createAuthAdapterPlugin, createBetterAuthAdapter } from '@veloxts/auth';
|
|
60
67
|
import { betterAuth } from 'better-auth';
|
|
61
68
|
import { prismaAdapter } from 'better-auth/adapters/prisma';
|
|
@@ -82,14 +89,14 @@ const betterAuthAdapter = createBetterAuthAdapter({
|
|
|
82
89
|
});
|
|
83
90
|
|
|
84
91
|
// Create the plugin
|
|
85
|
-
const
|
|
92
|
+
const authAdapterPlugin = createAuthAdapterPlugin({
|
|
86
93
|
adapter: betterAuthAdapter,
|
|
87
94
|
config: betterAuthAdapter.config,
|
|
88
95
|
});
|
|
89
96
|
|
|
90
97
|
// Create app and register plugin
|
|
91
|
-
const app =
|
|
92
|
-
await app.register(
|
|
98
|
+
const app = await veloxApp();
|
|
99
|
+
await app.register(authAdapterPlugin);
|
|
93
100
|
```
|
|
94
101
|
|
|
95
102
|
#### Using Authentication in Procedures
|
|
@@ -342,11 +349,11 @@ Cookie-based session management with secure defaults and pluggable storage backe
|
|
|
342
349
|
### Quick Start
|
|
343
350
|
|
|
344
351
|
```typescript
|
|
345
|
-
import {
|
|
352
|
+
import { sessionMiddleware, createInMemorySessionStore } from '@veloxts/auth';
|
|
346
353
|
import { defineProcedures, procedure } from '@veloxts/router';
|
|
347
354
|
|
|
348
355
|
// Create session middleware
|
|
349
|
-
const session =
|
|
356
|
+
const session = sessionMiddleware({
|
|
350
357
|
secret: process.env.SESSION_SECRET!, // Min 32 characters
|
|
351
358
|
cookie: {
|
|
352
359
|
secure: process.env.NODE_ENV === 'production',
|
|
@@ -420,7 +427,7 @@ const sessionManager = createSessionManager({
|
|
|
420
427
|
### Middleware Variants
|
|
421
428
|
|
|
422
429
|
```typescript
|
|
423
|
-
const session =
|
|
430
|
+
const session = sessionMiddleware(config);
|
|
424
431
|
|
|
425
432
|
// Basic session middleware - creates session for all requests
|
|
426
433
|
const getPreferences = procedure
|
|
@@ -623,7 +630,7 @@ class RedisSessionStore implements SessionStore {
|
|
|
623
630
|
|
|
624
631
|
// Use custom store
|
|
625
632
|
const redisStore = new RedisSessionStore(redisClient);
|
|
626
|
-
const session =
|
|
633
|
+
const session = sessionMiddleware({
|
|
627
634
|
secret: process.env.SESSION_SECRET!,
|
|
628
635
|
store: redisStore,
|
|
629
636
|
});
|
|
@@ -706,16 +713,182 @@ The session implementation includes several security protections by default:
|
|
|
706
713
|
|
|
707
714
|
## JWT Authentication
|
|
708
715
|
|
|
709
|
-
|
|
716
|
+
Stateless token-based authentication using HMAC-SHA256 signed JWTs.
|
|
717
|
+
|
|
718
|
+
### Quick Start
|
|
719
|
+
|
|
720
|
+
```typescript
|
|
721
|
+
import { jwtManager, authMiddleware } from '@veloxts/auth';
|
|
722
|
+
import { defineProcedures, procedure } from '@veloxts/router';
|
|
723
|
+
|
|
724
|
+
// Create JWT manager
|
|
725
|
+
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
|
+
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,
|
|
832
|
+
});
|
|
833
|
+
|
|
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);
|
|
851
|
+
|
|
852
|
+
// Require authentication (throws 401 if no valid token)
|
|
853
|
+
const getProfile = procedure
|
|
854
|
+
.use(auth.requireAuth())
|
|
855
|
+
.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
|
|
710
883
|
|
|
711
884
|
## CSRF Protection
|
|
712
885
|
|
|
713
886
|
CSRF protection is already implemented using the signed double-submit cookie pattern with timing-safe comparison and entropy validation.
|
|
714
887
|
|
|
715
888
|
```typescript
|
|
716
|
-
import {
|
|
889
|
+
import { csrfMiddleware } from '@veloxts/auth';
|
|
717
890
|
|
|
718
|
-
const csrf =
|
|
891
|
+
const csrf = csrfMiddleware({
|
|
719
892
|
secret: process.env.CSRF_SECRET!,
|
|
720
893
|
});
|
|
721
894
|
|
|
@@ -733,18 +906,129 @@ See the CSRF documentation for complete details on configuration and usage.
|
|
|
733
906
|
|
|
734
907
|
Guards and policies provide declarative authorization for procedures.
|
|
735
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
|
+
|
|
736
929
|
```typescript
|
|
737
|
-
import {
|
|
930
|
+
import { hasRole, hasPermission, allOf, anyOf } from '@veloxts/auth';
|
|
738
931
|
|
|
739
|
-
//
|
|
932
|
+
// Require a single role
|
|
740
933
|
const adminOnly = procedure
|
|
741
|
-
.use(
|
|
742
|
-
.use(guard(hasRole('admin')))
|
|
934
|
+
.use(auth.middleware({ guards: [hasRole('admin')] }))
|
|
743
935
|
.query(async ({ ctx }) => {
|
|
744
|
-
// Only
|
|
936
|
+
// Only users with 'admin' role can access
|
|
745
937
|
});
|
|
746
938
|
|
|
747
|
-
//
|
|
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
|
+
|
|
748
1032
|
const postPolicy = definePolicy<{ postId: string }>('post', {
|
|
749
1033
|
view: async (user, { postId }) => {
|
|
750
1034
|
// Anyone can view public posts
|
|
@@ -752,7 +1036,13 @@ const postPolicy = definePolicy<{ postId: string }>('post', {
|
|
|
752
1036
|
},
|
|
753
1037
|
edit: async (user, { postId }) => {
|
|
754
1038
|
const post = await db.post.findUnique({ where: { id: postId } });
|
|
755
|
-
|
|
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;
|
|
756
1046
|
},
|
|
757
1047
|
});
|
|
758
1048
|
```
|
|
@@ -773,7 +1063,120 @@ const valid = await verifyPassword('user-password', hash);
|
|
|
773
1063
|
|
|
774
1064
|
## Rate Limiting
|
|
775
1065
|
|
|
776
|
-
|
|
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
|
+
```
|
|
777
1180
|
|
|
778
1181
|
## Related Packages
|
|
779
1182
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration test fixtures and helpers
|
|
3
|
+
* @module __integration__/fixtures
|
|
4
|
+
*/
|
|
5
|
+
import { TEST_SECRETS } from '@veloxts/testing';
|
|
6
|
+
import type { User } from '../types.js';
|
|
7
|
+
export { TEST_SECRETS };
|
|
8
|
+
export declare const TEST_USERS: Record<string, User>;
|
|
9
|
+
/**
|
|
10
|
+
* Mock user loader for tests
|
|
11
|
+
*/
|
|
12
|
+
export declare function testUserLoader(userId: string): Promise<User | null>;
|
|
13
|
+
/**
|
|
14
|
+
* Auth config with user loader (for tests that use TEST_USERS)
|
|
15
|
+
*/
|
|
16
|
+
export declare function createTestAuthConfig(): {
|
|
17
|
+
jwt: {
|
|
18
|
+
secret: "test-access-secret-key-for-integration-tests-must-be-64-characters-long-at-minimum-for-hmac";
|
|
19
|
+
refreshSecret: "test-refresh-secret-key-for-integration-tests-must-be-64-characters-long-at-minimum-for-hmac";
|
|
20
|
+
accessTokenExpiry: string;
|
|
21
|
+
refreshTokenExpiry: string;
|
|
22
|
+
issuer: string;
|
|
23
|
+
audience: string;
|
|
24
|
+
};
|
|
25
|
+
userLoader: typeof testUserLoader;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Auth config without user loader (for tests with ad-hoc users)
|
|
29
|
+
* User info comes directly from token claims
|
|
30
|
+
*/
|
|
31
|
+
export declare function createTestAuthConfigNoLoader(): {
|
|
32
|
+
jwt: {
|
|
33
|
+
secret: "test-access-secret-key-for-integration-tests-must-be-64-characters-long-at-minimum-for-hmac";
|
|
34
|
+
refreshSecret: "test-refresh-secret-key-for-integration-tests-must-be-64-characters-long-at-minimum-for-hmac";
|
|
35
|
+
accessTokenExpiry: string;
|
|
36
|
+
refreshTokenExpiry: string;
|
|
37
|
+
issuer: string;
|
|
38
|
+
audience: string;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
//# sourceMappingURL=fixtures.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fixtures.d.ts","sourceRoot":"","sources":["../../src/__integration__/fixtures.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAEhD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAGxC,OAAO,EAAE,YAAY,EAAE,CAAC;AAMxB,eAAO,MAAM,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAqB3C,CAAC;AAMF;;GAEG;AACH,wBAAsB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,CAGzE;AAMD;;GAEG;AACH,wBAAgB,oBAAoB;;;;;;;;;;EAYnC;AAED;;;GAGG;AACH,wBAAgB,4BAA4B;;;;;;;;;EAY3C"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration test fixtures and helpers
|
|
3
|
+
* @module __integration__/fixtures
|
|
4
|
+
*/
|
|
5
|
+
import { TEST_SECRETS } from '@veloxts/testing';
|
|
6
|
+
// Re-export shared test secrets
|
|
7
|
+
export { TEST_SECRETS };
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// Test Users
|
|
10
|
+
// ============================================================================
|
|
11
|
+
export const TEST_USERS = {
|
|
12
|
+
admin: {
|
|
13
|
+
id: 'user-admin-123',
|
|
14
|
+
email: 'admin@example.com',
|
|
15
|
+
roles: ['admin'],
|
|
16
|
+
},
|
|
17
|
+
user: {
|
|
18
|
+
id: 'user-regular-456',
|
|
19
|
+
email: 'user@example.com',
|
|
20
|
+
roles: ['user'],
|
|
21
|
+
},
|
|
22
|
+
guest: {
|
|
23
|
+
id: 'user-guest-789',
|
|
24
|
+
email: 'guest@example.com',
|
|
25
|
+
roles: [], // Empty roles - tests guard failure
|
|
26
|
+
},
|
|
27
|
+
multiRole: {
|
|
28
|
+
id: 'user-multi-101',
|
|
29
|
+
email: 'multi@example.com',
|
|
30
|
+
roles: ['user', 'editor', 'moderator'],
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
// ============================================================================
|
|
34
|
+
// User Loader
|
|
35
|
+
// ============================================================================
|
|
36
|
+
/**
|
|
37
|
+
* Mock user loader for tests
|
|
38
|
+
*/
|
|
39
|
+
export async function testUserLoader(userId) {
|
|
40
|
+
const user = Object.values(TEST_USERS).find((u) => u.id === userId);
|
|
41
|
+
return user ?? null;
|
|
42
|
+
}
|
|
43
|
+
// ============================================================================
|
|
44
|
+
// Auth Config Factory
|
|
45
|
+
// ============================================================================
|
|
46
|
+
/**
|
|
47
|
+
* Auth config with user loader (for tests that use TEST_USERS)
|
|
48
|
+
*/
|
|
49
|
+
export function createTestAuthConfig() {
|
|
50
|
+
return {
|
|
51
|
+
jwt: {
|
|
52
|
+
secret: TEST_SECRETS.access,
|
|
53
|
+
refreshSecret: TEST_SECRETS.refresh,
|
|
54
|
+
accessTokenExpiry: '15m',
|
|
55
|
+
refreshTokenExpiry: '7d',
|
|
56
|
+
issuer: 'velox-test',
|
|
57
|
+
audience: 'velox-test-app',
|
|
58
|
+
},
|
|
59
|
+
userLoader: testUserLoader,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Auth config without user loader (for tests with ad-hoc users)
|
|
64
|
+
* User info comes directly from token claims
|
|
65
|
+
*/
|
|
66
|
+
export function createTestAuthConfigNoLoader() {
|
|
67
|
+
return {
|
|
68
|
+
jwt: {
|
|
69
|
+
secret: TEST_SECRETS.access,
|
|
70
|
+
refreshSecret: TEST_SECRETS.refresh,
|
|
71
|
+
accessTokenExpiry: '15m',
|
|
72
|
+
refreshTokenExpiry: '7d',
|
|
73
|
+
issuer: 'velox-test',
|
|
74
|
+
audience: 'velox-test-app',
|
|
75
|
+
},
|
|
76
|
+
// No userLoader - user info comes from token
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=fixtures.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fixtures.js","sourceRoot":"","sources":["../../src/__integration__/fixtures.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAIhD,gCAAgC;AAChC,OAAO,EAAE,YAAY,EAAE,CAAC;AAExB,+EAA+E;AAC/E,aAAa;AACb,+EAA+E;AAE/E,MAAM,CAAC,MAAM,UAAU,GAAyB;IAC9C,KAAK,EAAE;QACL,EAAE,EAAE,gBAAgB;QACpB,KAAK,EAAE,mBAAmB;QAC1B,KAAK,EAAE,CAAC,OAAO,CAAC;KACjB;IACD,IAAI,EAAE;QACJ,EAAE,EAAE,kBAAkB;QACtB,KAAK,EAAE,kBAAkB;QACzB,KAAK,EAAE,CAAC,MAAM,CAAC;KAChB;IACD,KAAK,EAAE;QACL,EAAE,EAAE,gBAAgB;QACpB,KAAK,EAAE,mBAAmB;QAC1B,KAAK,EAAE,EAAE,EAAE,oCAAoC;KAChD;IACD,SAAS,EAAE;QACT,EAAE,EAAE,gBAAgB;QACpB,KAAK,EAAE,mBAAmB;QAC1B,KAAK,EAAE,CAAC,MAAM,EAAE,QAAQ,EAAE,WAAW,CAAC;KACvC;CACF,CAAC;AAEF,+EAA+E;AAC/E,cAAc;AACd,+EAA+E;AAE/E;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,MAAc;IACjD,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,MAAM,CAAC,CAAC;IACpE,OAAO,IAAI,IAAI,IAAI,CAAC;AACtB,CAAC;AAED,+EAA+E;AAC/E,sBAAsB;AACtB,+EAA+E;AAE/E;;GAEG;AACH,MAAM,UAAU,oBAAoB;IAClC,OAAO;QACL,GAAG,EAAE;YACH,MAAM,EAAE,YAAY,CAAC,MAAM;YAC3B,aAAa,EAAE,YAAY,CAAC,OAAO;YACnC,iBAAiB,EAAE,KAAK;YACxB,kBAAkB,EAAE,IAAI;YACxB,MAAM,EAAE,YAAY;YACpB,QAAQ,EAAE,gBAAgB;SAC3B;QACD,UAAU,EAAE,cAAc;KAC3B,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,4BAA4B;IAC1C,OAAO;QACL,GAAG,EAAE;YACH,MAAM,EAAE,YAAY,CAAC,MAAM;YAC3B,aAAa,EAAE,YAAY,CAAC,OAAO;YACnC,iBAAiB,EAAE,KAAK;YACxB,kBAAkB,EAAE,IAAI;YACxB,MAAM,EAAE,YAAY;YACpB,QAAQ,EAAE,gBAAgB;SAC3B;QACD,6CAA6C;KAC9C,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration test setup utilities
|
|
3
|
+
* @module __integration__/setup
|
|
4
|
+
*/
|
|
5
|
+
import type { FastifyInstance } from 'fastify';
|
|
6
|
+
import { type AuthPluginOptions } from '../plugin.js';
|
|
7
|
+
export { authHeader } from '@veloxts/testing';
|
|
8
|
+
/**
|
|
9
|
+
* Options for creating a test server
|
|
10
|
+
*/
|
|
11
|
+
export interface TestServerOptions {
|
|
12
|
+
/** Auth plugin options (defaults to test config) */
|
|
13
|
+
authOptions?: AuthPluginOptions;
|
|
14
|
+
/** Skip auth plugin registration */
|
|
15
|
+
skipAuth?: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Creates a Fastify server configured for integration testing
|
|
19
|
+
*
|
|
20
|
+
* This sets up:
|
|
21
|
+
* - Request context decoration (mimics VeloxApp behavior)
|
|
22
|
+
* - Auth plugin with test configuration
|
|
23
|
+
* - Logging disabled for cleaner test output
|
|
24
|
+
*/
|
|
25
|
+
export declare function createTestServer(options?: TestServerOptions): Promise<FastifyInstance>;
|
|
26
|
+
//# sourceMappingURL=setup.d.ts.map
|