diff --git a/apps/backend/package.json b/apps/backend/package.json index f28961b..11e7307 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -27,6 +27,7 @@ "@nestjs/core": "^11.0.1", "@nestjs/passport": "^11.0.5", "@nestjs/platform-express": "^11.0.1", + "@types/jsonwebtoken": "^9.0.10", "@types/passport-local": "^1.0.38", "argon2": "^0.44.0", "dayjs": "^1.11.13", @@ -34,6 +35,7 @@ "ejs": "^3.1.10", "i18next": "^25.3.2", "i18next-fs-backend": "^2.6.0", + "jsonwebtoken": "^9.0.2", "kysely": "^0.28.3", "nodemailer": "^7.0.5", "passport": "^0.7.0", diff --git a/apps/backend/src/app/auth/auth-core/auth-core.service.ts b/apps/backend/src/app/auth/auth-core/auth-core.service.ts index 3060765..1c4ed33 100644 --- a/apps/backend/src/app/auth/auth-core/auth-core.service.ts +++ b/apps/backend/src/app/auth/auth-core/auth-core.service.ts @@ -1,20 +1,23 @@ import { Injectable } from '@nestjs/common'; import { AuthRegistry } from './auth-registry'; -import { generateSecureToken } from '../../../lib/crypto'; import * as dayjs from 'dayjs'; import { isDefined, ResetAuth } from '@my-monorepo/common'; +import { CryptoService } from '../../../services/core/crypto/crypto.service'; const RESET_TOKEN_VALIDITY_MINUTES = 15; @Injectable() export class AuthCoreService { - constructor(private authRegistry: AuthRegistry) {} + constructor( + private authRegistry: AuthRegistry, + private cryptoService: CryptoService, + ) {} async addNewResetToken( userId: string, ): Promise<{ token: string; user_id: string }> { const res = await this.authRegistry.addResetToken({ - token: generateSecureToken(), + token: this.cryptoService.generateSecureToken(), userId, validUntil: dayjs().add(RESET_TOKEN_VALIDITY_MINUTES, 'minutes'), }); diff --git a/apps/backend/src/app/auth/auth-feature/auth-controller.service.ts b/apps/backend/src/app/auth/auth-feature/auth-controller.service.ts index 487d3e1..f079b89 100644 --- a/apps/backend/src/app/auth/auth-feature/auth-controller.service.ts +++ b/apps/backend/src/app/auth/auth-feature/auth-controller.service.ts @@ -6,13 +6,14 @@ import { import { UsersCoreService } from '../../users/users-core/users-core.service'; import { AuthCoreService } from '../auth-core/auth-core.service'; import { ResetAuth } from '@my-monorepo/common'; -import { hashPassword } from '../../../lib/crypto'; +import { CryptoService } from '../../../services/core/crypto/crypto.service'; @Injectable() export class AuthControllerService { constructor( private userCoreService: UsersCoreService, private authCoreService: AuthCoreService, + private cryptoService: CryptoService, ) {} async verifyCredentials() { @@ -30,7 +31,7 @@ export class AuthControllerService { } await this.userCoreService.setHashPassword( resetData.userId, - await hashPassword(resetData.newPassword), + await this.cryptoService.hashPassword(resetData.newPassword), ); } } diff --git a/apps/backend/src/lib/crypto.ts b/apps/backend/src/lib/crypto.ts deleted file mode 100644 index 1442e68..0000000 --- a/apps/backend/src/lib/crypto.ts +++ /dev/null @@ -1,17 +0,0 @@ -import * as crypto from 'crypto'; -import * as argon2 from 'argon2'; - -export function generateSecureToken(length = 32): string { - return crypto.randomBytes(length).toString('hex'); -} - -export function hashPassword(password: string): Promise { - return argon2.hash(password); -} - -export function verifyPassword( - passwordHash: string, - clearTextPassword: string, -): Promise { - return argon2.verify(passwordHash, clearTextPassword); -} diff --git a/apps/backend/src/services/core/core.module.ts b/apps/backend/src/services/core/core.module.ts index b7767f1..a6950fd 100644 --- a/apps/backend/src/services/core/core.module.ts +++ b/apps/backend/src/services/core/core.module.ts @@ -6,6 +6,7 @@ import { LoggerService } from './logger/logger.service'; import { MailerService } from './mailer/mailer.service'; import { TemplateRendererService } from './template-renderer/template-renderer.service'; import { LinkBuilderService } from './link-builder/link-builder.service'; +import { CryptoService } from './crypto/crypto.service'; const DECLARATIONS = [ DatabaseService, @@ -15,6 +16,7 @@ const DECLARATIONS = [ MailerService, TemplateRendererService, LinkBuilderService, + CryptoService, ]; @Global() diff --git a/apps/backend/src/services/core/crypto/crypto.service.spec.ts b/apps/backend/src/services/core/crypto/crypto.service.spec.ts new file mode 100644 index 0000000..607ee10 --- /dev/null +++ b/apps/backend/src/services/core/crypto/crypto.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { CryptoService } from './crypto.service'; + +describe('CryptoService', () => { + let service: CryptoService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [CryptoService], + }).compile(); + + service = module.get(CryptoService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/backend/src/services/core/crypto/crypto.service.ts b/apps/backend/src/services/core/crypto/crypto.service.ts new file mode 100644 index 0000000..4f85818 --- /dev/null +++ b/apps/backend/src/services/core/crypto/crypto.service.ts @@ -0,0 +1,41 @@ +import { Injectable } from '@nestjs/common'; +import * as crypto from 'crypto'; +import * as argon2 from 'argon2'; +import * as jwt from 'jsonwebtoken'; +import { EnvService } from '../env/env.service'; +import { isDefined } from '@my-monorepo/common'; + +@Injectable() +export class CryptoService { + constructor(private envService: EnvService) {} + + generateSecureToken(length = 32): string { + return crypto.randomBytes(length).toString('hex'); + } + + hashPassword(password: string): Promise { + return argon2.hash(password); + } + + verifyPassword( + passwordHash: string, + clearTextPassword: string, + ): Promise { + return argon2.verify(passwordHash, clearTextPassword); + } + + createJwt(payload: string | object): string { + return jwt.sign(payload, this.envService.config.secrets.authSign); + } + + async verifyJwt(token: string): Promise { + return await new Promise((resolve) => { + jwt.verify( + token, + this.envService.config.secrets.authSign, + (err, decoded) => + resolve(!isDefined(err) && isDefined(decoded) ? decoded : undefined), + ); + }); + } +} diff --git a/apps/backend/src/services/core/env/env.service.ts b/apps/backend/src/services/core/env/env.service.ts index b49a8ad..2c0fd70 100644 --- a/apps/backend/src/services/core/env/env.service.ts +++ b/apps/backend/src/services/core/env/env.service.ts @@ -14,6 +14,7 @@ const envParser = z MAIL_USER: z.string().email(), MAIL_PASSWORD: z.string(), APP_FRONTEND_URL: z.string(), + SECRET_AUTH_SIGN: z.string(), }) .transform((parsed) => ({ database: { @@ -32,6 +33,9 @@ const envParser = z app: { frontendUrl: parsed.APP_FRONTEND_URL, }, + secrets: { + authSign: parsed.SECRET_AUTH_SIGN, + }, })); export type EnvConfig = Readonly>; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a2fc8c3..8513acd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,6 +25,9 @@ importers: '@nestjs/platform-express': specifier: ^11.0.1 version: 11.1.3(@nestjs/common@11.1.3(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.3) + '@types/jsonwebtoken': + specifier: ^9.0.10 + version: 9.0.10 '@types/passport-local': specifier: ^1.0.38 version: 1.0.38 @@ -46,6 +49,9 @@ importers: i18next-fs-backend: specifier: ^2.6.0 version: 2.6.0 + jsonwebtoken: + specifier: ^9.0.2 + version: 9.0.2 kysely: specifier: ^0.28.3 version: 0.28.3 @@ -1081,12 +1087,18 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/jsonwebtoken@9.0.10': + resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} + '@types/methods@1.1.4': resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} '@types/mime@1.3.5': resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node@22.16.3': resolution: {integrity: sha512-sr4Xz74KOUeYadexo1r8imhRtlVXcs+j3XK3TcoiYk7B1t3YRVJgtaD3cwX73NYb71pmVuMLNRhJ9XKdoDB74g==} @@ -1480,6 +1492,9 @@ packages: buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -1792,6 +1807,9 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -2556,6 +2574,16 @@ packages: jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + jsonwebtoken@9.0.2: + resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} + engines: {node: '>=12', npm: '>=6'} + + jwa@1.4.2: + resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==} + + jws@3.2.2: + resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -2633,12 +2661,33 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} @@ -3183,11 +3232,6 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.7.1: - resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} - engines: {node: '>=10'} - hasBin: true - semver@7.7.2: resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} engines: {node: '>=10'} @@ -4870,10 +4914,17 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/jsonwebtoken@9.0.10': + dependencies: + '@types/ms': 2.1.0 + '@types/node': 22.16.3 + '@types/methods@1.1.4': {} '@types/mime@1.3.5': {} + '@types/ms@2.1.0': {} + '@types/node@22.16.3': dependencies: undici-types: 6.21.0 @@ -5446,6 +5497,8 @@ snapshots: buffer-crc32@0.2.13: {} + buffer-equal-constant-time@1.0.1: {} + buffer-from@1.1.2: {} buffer@5.7.1: @@ -5713,6 +5766,10 @@ snapshots: eastasianwidth@0.2.0: {} + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + ee-first@1.1.1: {} ejs@3.1.10: @@ -6312,7 +6369,7 @@ snapshots: '@babel/parser': 7.28.0 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 - semver: 7.7.1 + semver: 7.7.2 transitivePeerDependencies: - supports-color @@ -6699,6 +6756,30 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + jsonwebtoken@9.0.2: + dependencies: + jws: 3.2.2 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.7.2 + + jwa@1.4.2: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@3.2.2: + dependencies: + jwa: 1.4.2 + safe-buffer: 5.2.1 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -6747,10 +6828,24 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash.includes@4.3.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + lodash.memoize@4.1.2: {} lodash.merge@4.6.2: {} + lodash.once@4.1.1: {} + lodash@4.17.21: {} log-symbols@4.1.0: @@ -7216,8 +7311,6 @@ snapshots: semver@6.3.1: {} - semver@7.7.1: {} - semver@7.7.2: {} send@1.2.0: