Skip to content

Commit 637711f

Browse files
committed
feat: add cookie-parser and authentication middleware support
- Integrated cookie-parser for handling cookies in the application. - Added COOKIE_AUTH_ENABLED and COOKIE_AUTH_NONCE configuration options to manage cookie-based authentication. - Implemented checkAuthCookieMiddleware to validate authentication cookies.
1 parent b447dd7 commit 637711f

File tree

6 files changed

+154
-6
lines changed

6 files changed

+154
-6
lines changed

package-lock.json

Lines changed: 41 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
"class-transformer": "^0.5.1",
8080
"compression": "^1.8.0",
8181
"consola": "^3.4.2",
82+
"cookie-parser": "^1.4.7",
8283
"country-code-emoji": "^2.3.0",
8384
"dayjs": "^1.11.13",
8485
"enhanced-ms": "^4.1.0",
@@ -108,6 +109,7 @@
108109
"swagger-themes": "^1.4.3",
109110
"systeminformation": "^5.25.11",
110111
"table": "^6.9.0",
112+
"ufo": "^1.6.1",
111113
"winston": "^3.17.0",
112114
"xbytes": "^1.9.1",
113115
"yaml": "^2.7.1",
@@ -117,6 +119,7 @@
117119
"@nestjs/cli": "11.0.7",
118120
"@nestjs/schematics": "11.0.5",
119121
"@types/compression": "^1.7.5",
122+
"@types/cookie-parser": "^1.4.8",
120123
"@types/cors": "^2.8.18",
121124
"@types/express": "^5.0.1",
122125
"@types/js-yaml": "^4.0.9",
@@ -144,4 +147,4 @@
144147
"typescript": "~5.8.3",
145148
"typescript-eslint": "^8.32.1"
146149
}
147-
}
150+
}

src/common/config/app-config/config.schema.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,18 @@ export const configSchema = z
9090
),
9191
HWID_MAX_DEVICES_ANNOUNCE: z.optional(z.string()),
9292
PROVIDER_ID: z.optional(z.string()),
93+
94+
COOKIE_AUTH_ENABLED: z
95+
.string()
96+
.default('false')
97+
.transform((val) => val === 'true'),
98+
COOKIE_AUTH_NONCE: z.optional(
99+
z
100+
.string()
101+
.regex(/^[a-zA-Z0-9]+$/, 'Nonce can only contain letters and numbers')
102+
.max(64, 'Nonce must be less than 64 characters')
103+
.min(6, 'Nonce must be at least 6 characters'),
104+
),
93105
})
94106
.superRefine((data, ctx) => {
95107
if (data.WEBHOOK_ENABLED === 'true') {
@@ -214,6 +226,16 @@ export const configSchema = z
214226
});
215227
}
216228
}
229+
230+
if (data.COOKIE_AUTH_ENABLED) {
231+
if (!data.COOKIE_AUTH_NONCE) {
232+
ctx.addIssue({
233+
code: z.ZodIssueCode.custom,
234+
message: 'COOKIE_AUTH_NONCE is required when COOKIE_AUTH_ENABLED is true',
235+
path: ['COOKIE_AUTH_NONCE'],
236+
});
237+
}
238+
}
217239
});
218240

219241
export type ConfigSchema = z.infer<typeof configSchema>;
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { NextFunction, Request, Response } from 'express';
2+
import * as jwt from 'jsonwebtoken';
3+
import { nanoid } from 'nanoid';
4+
import { getQuery } from 'ufo';
5+
6+
import { Logger } from '@nestjs/common';
7+
8+
const logger = new Logger('CookieAuth');
9+
10+
export function checkAuthCookieMiddleware(authJwtSecret: string, nonce: string) {
11+
return (req: Request, res: Response, next: NextFunction) => {
12+
const query = getQuery(req.originalUrl);
13+
14+
if (query.nonce && query.nonce === nonce) {
15+
const token = signJwt(authJwtSecret);
16+
17+
res.cookie('nonce', token, {
18+
httpOnly: true,
19+
secure: true,
20+
maxAge: 2_592_000_000, // 30 days
21+
sameSite: 'strict',
22+
});
23+
24+
logger.warn(`Nonce access granted. Request: ${req.originalUrl}. IP: ${req.ip}`);
25+
26+
return next();
27+
}
28+
29+
if ('nonce' in req.cookies) {
30+
const isVerified = verifyJwt(req.cookies.nonce, authJwtSecret);
31+
32+
if (!isVerified) {
33+
logger.error(`Cookie mismatch. Request: ${req.originalUrl}. IP: ${req.ip}`);
34+
35+
res.socket?.destroy();
36+
37+
return;
38+
}
39+
40+
return next();
41+
}
42+
43+
res.socket?.destroy();
44+
45+
logger.error(`Cookie not found. Request: ${req.originalUrl}. IP: ${req.ip}`);
46+
47+
return;
48+
};
49+
}
50+
51+
function signJwt(authJwtSecret: string) {
52+
return jwt.sign({ sessionId: nanoid(48) }, authJwtSecret, {
53+
expiresIn: '2592000s',
54+
issuer: 'Remnawave',
55+
});
56+
}
57+
58+
function verifyJwt(token: string, authJwtSecret: string): boolean {
59+
try {
60+
const decoded = jwt.verify(token, authJwtSecret, {
61+
issuer: 'Remnawave',
62+
});
63+
64+
if (typeof decoded === 'object' && 'sessionId' in decoded) {
65+
return true;
66+
}
67+
68+
return false;
69+
} catch (error) {
70+
logger.error(`Cookie mismatch: ${error}`);
71+
72+
return false;
73+
}
74+
}

src/common/middlewares/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './basic-auth.middleware';
2+
export * from './check-auth-cookie.middleware';
23
export * from './get-real-ip';
34
export * from './proxy-check.middleware';

src/main.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { utilities as nestWinstonModuleUtilities, WinstonModule } from 'nest-winston';
22
import { patchNestJsSwagger, ZodValidationPipe } from 'nestjs-zod';
3+
import cookieParser from 'cookie-parser';
34
import { createLogger } from 'winston';
45
import compression from 'compression';
56
import * as winston from 'winston';
@@ -13,11 +14,10 @@ import { ROOT } from '@contract/api';
1314
import { ConfigService } from '@nestjs/config';
1415
import { NestFactory } from '@nestjs/core';
1516

17+
import { proxyCheckMiddleware, checkAuthCookieMiddleware, getRealIp } from '@common/middlewares';
1618
import { getDocs, isDevelopment, isProduction } from '@common/utils/startup-app';
1719
// import { ProxyCheckGuard } from '@common/guards/proxy-check/proxy-check.guard';
1820
import { getStartMessage } from '@common/utils/startup-app/get-start-message';
19-
import { getRealIp } from '@common/middlewares/get-real-ip';
20-
import { proxyCheckMiddleware } from '@common/middlewares';
2121
import { AxiosService } from '@common/axios';
2222

2323
import { AppModule } from './app.module';
@@ -110,6 +110,16 @@ async function bootstrap(): Promise<void> {
110110

111111
app.use(proxyCheckMiddleware);
112112

113+
if (config.getOrThrow<boolean>('COOKIE_AUTH_ENABLED')) {
114+
app.use(cookieParser());
115+
app.use(
116+
checkAuthCookieMiddleware(
117+
config.getOrThrow<string>('JWT_AUTH_SECRET'),
118+
config.getOrThrow<string>('COOKIE_AUTH_NONCE'),
119+
),
120+
);
121+
}
122+
113123
app.setGlobalPrefix(ROOT);
114124

115125
await getDocs(app, config);

0 commit comments

Comments
 (0)