1
0
Files
bigcapital/packages/server/src/modules/Auth/commands/AuthSignin.service.ts
T
Ahmed Bouhuolia 7efac090a9 fix(server): prevent cross-tenant access via organization-id header
Resolve a CLS middleware in App.module.ts to copy the request
`organization-id` header straight into `cls.organizationId`, which the
TenancyDB factory used to pick the per-tenant database. The JWT path
never set `organizationId` from the authenticated user, and
TenancyGlobalGuard only checked that the header was present — so any
authenticated user could read or write another tenant's database by
sending their own JWT plus the victim's `organization-id`.

Make the JWT-resolved tenant the source of truth and validate the
header at the edge:

- AuthSigninService.verifyPayload now loads the user's tenant and sets
  `cls.organizationId` from `tenant.organizationId`, mirroring the
  API-key path in AuthApiKeyAuthorizeService.
- TenancyGlobalGuard rejects with `Organization mismatch.` when the
  request header disagrees with the CLS value set by the auth guard.
- App.module.ts no longer seeds `cls.organizationId` from the
  attacker-controlled request header.

GHSA-2g96-86rw-qmvg

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 20:43:14 +02:00

91 lines
2.5 KiB
TypeScript

import { ClsService } from 'nestjs-cls';
import { Inject, Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { SystemUser } from '@/modules/System/models/SystemUser';
import { TenantModel } from '@/modules/System/models/TenantModel';
import { ModelObject } from 'objection';
import { JwtPayload } from '../Auth.interfaces';
import { InvalidEmailPasswordException } from '../exceptions/InvalidEmailPassword.exception';
import { UserNotFoundException } from '../exceptions/UserNotFound.exception';
@Injectable()
export class AuthSigninService {
constructor(
@Inject(SystemUser.name)
private readonly systemUserModel: typeof SystemUser,
@Inject(TenantModel.name)
private readonly tenantModel: typeof TenantModel,
private readonly jwtService: JwtService,
private readonly clsService: ClsService,
) { }
/**
* Validates the given email and password.
* @param {string} email - Signin email address.
* @param {string} password - Signin password.
* @returns {Promise<ModelObject<SystemUser>>}
*/
async signin(
email: string,
password: string,
): Promise<ModelObject<SystemUser>> {
let user: SystemUser;
try {
user = await this.systemUserModel
.query()
.findOne({ email })
.throwIfNotFound();
} catch (err) {
throw new InvalidEmailPasswordException(email);
}
if (!(await user.checkPassword(password))) {
throw new InvalidEmailPasswordException(email);
}
return user;
}
/**
* Verifies the given jwt payload.
* @param {JwtPayload} payload
* @returns {Promise<any>}
*/
async verifyPayload(payload: JwtPayload): Promise<any> {
let user: SystemUser;
let tenant: TenantModel | undefined;
try {
user = await this.systemUserModel
.query()
.findOne({ email: payload.sub })
.throwIfNotFound();
tenant = await this.tenantModel
.query()
.findById(user.tenantId)
.throwIfNotFound();
this.clsService.set('tenantId', user.tenantId);
this.clsService.set('userId', user.id);
this.clsService.set('organizationId', tenant.organizationId);
} catch (error) {
throw new UserNotFoundException(String(payload.sub));
}
return payload;
}
/**
*
* @param {SystemUser} user
* @returns {string}
*/
signToken(user: SystemUser): string {
const payload = {
sub: user.email,
};
return this.jwtService.sign(payload);
}
}