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 Dec 8, 2023
1 parent c14178f commit 558197f
Show file tree
Hide file tree
Showing 15 changed files with 204 additions and 18 deletions.
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,8 @@ REDIS_PASSWORD=''
REDIS_PORT=6379
REDIS_USERNAME=''
JOBS_RETENTION_HOURS=24

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 @@ -29,6 +29,10 @@ env:
REDIS_PORT: '6379'
REDIS_USERNAME: 'default'
JOBS_RETENTION_HOURS: '24'
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 @@ -21,6 +21,10 @@ x-common: &common
- REDIS_PORT=6379
- REDIS_USERNAME=default
- JOBS_RETENTION_HOURS=24
- 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 @@ -65,10 +65,12 @@
},
"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",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"cross-spawn": "^7.0.3",
"dotenv": "^16.0.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 @@ -47,6 +47,16 @@ const envVarsSchema = z
'JOBS RETENTION HOURS must be a number',
),
REDIS_USERNAME: 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 @@ -55,6 +65,8 @@ const envVars = envVarsSchema.parse(process.env);
export const isDevelopment = envVars.NODE_ENV === 'development';
export const isTest = envVars.NODE_ENV === 'test';
export const isProduction = envVars.NODE_ENV === 'production';
export const cookieEnabled = envVars.ENABLE_COOKIE === 'true';
export const JWTEnabled = envVars.ENABLE_JWT === 'true';

export const config: Config = {
env: envVars.NODE_ENV,
Expand All @@ -76,4 +88,6 @@ export const config: Config = {
redisPort: envVars.REDIS_PORT,
redisUsername: envVars.REDIS_USERNAME,
jobsRetentionHours: envVars.JOBS_RETENTION_HOURS,
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 @@ -12,11 +12,14 @@ import {
} from 'tsoa';
import { UserService } from 'services';
import { ReturnUser, UpdateUserParams, AuthenticatedRequest } 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 @@ -25,6 +28,7 @@ export class UsersControllerV1 extends Controller {

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

@Put('{id}')
@Security('jwt')
@Security('cookie')
public async update(
@Path() id: string,
@Body() requestBody: UpdateUserParams,
Expand All @@ -54,8 +60,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);
}
}
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);
}
4 changes: 4 additions & 0 deletions src/middlewares/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import express, { Application } from 'express';
import helmet from 'helmet';
import compression from 'compression';
import cors from 'cors';
import cookieParser from 'cookie-parser';
import { globalLimiter } 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 +32,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 558197f

Please sign in to comment.