authenik8-core 0.1.0

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.
Files changed (98) hide show
  1. package/.env +2 -0
  2. package/README.md +93 -0
  3. package/authenik8-core-0.1.0.tgz +0 -0
  4. package/dist/auth/guestModeService.d.ts +3 -0
  5. package/dist/auth/guestModeService.d.ts.map +1 -0
  6. package/dist/auth/guestModeService.js +24 -0
  7. package/dist/auth/guestModeService.js.map +1 -0
  8. package/dist/auth/jwtAuth.d.ts +28 -0
  9. package/dist/auth/jwtAuth.d.ts.map +1 -0
  10. package/dist/auth/jwtAuth.js +67 -0
  11. package/dist/auth/jwtAuth.js.map +1 -0
  12. package/dist/auth/refreshService.d.ts +41 -0
  13. package/dist/auth/refreshService.d.ts.map +1 -0
  14. package/dist/auth/refreshService.js +77 -0
  15. package/dist/auth/refreshService.js.map +1 -0
  16. package/dist/creatAuthenik8.d.ts +2 -0
  17. package/dist/creatAuthenik8.d.ts.map +1 -0
  18. package/dist/creatAuthenik8.js +3 -0
  19. package/dist/creatAuthenik8.js.map +1 -0
  20. package/dist/createAuthenik8.d.ts +4 -0
  21. package/dist/createAuthenik8.d.ts.map +1 -0
  22. package/dist/createAuthenik8.js +58 -0
  23. package/dist/createAuthenik8.js.map +1 -0
  24. package/dist/index.d.ts +2 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +6 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/middleware/adminService.d.ts +4 -0
  29. package/dist/middleware/adminService.d.ts.map +1 -0
  30. package/dist/middleware/adminService.js +40 -0
  31. package/dist/middleware/adminService.js.map +1 -0
  32. package/dist/redis/redisService.d.ts +26 -0
  33. package/dist/redis/redisService.d.ts.map +1 -0
  34. package/dist/redis/redisService.js +104 -0
  35. package/dist/redis/redisService.js.map +1 -0
  36. package/dist/security/ipService.d.ts +36 -0
  37. package/dist/security/ipService.d.ts.map +1 -0
  38. package/dist/security/ipService.js +160 -0
  39. package/dist/security/ipService.js.map +1 -0
  40. package/dist/security/limiter.d.ts +5 -0
  41. package/dist/security/limiter.d.ts.map +1 -0
  42. package/dist/security/limiter.js +93 -0
  43. package/dist/security/limiter.js.map +1 -0
  44. package/dist/storage/RedisTokenStore.d.ts +21 -0
  45. package/dist/storage/RedisTokenStore.d.ts.map +1 -0
  46. package/dist/storage/RedisTokenStore.js +86 -0
  47. package/dist/storage/RedisTokenStore.js.map +1 -0
  48. package/dist/storage/userStorage.d.ts +7 -0
  49. package/dist/storage/userStorage.d.ts.map +1 -0
  50. package/dist/storage/userStorage.js +18 -0
  51. package/dist/storage/userStorage.js.map +1 -0
  52. package/dist/tests/full.intergration.test.d.ts +2 -0
  53. package/dist/tests/full.intergration.test.d.ts.map +1 -0
  54. package/dist/tests/full.intergration.test.js +79 -0
  55. package/dist/tests/full.intergration.test.js.map +1 -0
  56. package/dist/tests/testApp.d.ts +7 -0
  57. package/dist/tests/testApp.d.ts.map +1 -0
  58. package/dist/tests/testApp.js +53 -0
  59. package/dist/tests/testApp.js.map +1 -0
  60. package/dist/types/admin.d.ts +6 -0
  61. package/dist/types/admin.d.ts.map +1 -0
  62. package/dist/types/admin.js +3 -0
  63. package/dist/types/admin.js.map +1 -0
  64. package/dist/types/config.d.ts +9 -0
  65. package/dist/types/config.d.ts.map +1 -0
  66. package/dist/types/config.js +3 -0
  67. package/dist/types/config.js.map +1 -0
  68. package/dist/types/public.d.ts +17 -0
  69. package/dist/types/public.d.ts.map +1 -0
  70. package/dist/types/public.js +3 -0
  71. package/dist/types/public.js.map +1 -0
  72. package/dist/types/storage.d.ts +14 -0
  73. package/dist/types/storage.d.ts.map +1 -0
  74. package/dist/types/storage.js +3 -0
  75. package/dist/types/storage.js.map +1 -0
  76. package/dump.rdb +0 -0
  77. package/jest.config.js +11 -0
  78. package/package.json +56 -0
  79. package/src/1 +22 -0
  80. package/src/auth/guestModeService.ts +31 -0
  81. package/src/auth/jwtAuth.ts +99 -0
  82. package/src/auth/refreshService.ts +134 -0
  83. package/src/creatAuthenik8.ts +0 -0
  84. package/src/createAuthenik8.ts +66 -0
  85. package/src/index.ts +1 -0
  86. package/src/middleware/adminService.ts +50 -0
  87. package/src/redis/redisService.ts +137 -0
  88. package/src/security/ipService.ts +180 -0
  89. package/src/security/limiter.ts +116 -0
  90. package/src/storage/RedisTokenStore.ts +99 -0
  91. package/src/storage/userStorage.ts +16 -0
  92. package/src/tests/full.intergration.test.ts +100 -0
  93. package/src/tests/testApp.ts +56 -0
  94. package/src/types/admin.ts +7 -0
  95. package/src/types/config.ts +11 -0
  96. package/src/types/public.ts +22 -0
  97. package/src/types/storage.ts +15 -0
  98. package/tsconfig.json +51 -0
@@ -0,0 +1,66 @@
1
+ import {SecurityModule} from "./security/ipService";
2
+ import {RefreshService } from "./auth/refreshService"
3
+ import {Authenik8Config} from "./types/config";
4
+ import {Incognito} from "./auth/guestModeService"
5
+ import {requireAdmin} from "./middleware/adminService";
6
+ import {JWTService} from "./auth/jwtAuth"
7
+ import { initializeRedisClient } from "./redis/redisService"
8
+ import {Authenik8Instance} from "./types/public"
9
+ import {RedisTokenStore} from "./storage/RedisTokenStore"
10
+
11
+ export const createAuthenik8 = async (config:Authenik8Config): Promise<Authenik8Instance> =>{
12
+
13
+ const redisClient = config.redis ?? await initializeRedisClient()
14
+ const tokenStore = new RedisTokenStore(redisClient);
15
+
16
+ const jwtService =new JWTService({
17
+ jwtSecret:config.jwtSecret,
18
+ expiry:config.jwtExpiry,
19
+ redisClient:redisClient
20
+ });
21
+
22
+ const refreshService = new RefreshService({
23
+ tokenStore,
24
+ accessTokenSecret:config.jwtSecret,
25
+ refreshTokenSecret:config.refreshSecret,
26
+ accessTokenExpiry:config.jwtExpiry,
27
+ rotateRefreshTokens:true
28
+ });
29
+
30
+ const security = new SecurityModule({
31
+ redisClient:config.redis,
32
+ rateLimiterEnabled: true,
33
+ helmetEnabled:true,
34
+ whiteListEnabled:true
35
+ });
36
+ return{
37
+ //auth
38
+ redis:redisClient,
39
+ signToken:jwtService.signToken.bind(jwtService),
40
+ verifyToken:jwtService.verifyToken.bind(jwtService),
41
+ guestToken:jwtService.guestToken.bind(jwtService),
42
+
43
+ //refresh
44
+ refreshToken:
45
+ refreshService.refresh.bind(refreshService),
46
+ generateRefreshToken: refreshService.generateRefreshToken.bind(refreshService),
47
+ //security
48
+ rateLimit: security.rateLimiterMiddleware(),
49
+ ipWhitelist: security.whiteListMiddleware(),
50
+ helmet: security.helmetMiddleware(),
51
+
52
+ //Whitelist management
53
+ addIP: security.addIP.bind(security),
54
+ removeIP: security.removeIP.bind(security),
55
+ listIPs: security.listIPs.bind(security),
56
+
57
+
58
+ //middleware
59
+ requireAdmin :requireAdmin({ jwtSecret:
60
+ config.jwtSecret,
61
+ redis:redisClient
62
+ }),
63
+ incognito:Incognito
64
+
65
+ }
66
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export {createAuthenik8} from "./createAuthenik8";
@@ -0,0 +1,50 @@
1
+
2
+ import {Request, Response, NextFunction} from "express";
3
+ import jwt from "jsonwebtoken";
4
+ import {Authenik8Config} from "../types/config";
5
+ import { RequireAdminOptions } from "../types/admin";
6
+
7
+
8
+ interface JwtPayload {
9
+ id:string;
10
+ role: string;
11
+ }
12
+
13
+
14
+
15
+ export const requireAdmin=(options:RequireAdminOptions)=>{
16
+ const {jwtSecret,redis}= options;
17
+ return(req:Request, res:Response,next:NextFunction)=>{
18
+
19
+ const authHeader = req.headers.authorization
20
+ const cookieToken =req.cookies?.token
21
+
22
+ let token:string | undefined;
23
+
24
+
25
+ if(authHeader && authHeader.startsWith("Bearer")){
26
+ token=authHeader.split(" ")[1];
27
+
28
+ }
29
+ if (!token && cookieToken){
30
+ token = cookieToken
31
+ }
32
+
33
+ if(!token){
34
+ return res.status(401).json({error:"Unauthorized:No token provided"});}
35
+ try{
36
+ const decoded = jwt.verify(token,options.jwtSecret) as JwtPayload;
37
+
38
+ if(typeof decoded.role !== "string"){
39
+ res.status(403).json({error:"Forbidden: Admin only"})
40
+ }
41
+ const payload = decoded as JwtPayload
42
+
43
+ (req as any).user =decoded;
44
+ next();
45
+ }catch(error){
46
+ return
47
+ res.status(401).json({error:"Invalid or expired token"})
48
+ }
49
+ }
50
+ }
@@ -0,0 +1,137 @@
1
+ import dotenv from "dotenv";
2
+ import {RedisStore} from "connect-redis";
3
+ import Redis ,{ Redis as RedisCon }from "ioredis";
4
+ dotenv.config();
5
+
6
+
7
+
8
+
9
+ interface RedisConfig{
10
+ url?: string;
11
+ host?: string
12
+ port?: number;
13
+ password?: string;
14
+ maxRetriesPerRequest?:number;
15
+ connectTimeout: number;
16
+ }
17
+
18
+ interface RedisStoreOptions{
19
+ prefix?: string;
20
+ ttl?: number;
21
+ }
22
+
23
+ interface SetupRedisOptions{
24
+ redisConfig?: Partial<RedisConfig>;
25
+ storeOptions?:Partial<RedisStoreOptions>;
26
+ }
27
+
28
+ let redisClientInstance:RedisCon | null = null
29
+ let redisStoreInstance:RedisStore |null = null
30
+
31
+ const DEFAULT_REDIS_CONFIG: RedisConfig = {
32
+ host:process.env.REDIS_HOST ?? "redis",
33
+ port: Number(process.env.REDIS_PORT ?? "6379"),
34
+ maxRetriesPerRequest: 10,
35
+ connectTimeout: 5000
36
+ }
37
+
38
+ const DEFAULT_STORE_OPTIONS : RedisStoreOptions = {
39
+ prefix: "session",
40
+ ttl: 86400
41
+ }
42
+
43
+ const validateRedisConfig = (config:RedisConfig) => {
44
+ if (!config.url && !config.host) {
45
+ throw new Error("Redis configuration requires either URL or host/port");
46
+ }
47
+
48
+ if (config.url && !config.url.startsWith("redis://") && !config.url.startsWith("rediss://")
49
+ ) {
50
+ throw new Error("Redis URL must use 'redis://' protocol");
51
+ }
52
+
53
+ };
54
+
55
+ const getRedisConfig = (options?:Partial<RedisConfig>): RedisConfig => {
56
+ const port = options?.port ?
57
+ Number(options.port) :
58
+ process.env.REDIS_PORT ? Number(process.env.REDIS_PORT) :
59
+ Number(DEFAULT_REDIS_CONFIG.port);
60
+
61
+ const config: RedisConfig = {
62
+ ...DEFAULT_REDIS_CONFIG,
63
+ host: options?.host || process.env.REDIS_HOST|| DEFAULT_REDIS_CONFIG.host,
64
+ port: port,
65
+ password: options?.password || process.env.REDIS_PASSWORD|| undefined,
66
+ ...options
67
+ };
68
+
69
+ validateRedisConfig(config);
70
+ return config;
71
+ };
72
+
73
+ const setupRedis = async (options?: SetupRedisOptions) => {
74
+
75
+ try {
76
+ const config = getRedisConfig(options?.redisConfig);
77
+ const storeOptions = {...DEFAULT_STORE_OPTIONS, ...options?.storeOptions}
78
+
79
+ const redisClient = new Redis({
80
+ host: config.host as string,
81
+ port:Number(config.port) ,
82
+ connectTimeout:config.connectTimeout,
83
+ password :config.password,
84
+ retryStrategy:(times:number)=> Math.min(times * 50,2000),
85
+ maxRetriesPerRequest:config.maxRetriesPerRequest
86
+ });
87
+
88
+ await new Promise<void>((resolve, reject)=>{
89
+ redisClient.once("ready", async () =>{
90
+ try{
91
+ const pong = await redisClient.ping();
92
+ console.log("Redis ping response:",pong);
93
+ resolve();
94
+ }catch(err){
95
+ reject(err);
96
+ }
97
+ })
98
+ redisClient.once("error",(err)=>{
99
+ reject(err);
100
+ })
101
+ });
102
+
103
+ const redisStore = new RedisStore({
104
+ client: redisClient,
105
+ prefix:storeOptions.prefix,
106
+ ttl:storeOptions.ttl
107
+ });
108
+
109
+
110
+ redisClient.on("error", (err:Error) => {
111
+ console.error("Redis client error:", err);
112
+ });
113
+
114
+ redisClient.on("ready", () => {
115
+ console.log("Redis client is ready");
116
+ });
117
+
118
+ redisClient.on("reconnecting", () => {
119
+ console.log("Redis client reconnecting...");
120
+ });
121
+
122
+ return{ redisClient, redisStore };
123
+ } catch (error) {
124
+ console.error("Redis setup failed:", error);
125
+ throw error;
126
+ }
127
+ };
128
+ const initializeRedisClient =async () => {
129
+ if(!redisClientInstance){
130
+ const {redisClient} = await setupRedis();
131
+ redisClientInstance= redisClient;
132
+ }
133
+ return redisClientInstance
134
+ };
135
+
136
+ export {setupRedis, initializeRedisClient};
137
+ export type {RedisConfig, RedisStoreOptions, SetupRedisOptions}
@@ -0,0 +1,180 @@
1
+
2
+ import helmet from "helmet";
3
+ import Redis from "ioredis";
4
+ import type {StringValue} from "ms"
5
+ import { RequestHandler } from "express";
6
+ import {RateLimiterRedis} from "rate-limiter-flexible";
7
+ import { Request, Response, NextFunction } from "express";
8
+ import { HelmetOptions } from "helmet";
9
+
10
+
11
+ const WHITELIST_KEY ="whitelist:ips";
12
+ const IP_EXPIRATION_SECONDS = 7 * 24 * 60 * 60;
13
+ const DEFAULT_JWT_EXPIRY = "1h"
14
+
15
+ interface CSPDirectives{
16
+ [key: string]: Array<string | boolean>;
17
+ }
18
+
19
+ interface DynamicWhiteList{
20
+ isAllowed : (ip:string)=> Promise<boolean>;
21
+ addIp: (ip: string)=> Promise<void>;
22
+ removeIp: (ip: string)=> Promise<void>;
23
+ getAll:() => Promise<string[]>;
24
+ middleware: () => (req:Request, res: Response, next:any)=> Promise<void>;
25
+ listIP:(ip:string)=> Promise<void>;
26
+ }
27
+
28
+ const JWT_SECRET =process.env.JWT_SECRET || "Boo";
29
+ const EXPIRY = "1h";
30
+
31
+ export interface SecurityOptions{
32
+ redisClient?: Redis;
33
+ jwtSecret?:string;
34
+ jwtExpiry?:StringValue;
35
+ rateLimitPoints?:number;
36
+ rateLimitDuration?:number;
37
+ rateLimitBlock?:number;
38
+ rateLimiterEnabled?:boolean;
39
+ enableWhitelist?:boolean;
40
+ enableRateLimiter?:boolean;
41
+ enableHelmet?:boolean;
42
+ whiteListEnabled?:boolean;
43
+ helmetEnabled?:boolean;
44
+ }
45
+
46
+ export class SecurityModule{
47
+ private redisClient:Redis;
48
+ private jwtSecret:string;
49
+ private jwtExpiry:StringValue;
50
+ private rateLimiter?: RateLimiterRedis;
51
+ private whiteListEnabled:boolean;
52
+ private helmetEnabled:boolean;
53
+ private rateLimiterEnabled:boolean;
54
+
55
+
56
+ constructor(options: SecurityOptions = {})
57
+ {
58
+ this.jwtSecret=options.jwtSecret || process.env.JWT_SECRET || "Boo" ;
59
+ this.jwtExpiry = options.jwtExpiry || DEFAULT_JWT_EXPIRY;
60
+ this.whiteListEnabled = options.whiteListEnabled ?? true;
61
+ this.helmetEnabled = options.helmetEnabled ?? true;
62
+ this.rateLimiterEnabled = options.rateLimiterEnabled ?? true;
63
+
64
+ this.redisClient = options.redisClient || new Redis({
65
+ host:process.env.REDIS_HOST || "127.0.0.1",
66
+ port:Number(process.env.REDIS_PORT || 6379),
67
+ enableOfflineQueue: false,
68
+ retryStrategy:(times)=> Math.min(times * 50 , 2000),
69
+ maxRetriesPerRequest:10,
70
+ })
71
+
72
+ if(this.rateLimiterEnabled){
73
+ this.rateLimiter = new RateLimiterRedis({
74
+ storeClient: this.redisClient,
75
+ keyPrefix:"rate_limit",
76
+ points:options.rateLimitPoints || 100,
77
+ duration:options.rateLimitDuration || 60,
78
+ blockDuration: options.rateLimitBlock || 300,
79
+
80
+ })
81
+ }
82
+ this.redisClient.on("error",(err)=>
83
+ console.error("Security Redis error:",err)
84
+ );
85
+ this.redisClient.on("connect",() =>{
86
+ console.log("SecurityRedis Connected to:",this.redisClient.options.host);
87
+
88
+ })
89
+ }
90
+
91
+
92
+ async isAllowed(ip:string):Promise<boolean>{
93
+ if(!this.whiteListEnabled)
94
+ return true;
95
+
96
+ try{
97
+ const exists = await this.redisClient.sismember(WHITELIST_KEY,ip);
98
+ if (exists ===1) return true;
99
+
100
+
101
+ if( ip === "::1" || ip === "127.0.0.1")
102
+ return true;
103
+ const entries = await this.redisClient.smembers(WHITELIST_KEY)
104
+
105
+ for (const entry of entries){
106
+ if(entry.includes("/"))
107
+ {
108
+ const CIDR = (await import("ip-cidr")).default;
109
+ if(new CIDR(entry).contains(ip)) return true;
110
+ }
111
+ }
112
+ return false;
113
+
114
+ }catch(err){
115
+ console.error("whitelist check error:",err);
116
+ return false;
117
+ }
118
+ }
119
+
120
+ async addIP(ipOrCIDR:string,ttl:number =IP_EXPIRATION_SECONDS){
121
+ await this.redisClient.sadd(WHITELIST_KEY,ipOrCIDR);
122
+ await this.redisClient.expire(WHITELIST_KEY,ttl)
123
+ }
124
+ async removeIP(ipOrCIDR:string){
125
+ await this.redisClient.srem(WHITELIST_KEY,ipOrCIDR);
126
+ }
127
+
128
+ async listIPs(): Promise<string[]>{
129
+ return await this.redisClient.smembers(WHITELIST_KEY);
130
+ }
131
+
132
+ whiteListMiddleware(){
133
+ return async(req: Request,res:Response, next:NextFunction)=>{
134
+ if(!this.whiteListEnabled) return next();
135
+
136
+ const clientIP = req.headers["x-forwarded-for"]?.toString().split(",")[0]?.trim() || req.ip!;
137
+
138
+ if(await this.isAllowed(clientIP!))return next();
139
+ res.status(403).json({error:"Access denied"});
140
+ }
141
+ }
142
+ rateLimiterMiddleware(){
143
+ return (req:Request, res:Response, next:NextFunction)=>{
144
+ if (!this.rateLimiter || !this.rateLimiterEnabled)return next();
145
+ const ip = req.ip || req.socket.remoteAddress || "unknown";
146
+ this.rateLimiter.consume(ip).then(()=> next()).catch(()=> res.status(429).send("Too many Requests"));
147
+ };
148
+ }
149
+
150
+ helmetMiddleware():RequestHandler{
151
+ if(!this.helmetEnabled){
152
+ return(req: Request, res:Response, next:NextFunction)=> next();}
153
+
154
+ const helmetDirectives={
155
+ defaultSrc:["'self'"],
156
+ scriptSrc:["'self'","'unsafe-inline'","trusted-cdn.com"],
157
+ styleSrc:["'self'"],
158
+ imgSrc:["'self'","data:","trusted-cdn.com"],
159
+ fontSrc:["'self'","trusted-cdn.com"],
160
+ connectSrc:["'self'","api.trusted-domain.com"],
161
+ frameSrc:["'none'"],
162
+ objectSrc:["'none'"],
163
+ upgradeInsecureRequests: [],
164
+ reportUri: "/csp-violation-report",
165
+ };
166
+
167
+ return helmet({
168
+ contentSecurityPolicy:{
169
+ directives:helmetDirectives, reportOnly:process.env.NODE_ENV !== "production"},
170
+ hsts:{maxAge:315366000,
171
+ includeSubDomains:true,preload:true},
172
+ xxsFilter:true,
173
+ noSniff:true,
174
+ frameguard:{action:"deny"},
175
+ referrerPolicy:{policy:"same-origin"},
176
+ }as HelmetOptions);
177
+ }
178
+ }
179
+
180
+
@@ -0,0 +1,116 @@
1
+ import dotenv from "dotenv";
2
+ import Redis,{Redis as RedisClient} from "ioredis"
3
+ import {setupRedis} from "../redis/redisService"
4
+ import {Request, Response, NextFunction} from "express"
5
+ dotenv.config()
6
+
7
+
8
+ class TokenBucket{
9
+ private redis: RedisClient
10
+
11
+
12
+ constructor(redisClient:RedisClient){
13
+ this.redis = redisClient;
14
+ }
15
+
16
+ async consume(key:string, capacity:number, refillRate:number): Promise<{
17
+ allowed:boolean;
18
+ remaining:number;
19
+ retryAfter?:number;
20
+ }>{
21
+ const now = Date.now();
22
+ const results = await this.redis
23
+ .pipeline()
24
+ .hgetall(`rate_limit:${key}`)
25
+ .exec();
26
+ const data= results?.[0]?.[1] ?? {}
27
+ const bucket = data as {tokens?: string;lastRefill?: string}
28
+ || {} ;
29
+ const currentToken = parseFloat(bucket.tokens || capacity.toString ());
30
+ const lastRefill = parseFloat(bucket.lastRefill || now.toString())
31
+ const timeElapsed= (now - lastRefill) / 1000;
32
+ const newToken = Math.min(capacity, currentToken + (timeElapsed * refillRate))
33
+
34
+ if (newToken < 1){
35
+ return{
36
+ allowed:false,
37
+ remaining:Math.floor(newToken),
38
+ retryAfter:Math.ceil((1 - newToken) / refillRate)
39
+ }
40
+ }
41
+ await this.redis.hset(`rate_limit:${key}`,{
42
+ tokens:(newToken - 1).toString(),
43
+ lastRefill: now.toString()
44
+ })
45
+ this.redis.expire(`rate_limit:${key}`, 3600)
46
+
47
+
48
+
49
+ return {allowed:true ,remaining:Math.floor(newToken - 1)};
50
+ }
51
+ }
52
+
53
+ let tokenBucket: TokenBucket;
54
+
55
+
56
+ export const initializeRateLimiter = async () => {
57
+ const redisClient = (await setupRedis()).redisClient;
58
+ tokenBucket = new TokenBucket(redisClient);
59
+ };
60
+ const createRatelimiter = (config:{
61
+ capacity:number;
62
+ refillRate: number;
63
+ keyGenerator: (req:Request)=> string;
64
+ })=>{
65
+ return async(req:Request, res:Response, next:NextFunction): Promise<void> =>{
66
+ const key = config.keyGenerator(req);
67
+ const {allowed, remaining, retryAfter}= await tokenBucket.consume(
68
+ key,
69
+ config.capacity,
70
+ config.refillRate
71
+ )
72
+ res.set({
73
+ "X-RateLimit-Limit": config.capacity.toString(),
74
+ "X-RateLimit-Remaining":remaining.toString(),
75
+ ...(!allowed && {"Retry-After": retryAfter?.toString() || "1"})
76
+ })
77
+ if(allowed){
78
+
79
+ return next();
80
+ }else{
81
+ res.status(429).json({
82
+ error:`Too many requests`
83
+ })
84
+ }
85
+ }
86
+ }
87
+
88
+
89
+ const RATE_LIMIT_CONFIGS= {
90
+ OTP:{
91
+ keyPrefix:"otp_limiter",
92
+ refillRate: 0.1,
93
+ capacity:3,
94
+ keyGenerator: (req:Request)=>{
95
+ const email =req.body?.email;
96
+ return email || req.ip || "unknown"
97
+ }
98
+ },
99
+
100
+
101
+ LOGIN:{
102
+ keyPrefix:"login_limiter",
103
+ capacity:10,
104
+ refillRate:2,
105
+ keyGenerator:(req: Request): string => req.ip || "unknown"
106
+ }
107
+ };
108
+
109
+ initializeRateLimiter().catch((err) => {
110
+ console.error("Failed to initialize rate limiters:", err);
111
+ process.exit(1);
112
+ })
113
+
114
+
115
+ export const OTPLimiterMiddleware =createRatelimiter(RATE_LIMIT_CONFIGS.OTP);
116
+ export const LoginLimiterMiddleware = () => createRatelimiter(RATE_LIMIT_CONFIGS.LOGIN);
@@ -0,0 +1,99 @@
1
+
2
+ import { Redis } from "ioredis";
3
+
4
+ export class RedisTokenStore {
5
+ private prefix = "auth:v1";
6
+
7
+ constructor(private redis: Redis,private debug = false) {}
8
+
9
+
10
+ private key(...parts:string[]){
11
+ return `${this.prefix}:${parts.join(":")}`;
12
+ }
13
+
14
+ private log(action:string,key:string, value?:any){
15
+ if (this.debug){
16
+ console.log(`[Redis ${action}]`,{key, value})
17
+ }
18
+ }
19
+ async storeRefreshToken(token:string,userId:string,ttl:number){
20
+ const key = this.key("refresh", token);
21
+ await this.redis.set(key,userId, "EX",ttl);
22
+ this.log("SET", key, userId)
23
+
24
+ }
25
+
26
+ async getRefreshToken(token:string){
27
+ const key = this.key("refresh",token);
28
+ const value = await this.redis.get(key);
29
+ this.log("GET",key , value);
30
+ return value;
31
+ }
32
+
33
+ async deleteRefreshToken(token:string){
34
+ const key = this.key("refresh",token);
35
+ await this.redis.del(key);
36
+ this.log("DEL",key);
37
+
38
+ }
39
+
40
+ async blacklistToken(token: string, ttl: number) {
41
+ const key = this.key("blacklist", token);
42
+ await this.redis.set(key, "1", "EX", ttl);
43
+ this.log("SET", key, "blacklisted");
44
+ }
45
+
46
+ async isBlacklisted(token: string) {
47
+ const key = this.key("blacklist", token);
48
+ const exists = await this.redis.exists(key);
49
+ this.log("CHECK", key, exists);
50
+ return exists === 1;
51
+ }
52
+
53
+ // Rate Limiting
54
+ async incrementRateLimit(ip: string, ttl: number) {
55
+ const key = this.key("rate", ip);
56
+
57
+ const count = await this.redis.incr(key);
58
+ if (count === 1) {
59
+ await this.redis.expire(key, ttl);
60
+ }
61
+
62
+ this.log("INCR", key, count);
63
+ return count;
64
+ }
65
+
66
+ // IP Whitelist
67
+ async addToWhitelist(ip: string) {
68
+ const key = this.key("whitelist", ip);
69
+ await this.redis.set(key, "1");
70
+ this.log("SET", key, "whitelisted");
71
+ }
72
+
73
+ async removeFromWhitelist(ip: string) {
74
+ const key = this.key("whitelist", ip);
75
+ await this.redis.del(key);
76
+ this.log("DEL", key);
77
+ }
78
+
79
+ async isWhitelisted(ip: string) {
80
+ const key = this.key("whitelist", ip);
81
+ const exists = await this.redis.exists(key);
82
+ this.log("CHECK", key, exists);
83
+ return exists === 1;
84
+ }
85
+ async set(key: string, value: string, expiry?: number): Promise<void> {
86
+ console.log("REDIS SET:", key, value);
87
+
88
+ if (expiry) {
89
+ await this.redis.set(key, value, "EX", expiry);
90
+ } else {
91
+ await this.redis.set(key, value);
92
+ }
93
+ }
94
+
95
+
96
+ async get(key: string): Promise<string | null> {
97
+ return this.redis.get(key);
98
+ }
99
+ }
@@ -0,0 +1,16 @@
1
+ import {UserStore} from "../types/storage"
2
+
3
+ export class Store{
4
+ constructor(private userStore:UserStore){}
5
+
6
+ async register(email:string, password:string){
7
+ const exists = await this.userStore.findByEmail(email)
8
+
9
+ if (exists){
10
+ throw new Error("If a record of user exists an email will be sent");
11
+ }
12
+ return
13
+ this.userStore.create({email,password});
14
+ }
15
+
16
+ }