Skip to content

4 · Auth model + external integrations (deep dive)

บทนี้อธิบาย auth pipeline ของ benefit-api (NestJS JWT guard + @Public() override) และทุก integration ภายนอกที่ยืนยันจากโค้ด — centralize server, vtrc-api, vtrc-common, SFTP/FMIS


Auth architecture overview

benefit-api ใช้ Passport JWT strategy แบบ global guard — ทุก route ถูก guard เว้นแต่ประกาศ @Public() override

HTTP request
  → JwtAuthGuard.canActivate()
      ├── if @Public() → allow (no JWT check)
      └── else → PassportStrategy('jwt').validate(payload)
            ├── ExtractJwt.fromAuthHeaderAsBearerToken()
            ├── jwt.verify(token, SECRET_KEY)
            └── attach payload to request.user
  → controller → service → entityManager

ไม่มี RBAC (role-based access control) ระดับ guard — ถ้า login ได้แล้วเรียก endpoint ที่ไม่ใช่ @Public() ได้ทุกตัว การ authorize ตาม role/menu (ถ้ามี) ต้องทำเองใน service layer แต่จากการอ่าน service ทุกตัวในรอบนี้ ไม่พบ role check ใน service layer เช่นกัน


JwtAuthGuard — global guard

benefit-api/libs/jwt/jwt-auth.guard.ts:1-41 (branch: prod) — อยู่ที่ libs/ ไม่ใช่ src/libs/:

typescript
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') implements CanActivate {
  constructor(private reflector: Reflector) {
    super();
  }

  private logger = new Logger(JwtAuthGuard.name);
  private serviceName = '';

  canActivate(context: ExecutionContext) {
    const isPublic = this.reflector.getAllAndOverride<boolean>(isPublicKey, [context.getHandler(), context.getClass()]);

    const request = context.switchToHttp().getRequest();
    this.serviceName = request.url;
    if (isPublic) {
      return true;
    }
    return super.canActivate(context);
  }

  handleRequest(err, user, info) {
    if (err || !user) {
      throw err || new UnauthorizedException({ statusCode: 401, error: info.message });
    }
    return user;
  }
}

export const isPublicKey = 'isPublic';
export const Public = () => SetMetadata(isPublicKey, true);

จุดสำคัญ:

  1. @Public() bypassreflector.getAllAndOverride ตรวจ metadata ทั้ง handler และ class — ถ้ามี @Public() ที่ method หรือ class → return true โดยไม่ verify JWT
  2. handleRequest — ถ้า verify fail → UnauthorizedException กับ info.message (เช่น jwt expired, invalid signature)
  3. ไม่มี role check — comment // we can protect guard here ที่ jwt-auth.guard.ts:30 บ่งชี้ว่าทีมเคยคิดจะเพิ่มแต่ยังไม่ทำ
  4. Global registration — ประกาศที่ benefit-api/src/main.ts (หรือ app.module.ts APP_GUARD provider) ทำให้ทุก controller ถูก guard โดยอัตโนมัติ

JwtStrategy — token validation

benefit-api/libs/jwt/jwt.strategy.ts:1-23 (branch: prod):

typescript
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
  private logger = new Logger(JwtStrategy.name);
  data: any;
  constructor(private configService: ConfigService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: configService.get('jwtSecretKey'),
    });
  }

  async validate(payload: any) {
    this.data = payload;
    return payload;
  }
}

จุดสำคัญ:

  1. secretOrKey: configService.get('jwtSecretKey') — map จาก process.env.SECRET_KEY ผ่าน benefit-api/config/configuration.ts:5
  2. ignoreExpiration: false — ตรวจ expiry จริง (ดีกว่าบาง service ที่ ignore)
  3. validate() return payload ทั้งก้อน — ไม่ตรวจ field บังคับใด ๆ (issuer, audience, empCode) — ถ้า JWT ลงชื่อด้วย secret เดียวกัน แม้ payload จะเป็นของ service อื่น ก็ผ่าน
  4. ไม่มี algorithms ระบุ — default ของ passport-jwt ยอมรับหลาย algorithm — เปิดช่อง alg-confusion attack ในทางทฤษฎี

Secret source

benefit-api/config/configuration.ts:3-5:

typescript
export const configuration = () => ({
  port: process.env.PORT,
  jwtSecretKey: process.env.SECRET_KEY,
  // ...

SECRET_KEY อยู่ใน .env ที่ commit อยู่ใน repo (ดู SEC-BENEFIT-01) — shared secret กับ service อื่นหรือไม่ยัง UNVERIFIED แต่ pattern ชื่อ SECRET_KEY เหมือนกับหลาย NestJS service ใน platform


@Public() endpoints — catalog ครบ

ทุก endpoint ที่ bypass auth (ยืนยันจาก rg "@Public()" src/):

Critical — ควรมี auth แต่ไม่มี

ControllerMethodPathImpact
WdhospitalsControllerPOST/wdHospitals/addสร้างรายการเบิกค่ารักษาพยาบาลได้โดยไม่ login
WdhospitalsControllerPOST/wdHospitals/update/:statusเปลี่ยนสถานะเอกสารเบิก (approve/cancel) ได้โดยไม่ login
WdhospitalsControllerGET/wdHospitals/detail/:idอ่านข้อมูลส่วนบุคคล (identity card, บัญชีธนาคาร) ได้โดยไม่ login
ManualBatchControllerPOST/batchJob/importFuneraltrigger batch import funeral โดยไม่ auth
ManualBatchControllerPOST/batchJob/importInheritancetrigger batch import inheritance โดยไม่ auth
ManualBatchControllerPOST/batchJob/updateFuneralupdate funeral ไป HRMI โดยไม่ auth
ManualBatchControllerPOST/batchJob/updateInheritanceupdate inheritance ไป HRMI โดยไม่ auth
UploadControllerGET/uploadFiles/:idอ่านไฟล์แนบโดยไม่ auth
UploadControllerPOST/uploadFilesอัปโหลดไฟล์โดยไม่ auth
ExportPaymentReportControllerPOST/exportPaymentReport/exportReportByOrderExcelexport Excel report โดยไม่ auth

หลักฐาน — benefit-api/src/modules/wdhospitals/wdhospitals.controller.ts:24-40:

typescript
@Public()
@Post('/add')
insertWdHospitals(@Body() body: RequestWdHospitalsDto) {
  return this.service.addWdHospitals(body);
}

@Public()
@Post('/update/:status')
updateWdHospitals(@Param() status: WdHospitalsStatusEnumDto, @Body() body: UpdateWdHospitalsDto) {
  return this.service.updateWdHospitals(status.status, body);
}

@Public()
@Get('/detail/:id')
getWdHospitalsById(@Param() param: GetWdHospitalsDto) {
  return this.service.getWdHospitalsById(param.id);
}

Medium — dropdown / health (อาจตั้งใจเปิด)

ControllerMethodPathหมายเหตุ
DropdownControllerGET/dropdown/get_dropdown_by_typemaster data — อาจตั้งใจ public
DropdownControllerGET/dropdown/getWelfareTypeListmaster data
DropdownControllerGET/dropdown/getWelfareTypeListByEmpต้องการ empCode — ควรมี auth
DropdownControllerPOST/dropdown/getCOAListchart of account
DropdownControllerPOST/dropdown/getDropDownListgeneric dropdown
AppControllerGET/healthhealth check — ตั้งใจ public
AppControllerGET/ping-centrailping external URL — อันตราย (SSRF risk)
AppControllerGET/testProfiletest JWT decode — debug endpoint ค้างใน prod

Commented-out @Public() — landmine

ControllerPathสถานะ
PaymentsControllerPOST createExportWDHospital@Public() ถูก comment — ปัจจุบันมี auth
UploadControllerPUT/DELETE@Public() ถูก comment — ปัจจุบันมี auth

ถ้า uncomment โดยไม่ตั้งใจ → endpoint กลับเป็น public อีกครั้ง


External integrations

1. Centralize server (NestJS REST)

FieldValue
env varCENTRALIZE_SERVERcentralizeServer
Base URL (จาก .env)http://10.188.128.60:3100/
เรียกจากapprovedisaster.service.ts:185-197, payments.service.ts:127, disbursement.service.ts:120,588
Endpoints ที่เรียกapi/employee/getEmployeeDisaster, api/employee/getByEmpCode, api/employee/bankAccount
ใช้ทำอะไรดึงข้อมูลพนักงาน + เลขบัญชีธนาคาร

หมายเหตุชื่อ centralizeServer — port 3100 ไม่ตรงกับ cu-central-api default 8080 หรือ NestJS centralize-api default 3000 — จาก path (api/employee/...) มีรูปแบบคล้าย REST ของ NestJS centralize-api มากกว่า GraphQL ของ cu-central-api แต่ UNVERIFIED 100% ว่าปลายทางจริงคือ repo ไหน

2. vtrc-api (legacy core) — dual coupling

FieldValue
env varVTRC_SERVERvtrcServer
Base URLhttps://vtrcapi.redcross.or.th/
HTTP callsapprovedisaster.service.ts:389 (listTermFeeForApprover), payments.service.ts:589,737 (cancelStudyEducation)
DB couplingraw SQL เข้าตาราง vtrc.dbo.WDHospitals / WDHistories ผ่าน vtrcEntityManager

นี่คือ coupling แบบสองทาง (API + database) ระหว่าง domain microservice กับ legacy core — ไม่ตรงกับสมมติฐานที่ว่า domain microservice ไม่ควรพึ่งพา vtrc-api

3. vtrc-common (NestJS file service)

FieldValue
env varVTRC_COMMON_SERVER
Base URLhttp://vtrc-common:9310/
เรียกจากwdhospitals.service.ts:67, disbursement.service.ts:143,161,605,639,645, payments.service.ts:902
Endpointsfile/save, file/delete, file/getFileArray
ใช้ทำอะไรเก็บ/ลบ/ดึงไฟล์แนบ

4. SFTP server (FMIS export)

FieldValue
Confighardcode ในโค้ด (ไม่ผ่าน env)
Host10.188.128.22:22
เรียกจากexportpaymentreport.service.ts:5267-5274, 5820-5827, 6146-6152
Pathsupload/fmis/bgdoc/, upload/fmis/ledger/, shared/fmis/
ใช้ทำอะไรอัปโหลด/ดาวน์โหลดไฟล์ JSON บัญชี FMIS

SFTP password hardcode ในโค้ด 3 จุด — ดู SEC-BENEFIT-01


Triple DB connection

benefit-api เชื่อม 3 database พร้อมกัน (configuration.ts:8-61):

Connectionenv prefixSchemaใช้ทำอะไร
db (main)DB_*benefitentity หลักของ benefit-api
dbHRMIDB_HRMI_*HRMIดึง profile พนักงานจาก HRMI
vtrcDBVTRC_DB_*vtrcraw SQL เข้า WDHospitals / MasterApprovers

ทั้ง 3 password อยู่ใน .env ที่ commit — ดู SEC-BENEFIT-01


Secret ที่หลุดเข้า source control

พบ credential จริงที่ commit อยู่ใน repo — ไม่แสดงค่าจริงในเอกสารนี้ ตามนโยบาย:

ประเภท secretไฟล์บรรทัด
MSSQL password (main/hrmi/vtrc ×3)benefit-api/.env (branch: prod)18, 25, 32
JWT secret (SECRET_KEY)benefit-api/.env (branch: prod)64
SFTP password (FMIS)exportpaymentreport.service.ts5271, 5824, 6151 (ซ้ำ 3 จุด)

Auth vs other NestJS services

ด้านbenefit-apicentralize-apisso-api
GuardJwtAuthGuard globalJwtAuthGuard + @Public() เกือบทุก routeJWT stub (issue commented)
Secret envSECRET_KEY(ดู Vol 3.3)(ดู Vol 6)
RBACไม่มีไม่มีไม่มี
@Public() count~20 endpointsเกือบทั้งหมด
Dangerous publicwdHospitals CRUD, batchJob, upload(ดู Vol 3.3)

ขั้นตอนถัดไป

ไป บท 5 Cron jobs → หรือ บท 6 Scorecard