@veloxts/auth 0.3.4 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +421 -19
- 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 +8 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +38 -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/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
|
@@ -23,6 +23,12 @@ Authentication and authorization system for VeloxTS Framework.
|
|
|
23
23
|
- [JWT Authentication](#jwt-authentication)
|
|
24
24
|
- [CSRF Protection](#csrf-protection)
|
|
25
25
|
- [Guards and Policies](#guards-and-policies)
|
|
26
|
+
- [User Roles and Permissions](#user-roles-and-permissions)
|
|
27
|
+
- [Role-Based Guards](#role-based-guards)
|
|
28
|
+
- [Permission-Based Guards](#permission-based-guards)
|
|
29
|
+
- [Combining Guards](#combining-guards)
|
|
30
|
+
- [Custom Guards](#custom-guards)
|
|
31
|
+
- [Policies](#policies)
|
|
26
32
|
- [Password Hashing](#password-hashing)
|
|
27
33
|
- [Rate Limiting](#rate-limiting)
|
|
28
34
|
|
|
@@ -55,7 +61,7 @@ npm install better-auth @veloxts/auth
|
|
|
55
61
|
#### Basic Setup
|
|
56
62
|
|
|
57
63
|
```typescript
|
|
58
|
-
import {
|
|
64
|
+
import { veloxApp } from '@veloxts/core';
|
|
59
65
|
import { createAuthAdapterPlugin, createBetterAuthAdapter } from '@veloxts/auth';
|
|
60
66
|
import { betterAuth } from 'better-auth';
|
|
61
67
|
import { prismaAdapter } from 'better-auth/adapters/prisma';
|
|
@@ -82,14 +88,14 @@ const betterAuthAdapter = createBetterAuthAdapter({
|
|
|
82
88
|
});
|
|
83
89
|
|
|
84
90
|
// Create the plugin
|
|
85
|
-
const
|
|
91
|
+
const authAdapterPlugin = createAuthAdapterPlugin({
|
|
86
92
|
adapter: betterAuthAdapter,
|
|
87
93
|
config: betterAuthAdapter.config,
|
|
88
94
|
});
|
|
89
95
|
|
|
90
96
|
// Create app and register plugin
|
|
91
|
-
const app =
|
|
92
|
-
await app.register(
|
|
97
|
+
const app = await veloxApp();
|
|
98
|
+
await app.register(authAdapterPlugin);
|
|
93
99
|
```
|
|
94
100
|
|
|
95
101
|
#### Using Authentication in Procedures
|
|
@@ -342,11 +348,11 @@ Cookie-based session management with secure defaults and pluggable storage backe
|
|
|
342
348
|
### Quick Start
|
|
343
349
|
|
|
344
350
|
```typescript
|
|
345
|
-
import {
|
|
351
|
+
import { sessionMiddleware, createInMemorySessionStore } from '@veloxts/auth';
|
|
346
352
|
import { defineProcedures, procedure } from '@veloxts/router';
|
|
347
353
|
|
|
348
354
|
// Create session middleware
|
|
349
|
-
const session =
|
|
355
|
+
const session = sessionMiddleware({
|
|
350
356
|
secret: process.env.SESSION_SECRET!, // Min 32 characters
|
|
351
357
|
cookie: {
|
|
352
358
|
secure: process.env.NODE_ENV === 'production',
|
|
@@ -420,7 +426,7 @@ const sessionManager = createSessionManager({
|
|
|
420
426
|
### Middleware Variants
|
|
421
427
|
|
|
422
428
|
```typescript
|
|
423
|
-
const session =
|
|
429
|
+
const session = sessionMiddleware(config);
|
|
424
430
|
|
|
425
431
|
// Basic session middleware - creates session for all requests
|
|
426
432
|
const getPreferences = procedure
|
|
@@ -623,7 +629,7 @@ class RedisSessionStore implements SessionStore {
|
|
|
623
629
|
|
|
624
630
|
// Use custom store
|
|
625
631
|
const redisStore = new RedisSessionStore(redisClient);
|
|
626
|
-
const session =
|
|
632
|
+
const session = sessionMiddleware({
|
|
627
633
|
secret: process.env.SESSION_SECRET!,
|
|
628
634
|
store: redisStore,
|
|
629
635
|
});
|
|
@@ -706,16 +712,182 @@ The session implementation includes several security protections by default:
|
|
|
706
712
|
|
|
707
713
|
## JWT Authentication
|
|
708
714
|
|
|
709
|
-
|
|
715
|
+
Stateless token-based authentication using HMAC-SHA256 signed JWTs.
|
|
716
|
+
|
|
717
|
+
### Quick Start
|
|
718
|
+
|
|
719
|
+
```typescript
|
|
720
|
+
import { jwtManager, authMiddleware } from '@veloxts/auth';
|
|
721
|
+
import { defineProcedures, procedure } from '@veloxts/router';
|
|
722
|
+
|
|
723
|
+
// Create JWT manager
|
|
724
|
+
const jwt = jwtManager({
|
|
725
|
+
secret: process.env.JWT_SECRET!, // Min 64 characters
|
|
726
|
+
accessTokenExpiry: '15m',
|
|
727
|
+
refreshTokenExpiry: '7d',
|
|
728
|
+
issuer: 'my-app',
|
|
729
|
+
audience: 'my-app-users',
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
// Create auth middleware
|
|
733
|
+
const auth = authMiddleware({
|
|
734
|
+
jwt: {
|
|
735
|
+
secret: process.env.JWT_SECRET!,
|
|
736
|
+
accessTokenExpiry: '15m',
|
|
737
|
+
refreshTokenExpiry: '7d',
|
|
738
|
+
},
|
|
739
|
+
userLoader: async (userId) => {
|
|
740
|
+
return db.user.findUnique({ where: { id: userId } });
|
|
741
|
+
},
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
// Use in procedures
|
|
745
|
+
export const authProcedures = defineProcedures('auth', {
|
|
746
|
+
// Login - return tokens
|
|
747
|
+
login: procedure
|
|
748
|
+
.input(z.object({ email: z.string().email(), password: z.string() }))
|
|
749
|
+
.mutation(async ({ input }) => {
|
|
750
|
+
const user = await db.user.findUnique({ where: { email: input.email } });
|
|
751
|
+
if (!user || !await verifyPassword(input.password, user.passwordHash)) {
|
|
752
|
+
throw new AuthError('Invalid credentials', 401);
|
|
753
|
+
}
|
|
754
|
+
return jwt.createTokenPair(user);
|
|
755
|
+
}),
|
|
756
|
+
|
|
757
|
+
// Protected route
|
|
758
|
+
getProfile: procedure
|
|
759
|
+
.use(auth.requireAuth())
|
|
760
|
+
.query(async ({ ctx }) => {
|
|
761
|
+
return ctx.user; // Guaranteed to exist
|
|
762
|
+
}),
|
|
763
|
+
|
|
764
|
+
// Refresh tokens
|
|
765
|
+
refresh: procedure
|
|
766
|
+
.input(z.object({ refreshToken: z.string() }))
|
|
767
|
+
.mutation(async ({ input }) => {
|
|
768
|
+
return jwt.refreshTokens(input.refreshToken);
|
|
769
|
+
}),
|
|
770
|
+
});
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
### Configuration Options
|
|
774
|
+
|
|
775
|
+
```typescript
|
|
776
|
+
import { jwtManager } from '@veloxts/auth';
|
|
777
|
+
|
|
778
|
+
const jwt = jwtManager({
|
|
779
|
+
// Required: Secret for signing tokens (min 64 chars)
|
|
780
|
+
// Generate with: openssl rand -base64 64
|
|
781
|
+
secret: process.env.JWT_SECRET!,
|
|
782
|
+
|
|
783
|
+
// Optional: Token expiration times
|
|
784
|
+
accessTokenExpiry: '15m', // Default: 15 minutes
|
|
785
|
+
refreshTokenExpiry: '7d', // Default: 7 days
|
|
786
|
+
|
|
787
|
+
// Optional: Token claims
|
|
788
|
+
issuer: 'my-app', // iss claim
|
|
789
|
+
audience: 'my-app-users', // aud claim
|
|
790
|
+
});
|
|
791
|
+
```
|
|
792
|
+
|
|
793
|
+
### Token Operations
|
|
794
|
+
|
|
795
|
+
```typescript
|
|
796
|
+
// Create token pair for user
|
|
797
|
+
const tokens = jwt.createTokenPair(user);
|
|
798
|
+
// Returns: { accessToken, refreshToken, expiresIn, tokenType }
|
|
799
|
+
|
|
800
|
+
// Add custom claims (cannot override reserved claims)
|
|
801
|
+
const tokens = jwt.createTokenPair(user, {
|
|
802
|
+
role: 'admin',
|
|
803
|
+
permissions: ['read', 'write'],
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
// Verify access token
|
|
807
|
+
const payload = jwt.verifyToken(accessToken);
|
|
808
|
+
// Returns: { sub, email, iat, exp, type, jti, ... }
|
|
809
|
+
|
|
810
|
+
// Refresh tokens using refresh token
|
|
811
|
+
const newTokens = jwt.refreshTokens(refreshToken);
|
|
812
|
+
|
|
813
|
+
// Extract token from Authorization header
|
|
814
|
+
const token = jwt.extractFromHeader(request.headers.authorization);
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
### Token Revocation
|
|
818
|
+
|
|
819
|
+
For security-critical applications, implement token revocation:
|
|
820
|
+
|
|
821
|
+
```typescript
|
|
822
|
+
import { createInMemoryTokenStore } from '@veloxts/auth';
|
|
823
|
+
|
|
824
|
+
// Development/testing (NOT for production!)
|
|
825
|
+
const tokenStore = createInMemoryTokenStore();
|
|
826
|
+
|
|
827
|
+
// Configure auth to check revocation
|
|
828
|
+
const auth = authMiddleware({
|
|
829
|
+
jwt: { secret: process.env.JWT_SECRET! },
|
|
830
|
+
isTokenRevoked: tokenStore.isRevoked,
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
// Revoke on logout
|
|
834
|
+
const logout = procedure
|
|
835
|
+
.use(auth.requireAuth())
|
|
836
|
+
.mutation(async ({ ctx }) => {
|
|
837
|
+
if (ctx.auth.token?.jti) {
|
|
838
|
+
tokenStore.revoke(ctx.auth.token.jti);
|
|
839
|
+
}
|
|
840
|
+
return { success: true };
|
|
841
|
+
});
|
|
842
|
+
```
|
|
843
|
+
|
|
844
|
+
For production, use Redis or database-backed storage instead of the in-memory store.
|
|
845
|
+
|
|
846
|
+
### Auth Middleware Options
|
|
847
|
+
|
|
848
|
+
```typescript
|
|
849
|
+
const auth = authMiddleware(config);
|
|
850
|
+
|
|
851
|
+
// Require authentication (throws 401 if no valid token)
|
|
852
|
+
const getProfile = procedure
|
|
853
|
+
.use(auth.requireAuth())
|
|
854
|
+
.query(({ ctx }) => ctx.user);
|
|
855
|
+
|
|
856
|
+
// Optional authentication (user may be undefined)
|
|
857
|
+
const getPosts = procedure
|
|
858
|
+
.use(auth.optionalAuth())
|
|
859
|
+
.query(({ ctx }) => {
|
|
860
|
+
if (ctx.user) {
|
|
861
|
+
return getPrivatePosts(ctx.user.id);
|
|
862
|
+
}
|
|
863
|
+
return getPublicPosts();
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
// With guards (after authentication)
|
|
867
|
+
const adminOnly = procedure
|
|
868
|
+
.use(auth.middleware({ guards: [hasRole('admin')] }))
|
|
869
|
+
.query(({ ctx }) => getAdminData());
|
|
870
|
+
```
|
|
871
|
+
|
|
872
|
+
### Security Features
|
|
873
|
+
|
|
874
|
+
The JWT implementation includes several security protections:
|
|
875
|
+
|
|
876
|
+
- **HS256 algorithm enforcement** - Rejects `none`, RS256, and other algorithms to prevent confusion attacks
|
|
877
|
+
- **Timing-safe signature verification** - Prevents timing attacks on token validation
|
|
878
|
+
- **Secret entropy validation** - Requires at least 64 characters with 16+ unique characters
|
|
879
|
+
- **Reserved claim protection** - Prevents overriding `sub`, `exp`, `iat`, etc. via custom claims
|
|
880
|
+
- **Token expiration** - Access and refresh tokens have separate expiration times
|
|
881
|
+
- **Not-before claim support** - Optionally delay token validity
|
|
710
882
|
|
|
711
883
|
## CSRF Protection
|
|
712
884
|
|
|
713
885
|
CSRF protection is already implemented using the signed double-submit cookie pattern with timing-safe comparison and entropy validation.
|
|
714
886
|
|
|
715
887
|
```typescript
|
|
716
|
-
import {
|
|
888
|
+
import { csrfMiddleware } from '@veloxts/auth';
|
|
717
889
|
|
|
718
|
-
const csrf =
|
|
890
|
+
const csrf = csrfMiddleware({
|
|
719
891
|
secret: process.env.CSRF_SECRET!,
|
|
720
892
|
});
|
|
721
893
|
|
|
@@ -733,18 +905,129 @@ See the CSRF documentation for complete details on configuration and usage.
|
|
|
733
905
|
|
|
734
906
|
Guards and policies provide declarative authorization for procedures.
|
|
735
907
|
|
|
908
|
+
### User Roles and Permissions
|
|
909
|
+
|
|
910
|
+
Users can have multiple roles and permissions:
|
|
911
|
+
|
|
912
|
+
```typescript
|
|
913
|
+
import type { User } from '@veloxts/auth';
|
|
914
|
+
|
|
915
|
+
// User with multiple roles
|
|
916
|
+
const user: User = {
|
|
917
|
+
id: '1',
|
|
918
|
+
email: 'admin@example.com',
|
|
919
|
+
roles: ['admin', 'editor'], // Multiple roles
|
|
920
|
+
permissions: ['posts.read', 'posts.write', 'users.manage'],
|
|
921
|
+
};
|
|
922
|
+
```
|
|
923
|
+
|
|
924
|
+
### Role-Based Guards
|
|
925
|
+
|
|
926
|
+
The `hasRole` guard checks if the user has ANY of the specified roles:
|
|
927
|
+
|
|
736
928
|
```typescript
|
|
737
|
-
import {
|
|
929
|
+
import { hasRole, hasPermission, allOf, anyOf } from '@veloxts/auth';
|
|
738
930
|
|
|
739
|
-
//
|
|
931
|
+
// Require a single role
|
|
740
932
|
const adminOnly = procedure
|
|
741
|
-
.use(
|
|
742
|
-
.use(guard(hasRole('admin')))
|
|
933
|
+
.use(auth.middleware({ guards: [hasRole('admin')] }))
|
|
743
934
|
.query(async ({ ctx }) => {
|
|
744
|
-
// Only
|
|
935
|
+
// Only users with 'admin' role can access
|
|
745
936
|
});
|
|
746
937
|
|
|
747
|
-
//
|
|
938
|
+
// Require ANY of multiple roles (OR logic)
|
|
939
|
+
const staffAccess = procedure
|
|
940
|
+
.use(auth.middleware({ guards: [hasRole(['admin', 'moderator', 'editor'])] }))
|
|
941
|
+
.query(async ({ ctx }) => {
|
|
942
|
+
// Users with 'admin', 'moderator', OR 'editor' role can access
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
// User with roles: ['editor', 'reviewer'] passes hasRole(['admin', 'editor'])
|
|
946
|
+
// because they have the 'editor' role
|
|
947
|
+
```
|
|
948
|
+
|
|
949
|
+
### Permission-Based Guards
|
|
950
|
+
|
|
951
|
+
```typescript
|
|
952
|
+
// Require ALL specified permissions (AND logic)
|
|
953
|
+
const canManagePosts = procedure
|
|
954
|
+
.use(auth.middleware({ guards: [hasPermission(['posts.read', 'posts.write'])] }))
|
|
955
|
+
.query(async ({ ctx }) => {
|
|
956
|
+
// User must have BOTH permissions
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
// Require ANY of the permissions (OR logic)
|
|
960
|
+
const canViewPosts = procedure
|
|
961
|
+
.use(auth.middleware({ guards: [hasAnyPermission(['posts.read', 'posts.admin'])] }))
|
|
962
|
+
.query(async ({ ctx }) => {
|
|
963
|
+
// User needs at least one of these permissions
|
|
964
|
+
});
|
|
965
|
+
```
|
|
966
|
+
|
|
967
|
+
### Combining Guards
|
|
968
|
+
|
|
969
|
+
```typescript
|
|
970
|
+
// Require BOTH role AND permission (AND logic)
|
|
971
|
+
const adminWithPermission = procedure
|
|
972
|
+
.use(auth.middleware({
|
|
973
|
+
guards: [hasRole('admin'), hasPermission('users.delete')]
|
|
974
|
+
}))
|
|
975
|
+
.mutation(async ({ ctx }) => {
|
|
976
|
+
// Must be admin AND have users.delete permission
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
// Using allOf for explicit AND
|
|
980
|
+
const strictAccess = procedure
|
|
981
|
+
.use(auth.middleware({
|
|
982
|
+
guards: [allOf([hasRole('admin'), hasPermission('sensitive.access')])]
|
|
983
|
+
}))
|
|
984
|
+
.query(async ({ ctx }) => {
|
|
985
|
+
// Both conditions must pass
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
// Using anyOf for explicit OR
|
|
989
|
+
const flexibleAccess = procedure
|
|
990
|
+
.use(auth.middleware({
|
|
991
|
+
guards: [anyOf([hasRole('admin'), hasPermission('special.access')])]
|
|
992
|
+
}))
|
|
993
|
+
.query(async ({ ctx }) => {
|
|
994
|
+
// Either condition can pass
|
|
995
|
+
});
|
|
996
|
+
```
|
|
997
|
+
|
|
998
|
+
### Custom Guards
|
|
999
|
+
|
|
1000
|
+
```typescript
|
|
1001
|
+
import { guard, defineGuard } from '@veloxts/auth';
|
|
1002
|
+
|
|
1003
|
+
// Simple custom guard
|
|
1004
|
+
const isVerifiedEmail = guard('isVerifiedEmail', (ctx) => {
|
|
1005
|
+
return ctx.user?.emailVerified === true;
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
// Guard with configuration
|
|
1009
|
+
const isPremiumUser = defineGuard({
|
|
1010
|
+
name: 'isPremiumUser',
|
|
1011
|
+
check: (ctx) => ctx.user?.subscription === 'premium',
|
|
1012
|
+
message: 'Premium subscription required',
|
|
1013
|
+
statusCode: 403,
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
// Use in procedures
|
|
1017
|
+
const premiumContent = procedure
|
|
1018
|
+
.use(auth.middleware({ guards: [isPremiumUser] }))
|
|
1019
|
+
.query(async ({ ctx }) => {
|
|
1020
|
+
return getPremiumContent();
|
|
1021
|
+
});
|
|
1022
|
+
```
|
|
1023
|
+
|
|
1024
|
+
### Policies
|
|
1025
|
+
|
|
1026
|
+
Define resource-specific authorization logic:
|
|
1027
|
+
|
|
1028
|
+
```typescript
|
|
1029
|
+
import { definePolicy } from '@veloxts/auth';
|
|
1030
|
+
|
|
748
1031
|
const postPolicy = definePolicy<{ postId: string }>('post', {
|
|
749
1032
|
view: async (user, { postId }) => {
|
|
750
1033
|
// Anyone can view public posts
|
|
@@ -752,7 +1035,13 @@ const postPolicy = definePolicy<{ postId: string }>('post', {
|
|
|
752
1035
|
},
|
|
753
1036
|
edit: async (user, { postId }) => {
|
|
754
1037
|
const post = await db.post.findUnique({ where: { id: postId } });
|
|
755
|
-
|
|
1038
|
+
// Only author or admin can edit
|
|
1039
|
+
return post?.authorId === user.id || user.roles?.includes('admin');
|
|
1040
|
+
},
|
|
1041
|
+
delete: async (user, { postId }) => {
|
|
1042
|
+
const post = await db.post.findUnique({ where: { id: postId } });
|
|
1043
|
+
// Only admin can delete
|
|
1044
|
+
return user.roles?.includes('admin') ?? false;
|
|
756
1045
|
},
|
|
757
1046
|
});
|
|
758
1047
|
```
|
|
@@ -773,7 +1062,120 @@ const valid = await verifyPassword('user-password', hash);
|
|
|
773
1062
|
|
|
774
1063
|
## Rate Limiting
|
|
775
1064
|
|
|
776
|
-
|
|
1065
|
+
Protect your endpoints from abuse with request rate limiting.
|
|
1066
|
+
|
|
1067
|
+
### Quick Start
|
|
1068
|
+
|
|
1069
|
+
```typescript
|
|
1070
|
+
import { rateLimitMiddleware } from '@veloxts/auth';
|
|
1071
|
+
import { defineProcedures, procedure } from '@veloxts/router';
|
|
1072
|
+
|
|
1073
|
+
// Create rate limit middleware
|
|
1074
|
+
const rateLimit = rateLimitMiddleware({
|
|
1075
|
+
max: 100, // Maximum requests per window
|
|
1076
|
+
windowMs: 60000, // Window size in milliseconds (1 minute)
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
// Stricter limit for auth endpoints
|
|
1080
|
+
const authRateLimit = rateLimitMiddleware({
|
|
1081
|
+
max: 5,
|
|
1082
|
+
windowMs: 60000,
|
|
1083
|
+
message: 'Too many login attempts, please try again later',
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
export const authProcedures = defineProcedures('auth', {
|
|
1087
|
+
// Protected with stricter rate limit
|
|
1088
|
+
login: procedure
|
|
1089
|
+
.use(authRateLimit)
|
|
1090
|
+
.input(LoginSchema)
|
|
1091
|
+
.mutation(async ({ input }) => {
|
|
1092
|
+
// Login logic
|
|
1093
|
+
}),
|
|
1094
|
+
|
|
1095
|
+
// Normal rate limit
|
|
1096
|
+
getProfile: procedure
|
|
1097
|
+
.use(rateLimit)
|
|
1098
|
+
.use(auth.requireAuth())
|
|
1099
|
+
.query(({ ctx }) => ctx.user),
|
|
1100
|
+
});
|
|
1101
|
+
```
|
|
1102
|
+
|
|
1103
|
+
### Configuration Options
|
|
1104
|
+
|
|
1105
|
+
```typescript
|
|
1106
|
+
const rateLimit = rateLimitMiddleware({
|
|
1107
|
+
// Maximum requests allowed in window
|
|
1108
|
+
max: 100, // Default: 100
|
|
1109
|
+
|
|
1110
|
+
// Time window in milliseconds
|
|
1111
|
+
windowMs: 60000, // Default: 60000 (1 minute)
|
|
1112
|
+
|
|
1113
|
+
// Custom key generator (default: request IP)
|
|
1114
|
+
keyGenerator: (ctx) => {
|
|
1115
|
+
// Rate limit by user ID if authenticated
|
|
1116
|
+
return ctx.user?.id ?? ctx.request.ip ?? 'anonymous';
|
|
1117
|
+
},
|
|
1118
|
+
|
|
1119
|
+
// Custom error message
|
|
1120
|
+
message: 'Rate limit exceeded',
|
|
1121
|
+
});
|
|
1122
|
+
```
|
|
1123
|
+
|
|
1124
|
+
### Response Headers
|
|
1125
|
+
|
|
1126
|
+
Rate limit info is included in response headers:
|
|
1127
|
+
|
|
1128
|
+
```
|
|
1129
|
+
X-RateLimit-Limit: 100 # Max requests allowed
|
|
1130
|
+
X-RateLimit-Remaining: 95 # Remaining requests in window
|
|
1131
|
+
X-RateLimit-Reset: 1234567890 # Unix timestamp when window resets
|
|
1132
|
+
```
|
|
1133
|
+
|
|
1134
|
+
### Production Considerations
|
|
1135
|
+
|
|
1136
|
+
The built-in rate limiter uses in-memory storage, which:
|
|
1137
|
+
- Does **not** persist across server restarts
|
|
1138
|
+
- Does **not** work across multiple server instances
|
|
1139
|
+
|
|
1140
|
+
For production, implement a custom middleware using Redis:
|
|
1141
|
+
|
|
1142
|
+
```typescript
|
|
1143
|
+
import { Redis } from 'ioredis';
|
|
1144
|
+
import type { MiddlewareFunction } from '@veloxts/router';
|
|
1145
|
+
import { AuthError } from '@veloxts/auth';
|
|
1146
|
+
|
|
1147
|
+
const redis = new Redis();
|
|
1148
|
+
|
|
1149
|
+
function redisRateLimitMiddleware(options: {
|
|
1150
|
+
max: number;
|
|
1151
|
+
windowMs: number;
|
|
1152
|
+
prefix?: string;
|
|
1153
|
+
}): MiddlewareFunction {
|
|
1154
|
+
const { max, windowMs, prefix = 'ratelimit:' } = options;
|
|
1155
|
+
|
|
1156
|
+
return async ({ ctx, next }) => {
|
|
1157
|
+
const key = `${prefix}${ctx.request.ip}`;
|
|
1158
|
+
const current = await redis.incr(key);
|
|
1159
|
+
|
|
1160
|
+
if (current === 1) {
|
|
1161
|
+
await redis.pexpire(key, windowMs);
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
const ttl = await redis.pttl(key);
|
|
1165
|
+
const remaining = Math.max(0, max - current);
|
|
1166
|
+
|
|
1167
|
+
ctx.reply.header('X-RateLimit-Limit', String(max));
|
|
1168
|
+
ctx.reply.header('X-RateLimit-Remaining', String(remaining));
|
|
1169
|
+
ctx.reply.header('X-RateLimit-Reset', String(Math.ceil((Date.now() + ttl) / 1000)));
|
|
1170
|
+
|
|
1171
|
+
if (current > max) {
|
|
1172
|
+
throw new AuthError('Too many requests', 429, 'RATE_LIMIT_EXCEEDED');
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
return next();
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
```
|
|
777
1179
|
|
|
778
1180
|
## Related Packages
|
|
779
1181
|
|
|
@@ -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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../src/__integration__/setup.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE/C,OAAO,EAAE,KAAK,iBAAiB,EAAc,MAAM,cAAc,CAAC;AAIlE,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAM9C;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,oDAAoD;IACpD,WAAW,CAAC,EAAE,iBAAiB,CAAC;IAChC,oCAAoC;IACpC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,GAAE,iBAAsB,GAAG,OAAO,CAAC,eAAe,CAAC,CAYhG"}
|