Update package dependencies and refactor authentication module
- Added sharp dependency for image processing in package.json. - Updated argon2 dependency version to 0.44.0 for enhanced security. - Removed unused @nestjs/jwt dependency and refactored authentication module to utilize JoseJwtService for JWT handling. - Adjusted type definitions for @types/node and @types/pg to ensure compatibility across applications. - Cleaned up package.json files in BFF and Portal applications for consistency and improved dependency management.
This commit is contained in:
parent
fee93cc02b
commit
424f257bd7
@ -36,13 +36,12 @@
|
||||
"@nestjs/common": "^11.1.9",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.1.9",
|
||||
"@nestjs/jwt": "^11.0.2",
|
||||
"@nestjs/passport": "^11.0.5",
|
||||
"@nestjs/platform-express": "^11.1.9",
|
||||
"@prisma/adapter-pg": "^7.1.0",
|
||||
"@prisma/client": "^7.1.0",
|
||||
"@sendgrid/mail": "^8.1.6",
|
||||
"argon2": "^0.43.0",
|
||||
"argon2": "^0.44.0",
|
||||
"bullmq": "^5.65.1",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"helmet": "^8.1.0",
|
||||
@ -70,10 +69,9 @@
|
||||
"@types/cookie-parser": "^1.4.10",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "24.10.3",
|
||||
"@types/passport-jwt": "^4.0.1",
|
||||
"@types/passport-local": "^1.0.38",
|
||||
"@types/pg": "^8.15.6",
|
||||
"@types/pg": "^8.16.0",
|
||||
"@types/ssh2-sftp-client": "^9.0.6",
|
||||
"@types/supertest": "^6.0.3",
|
||||
"jest": "^30.2.0",
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { Injectable, UnauthorizedException, BadRequestException, Inject } from "@nestjs/common";
|
||||
import { JwtService } from "@nestjs/jwt";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import * as argon2 from "argon2";
|
||||
import { UsersFacade } from "@bff/modules/users/application/users.facade.js";
|
||||
@ -38,7 +37,6 @@ export class AuthFacade {
|
||||
constructor(
|
||||
private readonly usersFacade: UsersFacade,
|
||||
private readonly mappingsService: MappingsService,
|
||||
private readonly jwtService: JwtService,
|
||||
private readonly configService: ConfigService,
|
||||
private readonly whmcsService: WhmcsService,
|
||||
private readonly salesforceService: SalesforceService,
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { JwtModule } from "@nestjs/jwt";
|
||||
import { PassportModule } from "@nestjs/passport";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { APP_GUARD } from "@nestjs/core";
|
||||
import { AuthFacade } from "./application/auth.facade.js";
|
||||
import { AuthController } from "./presentation/http/auth.controller.js";
|
||||
@ -15,6 +13,7 @@ import { TokenBlacklistService } from "./infra/token/token-blacklist.service.js"
|
||||
import { EmailModule } from "@bff/infra/email/email.module.js";
|
||||
import { CacheModule } from "@bff/infra/cache/cache.module.js";
|
||||
import { AuthTokenService } from "./infra/token/token.service.js";
|
||||
import { JoseJwtService } from "./infra/token/jose-jwt.service.js";
|
||||
import { SignupWorkflowService } from "./infra/workflows/workflows/signup-workflow.service.js";
|
||||
import { PasswordWorkflowService } from "./infra/workflows/workflows/password-workflow.service.js";
|
||||
import { WhmcsLinkWorkflowService } from "./infra/workflows/workflows/whmcs-link-workflow.service.js";
|
||||
@ -25,13 +24,6 @@ import { AuthRateLimitService } from "./infra/rate-limiting/auth-rate-limit.serv
|
||||
@Module({
|
||||
imports: [
|
||||
PassportModule,
|
||||
JwtModule.registerAsync({
|
||||
useFactory: (configService: ConfigService) => ({
|
||||
secret: configService.get("JWT_SECRET"),
|
||||
signOptions: { expiresIn: configService.get("JWT_EXPIRES_IN", "7d") },
|
||||
}),
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
UsersModule,
|
||||
MappingsModule,
|
||||
IntegrationsModule,
|
||||
@ -45,6 +37,7 @@ import { AuthRateLimitService } from "./infra/rate-limiting/auth-rate-limit.serv
|
||||
LocalStrategy,
|
||||
TokenBlacklistService,
|
||||
AuthTokenService,
|
||||
JoseJwtService,
|
||||
SignupWorkflowService,
|
||||
PasswordWorkflowService,
|
||||
WhmcsLinkWorkflowService,
|
||||
|
||||
53
apps/bff/src/modules/auth/infra/token/jose-jwt.service.ts
Normal file
53
apps/bff/src/modules/auth/infra/token/jose-jwt.service.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { SignJWT, decodeJwt, jwtVerify, errors, type JWTPayload } from "jose";
|
||||
import { parseJwtExpiry } from "../../utils/jwt-expiry.util.js";
|
||||
|
||||
@Injectable()
|
||||
export class JoseJwtService {
|
||||
private readonly secretKey: Uint8Array;
|
||||
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
const secret = configService.get<string>("JWT_SECRET");
|
||||
if (!secret) {
|
||||
throw new Error("JWT_SECRET is required in environment variables");
|
||||
}
|
||||
this.secretKey = new TextEncoder().encode(secret);
|
||||
}
|
||||
|
||||
async sign(payload: JWTPayload, expiresIn: string): Promise<string> {
|
||||
const expiresInSeconds = parseJwtExpiry(expiresIn);
|
||||
const nowSeconds = Math.floor(Date.now() / 1000);
|
||||
|
||||
return new SignJWT(payload)
|
||||
.setProtectedHeader({ alg: "HS256" })
|
||||
.setIssuedAt(nowSeconds)
|
||||
.setExpirationTime(nowSeconds + expiresInSeconds)
|
||||
.sign(this.secretKey);
|
||||
}
|
||||
|
||||
async verify<T extends JWTPayload>(token: string): Promise<T> {
|
||||
const { payload } = await jwtVerify(token, this.secretKey);
|
||||
return payload as T;
|
||||
}
|
||||
|
||||
async verifyAllowExpired<T extends JWTPayload>(token: string): Promise<T | null> {
|
||||
try {
|
||||
return await this.verify<T>(token);
|
||||
} catch (err) {
|
||||
if (err instanceof errors.JWTExpired) {
|
||||
return this.decode<T>(token);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
decode<T extends JWTPayload>(token: string): T | null {
|
||||
try {
|
||||
return decodeJwt(token) as T;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,17 +1,17 @@
|
||||
import { Injectable, Inject } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { JwtService } from "@nestjs/jwt";
|
||||
import { createHash } from "crypto";
|
||||
import { Redis } from "ioredis";
|
||||
import { Logger } from "nestjs-pino";
|
||||
import { parseJwtExpiry } from "../../utils/jwt-expiry.util.js";
|
||||
import { JoseJwtService } from "./jose-jwt.service.js";
|
||||
|
||||
@Injectable()
|
||||
export class TokenBlacklistService {
|
||||
constructor(
|
||||
@Inject("REDIS_CLIENT") private readonly redis: Redis,
|
||||
private readonly configService: ConfigService,
|
||||
private readonly jwtService: JwtService,
|
||||
private readonly jwtService: JoseJwtService,
|
||||
@Inject(Logger) private readonly logger: Logger
|
||||
) {}
|
||||
|
||||
@ -24,14 +24,14 @@ export class TokenBlacklistService {
|
||||
|
||||
// Use JwtService to safely decode and validate token
|
||||
try {
|
||||
const decoded: unknown = this.jwtService.decode(token);
|
||||
const decoded = this.jwtService.decode<{ sub?: string; exp?: number }>(token);
|
||||
|
||||
if (!decoded || typeof decoded !== "object" || Array.isArray(decoded)) {
|
||||
this.logger.warn("Invalid JWT payload structure for blacklisting");
|
||||
return;
|
||||
}
|
||||
|
||||
const { sub, exp } = decoded as { sub?: unknown; exp?: unknown };
|
||||
const { sub, exp } = decoded;
|
||||
if (typeof sub !== "string" || typeof exp !== "number") {
|
||||
this.logger.warn("Invalid JWT payload structure for blacklisting");
|
||||
return;
|
||||
|
||||
@ -4,17 +4,18 @@ import {
|
||||
UnauthorizedException,
|
||||
ServiceUnavailableException,
|
||||
} from "@nestjs/common";
|
||||
import { JwtService } from "@nestjs/jwt";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { Redis } from "ioredis";
|
||||
import { Logger } from "nestjs-pino";
|
||||
import { randomBytes, createHash } from "crypto";
|
||||
import type { JWTPayload } from "jose";
|
||||
import type { AuthTokens } from "@customer-portal/domain/auth";
|
||||
import type { User } from "@customer-portal/domain/customer";
|
||||
import { UsersFacade } from "@bff/modules/users/application/users.facade.js";
|
||||
import { mapPrismaUserToDomain } from "@bff/infra/mappers/index.js";
|
||||
import { JoseJwtService } from "./jose-jwt.service.js";
|
||||
|
||||
export interface RefreshTokenPayload {
|
||||
export interface RefreshTokenPayload extends JWTPayload {
|
||||
userId: string;
|
||||
tokenId: string;
|
||||
deviceId?: string;
|
||||
@ -49,7 +50,7 @@ export class AuthTokenService {
|
||||
private readonly maintenanceMessage: string;
|
||||
|
||||
constructor(
|
||||
private readonly jwtService: JwtService,
|
||||
private readonly jwtService: JoseJwtService,
|
||||
private readonly configService: ConfigService,
|
||||
@Inject("REDIS_CLIENT") private readonly redis: Redis,
|
||||
@Inject(Logger) private readonly logger: Logger,
|
||||
@ -124,13 +125,9 @@ export class AuthTokenService {
|
||||
};
|
||||
|
||||
// Generate tokens
|
||||
const accessToken = this.jwtService.sign(accessPayload, {
|
||||
expiresIn: this.ACCESS_TOKEN_EXPIRY,
|
||||
});
|
||||
const accessToken = await this.jwtService.sign(accessPayload, this.ACCESS_TOKEN_EXPIRY);
|
||||
|
||||
const refreshToken = this.jwtService.sign(refreshPayload, {
|
||||
expiresIn: this.REFRESH_TOKEN_EXPIRY,
|
||||
});
|
||||
const refreshToken = await this.jwtService.sign(refreshPayload, this.REFRESH_TOKEN_EXPIRY);
|
||||
|
||||
// Store refresh token family in Redis
|
||||
const refreshTokenHash = this.hashToken(refreshToken);
|
||||
@ -211,7 +208,7 @@ export class AuthTokenService {
|
||||
}
|
||||
try {
|
||||
// Verify refresh token
|
||||
const payload = this.jwtService.verify<RefreshTokenPayload>(refreshToken);
|
||||
const payload = await this.jwtService.verify<RefreshTokenPayload>(refreshToken);
|
||||
|
||||
if (payload.type !== "refresh") {
|
||||
this.logger.warn("Token presented to refresh endpoint is not a refresh token", {
|
||||
|
||||
@ -6,7 +6,6 @@ import {
|
||||
NotFoundException,
|
||||
} from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { JwtService } from "@nestjs/jwt";
|
||||
import { Logger } from "nestjs-pino";
|
||||
import * as argon2 from "argon2";
|
||||
import type { Request } from "express";
|
||||
@ -16,6 +15,7 @@ import { EmailService } from "@bff/infra/email/email.service.js";
|
||||
import { getErrorMessage } from "@bff/core/utils/error.util.js";
|
||||
import { AuthTokenService } from "../../token/token.service.js";
|
||||
import { AuthRateLimitService } from "../../rate-limiting/auth-rate-limit.service.js";
|
||||
import { JoseJwtService } from "../../token/jose-jwt.service.js";
|
||||
import {
|
||||
type PasswordChangeResult,
|
||||
type ChangePasswordRequest,
|
||||
@ -30,7 +30,7 @@ export class PasswordWorkflowService {
|
||||
private readonly auditService: AuditService,
|
||||
private readonly configService: ConfigService,
|
||||
private readonly emailService: EmailService,
|
||||
private readonly jwtService: JwtService,
|
||||
private readonly jwtService: JoseJwtService,
|
||||
private readonly tokenService: AuthTokenService,
|
||||
private readonly authRateLimitService: AuthRateLimitService,
|
||||
@Inject(Logger) private readonly logger: Logger
|
||||
@ -99,10 +99,7 @@ export class PasswordWorkflowService {
|
||||
return;
|
||||
}
|
||||
|
||||
const token = this.jwtService.sign(
|
||||
{ sub: user.id, purpose: "password_reset" },
|
||||
{ expiresIn: "15m" }
|
||||
);
|
||||
const token = await this.jwtService.sign({ sub: user.id, purpose: "password_reset" }, "15m");
|
||||
|
||||
const appBase = this.configService.get<string>("APP_BASE_URL", "http://localhost:3000");
|
||||
const resetUrl = `${appBase}/auth/reset-password?token=${encodeURIComponent(token)}`;
|
||||
@ -130,7 +127,7 @@ export class PasswordWorkflowService {
|
||||
|
||||
async resetPassword(token: string, newPassword: string): Promise<PasswordChangeResult> {
|
||||
try {
|
||||
const payload = this.jwtService.verify<{ sub: string; purpose: string }>(token);
|
||||
const payload = await this.jwtService.verify<{ sub: string; purpose: string }>(token);
|
||||
if (payload.purpose !== "password_reset") {
|
||||
throw new BadRequestException("Invalid token");
|
||||
}
|
||||
|
||||
@ -12,7 +12,6 @@ import {
|
||||
} from "@nestjs/common";
|
||||
import type { Request, Response } from "express";
|
||||
import { RateLimitGuard, RateLimit } from "@bff/core/rate-limiting/index.js";
|
||||
import { JwtService } from "@nestjs/jwt";
|
||||
import { AuthFacade } from "@bff/modules/auth/application/auth.facade.js";
|
||||
import { LocalAuthGuard } from "./guards/local-auth.guard.js";
|
||||
import {
|
||||
@ -25,6 +24,7 @@ import { ZodValidationPipe } from "nestjs-zod";
|
||||
import type { RequestWithUser } from "@bff/modules/auth/auth.types.js";
|
||||
import { SalesforceReadThrottleGuard } from "@bff/integrations/salesforce/guards/salesforce-read-throttle.guard.js";
|
||||
import { SalesforceWriteThrottleGuard } from "@bff/integrations/salesforce/guards/salesforce-write-throttle.guard.js";
|
||||
import { JoseJwtService } from "../../infra/token/jose-jwt.service.js";
|
||||
|
||||
// Import Zod schemas from domain
|
||||
import {
|
||||
@ -108,7 +108,7 @@ const calculateCookieMaxAge = (isoTimestamp: string): number => {
|
||||
export class AuthController {
|
||||
constructor(
|
||||
private authFacade: AuthFacade,
|
||||
private readonly jwtService: JwtService
|
||||
private readonly jwtService: JoseJwtService
|
||||
) {}
|
||||
|
||||
private setAuthCookies(res: Response, tokens: AuthTokens): void {
|
||||
@ -205,16 +205,9 @@ export class AuthController {
|
||||
let userId = req.user?.id;
|
||||
|
||||
if (!userId && token) {
|
||||
try {
|
||||
const payload = await this.jwtService.verifyAsync<{ sub?: string }>(token, {
|
||||
ignoreExpiration: true,
|
||||
});
|
||||
|
||||
if (payload?.sub) {
|
||||
userId = payload.sub;
|
||||
}
|
||||
} catch {
|
||||
// Ignore verification errors – we still want to clear client cookies.
|
||||
const payload = await this.jwtService.verifyAllowExpired<{ sub?: string }>(token);
|
||||
if (payload?.sub) {
|
||||
userId = payload.sub;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
2
apps/portal/next-env.d.ts
vendored
2
apps/portal/next-env.d.ts
vendored
@ -1,6 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/dev/types/routes.d.ts";
|
||||
import "./.next/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
@ -28,7 +28,6 @@
|
||||
"react": "19.2.1",
|
||||
"react-dom": "19.2.1",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"world-countries": "^5.1.0",
|
||||
"zod": "4.1.13",
|
||||
"zustand": "^5.0.9"
|
||||
@ -37,7 +36,6 @@
|
||||
"@next/bundle-analyzer": "^16.0.8",
|
||||
"@tailwindcss/postcss": "^4.1.17",
|
||||
"@tanstack/react-query-devtools": "^5.91.1",
|
||||
"@types/node": "24.10.3",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"tailwindcss": "^4.1.17",
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
@import "../styles/tokens.css";
|
||||
@import "../styles/utilities.css";
|
||||
@import "../styles/responsive.css";
|
||||
@import "tw-animate-css";
|
||||
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
|
||||
@ -58,6 +58,7 @@
|
||||
"globals": "^16.5.0",
|
||||
"husky": "^9.1.7",
|
||||
"prettier": "^3.7.4",
|
||||
"sharp": "^0.33.5",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.49.0"
|
||||
|
||||
@ -136,7 +136,6 @@
|
||||
"zod": "4.1.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "24.10.3",
|
||||
"typescript": "5.9.3"
|
||||
}
|
||||
}
|
||||
|
||||
690
pnpm-lock.yaml
generated
690
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user