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.
- package/.env +2 -0
- package/README.md +93 -0
- package/authenik8-core-0.1.0.tgz +0 -0
- package/dist/auth/guestModeService.d.ts +3 -0
- package/dist/auth/guestModeService.d.ts.map +1 -0
- package/dist/auth/guestModeService.js +24 -0
- package/dist/auth/guestModeService.js.map +1 -0
- package/dist/auth/jwtAuth.d.ts +28 -0
- package/dist/auth/jwtAuth.d.ts.map +1 -0
- package/dist/auth/jwtAuth.js +67 -0
- package/dist/auth/jwtAuth.js.map +1 -0
- package/dist/auth/refreshService.d.ts +41 -0
- package/dist/auth/refreshService.d.ts.map +1 -0
- package/dist/auth/refreshService.js +77 -0
- package/dist/auth/refreshService.js.map +1 -0
- package/dist/creatAuthenik8.d.ts +2 -0
- package/dist/creatAuthenik8.d.ts.map +1 -0
- package/dist/creatAuthenik8.js +3 -0
- package/dist/creatAuthenik8.js.map +1 -0
- package/dist/createAuthenik8.d.ts +4 -0
- package/dist/createAuthenik8.d.ts.map +1 -0
- package/dist/createAuthenik8.js +58 -0
- package/dist/createAuthenik8.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/adminService.d.ts +4 -0
- package/dist/middleware/adminService.d.ts.map +1 -0
- package/dist/middleware/adminService.js +40 -0
- package/dist/middleware/adminService.js.map +1 -0
- package/dist/redis/redisService.d.ts +26 -0
- package/dist/redis/redisService.d.ts.map +1 -0
- package/dist/redis/redisService.js +104 -0
- package/dist/redis/redisService.js.map +1 -0
- package/dist/security/ipService.d.ts +36 -0
- package/dist/security/ipService.d.ts.map +1 -0
- package/dist/security/ipService.js +160 -0
- package/dist/security/ipService.js.map +1 -0
- package/dist/security/limiter.d.ts +5 -0
- package/dist/security/limiter.d.ts.map +1 -0
- package/dist/security/limiter.js +93 -0
- package/dist/security/limiter.js.map +1 -0
- package/dist/storage/RedisTokenStore.d.ts +21 -0
- package/dist/storage/RedisTokenStore.d.ts.map +1 -0
- package/dist/storage/RedisTokenStore.js +86 -0
- package/dist/storage/RedisTokenStore.js.map +1 -0
- package/dist/storage/userStorage.d.ts +7 -0
- package/dist/storage/userStorage.d.ts.map +1 -0
- package/dist/storage/userStorage.js +18 -0
- package/dist/storage/userStorage.js.map +1 -0
- package/dist/tests/full.intergration.test.d.ts +2 -0
- package/dist/tests/full.intergration.test.d.ts.map +1 -0
- package/dist/tests/full.intergration.test.js +79 -0
- package/dist/tests/full.intergration.test.js.map +1 -0
- package/dist/tests/testApp.d.ts +7 -0
- package/dist/tests/testApp.d.ts.map +1 -0
- package/dist/tests/testApp.js +53 -0
- package/dist/tests/testApp.js.map +1 -0
- package/dist/types/admin.d.ts +6 -0
- package/dist/types/admin.d.ts.map +1 -0
- package/dist/types/admin.js +3 -0
- package/dist/types/admin.js.map +1 -0
- package/dist/types/config.d.ts +9 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +3 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/public.d.ts +17 -0
- package/dist/types/public.d.ts.map +1 -0
- package/dist/types/public.js +3 -0
- package/dist/types/public.js.map +1 -0
- package/dist/types/storage.d.ts +14 -0
- package/dist/types/storage.d.ts.map +1 -0
- package/dist/types/storage.js +3 -0
- package/dist/types/storage.js.map +1 -0
- package/dump.rdb +0 -0
- package/jest.config.js +11 -0
- package/package.json +56 -0
- package/src/1 +22 -0
- package/src/auth/guestModeService.ts +31 -0
- package/src/auth/jwtAuth.ts +99 -0
- package/src/auth/refreshService.ts +134 -0
- package/src/creatAuthenik8.ts +0 -0
- package/src/createAuthenik8.ts +66 -0
- package/src/index.ts +1 -0
- package/src/middleware/adminService.ts +50 -0
- package/src/redis/redisService.ts +137 -0
- package/src/security/ipService.ts +180 -0
- package/src/security/limiter.ts +116 -0
- package/src/storage/RedisTokenStore.ts +99 -0
- package/src/storage/userStorage.ts +16 -0
- package/src/tests/full.intergration.test.ts +100 -0
- package/src/tests/testApp.ts +56 -0
- package/src/types/admin.ts +7 -0
- package/src/types/config.ts +11 -0
- package/src/types/public.ts +22 -0
- package/src/types/storage.ts +15 -0
- 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
|
+
}
|