Skip to content

Commit 67a2668

Browse files
committed
feat: Add cookie authentication
1 parent 6c37c1d commit 67a2668

File tree

15 files changed

+204
-18
lines changed

15 files changed

+204
-18
lines changed

.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,7 @@ JOBS_RETENTION_HOURS=24
2424

2525
OTP_EXPIRATION_MINUTES=15
2626
ENABLE_RATE_LIMIT='true'
27+
COOKIE_SECRET="secret"
28+
COOKIE_EXPIRATION_SECONDS=86400 # 24 hours
29+
ENABLE_COOKIE="true"
30+
ENABLE_JWT="true"

.github/workflows/node.js.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ env:
3131
JOBS_RETENTION_HOURS: '24'
3232
OTP_EXPIRATION_MINUTES: '15'
3333
ENABLE_RATE_LIMIT: 'true'
34+
COOKIE_SECRET: 'secret'
35+
COOKIE_EXPIRATION_SECONDS: '3600'
36+
ENABLE_COOKIE: 'true'
37+
ENABLE_JWT: 'true'
3438

3539
jobs:
3640
build:

.woodpecker/.backend-ci.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ x-common: &common
2323
- JOBS_RETENTION_HOURS=24
2424
- OTP_EXPIRATION_MINUTES=15
2525
- ENABLE_RATE_LIMIT=true
26+
- COOKIE_SECRET=secret
27+
- COOKIE_EXPIRATION_SECONDS=3600
28+
- ENABLE_COOKIE=true
29+
- ENABLE_JWT=true
2630

2731
pipeline:
2832
setup:

package-lock.json

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

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,13 @@
6363
},
6464
"dependencies": {
6565
"@prisma/client": "^5.5.2",
66+
"@types/cookie-parser": "^1.4.6",
6667
"bcryptjs": "^2.4.3",
6768
"body-parser": "^1.19.2",
6869
"bullmq": "^4.13.2",
6970
"compression": "^1.7.4",
7071
"concurrently": "^8.2.0",
72+
"cookie-parser": "^1.4.6",
7173
"cors": "^2.8.5",
7274
"cross-spawn": "^7.0.3",
7375
"date-fns": "^2.30.0",

src/config/config.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,16 @@ const envVarsSchema = z
5555
'OTP EXPIRATION TIME must be a number',
5656
),
5757
ENABLE_RATE_LIMIT: z.string(),
58+
COOKIE_SECRET: z.string(),
59+
COOKIE_EXPIRATION_SECONDS: z
60+
.string()
61+
.transform((val) => Number(val))
62+
.refine(
63+
(val) => !Number.isNaN(val),
64+
'COOKIE EXPIRATION SECONDS must be a number',
65+
),
66+
ENABLE_COOKIE: z.string(),
67+
ENABLE_JWT: z.string(),
5868
})
5969
.passthrough();
6070

@@ -65,6 +75,8 @@ export const isTest = envVars.NODE_ENV === 'test';
6575
export const isProduction = envVars.NODE_ENV === 'production';
6676
export const hasToApplyRateLimit =
6777
envVars.ENABLE_RATE_LIMIT.toLocaleLowerCase() === 'true';
78+
export const cookieEnabled = envVars.ENABLE_COOKIE === 'true';
79+
export const JWTEnabled = envVars.ENABLE_JWT === 'true';
6880

6981
export const config: Config = {
7082
env: envVars.NODE_ENV,
@@ -87,4 +99,6 @@ export const config: Config = {
8799
redisUsername: envVars.REDIS_USERNAME,
88100
jobsRetentionHours: envVars.JOBS_RETENTION_HOURS,
89101
otpExpirationMinutes: envVars.OTP_EXPIRATION_MINUTES,
102+
cookieSecret: envVars.COOKIE_SECRET,
103+
cookieExpirationSeconds: envVars.COOKIE_EXPIRATION_SECONDS,
90104
};

src/controllers/auth.ts

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import httpStatus from 'http-status';
22
import { Body, Controller, Post, Request, Route, Security } from 'tsoa';
3+
34
import { AuthService } from 'services/auth';
45
import {
56
CreateUserParams,
@@ -8,27 +9,48 @@ import {
89
AuthenticatedRequest,
910
LoginParams,
1011
} from 'types';
12+
import { cookieEnabled, JWTEnabled } from 'config/config';
13+
import { COOKIE_NAME, cookieConfig } from 'utils/auth';
1114

1215
@Route('v1/auth')
1316
export class AuthControllerV1 extends Controller {
1417
@Post('/register')
15-
public async register(@Body() user: CreateUserParams): Promise<ReturnAuth> {
16-
const authReturn = await AuthService.register(user);
18+
public async register(
19+
@Body() user: CreateUserParams,
20+
@Request() req: AuthenticatedRequest,
21+
): Promise<ReturnAuth | null> {
22+
const { sessionId, ...authReturn } = await AuthService.register(user);
23+
const { res } = req;
24+
if (cookieEnabled) {
25+
res?.cookie(COOKIE_NAME, sessionId, cookieConfig);
26+
}
1727
this.setStatus(httpStatus.CREATED);
18-
return authReturn;
28+
if (JWTEnabled) return authReturn;
29+
return null;
1930
}
2031

2132
@Post('/login')
22-
public async login(@Body() loginParams: LoginParams): Promise<ReturnAuth> {
23-
const authReturn = await AuthService.login(loginParams);
33+
public async login(
34+
@Body() loginParams: LoginParams,
35+
@Request() req: AuthenticatedRequest,
36+
): Promise<ReturnAuth | null> {
37+
const { sessionId, ...authReturn } = await AuthService.login(loginParams);
38+
const { res } = req;
39+
if (cookieEnabled) {
40+
res?.cookie(COOKIE_NAME, sessionId, cookieConfig);
41+
}
2442
this.setStatus(httpStatus.OK);
25-
return authReturn;
43+
if (JWTEnabled) return authReturn;
44+
return null;
2645
}
2746

2847
@Post('/logout')
48+
@Security('cookie')
2949
@Security('jwt')
3050
public async logout(@Request() req: AuthenticatedRequest): Promise<void> {
3151
await AuthService.logout(req.user.token);
52+
const { res } = req;
53+
res?.clearCookie(COOKIE_NAME);
3254
this.setStatus(httpStatus.OK);
3355
}
3456

src/controllers/users.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,14 @@ import {
1919
PasswordResetCodeRequest,
2020
ResetPassword,
2121
} from 'types';
22+
import { cookieEnabled } from 'config/config';
23+
import { COOKIE_NAME } from 'utils/auth';
2224

2325
@Route('v1/users')
2426
export class UsersControllerV1 extends Controller {
2527
@Get()
2628
@Security('jwt')
29+
@Security('cookie')
2730
public async index(): Promise<ReturnUser[]> {
2831
const users = await UserService.all();
2932
this.setStatus(httpStatus.OK);
@@ -32,6 +35,7 @@ export class UsersControllerV1 extends Controller {
3235

3336
@Get('/me')
3437
@Security('jwt')
38+
@Security('cookie')
3539
public async getMe(
3640
@Request() req: AuthenticatedRequest,
3741
): Promise<ReturnUser | null> {
@@ -42,6 +46,7 @@ export class UsersControllerV1 extends Controller {
4246

4347
@Get('{id}')
4448
@Security('jwt')
49+
@Security('cookie')
4550
public async find(@Path() id: string): Promise<ReturnUser | null> {
4651
const user = await UserService.find(id);
4752
this.setStatus(httpStatus.OK);
@@ -50,6 +55,7 @@ export class UsersControllerV1 extends Controller {
5055

5156
@Put('{id}')
5257
@Security('jwt')
58+
@Security('cookie')
5359
public async update(
5460
@Path() id: string,
5561
@Body() requestBody: UpdateUserParams,
@@ -61,8 +67,14 @@ export class UsersControllerV1 extends Controller {
6167

6268
@Delete('{id}')
6369
@Security('jwt')
64-
public async destroy(@Path() id: string): Promise<void> {
70+
@Security('cookie')
71+
public async destroy(
72+
@Path() id: string,
73+
@Request() req: AuthenticatedRequest,
74+
): Promise<void> {
75+
const { user, res } = req;
6576
await UserService.destroy(id);
77+
if (cookieEnabled && user.id === id) res?.clearCookie(COOKIE_NAME);
6678
this.setStatus(httpStatus.NO_CONTENT);
6779
}
6880

src/middlewares/auth.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { Request } from 'express';
22
import jwt from 'jsonwebtoken';
3-
import { config } from 'config/config';
3+
import { config, JWTEnabled } from 'config/config';
44
import { ApiError } from 'utils/apiError';
55
import { errors } from 'config/errors';
6+
import { verifyCookie } from 'utils/auth';
67

78
export function expressAuthentication(
89
request: Request,
@@ -14,7 +15,7 @@ export function expressAuthentication(
1415
const token = request.headers.authorization!;
1516

1617
return new Promise((resolve, reject) => {
17-
if (!token) {
18+
if (!token || !JWTEnabled) {
1819
reject(new ApiError(errors.UNAUTHENTICATED));
1920
}
2021
jwt.verify(token, config.accessTokenSecret, (err: any, decoded: any) => {
@@ -32,5 +33,10 @@ export function expressAuthentication(
3233
});
3334
});
3435
}
36+
if (securityName === 'cookie') {
37+
const { signedCookies } = request;
38+
39+
return verifyCookie(signedCookies);
40+
}
3541
return Promise.resolve(null);
3642
}

src/middlewares/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@ import express, { Application } from 'express';
22
import helmet from 'helmet';
33
import compression from 'compression';
44
import cors from 'cors';
5+
import cookieParser from 'cookie-parser';
6+
57
import { applyRateLimit } from 'middlewares/rateLimiter';
68
import { morganHandlers } from 'config/morgan';
79
import { errorConverter, errorHandler } from 'middlewares/error';
810
import { Wrapper } from 'types';
11+
import { config } from 'config/config';
912

1013
export const preRoutesMiddleware = (app: Application) => {
1114
// Set security HTTP headers
@@ -30,6 +33,8 @@ export const preRoutesMiddleware = (app: Application) => {
3033
app.use(morganHandlers.successHandler);
3134
app.use(morganHandlers.errorHandler);
3235
app.use(morganHandlers.debugHandler);
36+
37+
app.use(cookieParser(config.cookieSecret));
3338
};
3439

3540
// Middleware separated to use our error handler when a route is not found

0 commit comments

Comments
 (0)