Refactor authentication to use argon2 and update package dependencies

- Replaced bcrypt with argon2 for password hashing and verification in the authentication workflow, enhancing security.
- Updated JWT signing implementation to use the jose library for improved token management.
- Removed outdated bcrypt dependencies from package.json and pnpm-lock.yaml, and added argon2 and jose.
- Adjusted pnpm-workspace.yaml to reflect changes in onlyBuiltDependencies, ensuring better package management.
This commit is contained in:
barsa 2025-12-11 11:38:43 +09:00
parent 1323600978
commit eb31fae344
9 changed files with 61 additions and 78 deletions

View File

@ -42,13 +42,13 @@
"@prisma/adapter-pg": "^7.1.0",
"@prisma/client": "^7.1.0",
"@sendgrid/mail": "^8.1.6",
"bcrypt": "^6.0.0",
"argon2": "^0.43.0",
"bullmq": "^5.65.1",
"cookie-parser": "^1.4.7",
"helmet": "^8.1.0",
"ioredis": "^5.8.2",
"jose": "^6.0.11",
"jsforce": "^3.10.10",
"jsonwebtoken": "^9.0.3",
"nestjs-pino": "^4.5.0",
"nestjs-zod": "^5.0.1",
"p-queue": "^9.0.1",
@ -68,11 +68,9 @@
"@nestjs/cli": "^11.0.14",
"@nestjs/schematics": "^11.0.9",
"@nestjs/testing": "^11.1.9",
"@types/bcrypt": "^6.0.0",
"@types/cookie-parser": "^1.4.10",
"@types/express": "^5.0.6",
"@types/jest": "^30.0.0",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^24.10.2",
"@types/passport-jwt": "^4.0.1",
"@types/passport-local": "^1.0.38",
@ -85,7 +83,6 @@
"supertest": "^7.1.4",
"ts-jest": "^29.4.6",
"tsc-alias": "^1.8.16",
"tsx": "^4.21.0",
"typescript": "^5.9.3"
},
"jest": {

View File

@ -0,0 +1,9 @@
-- Force password reset for all users due to migration from bcrypt to argon2
-- bcrypt hashes are incompatible with argon2, so all users must reset their passwords
-- Set all password hashes to NULL, which will require users to go through password reset flow
UPDATE "User" SET password_hash = NULL WHERE password_hash IS NOT NULL;
-- Log the migration for audit purposes (optional - create entry in audit_log if table exists)
-- This is a data migration, not a schema change

View File

@ -4,7 +4,7 @@ import { ConfigService } from "@nestjs/config";
import { getErrorMessage } from "@bff/core/utils/error.util.js";
import { SalesforceRequestQueueService } from "@bff/core/queue/services/salesforce-request-queue.service.js";
import jsforce from "jsforce";
import jwt from "jsonwebtoken";
import { SignJWT, importPKCS8 } from "jose";
import fs from "node:fs/promises";
import path from "node:path";
@ -138,15 +138,14 @@ export class SalesforceConnection {
throw new Error(isProd ? "Invalid Salesforce private key" : devMsg);
}
// Create JWT assertion
const payload = {
iss: clientId,
sub: username,
aud: audience,
exp: Math.floor(Date.now() / 1000) + 300, // 5 minutes
};
const assertion = jwt.sign(payload, privateKey, { algorithm: "RS256" });
// Create JWT assertion using jose
const key = await importPKCS8(privateKey, "RS256");
const assertion = await new SignJWT({ sub: username })
.setProtectedHeader({ alg: "RS256" })
.setIssuer(clientId)
.setAudience(audience)
.setExpirationTime("5m")
.sign(key);
// Get access token with timeout
const tokenUrl = `${audience}/services/oauth2/token`;

View File

@ -1,7 +1,7 @@
import { Injectable, UnauthorizedException, BadRequestException, Inject } from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { ConfigService } from "@nestjs/config";
import * as bcrypt from "bcrypt";
import * as argon2 from "argon2";
import { UsersFacade } from "@bff/modules/users/application/users.facade.js";
import { MappingsService } from "@bff/modules/id-mappings/mappings.service.js";
import { WhmcsService } from "@bff/integrations/whmcs/whmcs.service.js";
@ -222,7 +222,7 @@ export class AuthFacade {
}
try {
const isPasswordValid = await bcrypt.compare(password, user.passwordHash);
const isPasswordValid = await argon2.verify(user.passwordHash, password);
if (isPasswordValid) {
// Return sanitized user object matching the return type

View File

@ -8,7 +8,7 @@ import {
import { ConfigService } from "@nestjs/config";
import { JwtService } from "@nestjs/jwt";
import { Logger } from "nestjs-pino";
import * as bcrypt from "bcrypt";
import * as argon2 from "argon2";
import type { Request } from "express";
import { UsersFacade } from "@bff/modules/users/application/users.facade.js";
import { AuditService, AuditAction } from "@bff/infra/audit/audit.service.js";
@ -59,10 +59,7 @@ export class PasswordWorkflowService {
throw new BadRequestException("User already has a password set");
}
const saltRoundsConfig = this.configService.get<string | number>("BCRYPT_ROUNDS", 12);
const saltRounds =
typeof saltRoundsConfig === "string" ? Number(saltRoundsConfig) : saltRoundsConfig;
const passwordHash = await bcrypt.hash(password, saltRounds);
const passwordHash = await argon2.hash(password);
try {
await this.usersFacade.update(user.id, { passwordHash });
} catch (error) {
@ -141,10 +138,7 @@ export class PasswordWorkflowService {
const prismaUser = await this.usersFacade.findByIdInternal(payload.sub);
if (!prismaUser) throw new BadRequestException("Invalid token");
const saltRoundsConfig = this.configService.get<string | number>("BCRYPT_ROUNDS", 12);
const saltRounds =
typeof saltRoundsConfig === "string" ? Number(saltRoundsConfig) : saltRoundsConfig;
const passwordHash = await bcrypt.hash(newPassword, saltRounds);
const passwordHash = await argon2.hash(newPassword);
await this.usersFacade.update(prismaUser.id, { passwordHash });
const freshUser = await this.usersFacade.findByIdInternal(prismaUser.id);
@ -186,7 +180,7 @@ export class PasswordWorkflowService {
const parsed = changePasswordRequestSchema.parse(data);
const { currentPassword, newPassword } = parsed;
const isCurrentValid = await bcrypt.compare(currentPassword, user.passwordHash);
const isCurrentValid = await argon2.verify(user.passwordHash, currentPassword);
if (!isCurrentValid) {
await this.auditService.logAuthEvent(
AuditAction.PASSWORD_CHANGE,
@ -202,10 +196,7 @@ export class PasswordWorkflowService {
// Password validation is handled by changePasswordRequestSchema (uses passwordSchema from domain)
// No need for duplicate validation here
const saltRoundsConfig = this.configService.get<string | number>("BCRYPT_ROUNDS", 14);
const saltRounds =
typeof saltRoundsConfig === "string" ? Number(saltRoundsConfig) : saltRoundsConfig;
const passwordHash = await bcrypt.hash(newPassword, saltRounds);
const passwordHash = await argon2.hash(newPassword);
await this.usersFacade.update(user.id, { passwordHash });
const prismaUser = await this.usersFacade.findByIdInternal(user.id);

View File

@ -7,7 +7,7 @@ import {
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Logger } from "nestjs-pino";
import * as bcrypt from "bcrypt";
import * as argon2 from "argon2";
import type { Request } from "express";
import { AuditService, AuditAction } from "@bff/infra/audit/audit.service.js";
import { UsersFacade } from "@bff/modules/users/application/users.facade.js";
@ -185,10 +185,7 @@ export class SignupWorkflowService {
throw new ConflictException(message);
}
const saltRoundsConfig = this.configService.get<string | number>("BCRYPT_ROUNDS", 14);
const saltRounds =
typeof saltRoundsConfig === "string" ? Number(saltRoundsConfig) : saltRoundsConfig;
const passwordHash = await bcrypt.hash(password, saltRounds);
const passwordHash = await argon2.hash(password);
try {
const accountSnapshot = await this.getAccountSnapshot(sfNumber);

View File

@ -66,6 +66,9 @@
"overrides": {
"js-yaml": ">=4.1.1",
"glob": "^8.1.0"
}
},
"onlyBuiltDependencies": [
"argon2"
]
}
}

58
pnpm-lock.yaml generated
View File

@ -85,9 +85,9 @@ importers:
'@sendgrid/mail':
specifier: ^8.1.6
version: 8.1.6
bcrypt:
specifier: ^6.0.0
version: 6.0.0
argon2:
specifier: ^0.43.0
version: 0.43.1
bullmq:
specifier: ^5.65.1
version: 5.65.1
@ -100,12 +100,12 @@ importers:
ioredis:
specifier: ^5.8.2
version: 5.8.2
jose:
specifier: ^6.0.11
version: 6.1.3
jsforce:
specifier: ^3.10.10
version: 3.10.10(@types/node@24.10.2)
jsonwebtoken:
specifier: ^9.0.3
version: 9.0.3
nestjs-pino:
specifier: ^4.5.0
version: 4.5.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(pino-http@11.0.0)(pino@10.1.0)(rxjs@7.8.2)
@ -158,9 +158,6 @@ importers:
'@nestjs/testing':
specifier: ^11.1.9
version: 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/platform-express@11.1.9)
'@types/bcrypt':
specifier: ^6.0.0
version: 6.0.0
'@types/cookie-parser':
specifier: ^1.4.10
version: 1.4.10(@types/express@5.0.6)
@ -170,9 +167,6 @@ importers:
'@types/jest':
specifier: ^30.0.0
version: 30.0.0
'@types/jsonwebtoken':
specifier: ^9.0.10
version: 9.0.10
'@types/node':
specifier: ^24.10.2
version: 24.10.2
@ -209,9 +203,6 @@ importers:
tsc-alias:
specifier: ^1.8.16
version: 1.8.16
tsx:
specifier: ^4.21.0
version: 4.21.0
typescript:
specifier: ^5.9.3
version: 5.9.3
@ -1550,6 +1541,10 @@ packages:
'@paralleldrive/cuid2@2.3.1':
resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==}
'@phc/format@1.0.0':
resolution: {integrity: sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==}
engines: {node: '>=10'}
'@pinojs/redact@0.4.0':
resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==}
@ -1922,9 +1917,6 @@ packages:
'@types/babel__traverse@7.28.0':
resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
'@types/bcrypt@6.0.0':
resolution: {integrity: sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==}
'@types/body-parser@1.19.6':
resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
@ -2400,6 +2392,10 @@ packages:
arg@4.1.3:
resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
argon2@0.43.1:
resolution: {integrity: sha512-TfOzvDWUaQPurCT1hOwIeFNkgrAJDpbBGBGWDgzDsm11nNhImc13WhdGdCU6K7brkp8VpeY07oGtSex0Wmhg8w==}
engines: {node: '>=16.17.0'}
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
@ -2546,10 +2542,6 @@ packages:
bcrypt-pbkdf@1.0.2:
resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==}
bcrypt@6.0.0:
resolution: {integrity: sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==}
engines: {node: '>= 18'}
bin-version-check@5.1.0:
resolution: {integrity: sha512-bYsvMqJ8yNGILLz1KP9zKLzQ6YpljV3ln1gqhuLkUtyfGi3qXKGuK2p+U4NAvjVFzDFiBBtOpCOSFNuYYEGZ5g==}
engines: {node: '>=12'}
@ -4055,6 +4047,9 @@ packages:
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
hasBin: true
jose@6.1.3:
resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==}
joycon@3.1.1:
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
engines: {node: '>=10'}
@ -7123,6 +7118,8 @@ snapshots:
dependencies:
'@noble/hashes': 1.8.0
'@phc/format@1.0.0': {}
'@pinojs/redact@0.4.0': {}
'@pkgr/core@0.2.9': {}
@ -7504,10 +7501,6 @@ snapshots:
dependencies:
'@babel/types': 7.28.5
'@types/bcrypt@6.0.0':
dependencies:
'@types/node': 24.10.2
'@types/body-parser@1.19.6':
dependencies:
'@types/connect': 3.4.38
@ -8093,6 +8086,12 @@ snapshots:
arg@4.1.3:
optional: true
argon2@0.43.1:
dependencies:
'@phc/format': 1.0.0
node-addon-api: 8.5.0
node-gyp-build: 4.8.4
argparse@2.0.1: {}
aria-query@5.3.2: {}
@ -8274,11 +8273,6 @@ snapshots:
dependencies:
tweetnacl: 0.14.5
bcrypt@6.0.0:
dependencies:
node-addon-api: 8.5.0
node-gyp-build: 4.8.4
bin-version-check@5.1.0:
dependencies:
bin-version: 6.0.0
@ -10218,6 +10212,8 @@ snapshots:
jiti@2.6.1: {}
jose@6.1.3: {}
joycon@3.1.1: {}
js-tokens@4.0.0: {}

View File

@ -2,13 +2,4 @@ packages:
- apps/*
- packages/*
onlyBuiltDependencies:
- "@swc/core"
- "esbuild"
- "bcrypt"
- "ssh2"
- "cpu-features"
- "prisma"
- "@prisma/engines"
- "@prisma/client"
- "unrs-resolver"
onlyBuiltDependencies: '[]'