diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..8599e60 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "editor.formatOnSave": true, + "files.insertFinalNewline": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "eslint.validate": ["typescript", "typescriptreact", "html"], + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true + } +} diff --git a/ormconfig.js b/ormconfig.js index d0813d9..1bc00ba 100644 --- a/ormconfig.js +++ b/ormconfig.js @@ -9,6 +9,9 @@ dotenv.config(); const CONFIG_ENV = process.env.NODE_ENV === 'production' ? 'prod' : 'dev'; const CONFIG_FILENAME = process.env.CONFIG || `config.${CONFIG_ENV}.toml`; const CONFIG_PATH = join(process.cwd(), CONFIG_FILENAME); + +// toml.parse returns an object that doesn't have the correct prototype, +// thus this JSON workaround is used. const config = JSON.parse( JSON.stringify(toml.parse(readFileSync(CONFIG_PATH, { encoding: 'utf-8' }))), ); diff --git a/package-lock.json b/package-lock.json index 4125dfa..987be0f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,6 +49,7 @@ "@nestjs/testing": "^8.0.0", "@types/bcrypt": "^5.0.0", "@types/connect-redis": "^0.0.18", + "@types/cookie-parser": "^1.4.3", "@types/express": "^4.17.13", "@types/express-session": "^1.17.4", "@types/jest": "27.4.1", @@ -3146,6 +3147,15 @@ "@types/redis": "^2.8.0" } }, + "node_modules/@types/cookie-parser": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.3.tgz", + "integrity": "sha512-CqSKwFwefj4PzZ5n/iwad/bow2hTCh0FlNAeWLtQM3JA/NX/iYagIpWG2cf1bQKQ2c9gU2log5VUCrn7LDOs0w==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/cookiejar": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.2.tgz", @@ -14757,6 +14767,15 @@ "@types/redis": "^2.8.0" } }, + "@types/cookie-parser": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.3.tgz", + "integrity": "sha512-CqSKwFwefj4PzZ5n/iwad/bow2hTCh0FlNAeWLtQM3JA/NX/iYagIpWG2cf1bQKQ2c9gU2log5VUCrn7LDOs0w==", + "dev": true, + "requires": { + "@types/express": "*" + } + }, "@types/cookiejar": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.2.tgz", diff --git a/package.json b/package.json index eabe541..25eee8a 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "@nestjs/testing": "^8.0.0", "@types/bcrypt": "^5.0.0", "@types/connect-redis": "^0.0.18", + "@types/cookie-parser": "^1.4.3", "@types/express": "^4.17.13", "@types/express-session": "^1.17.4", "@types/jest": "27.4.1", diff --git a/src/app.module.ts b/src/app.module.ts index 4128aba..711ce56 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -17,6 +17,7 @@ import { OAuth2Module } from './modules/features/oauth2/oauth2.module'; import { RegisterModule } from './modules/features/register/register.module'; import { SettingsModule } from './modules/features/settings/settings.module'; import { TwoFactorModule } from './modules/features/two-factor/two-factor.module'; +import { JWTModule } from './modules/jwt/jwt.module'; import { DatabaseModule } from './modules/objects/database/database.module'; import { EmailModule } from './modules/objects/email/email.module'; import { OAuth2ClientModule } from './modules/objects/oauth2-client/oauth2-client.module'; @@ -47,6 +48,7 @@ import { UtilityModule } from './modules/utility/utility.module'; LoginModule, RegisterModule, OAuth2Module, + JWTModule, TwoFactorModule, SettingsModule, PrivilegeModule, diff --git a/src/main.ts b/src/main.ts index dba9d5c..41f7564 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,6 +4,7 @@ import * as dotenv from 'dotenv'; import * as session from 'express-session'; import * as connectRedis from 'connect-redis'; import * as redis from 'redis'; +import * as cookieParser from 'cookie-parser'; import { join } from 'path'; import { NestExpressApplication } from '@nestjs/platform-express'; @@ -19,7 +20,7 @@ async function bootstrap() { }); // app.use(express.urlencoded()); - // app.use(cookieParser()); + app.use(cookieParser()); // Production servers have to be behind a proxy. if (process.env.NODE_ENV === 'production') { diff --git a/src/middleware/csrf.middleware.ts b/src/middleware/csrf.middleware.ts index af3b3f0..0192791 100644 --- a/src/middleware/csrf.middleware.ts +++ b/src/middleware/csrf.middleware.ts @@ -2,17 +2,24 @@ import { Injectable, NestMiddleware } from '@nestjs/common'; import { NextFunction, Request, Response } from 'express'; import { TokenService } from 'src/modules/utility/services/token.service'; +const DEV = process.env.NODE_ENV !== 'production'; + @Injectable() export class CSRFMiddleware implements NestMiddleware { constructor(private readonly tokenService: TokenService) {} use(req: Request, res: Response, next: NextFunction) { - // TODO: do not store in session, keep the amount of pointless sessions down - if (!req.session.csrf) { - req.session.csrf = this.tokenService.csrf.secretSync(); + let secretToken = req.cookies.XSRF; + if (!secretToken) { + secretToken = this.tokenService.csrf.secretSync(); + res.cookie('XSRF', secretToken, { + maxAge: 60 * 60 * 1000, + secure: !DEV, + sameSite: 'strict', + }); } - req.csrfToken = () => this.tokenService.csrf.create(req.session.csrf); + req.csrfToken = () => this.tokenService.csrf.create(secretToken); next(); } diff --git a/src/modules/config/config.providers.ts b/src/modules/config/config.providers.ts index d5a9301..0e7a8d3 100644 --- a/src/modules/config/config.providers.ts +++ b/src/modules/config/config.providers.ts @@ -48,7 +48,7 @@ export const configProviders = [ const file = await readFile(path, { encoding: 'utf-8' }); return { ...def, - ...toml.parse(file), + ...JSON.parse(JSON.stringify(toml.parse(file))), }; } catch (e: any) { console.error('Failed to load configuration:', e.message); diff --git a/src/modules/features/login/login.controller.ts b/src/modules/features/login/login.controller.ts index 6939d4f..67a51e6 100644 --- a/src/modules/features/login/login.controller.ts +++ b/src/modules/features/login/login.controller.ts @@ -53,7 +53,7 @@ export class LoginController { @Body() body: { username: string; password: string }, @Query('redirectTo') redirectTo?: string, ) { - const { username, password } = body; + const { username, password } = this.formUtil.trimmed(body, ['username']); const user = await this.userService.getByUsername(username); // User exists and password matches diff --git a/src/modules/features/oauth2/oauth2.service.ts b/src/modules/features/oauth2/oauth2.service.ts index 1de7e93..32bf81d 100644 --- a/src/modules/features/oauth2/oauth2.service.ts +++ b/src/modules/features/oauth2/oauth2.service.ts @@ -1,5 +1,6 @@ import { OAuth2AdapterModel, OAuth2Provider } from '@icynet/oauth2-provider'; import { Injectable } from '@nestjs/common'; +import { ConfigurationService } from 'src/modules/config/config.service'; import { OAuth2ClientService } from 'src/modules/objects/oauth2-client/oauth2-client.service'; import { OAuth2TokenService } from 'src/modules/objects/oauth2-token/oauth2-token.service'; import { UserService } from 'src/modules/objects/user/user.service'; @@ -55,6 +56,7 @@ export class OAuth2Service { constructor( public token: TokenService, + public config: ConfigurationService, public userService: UserService, public clientService: OAuth2ClientService, public tokenService: OAuth2TokenService, diff --git a/src/modules/features/register/register.controller.ts b/src/modules/features/register/register.controller.ts index 9d0417a..1b49d36 100644 --- a/src/modules/features/register/register.controller.ts +++ b/src/modules/features/register/register.controller.ts @@ -35,7 +35,8 @@ export class RegisterController { @Body() body: RegisterDto, @Query('redirectTo') redirectTo?: string, ) { - const { username, display_name, email, password, password_repeat } = body; + const { username, display_name, email, password, password_repeat } = + this.formUtil.trimmed(body, ['username', 'display_name', 'email']); try { if ( diff --git a/src/modules/features/settings/settings.controller.ts b/src/modules/features/settings/settings.controller.ts index 99cf18b..ad383cb 100644 --- a/src/modules/features/settings/settings.controller.ts +++ b/src/modules/features/settings/settings.controller.ts @@ -56,7 +56,7 @@ export class SettingsController { @Body() body: { display_name?: string }, ) { try { - const { display_name } = body; + const { display_name } = this._form.trimmed(body, ['display_name']); if (!display_name) { throw new Error('Display name is required.'); } diff --git a/src/modules/jwt/jwt.service.ts b/src/modules/jwt/jwt.service.ts index 621c81a..8c922d7 100644 --- a/src/modules/jwt/jwt.service.ts +++ b/src/modules/jwt/jwt.service.ts @@ -2,6 +2,11 @@ import { Inject, Injectable } from '@nestjs/common'; import { ConfigurationService } from '../config/config.service'; import * as jwt from 'jsonwebtoken'; +/** + * Generate JWTs using the following commands: + * Private: ssh-keygen -t rsa -b 4096 -m PEM -f jwt.private.pem + * Public: openssl rsa -in jwt.private.pem -pubout -outform PEM -out jwt.public.pem + */ @Injectable() export class JWTService { constructor( diff --git a/src/modules/objects/user/email/forgot-password.email.ts b/src/modules/objects/user/email/forgot-password.email.ts index 2f9438d..095456b 100644 --- a/src/modules/objects/user/email/forgot-password.email.ts +++ b/src/modules/objects/user/email/forgot-password.email.ts @@ -15,7 +15,7 @@ Change your password: ${url} If you did not request a password change on Icy Network, you can safely ignore this email. `, - html: ` + html: /* html */ `
Hello, ${username}! You have requested a password reset on Icy Network.
diff --git a/src/modules/objects/user/email/registration.email.ts b/src/modules/objects/user/email/registration.email.ts index 2b71e92..3e0a280 100644 --- a/src/modules/objects/user/email/registration.email.ts +++ b/src/modules/objects/user/email/registration.email.ts @@ -13,7 +13,7 @@ In order to proceed with logging in, please click on the following link to activ Activate your account: ${url} `, - html: ` + html: /* html */ `Welcome to Icy Network, ${username}!
diff --git a/src/modules/objects/user/user.service.ts b/src/modules/objects/user/user.service.ts index 4a51e8c..1aa5b73 100644 --- a/src/modules/objects/user/user.service.ts +++ b/src/modules/objects/user/user.service.ts @@ -25,14 +25,26 @@ export class UserService { ) {} public async getById(id: number, relations?: string[]): Promise