Skip to content

Commit

Permalink
feat: Add cookie authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
gabrielmachin committed Jan 16, 2024
1 parent 6c37c1d commit 67a2668
Show file tree
Hide file tree
Showing 15 changed files with 204 additions and 18 deletions.
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,7 @@ JOBS_RETENTION_HOURS=24

OTP_EXPIRATION_MINUTES=15
ENABLE_RATE_LIMIT='true'
COOKIE_SECRET="secret"
COOKIE_EXPIRATION_SECONDS=86400 # 24 hours
ENABLE_COOKIE="true"
ENABLE_JWT="true"
4 changes: 4 additions & 0 deletions .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ env:
JOBS_RETENTION_HOURS: '24'
OTP_EXPIRATION_MINUTES: '15'
ENABLE_RATE_LIMIT: 'true'
COOKIE_SECRET: 'secret'
COOKIE_EXPIRATION_SECONDS: '3600'
ENABLE_COOKIE: 'true'
ENABLE_JWT: 'true'

jobs:
build:
Expand Down
4 changes: 4 additions & 0 deletions .woodpecker/.backend-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ x-common: &common
- JOBS_RETENTION_HOURS=24
- OTP_EXPIRATION_MINUTES=15
- ENABLE_RATE_LIMIT=true
- COOKIE_SECRET=secret
- COOKIE_EXPIRATION_SECONDS=3600
- ENABLE_COOKIE=true
- ENABLE_JWT=true

pipeline:
setup:
Expand Down
54 changes: 54 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,13 @@
},
"dependencies": {
"@prisma/client": "^5.5.2",
"@types/cookie-parser": "^1.4.6",
"bcryptjs": "^2.4.3",
"body-parser": "^1.19.2",
"bullmq": "^4.13.2",
"compression": "^1.7.4",
"concurrently": "^8.2.0",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"cross-spawn": "^7.0.3",
"date-fns": "^2.30.0",
Expand Down
14 changes: 14 additions & 0 deletions src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,16 @@ const envVarsSchema = z
'OTP EXPIRATION TIME must be a number',
),
ENABLE_RATE_LIMIT: z.string(),
COOKIE_SECRET: z.string(),
COOKIE_EXPIRATION_SECONDS: z
.string()
.transform((val) => Number(val))
.refine(
(val) => !Number.isNaN(val),
'COOKIE EXPIRATION SECONDS must be a number',
),
ENABLE_COOKIE: z.string(),
ENABLE_JWT: z.string(),
})
.passthrough();

Expand All @@ -65,6 +75,8 @@ export const isTest = envVars.NODE_ENV === 'test';
export const isProduction = envVars.NODE_ENV === 'production';
export const hasToApplyRateLimit =
envVars.ENABLE_RATE_LIMIT.toLocaleLowerCase() === 'true';
export const cookieEnabled = envVars.ENABLE_COOKIE === 'true';
export const JWTEnabled = envVars.ENABLE_JWT === 'true';

export const config: Config = {
env: envVars.NODE_ENV,
Expand All @@ -87,4 +99,6 @@ export const config: Config = {
redisUsername: envVars.REDIS_USERNAME,
jobsRetentionHours: envVars.JOBS_RETENTION_HOURS,
otpExpirationMinutes: envVars.OTP_EXPIRATION_MINUTES,
cookieSecret: envVars.COOKIE_SECRET,
cookieExpirationSeconds: envVars.COOKIE_EXPIRATION_SECONDS,
};
34 changes: 28 additions & 6 deletions src/controllers/auth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import httpStatus from 'http-status';
import { Body, Controller, Post, Request, Route, Security } from 'tsoa';

import { AuthService } from 'services/auth';
import {
CreateUserParams,
Expand All @@ -8,27 +9,48 @@ import {
AuthenticatedRequest,
LoginParams,
} from 'types';
import { cookieEnabled, JWTEnabled } from 'config/config';
import { COOKIE_NAME, cookieConfig } from 'utils/auth';

@Route('v1/auth')
export class AuthControllerV1 extends Controller {
@Post('/register')
public async register(@Body() user: CreateUserParams): Promise<ReturnAuth> {
const authReturn = await AuthService.register(user);
public async register(
@Body() user: CreateUserParams,
@Request() req: AuthenticatedRequest,
): Promise<ReturnAuth | null> {
const { sessionId, ...authReturn } = await AuthService.register(user);
const { res } = req;
if (cookieEnabled) {
res?.cookie(COOKIE_NAME, sessionId, cookieConfig);
}
this.setStatus(httpStatus.CREATED);
return authReturn;
if (JWTEnabled) return authReturn;
return null;
}

@Post('/login')
public async login(@Body() loginParams: LoginParams): Promise<ReturnAuth> {
const authReturn = await AuthService.login(loginParams);
public async login(
@Body() loginParams: LoginParams,
@Request() req: AuthenticatedRequest,
): Promise<ReturnAuth | null> {
const { sessionId, ...authReturn } = await AuthService.login(loginParams);
const { res } = req;
if (cookieEnabled) {
res?.cookie(COOKIE_NAME, sessionId, cookieConfig);
}
this.setStatus(httpStatus.OK);
return authReturn;
if (JWTEnabled) return authReturn;
return null;
}

@Post('/logout')
@Security('cookie')
@Security('jwt')
public async logout(@Request() req: AuthenticatedRequest): Promise<void> {
await AuthService.logout(req.user.token);
const { res } = req;
res?.clearCookie(COOKIE_NAME);
this.setStatus(httpStatus.OK);
}

Expand Down
14 changes: 13 additions & 1 deletion src/controllers/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,14 @@ import {
PasswordResetCodeRequest,
ResetPassword,
} from 'types';
import { cookieEnabled } from 'config/config';
import { COOKIE_NAME } from 'utils/auth';

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

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

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

@Put('{id}')
@Security('jwt')
@Security('cookie')
public async update(
@Path() id: string,
@Body() requestBody: UpdateUserParams,
Expand All @@ -61,8 +67,14 @@ export class UsersControllerV1 extends Controller {

@Delete('{id}')
@Security('jwt')
public async destroy(@Path() id: string): Promise<void> {
@Security('cookie')
public async destroy(
@Path() id: string,
@Request() req: AuthenticatedRequest,
): Promise<void> {
const { user, res } = req;
await UserService.destroy(id);
if (cookieEnabled && user.id === id) res?.clearCookie(COOKIE_NAME);
this.setStatus(httpStatus.NO_CONTENT);
}

Expand Down
10 changes: 8 additions & 2 deletions src/middlewares/auth.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Request } from 'express';
import jwt from 'jsonwebtoken';
import { config } from 'config/config';
import { config, JWTEnabled } from 'config/config';
import { ApiError } from 'utils/apiError';
import { errors } from 'config/errors';
import { verifyCookie } from 'utils/auth';

export function expressAuthentication(
request: Request,
Expand All @@ -14,7 +15,7 @@ export function expressAuthentication(
const token = request.headers.authorization!;

return new Promise((resolve, reject) => {
if (!token) {
if (!token || !JWTEnabled) {
reject(new ApiError(errors.UNAUTHENTICATED));
}
jwt.verify(token, config.accessTokenSecret, (err: any, decoded: any) => {
Expand All @@ -32,5 +33,10 @@ export function expressAuthentication(
});
});
}
if (securityName === 'cookie') {
const { signedCookies } = request;

return verifyCookie(signedCookies);
}
return Promise.resolve(null);
}
5 changes: 5 additions & 0 deletions src/middlewares/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ import express, { Application } from 'express';
import helmet from 'helmet';
import compression from 'compression';
import cors from 'cors';
import cookieParser from 'cookie-parser';

import { applyRateLimit } from 'middlewares/rateLimiter';
import { morganHandlers } from 'config/morgan';
import { errorConverter, errorHandler } from 'middlewares/error';
import { Wrapper } from 'types';
import { config } from 'config/config';

export const preRoutesMiddleware = (app: Application) => {
// Set security HTTP headers
Expand All @@ -30,6 +33,8 @@ export const preRoutesMiddleware = (app: Application) => {
app.use(morganHandlers.successHandler);
app.use(morganHandlers.errorHandler);
app.use(morganHandlers.debugHandler);

app.use(cookieParser(config.cookieSecret));
};

// Middleware separated to use our error handler when a route is not found
Expand Down
Loading

0 comments on commit 67a2668

Please sign in to comment.