Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: csrf and session integration using redis #30

Merged
merged 31 commits into from
Jun 9, 2023
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
a4634d2
chore: fix name in seed creator
jmcdo29 May 8, 2023
4fec975
chore: better dockerfile habits
jmcdo29 May 8, 2023
39acee2
chore: set defaults for nx nestt library
jmcdo29 May 8, 2023
834ca40
chore: create csrf library via nx
jmcdo29 May 8, 2023
f328839
chore: update caddy to handle url forwarding
jmcdo29 May 9, 2023
491ea45
feat: create redis module
jmcdo29 May 9, 2023
45f921d
chore: use swc for compilation over tsc
jmcdo29 May 11, 2023
4424d62
feat: add nest-cookies for cookie support
jmcdo29 May 11, 2023
1305f77
feat: create token creation for csrf
jmcdo29 May 11, 2023
1b7c654
feat: add redis dep and start session module
jmcdo29 May 11, 2023
31695f1
feat: make token module
jmcdo29 May 11, 2023
3beab83
chore: snapshot before disk changes
jmcdo29 May 17, 2023
b7b77e4
feat(session): guards for checking session info
jmcdo29 Jun 3, 2023
005c450
feat(csrf): create verify methods and guard for posts
jmcdo29 Jun 3, 2023
bfbe8b5
feat(session): create guard for session refresh
jmcdo29 Jun 3, 2023
6785df7
chore(docker): re-add docker executor schema
jmcdo29 Jun 3, 2023
a868c66
chore: create scripts to help populate db
jmcdo29 Jun 5, 2023
4360a16
chore: update production docker compose
jmcdo29 Jun 5, 2023
a3917da
fix: export the csrf guard from the library
jmcdo29 Jun 5, 2023
4955a7b
fix: export redis get options token
jmcdo29 Jun 5, 2023
1bc08ba
chore: remove unnecessary file
jmcdo29 Jun 5, 2023
4126870
chore: remove unnecessary file
jmcdo29 Jun 5, 2023
c22fac7
chore: re-add ui compoenent env file
jmcdo29 Jun 5, 2023
ca1645a
fix: update deity seed script
jmcdo29 Jun 5, 2023
c190681
feat: move server root module to library
jmcdo29 Jun 9, 2023
fe54331
chore: ignore pg dump file
jmcdo29 Jun 9, 2023
b25bfff
test: create e2e test to veryify cookies and csrf token
jmcdo29 Jun 9, 2023
184159b
chore: remove nx esbuild and the build from server-e2e
jmcdo29 Jun 9, 2023
b5e60c0
chore: better setup e2e test commands
jmcdo29 Jun 9, 2023
930fc95
chore: remove moved files that were originally copied
jmcdo29 Jun 9, 2023
af3108c
chore: remove unused files
jmcdo29 Jun 9, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Caddyfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
:80 {
root * ./dist/apps/site
file_server
try_files {path} /index.html
}
16 changes: 12 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
FROM node:18.15-alpine AS node-base
RUN npm i -g pnpm
RUN apk add dumb-init
RUN npm i -g pnpm && \
apk add --no-cache dumb-init=1.2.5-r2

FROM node-base AS common
WORKDIR /src
RUN apk add python3 make gcc g++
RUN apk add --no-cache \
python3=3.10.11-r0\
make=4.3-r1 \
gcc=12.2.1_git20220924-r4 \
g++=12.2.1_git20220924-r4
COPY package.json \
tsconfig* \
nx.json \
Expand Down Expand Up @@ -33,6 +37,7 @@ WORKDIR /src
COPY --from=server-build --chown=node:node /src/dist/apps/server ./
ENV NODE_ENV=production
RUN pnpm i
RUN pnpx node-prune
CMD ["dumb-init", "node", "main.js"]

####################
Expand All @@ -57,6 +62,7 @@ COPY --from=migrations-build --chown=node:node /src/dist ./dist
RUN cp ./dist/apps/kysely-cli/package.json ./package.json
ENV NODE_ENV=production
RUN pnpm i
RUN pnpx node-prune
CMD ["dumb-init", "node", "dist/apps/kysely-cli/main", "migrate"]

##############
Expand All @@ -76,4 +82,6 @@ RUN VITE_SERVER_URL="https://api.unteris.com" pnpm nx run site:build:production

FROM caddy:2.6.4-alpine as site-prod
WORKDIR /src
COPY --from=site-build /src/dist/apps/site/ /usr/share/caddy
COPY --from=site-build /src/dist/apps/site/ ./dist/apps/site
COPY Caddyfile ./Caddyfile
RUN ["caddy", "run", "--config", "Caddyfile"]
2 changes: 1 addition & 1 deletion apps/kysely-cli/src/app/kysely.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export class KyselyCliCommand extends CommandRunner {
throw new Error(migrationErrors.join('\n'));
}
} catch (err) {
this.logger.error(err);
this.logger.printError(err as Error);
} finally {
this.logger.log('Migrations Finished');
}
Expand Down
20 changes: 20 additions & 0 deletions apps/server/.swcrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@

{
"jsc": {
"parser": {
"syntax": "typescript",
"decorators": true
},
"transform": {
"legacyDecorator": true,
"decoratorMetadata": true
},
"target": "es2017",
"keepClassNames": true,
"baseUrl": "."
},
"module": {
"type": "commonjs",
"strictMode": true
}
}
24 changes: 23 additions & 1 deletion apps/server/src/app/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,40 @@
import { Module } from '@nestjs/common';
import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core';
import { ServerConfigModule } from '@unteris/server/config';
import { ServerCsrfModule } from '@unteris/server/csrf';
import { ServerDeitiesModule } from '@unteris/server/deities';
import { ServerLocationModule } from '@unteris/server/location';
import { ServerLoggingModule } from '@unteris/server/logging';
import {
ServerSessionModule,
SessionExistsGuard,
} from '@unteris/server/session';
import { CookieModule, CookiesInterceptor } from 'nest-cookies';

import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
imports: [
CookieModule,
ServerDeitiesModule,
ServerLocationModule,
ServerLoggingModule.forApplication('Unteris Server', 'DEBUG'),
ServerCsrfModule,
ServerSessionModule,
ServerConfigModule,
],
controllers: [AppController],
providers: [AppService],
providers: [
AppService,
{
provide: APP_GUARD,
useClass: SessionExistsGuard,
},
{
provide: APP_INTERCEPTOR,
useClass: CookiesInterceptor,
},
],
})
export class AppModule {}
9 changes: 8 additions & 1 deletion docker-compose.prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ services:
ports:
- '8080:80'
volumes:
- './images:/usr/share/caddy/images'
- './images:/src/images'
labels:
- 'com.centurylinklabs.watchtower.enable=true'
server:
Expand Down Expand Up @@ -50,3 +50,10 @@ services:
POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
POSTGRES_USER: ${DATABASE_USER}
POSTGRES_DB: ${DATABASE_NAME}
redis:
image: redis
ports:
- '6380:6379'
volumes:
- './redis.conf:/usr/local/etc/redis/redis.conf'
command: redis-server /usr/local/etc/redis/redis.conf
4 changes: 4 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@ services:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: unteris
redis:
image: redis
ports:
- '6379:6379'
9 changes: 9 additions & 0 deletions libs/server/config/src/lib/config.schema.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { z } from 'zod';

const hourInSeconds = 60 * 60;
const dayInSeconds = hourInSeconds * 24;

export const Config = z.object({
DATABASE_USER: z.string(),
DATABASE_PASSWORD: z.string(),
Expand All @@ -10,5 +13,11 @@ export const Config = z.object({
.optional(z.string().transform((val) => Number.parseInt(val, 10)))
.default('3333'),
CORS: z.optional(z.string()).default('http://localhost:4200'),
REDIS_URL: z.string(),
NODE_ENV: z.enum(['development', 'production']),
SESSION_EXPIRES_IN: z.number().optional().default(hourInSeconds),
REFRESH_EXPIRES_IN: z
.number()
.optional()
.default(7 * dayInSeconds),
});
18 changes: 18 additions & 0 deletions libs/server/csrf/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"extends": ["../../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}
7 changes: 7 additions & 0 deletions libs/server/csrf/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# server-csrf

This library was generated with [Nx](https://nx.dev).

## Running unit tests

Run `nx test server-csrf` to execute the unit tests via [Jest](https://jestjs.io).
11 changes: 11 additions & 0 deletions libs/server/csrf/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/* eslint-disable */
export default {
displayName: 'server-csrf',
preset: '../../../jest.preset.js',
testEnvironment: 'node',
transform: {
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../../coverage/libs/server/csrf',
};
30 changes: 30 additions & 0 deletions libs/server/csrf/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "server-csrf",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/server/csrf/src",
"projectType": "library",
"targets": {
"lint": {
"executor": "@nx/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["libs/server/csrf/**/*.ts"]
}
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/server/csrf/jest.config.ts",
"passWithNoTests": true
},
"configurations": {
"ci": {
"ci": true,
"codeCoverage": true
}
}
}
},
"tags": ["server", "security", "library"]
}
4 changes: 4 additions & 0 deletions libs/server/csrf/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './lib/csrf.controller';
export * from './lib/csrf.service';
export * from './lib/csrf.module';
export * from './lib/csrf.guard';
20 changes: 20 additions & 0 deletions libs/server/csrf/src/lib/csrf.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Test } from '@nestjs/testing';
import { ServerCsrfController } from './csrf.controller';
import { ServerCsrfService } from './csrf.service';

describe('ServerCsrfController', () => {
let controller: ServerCsrfController;

beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [ServerCsrfService],
controllers: [ServerCsrfController],
}).compile();

controller = module.get(ServerCsrfController);
});

it('should be defined', () => {
expect(controller).toBeTruthy();
});
});
20 changes: 20 additions & 0 deletions libs/server/csrf/src/lib/csrf.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Controller, Get, Post, Req, UseGuards } from '@nestjs/common';
import { CsrfGuard } from './csrf.guard';
import { ServerCsrfService } from './csrf.service';

@Controller('csrf')
export class ServerCsrfController {
constructor(private serverCsrfService: ServerCsrfService) {}
@Get()
async getCsrfToken(
@Req() { session }: { session: { id: string } }
): Promise<string> {
return this.serverCsrfService.generateToken(session.id);
}

@Post('verify')
@UseGuards(CsrfGuard)
verifyCsrf() {
return { success: true };
}
}
24 changes: 24 additions & 0 deletions libs/server/csrf/src/lib/csrf.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { ServerCsrfService } from './csrf.service';

const csrfHeader = 'x-unteris-csrf-protection';

@Injectable()
export class CsrfGuard implements CanActivate {
constructor(private readonly csrfService: ServerCsrfService) {}

async canActivate(context: ExecutionContext): Promise<boolean> {
const { headers, session, method } = context.switchToHttp().getRequest<{
headers: { [csrfHeader]: string };
session: { id: string };
[key: string]: any;
}>();
if (method === 'GET') {
return true;
}
return this.csrfService.verifyCsrfToken({
sessionId: session.id,
csrfToken: headers[csrfHeader],
});
}
}
13 changes: 13 additions & 0 deletions libs/server/csrf/src/lib/csrf.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { ServerSessionModule } from '@unteris/server/session';
import { ServerTokenModule } from '@unteris/server/token';
import { ServerCsrfController } from './csrf.controller';
import { ServerCsrfService } from './csrf.service';

@Module({
imports: [ServerTokenModule, ServerSessionModule],
controllers: [ServerCsrfController],
providers: [ServerCsrfService],
exports: [ServerCsrfService],
})
export class ServerCsrfModule {}
18 changes: 18 additions & 0 deletions libs/server/csrf/src/lib/csrf.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Test } from '@nestjs/testing';
import { ServerCsrfService } from './csrf.service';

describe('ServerCsrfService', () => {
let service: ServerCsrfService;

beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [ServerCsrfService],
}).compile();

service = module.get(ServerCsrfService);
});

it('should be defined', () => {
expect(service).toBeTruthy();
});
});
33 changes: 33 additions & 0 deletions libs/server/csrf/src/lib/csrf.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { ForbiddenException, Injectable } from '@nestjs/common';
import { ServerTokenService } from '@unteris/server/token';
import { ServerSessionService } from '@unteris/server/session';

@Injectable()
export class ServerCsrfService {
constructor(
private readonly tokenService: ServerTokenService,
private readonly sessionService: ServerSessionService
) {}
async generateToken(sessionId: string): Promise<string> {
const csrfToken = await this.tokenService.generateToken(256);
await this.sessionService.updateSession(sessionId, {
csrf: csrfToken,
});
return csrfToken;
}

async verifyCsrfToken({
sessionId,
csrfToken,
}: {
sessionId: string;
csrfToken: string;
}): Promise<boolean> {
const sessionData = await this.sessionService.getSession(sessionId);
if (!this.sessionService.isSession(sessionData)) {
throw new ForbiddenException('Invalid session data');
}
const { csrf } = sessionData;
return csrf === csrfToken;
}
}
Loading