Some progress

This commit is contained in:
Evert Prants 2022-08-17 21:56:47 +03:00
parent 3f399320b6
commit a21a2257db
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
19 changed files with 87 additions and 15 deletions

9
.vscode/settings.json vendored Normal file
View File

@ -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
}
}

View File

@ -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' }))),
);

19
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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,

View File

@ -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') {

View File

@ -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();
}

View File

@ -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);

View File

@ -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

View File

@ -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,

View File

@ -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 (

View File

@ -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.');
}

View File

@ -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(

View File

@ -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 */ `
<h1>Icy Network</h1>
<p><strong>Hello, ${username}! You have requested a password reset on Icy Network.</strong></p>

View File

@ -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 */ `
<h1>Icy Network</h1>
<p><strong>Welcome to Icy Network, ${username}!</strong></p>

View File

@ -25,14 +25,26 @@ export class UserService {
) {}
public async getById(id: number, relations?: string[]): Promise<User> {
if (!id) {
return null;
}
return this.userRepository.findOne({ id }, { relations });
}
public async getByUUID(uuid: string, relations?: string[]): Promise<User> {
if (!uuid) {
return null;
}
return this.userRepository.findOne({ uuid }, { relations });
}
public async getByEmail(email: string, relations?: string[]): Promise<User> {
if (!email) {
return null;
}
return this.userRepository.findOne({ email }, { relations });
}
@ -40,6 +52,10 @@ export class UserService {
username: string,
relations?: string[],
): Promise<User> {
if (!username) {
return null;
}
return this.userRepository.findOne({ username }, { relations });
}

View File

@ -15,10 +15,16 @@ export class FormUtilityService {
);
}
public trimmed<T>(entry: T, fields: string[]): T {
return fields.reduce<T>(
(object, key) => ({ ...object, [key]: object[key]?.trim() }),
entry,
);
}
/**
* Include CSRF token, messages and prefilled form values for a template with a form
* @param req Express request
* @param session Express session
* @param additional Additional data to pass
* @returns Template locals
*/

View File

@ -18,7 +18,7 @@ export class TokenService {
constructor(private config: ConfigurationService) {}
public verifyCSRF(req: Request, token?: string): boolean {
return this.csrf.verify(req.session.csrf, token || req.body._csrf);
return this.csrf.verify(req.cookies.XSRF, token || req.body._csrf);
}
public generateString(length: number): string {

View File

@ -17,7 +17,7 @@ block settings
h2 Change Password
form(method="post", action="/account/security/password", autocomplete="off")
div.form-container
input#csrf(type="hidden", name="_csrf", value=csrf)
input#csrfa(type="hidden", name="_csrf", value=csrf)
label.form-label(for="password") Current Password
input.form-control#password(type="password", name="password")
label.form-label(for="new_password") New Password
@ -29,7 +29,7 @@ block settings
h2 Change Email Address
form(method="post", action="/account/security/email", autocomplete="off")
div.form-container
input#csrf(type="hidden", name="_csrf", value=csrf)
input#csrfb(type="hidden", name="_csrf", value=csrf)
label.form-label(for="current_email") Current Email Address
input.form-control#current_email(type="email", name="current_email")
small.form-hint Hint: #{emailHint}